marion 3 роки тому
батько
коміт
c08f9dd61f
100 змінених файлів з 6894 додано та 2 видалено
  1. 3 0
      .idea/.gitignore
  2. 16 0
      .idea/ctjt_flutter.iml
  3. 748 0
      .idea/libraries/Dart_Packages.xml
  4. 28 0
      .idea/libraries/Dart_SDK.xml
  5. 32 0
      .idea/libraries/Flutter_Plugins.xml
  6. 7 0
      .idea/misc.xml
  7. 8 0
      .idea/modules.xml
  8. 6 0
      .idea/vcs.xml
  9. 10 0
      .metadata
  10. 39 2
      README.md
  11. 11 0
      android/.gitignore
  12. BIN
      android/app/android_key.keystore
  13. 110 0
      android/app/build.gradle
  14. 7 0
      android/app/src/debug/AndroidManifest.xml
  15. 71 0
      android/app/src/main/AndroidManifest.xml
  16. 6 0
      android/app/src/main/kotlin/com/example/ctjt_flutter/MainActivity.kt
  17. 12 0
      android/app/src/main/res/drawable/launch_background.xml
  18. BIN
      android/app/src/main/res/mipmap-hdpi/ic_launcher.png
  19. BIN
      android/app/src/main/res/mipmap-mdpi/ic_launcher.png
  20. BIN
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
  21. BIN
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
  22. BIN
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
  23. 18 0
      android/app/src/main/res/values/styles.xml
  24. 7 0
      android/app/src/profile/AndroidManifest.xml
  25. 44 0
      android/build.gradle
  26. 3 0
      android/gradle.properties
  27. 6 0
      android/gradle/wrapper/gradle-wrapper.properties
  28. 9 0
      android/proguard-rules.pro
  29. 11 0
      android/settings.gradle
  30. 32 0
      ios/.gitignore
  31. 26 0
      ios/Flutter/AppFrameworkInfo.plist
  32. 2 0
      ios/Flutter/Debug.xcconfig
  33. 2 0
      ios/Flutter/Release.xcconfig
  34. 41 0
      ios/Podfile
  35. 29 0
      ios/Podfile.lock
  36. 563 0
      ios/Runner.xcodeproj/project.pbxproj
  37. 7 0
      ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
  38. 8 0
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  39. 8 0
      ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  40. 91 0
      ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
  41. 10 0
      ios/Runner.xcworkspace/contents.xcworkspacedata
  42. 8 0
      ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
  43. 8 0
      ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
  44. 13 0
      ios/Runner/AppDelegate.swift
  45. 122 0
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
  46. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
  47. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
  48. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
  49. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
  50. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
  51. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
  52. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
  53. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
  54. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
  55. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
  56. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
  57. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
  58. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
  59. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
  60. BIN
      ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
  61. 23 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
  62. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
  63. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
  64. BIN
      ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
  65. 5 0
      ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
  66. 37 0
      ios/Runner/Base.lproj/LaunchScreen.storyboard
  67. 26 0
      ios/Runner/Base.lproj/Main.storyboard
  68. 69 0
      ios/Runner/Info.plist
  69. 1 0
      ios/Runner/Runner-Bridging-Header.h
  70. 123 0
      lib/common/states.dart
  71. 68 0
      lib/common/styles.dart
  72. 57 0
      lib/common/time.dart
  73. 101 0
      lib/common/trtc/GenerateTestUserSig.dart
  74. 39 0
      lib/common/trtc/ProfileManager_Mock.dart
  75. 100 0
      lib/common/trtc/TxUtils.dart
  76. 164 0
      lib/common/trtc/calling/model/TRTCCalling.dart
  77. 110 0
      lib/common/trtc/calling/model/TRTCCallingDef.dart
  78. 151 0
      lib/common/trtc/calling/model/TRTCCallingDelegate.dart
  79. 656 0
      lib/common/trtc/calling/model/impl/TRTCCallingImpl.dart
  80. 306 0
      lib/common/trtc/calling/ui/TRTCCallingContact.dart
  81. 640 0
      lib/common/trtc/calling/ui/VideoCall/TRTCCallingVideo.dart
  82. 4 0
      lib/common/trtc/calling/ui/base/CallStatus.dart
  83. 4 0
      lib/common/trtc/calling/ui/base/CallTypes.dart
  84. 4 0
      lib/common/trtc/calling/ui/base/CallingScenes.dart
  85. 44 0
      lib/common/trtc/calling/ui/base/ExtendButton.dart
  86. 137 0
      lib/common/utils.dart
  87. 106 0
      lib/main.dart
  88. 80 0
      lib/model/version_res.dart
  89. 278 0
      lib/pages/about.dart
  90. 91 0
      lib/pages/home.dart
  91. 36 0
      lib/pages/remote_call.dart
  92. 70 0
      lib/service/app_service.dart
  93. 138 0
      lib/service/base_service.dart
  94. 192 0
      lib/widget/button.dart
  95. 102 0
      lib/widget/contact.dart
  96. 100 0
      lib/widget/drawer.dart
  97. 432 0
      lib/widget/input.dart
  98. 91 0
      lib/widget/title.dart
  99. 97 0
      pubspec.yaml
  100. 30 0
      test/widget_test.dart

+ 3 - 0
.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 16 - 0
.idea/ctjt_flutter.iml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+      <excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
+    </content>
+    <orderEntry type="inheritedJdk" />
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Dart SDK" level="project" />
+    <orderEntry type="library" name="Dart Packages" level="project" />
+    <orderEntry type="library" name="Flutter Plugins" level="project" />
+  </component>
+</module>

+ 748 - 0
.idea/libraries/Dart_Packages.xml

@@ -0,0 +1,748 @@
+<component name="libraryTable">
+  <library name="Dart Packages" type="DartPackagesLibraryType">
+    <properties>
+      <option name="packageNameToDirsMap">
+        <entry key="async">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/async-2.8.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="boolean_selector">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="characters">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/characters-1.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="charcode">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/charcode-1.3.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="chewie">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/chewie-1.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="chewie_audio">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/chewie_audio-1.2.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="clock">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="collection">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/collection-1.15.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="crypto">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/crypto-3.0.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="csslib">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/csslib-0.17.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="cupertino_icons">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/cupertino_icons-1.0.3/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="device_info">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/device_info-2.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="device_info_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/device_info_platform_interface-2.0.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="dio">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/dio-4.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="fake_async">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.2.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="ffi">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/ffi-1.1.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="file">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/file-6.1.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/packages/flutter/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_html">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_html-2.1.5/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_layout_grid">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_layout_grid-1.0.3/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_localizations">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/packages/flutter_localizations/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_math_fork">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_math_fork-0.4.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_screenutil">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_screenutil-5.0.0+2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_styled_toast">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_styled_toast-2.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_svg">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_svg-0.22.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_test">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/packages/flutter_test/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="flutter_web_plugins">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/packages/flutter_web_plugins/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="fluttertoast">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-8.0.8/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="html">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/html-0.15.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="http_parser">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/http_parser-4.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="intl">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/intl-0.17.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="js">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/js-0.6.3/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="json_annotation">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/json_annotation-4.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="matcher">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.10/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="meta">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/meta-1.7.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="nested">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/nested-1.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="numerus">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/numerus-1.1.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="package_info">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/package_info-2.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path-1.8.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path_drawing">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_drawing-0.5.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path_parsing">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_parsing-0.2.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path_provider">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.0.5/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path_provider_linux">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path_provider_macos">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_macos-2.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path_provider_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_platform_interface-2.0.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="path_provider_windows">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.0.3/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="permission_handler">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/permission_handler-8.1.6/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="permission_handler_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/permission_handler_platform_interface-3.6.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="petitparser">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/petitparser-4.3.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="platform">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/platform-3.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="plugin_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/plugin_platform_interface-2.0.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="process">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/process-4.2.3/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="provider">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/provider-6.0.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="quiver">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/quiver-3.0.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="r_upgrade">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/r_upgrade-0.3.5/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="shared_preferences">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-2.0.8/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="shared_preferences_linux">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_linux-2.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="shared_preferences_macos">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_macos-2.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="shared_preferences_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_platform_interface-2.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="shared_preferences_web">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-2.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="shared_preferences_windows">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_windows-2.0.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="sky_engine">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/bin/cache/pkg/sky_engine/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="source_span">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.8.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="sprintf">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/sprintf-6.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="stack_trace">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.10.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="stream_channel">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="string_scanner">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="synchronized">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/synchronized-3.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="tencent_im_sdk_plugin">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tencent_im_sdk_plugin-3.5.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="tencent_trtc_cloud">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tencent_trtc_cloud-1.2.4/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="term_glyph">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="test_api">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.4.2/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="tuple">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tuple-2.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="typed_data">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/typed_data-1.3.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="vector_math">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="vibration">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vibration-1.7.4-nullsafety.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="vibration_web">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vibration_web-1.6.3-nullsafety.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="video_player">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player-2.2.5/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="video_player_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player_platform_interface-4.2.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="video_player_web">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player_web-2.0.4/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="wakelock">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock-0.5.6/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="wakelock_macos">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_macos-0.4.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="wakelock_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_platform_interface-0.3.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="wakelock_web">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_web-0.4.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="wakelock_windows">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_windows-0.2.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="webview_flutter">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-2.1.1/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="webview_flutter_android">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_android-2.0.15/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="webview_flutter_platform_interface">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_platform_interface-1.0.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="webview_flutter_wkwebview">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_wkwebview-2.0.14/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="win32">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/win32-2.2.9/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="xdg_directories">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-0.2.0/lib" />
+            </list>
+          </value>
+        </entry>
+        <entry key="xml">
+          <value>
+            <list>
+              <option value="$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/xml-5.3.0/lib" />
+            </list>
+          </value>
+        </entry>
+      </option>
+    </properties>
+    <CLASSES>
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/async-2.8.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/boolean_selector-2.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/characters-1.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/charcode-1.3.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/chewie-1.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/chewie_audio-1.2.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/clock-1.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/collection-1.15.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/crypto-3.0.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/csslib-0.17.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/cupertino_icons-1.0.3/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/device_info-2.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/device_info_platform_interface-2.0.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/dio-4.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/fake_async-1.2.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/ffi-1.1.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/file-6.1.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_html-2.1.5/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_layout_grid-1.0.3/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_math_fork-0.4.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_screenutil-5.0.0+2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_styled_toast-2.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/flutter_svg-0.22.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-8.0.8/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/html-0.15.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/http_parser-4.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/intl-0.17.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/js-0.6.3/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/json_annotation-4.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/matcher-0.12.10/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/meta-1.7.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/nested-1.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/numerus-1.1.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/package_info-2.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path-1.8.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_drawing-0.5.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_parsing-0.2.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.0.5/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_macos-2.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_platform_interface-2.0.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.0.3/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/permission_handler-8.1.6/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/permission_handler_platform_interface-3.6.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/petitparser-4.3.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/platform-3.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/plugin_platform_interface-2.0.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/process-4.2.3/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/provider-6.0.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/quiver-3.0.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/r_upgrade-0.3.5/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-2.0.8/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_linux-2.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_macos-2.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_platform_interface-2.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-2.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_windows-2.0.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/source_span-1.8.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/sprintf-6.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/stack_trace-1.10.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/stream_channel-2.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/string_scanner-1.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/synchronized-3.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tencent_im_sdk_plugin-3.5.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tencent_trtc_cloud-1.2.4/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/term_glyph-1.2.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/test_api-0.4.2/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tuple-2.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/typed_data-1.3.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vector_math-2.1.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vibration-1.7.4-nullsafety.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vibration_web-1.6.3-nullsafety.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player-2.2.5/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player_platform_interface-4.2.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player_web-2.0.4/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock-0.5.6/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_macos-0.4.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_platform_interface-0.3.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_web-0.4.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_windows-0.2.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-2.1.1/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_android-2.0.15/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_platform_interface-1.0.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_wkwebview-2.0.14/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/win32-2.2.9/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-0.2.0/lib" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/xml-5.3.0/lib" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/pkg/sky_engine/lib" />
+      <root url="file://$USER_HOME$/flutter/packages/flutter/lib" />
+      <root url="file://$USER_HOME$/flutter/packages/flutter_localizations/lib" />
+      <root url="file://$USER_HOME$/flutter/packages/flutter_test/lib" />
+      <root url="file://$USER_HOME$/flutter/packages/flutter_web_plugins/lib" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES />
+  </library>
+</component>

+ 28 - 0
.idea/libraries/Dart_SDK.xml

@@ -0,0 +1,28 @@
+<component name="libraryTable">
+  <library name="Dart SDK">
+    <CLASSES>
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/async" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/cli" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/collection" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/convert" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/core" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/developer" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/ffi" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/html" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/indexed_db" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/io" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/isolate" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/js" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/js_util" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/math" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/mirrors" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/svg" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/typed_data" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/web_audio" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/web_gl" />
+      <root url="file://$USER_HOME$/flutter/bin/cache/dart-sdk/lib/web_sql" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES />
+  </library>
+</component>

+ 32 - 0
.idea/libraries/Flutter_Plugins.xml

@@ -0,0 +1,32 @@
+<component name="libraryTable">
+  <library name="Flutter Plugins" type="FlutterPluginsLibraryType">
+    <CLASSES>
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/device_info-2.0.2" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.0.3" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_windows-2.0.2" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_linux-2.0.2" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-2.0.8" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/permission_handler-8.1.6" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/package_info-2.0.2" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_web-0.4.0" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/fluttertoast-8.0.8" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player-2.2.5" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/video_player_web-2.0.4" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-2.0.2" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock-0.5.6" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.0.5" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/wakelock_macos-0.4.0" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.1.0" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tencent_trtc_cloud-1.2.4" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter-2.1.1" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_wkwebview-2.0.14" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/webview_flutter_android-2.0.15" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vibration-1.7.4-nullsafety.0" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/vibration_web-1.6.3-nullsafety.0" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/r_upgrade-0.3.5" />
+      <root url="file://$USER_HOME$/flutter/.pub-cache/hosted/pub.flutter-io.cn/tencent_im_sdk_plugin-3.5.0" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES />
+  </library>
+</component>

+ 7 - 0
.idea/misc.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="FrameworkDetectionExcludesConfiguration">
+    <type id="android" />
+  </component>
+  <component name="ProjectRootManager" version="2" languageLevel="JDK_15" project-jdk-name="1.8" project-jdk-type="JavaSDK" />
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/ctjt_flutter.iml" filepath="$PROJECT_DIR$/.idea/ctjt_flutter.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 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: 78910062997c3a836feee883712c241a5fd22983
+  channel: stable
+
+project_type: app

+ 39 - 2
README.md

@@ -1,3 +1,40 @@
-# flutter-template
+# ctjt_flutter
 
-Flutter project template
+A new Flutter project.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
+
+## Flutter原型项目结构
+
+* lib/common   公共库,包括全局状态定义
+* lib/model    数据实体定义
+* lib/pages    页面
+* lib/service  数据访问处理逻辑
+* lib/widget   组件库
+  
+* pubspec.yaml Flutter项目主配置文件
+
+* ios/Runner/Info.plist IOS项目主配置文件
+
+* android/app/src/main/AndroidManifest.xml Android项目主配置文件
+* android/app/android_key.keystore         Android项目签名证书储存文件
+* android/app/build.gradle                 Android项目构建配置文件
+* android/build.gradle                     Android项目构建仓库配置文件
+* android/proguard-rules.pro               Android项目混淆配置文件
+
+Android证书储存文件生成命令如下,注意按照实际情况修改参数
+
+> keytool -genkey -v -keystore android_key.keystore -alias ctjt_flutter -keyalg RSA -keysize 2048 -validity 10000
+
+> keytool -importkeystore -srckeystore android_key.keystore -destkeystore android_key.keystore -deststoretype pkcs12

+ 11 - 0
android/.gitignore

@@ -0,0 +1,11 @@
+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

BIN
android/app/android_key.keystore


+ 110 - 0
android/app/build.gradle

@@ -0,0 +1,110 @@
+import java.text.SimpleDateFormat
+
+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"
+
+android {
+    compileSdkVersion 30
+//    ndkVersion "21.3.6528147"
+
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+
+    applicationVariants.all { variant ->
+        variant.outputs.all {
+            def versionName = defaultConfig.versionName
+            def date = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date())
+            if (variant.buildType.name == 'release') {
+                outputFileName = "ctjt_flutter_${variant.buildType.name}_${versionName}_${date}.apk"
+            } else if (variant.buildType.name == 'debug') {
+                outputFileName = "ctjt_flutter_${versionName}_${date}.apk"
+            }
+        }
+    }
+
+    signingConfigs {
+        debug {
+            storeFile file('android_key.keystore')
+            storePassword 'abcd1234'
+            keyAlias 'ctjt_flutter'
+            keyPassword 'abcd1234'
+        }
+        release {
+            storeFile file('android_key.keystore')
+            storePassword 'abcd1234'
+            keyAlias 'ctjt_flutter'
+            keyPassword 'abcd1234'
+        }
+    }
+
+    lintOptions {
+        disable 'InvalidPackage'
+        //如打包出现Failed to transform libs.jar to match attributes
+        //checkReleaseBuilds false
+    }
+
+    defaultConfig {
+        // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+        applicationId "com.example.ctjt_flutter"
+        minSdkVersion 19
+        targetSdkVersion 30
+        versionCode flutterVersionCode.toInteger()
+        versionName flutterVersionName
+        multiDexEnabled true
+        // Bugly相关配置
+//        ndk {
+//            //设置支持的SO库架构
+//            abiFilters 'armeabi-v7a'//, 'arm64-v8a', 'x86', 'x86_64'
+//        }
+    }
+
+    buildTypes {
+        debug {
+            signingConfig signingConfigs.debug
+        }
+        release {
+            // Signing with the debug keys for now, so `flutter run --release` works.
+            //signingConfig signingConfigs.debug
+            signingConfig signingConfigs.release
+            minifyEnabled false  // 资源压缩设置
+            shrinkResources false // 删除无用资源
+            useProguard true    // 代码压缩设置
+            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' // 读取代码压缩配置文件
+        }
+    }
+}
+
+flutter {
+    source '../..'
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    // 引入support支持库的multidex库
+    implementation 'com.android.support:multidex:1.0.3'
+}

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

@@ -0,0 +1,7 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.example.ctjt_flutter">
+    <!-- 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>

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

@@ -0,0 +1,71 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    package="com.example.ctjt_flutter">
+    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
+         calls FlutterMain.startInitialization(this); in its onCreate method.
+         In most cases you can leave this as-is, but you if you want to provide
+         additional functionality it is fine to subclass or reimplement
+         FlutterApplication and put your custom class here. -->
+
+    <!--dio-->
+    <uses-permission android:name="android.permission.INTERNET"/>
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+    <!--r_upgrade-->
+    <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <!--vibrate-->
+    <uses-permission android:name="android.permission.VIBRATE"/>
+
+    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
+    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK"/>
+    <!--    <uses-permission android:name="android.permission.BLUETOOTH" />-->
+    <!--    <uses-permission android:name="android.permission.CAMERA" />-->
+
+<!--    <uses-feature android:name="android.hardware.Camera"/>-->
+<!--    <uses-feature android:name="android.hardware.camera.autofocus" />-->
+
+    <application
+        tools:replace="android:label"
+        android:name="io.flutter.app.FlutterApplication"
+        android:label="ctjt_flutter"
+        android:icon="@mipmap/ic_launcher">
+        <activity
+            android:name=".MainActivity"
+            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"
+              />
+            <!-- Displays an Android View that continues showing the launch screen
+                 Drawable until Flutter paints its first frame, then this splash
+                 screen fades out. A splash screen is useful to avoid any visual
+                 gap between the end of Android's launch screen and the painting of
+                 Flutter's first frame. -->
+            <meta-data
+              android:name="io.flutter.embedding.android.SplashScreenDrawable"
+              android:resource="@drawable/launch_background"
+              />
+            <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>

+ 6 - 0
android/app/src/main/kotlin/com/example/ctjt_flutter/MainActivity.kt

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

+ 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/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 -->
+    <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:color/white</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.example.ctjt_flutter">
+    <!-- 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>

+ 44 - 0
android/build.gradle

@@ -0,0 +1,44 @@
+buildscript {
+    ext.kotlin_version = '1.3.50'
+    repositories {
+        maven { url 'http://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
+        maven { url 'https://maven.aliyun.com/repository/google' }
+        maven { url 'https://maven.aliyun.com/repository/jcenter' }
+        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        classpath 'com.android.tools.build:gradle:3.5.0'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+allprojects {
+    repositories {
+        maven { url 'http://mirrors.cloud.tencent.com/nexus/repository/maven-public/' }
+        maven { url 'https://maven.aliyun.com/repository/google' }
+        maven { url 'https://maven.aliyun.com/repository/jcenter' }
+        maven { url 'http://maven.aliyun.com/nexus/content/groups/public' }
+        google()
+        jcenter()
+    }
+    gradle.projectsEvaluated {
+        tasks.withType(JavaCompile) {
+            options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
+        }
+    }
+}
+
+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-5.6.2-all.zip

+ 9 - 0
android/proguard-rules.pro

@@ -0,0 +1,9 @@
+#Flutter Wrapper
+-dontwarn io.flutter.**
+-keep class io.flutter.app.**{*;}
+-keep class io.flutter.plugin.**{*;}
+-keep class io.flutter.util.**{*;}
+-keep class io.flutter.view.**{*;}
+-keep class io.flutter.**{*;}
+-keep class io.flutter.plugins.**{*;}
+-keep class com.tencent.** { *; }

+ 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"

+ 32 - 0
ios/.gitignore

@@ -0,0 +1,32 @@
+*.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/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>$(DEVELOPMENT_LANGUAGE)</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>8.0</string>
+</dict>
+</plist>

+ 2 - 0
ios/Flutter/Debug.xcconfig

@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"

+ 2 - 0
ios/Flutter/Release.xcconfig

@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"

+ 41 - 0
ios/Podfile

@@ -0,0 +1,41 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '9.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+  'Debug' => :debug,
+  'Profile' => :release,
+  'Release' => :release,
+}
+
+def flutter_root
+  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+  unless File.exist?(generated_xcode_build_settings_path)
+    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+  end
+
+  File.foreach(generated_xcode_build_settings_path) do |line|
+    matches = line.match(/FLUTTER_ROOT\=(.*)/)
+    return matches[1].strip if matches
+  end
+  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+  use_frameworks!
+  use_modular_headers!
+
+  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+  installer.pods_project.targets.each do |target|
+    flutter_additional_ios_build_settings(target)
+  end
+end

+ 29 - 0
ios/Podfile.lock

@@ -0,0 +1,29 @@
+PODS:
+  - Flutter (1.0.0)
+  - fluttertoast (0.0.2):
+    - Flutter
+    - Toast
+  - Toast (4.0.0)
+
+DEPENDENCIES:
+  - Flutter (from `Flutter`)
+  - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
+
+SPEC REPOS:
+  trunk:
+    - Toast
+
+EXTERNAL SOURCES:
+  Flutter:
+    :path: Flutter
+  fluttertoast:
+    :path: ".symlinks/plugins/fluttertoast/ios"
+
+SPEC CHECKSUMS:
+  Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c
+  fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
+  Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
+
+PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
+
+COCOAPODS: 1.10.2

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

@@ -0,0 +1,563 @@
+// !$*UTF8*$!
+{
+	archiveVersion = 1;
+	classes = {
+	};
+	objectVersion = 46;
+	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 */; };
+		4E79DE5D5CD9C37E75964DB7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 580ED78539D23D536817D012 /* Pods_Runner.framework */; };
+		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>"; };
+		580ED78539D23D536817D012 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+		6F7D375CE41160609F8BB353 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
+		701B7BA579879A93E0ADEAA8 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<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>"; };
+		8DBECE38B4F9A8ACB8174AB1 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<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 = (
+				4E79DE5D5CD9C37E75964DB7 /* Pods_Runner.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+		4461D4F8ACE98F714B66F93F /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				580ED78539D23D536817D012 /* Pods_Runner.framework */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		5B38E21070B63B4463ABE743 /* Pods */ = {
+			isa = PBXGroup;
+			children = (
+				6F7D375CE41160609F8BB353 /* Pods-Runner.debug.xcconfig */,
+				8DBECE38B4F9A8ACB8174AB1 /* Pods-Runner.release.xcconfig */,
+				701B7BA579879A93E0ADEAA8 /* Pods-Runner.profile.xcconfig */,
+			);
+			name = Pods;
+			path = Pods;
+			sourceTree = "<group>";
+		};
+		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 */,
+				5B38E21070B63B4463ABE743 /* Pods */,
+				4461D4F8ACE98F714B66F93F /* Frameworks */,
+			);
+			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 = (
+				ABB7BD0C3C9C21FB729E503B /* [CP] Check Pods Manifest.lock */,
+				9740EEB61CF901F6004384FC /* Run Script */,
+				97C146EA1CF9000F007C117D /* Sources */,
+				97C146EB1CF9000F007C117D /* Frameworks */,
+				97C146EC1CF9000F007C117D /* Resources */,
+				9705A1C41CF9048500538489 /* Embed Frameworks */,
+				3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+				87FDF53CA469D2B5E44FBE4C /* [CP] Embed Pods Frameworks */,
+			);
+			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 = 1020;
+				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";
+		};
+		87FDF53CA469D2B5E44FBE4C /* [CP] Embed Pods Frameworks */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+			);
+			name = "[CP] Embed Pods Frameworks";
+			outputFileListPaths = (
+				"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+			showEnvVarsInLog = 0;
+		};
+		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";
+		};
+		ABB7BD0C3C9C21FB729E503B /* [CP] Check Pods Manifest.lock */ = {
+			isa = PBXShellScriptBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			inputFileListPaths = (
+			);
+			inputPaths = (
+				"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+				"${PODS_ROOT}/Manifest.lock",
+			);
+			name = "[CP] Check Pods Manifest.lock";
+			outputFileListPaths = (
+			);
+			outputPaths = (
+				"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+			shellPath = /bin/sh;
+			shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n    # print error to STDERR\n    echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n    exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+			showEnvVarsInLog = 0;
+		};
+/* 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;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.ctjtFlutter;
+				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_OPTIMIZATION_LEVEL = "-Owholemodule";
+				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;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.ctjtFlutter;
+				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;
+				FRAMEWORK_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				INFOPLIST_FILE = Runner/Info.plist;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
+				LIBRARY_SEARCH_PATHS = (
+					"$(inherited)",
+					"$(PROJECT_DIR)/Flutter",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = com.example.ctjtFlutter;
+				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>

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

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1020"
+   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">
+      <Testables>
+      </Testables>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "97C146ED1CF9000F007C117D"
+            BuildableName = "Runner.app"
+            BlueprintName = "Runner"
+            ReferencedContainer = "container:Runner.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </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>
+      <AdditionalOptions>
+      </AdditionalOptions>
+   </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>

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

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

+ 69 - 0
ios/Runner/Info.plist

@@ -0,0 +1,69 @@
+<?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>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>ctjt_flutter</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>NSAppleMusicUsageDescription</key>
+	<string>App需要您的同意,才能访问音乐,以便控制音频播放。</string>
+    <key>NSCalendarsUsageDescription</key>
+    <string>App需要您的同意,才能访问日历,以便读取时间信息。</string>
+    <key>NSContactsUsageDescription</key>
+    <string>App需要您的同意,才能访问通讯录,以便访问联系人相关信息。</string>
+    <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
+    <string>App需要您的同意,才能访问位置信息。</string>
+    <key>NSLocationAlwaysUsageDescription</key>
+    <string>App需要您的同意,才能访问位置信息。</string>
+    <key>NSLocationWhenInUseUsageDescription</key>
+    <string>App需要您的同意,才能访问位置信息。</string>
+    <key>NSMotionUsageDescription</key>
+    <string>App需要您的同意,才能访问位移传感器,以便适配界面显示。</string>
+    <key>NSSpeechRecognitionUsageDescription</key>
+    <string>App需要您的同意,才能访问语音识别,以便支持语音输入。</string>
+    <key>NSCameraUsageDescription</key>
+   	<string>授权摄像头权限才能正常视频通话</string>
+   	<key>NSMicrophoneUsageDescription</key>
+   	<string>授权麦克风权限才能正常语音通话</string>
+   	<key>NSPhotoLibraryUsageDescription</key>
+   	<string>App需要您的同意,才能访问相册</string>
+	<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/>
+	<key>io.flutter.embedded_views_preview</key>
+    <true/>
+</dict>
+</plist>

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

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

+ 123 - 0
lib/common/states.dart

@@ -0,0 +1,123 @@
+import 'package:flutter/material.dart';
+import 'package:package_info/package_info.dart';
+import 'package:provider/provider.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+/// 状态储存工具类
+class States {
+  // 用于首页初始化时从本地持久化存储或应用配置中读取全局状态
+  static init(BuildContext context) {
+    _readStates(context);
+  }
+
+  // 读取状态,异步操作
+  static _readStates(BuildContext context) async {
+    // 从本地储存读缓存状态
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    var sc = prefs.getBool(StateKey.ShowContact);
+
+    Provider.of<UserStatus>(context, listen: false).userName = prefs.getString(StateKey.UserName) ?? '';
+    Provider.of<UserStatus>(context, listen: false).userToken = prefs.getString(StateKey.UserToken) ?? '';
+
+    Provider.of<AppVersion>(context, listen: false).showContact = (null == sc ? true : sc);
+
+    // 当前APP版本不从缓存读,直接从应用包信息获取(实际是在pubspec.yaml文件中配置的version配置项)
+    PackageInfo packageInfo = await PackageInfo.fromPlatform();
+    Provider.of<AppVersion>(context, listen: false).appVersion = packageInfo.version.split('+')[0]; // 不管+号后的构建号
+  }
+
+  // 重置缓存状态
+  static reset(BuildContext context) async {
+    Provider.of<UserStatus>(context, listen: false).userName = '';
+    Provider.of<UserStatus>(context, listen: false).userToken = '';
+
+    Provider.of<AppVersion>(context, listen: false).showContact = true;
+  }
+
+  // 本地持久化保存布尔值
+  static _saveBool(String key, bool value) async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    await prefs.setBool(key, value);
+  }
+
+  // 本地持久化保存整型值
+  static _saveInt(String key, int value) async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    await prefs.setInt(key, value);
+  }
+
+  // 本地持久化保存双精度浮点数值
+  static _saveDouble(String key, double value) async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    await prefs.setDouble(key, value);
+  }
+
+  // 本地持久化保存字符串值
+  static _saveString(String key, String value) async {
+    SharedPreferences prefs = await SharedPreferences.getInstance();
+    await prefs.setString(key, value);
+  }
+}
+
+/// 用于全局状态持久化存储的键名
+class StateKey {
+  static const UserName = 'userName';        // 用户名
+  static const UserToken = 'userToken';      // 用户登录凭据
+
+  static const ShowContact = 'showContact';  //
+}
+
+/// 用户登录全局状态,供Provider用,并带持久化存储
+class UserStatus with ChangeNotifier {
+  String _userName = '';
+  String _userToken = '';
+
+  String get userName => _userName;
+
+  set userName(String value) {
+    _userName = value;
+    States._saveString(StateKey.UserName, value);
+    notifyListeners();
+  }
+
+  String get userToken => _userToken;
+
+  set userToken(String value) {
+    _userToken = value;
+    States._saveString(StateKey.UserToken, value);
+    notifyListeners();
+  }
+}
+
+/// APP版本全局状态,供Provider用,并带持久化存储
+class AppVersion with ChangeNotifier {
+  String _appVersion = '';
+  bool _showContact = true;
+
+  String get appVersion => _appVersion;
+
+  set appVersion(String value) {
+    _appVersion = value;
+    notifyListeners();
+  }
+
+  bool get showContact => _showContact;
+
+  set showContact(bool value) {
+    _showContact = value;
+    States._saveBool(StateKey.ShowContact, value);
+    notifyListeners();
+  }
+}
+
+/// APP版下载进度状态,供Provider用
+class AppDownloadProcess with ChangeNotifier {
+  double _process = 0.0;
+
+  double get process => _process;
+
+  set process(double value) {
+    _process = value;
+    notifyListeners();
+  }
+}

+ 68 - 0
lib/common/styles.dart

@@ -0,0 +1,68 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+/// 样式工具类
+class Styles {
+  static double? buttonFontSize;
+  static double? iconBigSize;
+  static double? iconSize;
+  static EdgeInsetsGeometry? primaryPadding;
+
+  // Dart SDK >= 2.6 可简写
+  // ScreenUtil().setSp(10) -> 10.sp     适配字体
+  // ScreenUtil().setWidth(10) -> 10.w   根据屏幕宽度适配尺寸
+  // ScreenUtil().setHeight(10) -> 10.h  根据屏幕高度适配尺寸(一般根据宽度适配即可)
+  // ScreenUtil().radius(10) -> 10.r     根据宽度或高度中的较小者进行调整
+  static void initSize() {
+    buttonFontSize = 43.0.sp;
+    iconBigSize = 58.0.sp;
+    iconSize = ScreenUtil().radius(50.0);
+    primaryPadding =
+        EdgeInsets.only(left: 30.0.w, right: 30.0.w, bottom: 15.0.h);
+  }
+
+  static const primaryColor = Color(0xff484848);
+  static const linkColor = Color(0xff4899ee);
+
+  static final titleStyle = TextStyle(
+      fontSize: 80.0.sp,
+      fontWeight: FontWeight.w700,
+      color: primaryColor);
+
+  static final formLabelStyle = TextStyle(
+      fontSize: 56.0.sp,
+      fontWeight: FontWeight.w500,
+      color: primaryColor);
+
+  static final btnFontStyle = TextStyle(
+      fontSize: buttonFontSize,
+      fontWeight: FontWeight.normal,
+      color: primaryColor);
+
+  static final configDescStyle = TextStyle(
+      fontSize: 48.sp,
+      fontWeight: FontWeight.normal,
+      color: primaryColor);
+
+  static final drawerTileStyle = TextStyle(
+      fontSize: 58.0.sp,
+      fontWeight: FontWeight.w500,
+      color: Styles.primaryColor);
+
+  static final drawerTilePadding = EdgeInsets.fromLTRB(
+      70.0.w,
+      35.0.h,
+      0,
+      35.0.h);
+
+  /// 状态栏设置
+  static const SystemUiOverlayStyle statusBarStyle = SystemUiOverlayStyle(
+    systemNavigationBarColor: primaryColor,
+    systemNavigationBarDividerColor: null,
+    statusBarColor: Colors.transparent,
+    systemNavigationBarIconBrightness: Brightness.light,
+    statusBarIconBrightness: Brightness.dark,
+    statusBarBrightness: Brightness.light,
+  );
+}

+ 57 - 0
lib/common/time.dart

@@ -0,0 +1,57 @@
+/// 时间工具类
+class TimeUtils {
+  /// 获取现在的时间
+  static int getDayNow() {
+    var nowTime = DateTime.now();
+    return nowTime.millisecondsSinceEpoch;
+  }
+
+  /// 获取今天的开始时间
+  static int getDayBegin() {
+    var nowTime = DateTime.now();
+    var day = new DateTime(nowTime.year, nowTime.month, nowTime.day, 0, 0, 0);
+    return day.millisecondsSinceEpoch;
+  }
+
+  /// 获取昨天的开始时间
+  static int getBeginDayOfYesterday() {
+    var nowTime = DateTime.now();
+    var yesterday = nowTime.add(new Duration(days: -1));
+    var day =
+        new DateTime(yesterday.year, yesterday.month, yesterday.day, 0, 0, 0);
+    return day.millisecondsSinceEpoch;
+  }
+
+  /// 获取昨天的结束时间
+  static int getEndDayOfYesterDay() {
+    var nowTime = DateTime.now();
+    var yesterday = nowTime.add(new Duration(days: -1));
+    var day = new DateTime(
+        yesterday.year, yesterday.month, yesterday.day, 23, 59, 59);
+    return day.millisecondsSinceEpoch;
+  }
+
+  /// 获取本周的开始时间
+  static int getBeginDayOfWeek() {
+    var nowTime = DateTime.now();
+    var weekday = nowTime.weekday;
+    var yesterday = nowTime.add(new Duration(days: -(weekday - 1)));
+    var day =
+        new DateTime(yesterday.year, yesterday.month, yesterday.day, 0, 0, 0);
+    return day.millisecondsSinceEpoch;
+  }
+
+  /// 获取本月的开始时间
+  static int getBeginDayOfMonth() {
+    var nowTime = DateTime.now();
+    var day = new DateTime(nowTime.year, nowTime.month, 1, 0, 0, 0);
+    return day.millisecondsSinceEpoch;
+  }
+
+  /// 获取本年的开始时间
+  static int getBeginDayOfYear() {
+    var nowTime = DateTime.now();
+    var day = new DateTime(nowTime.year, 1, 1, 0, 0, 0);
+    return day.millisecondsSinceEpoch;
+  }
+}

+ 101 - 0
lib/common/trtc/GenerateTestUserSig.dart

@@ -0,0 +1,101 @@
+/*
+ * Module:   GenerateTestUserSig
+ *
+ * Function: 用于生成测试用的 UserSig,UserSig 是腾讯云为其云服务设计的一种安全保护签名。
+ *           其计算方法是对 SDKAppID、UserID 和 EXPIRETIME 进行加密,加密算法为 HMAC-SHA256。
+ *
+ * Attention: 请不要将如下代码发布到您的线上正式版本的 App 中,原因如下:
+ *
+ *            本文件中的代码虽然能够正确计算出 UserSig,但仅适合快速调通 SDK 的基本功能,不适合线上产品,
+ *            这是因为客户端代码中的 SECRETKEY 很容易被反编译逆向破解,尤其是 Web 端的代码被破解的难度几乎为零。
+ *            一旦您的密钥泄露,攻击者就可以计算出正确的 UserSig 来盗用您的腾讯云流量。
+ *
+ *            正确的做法是将 UserSig 的计算代码和加密密钥放在您的业务服务器上,然后由 App 按需向您的服务器获取实时算出的 UserSig。
+ *            由于破解服务器的成本要高于破解客户端 App,所以服务器计算的方案能够更好地保护您的加密密钥。
+ *
+ * Reference:https://cloud.tencent.com/document/product/647/17275#Server
+ */
+import 'dart:convert';
+import 'dart:io';
+import 'package:crypto/crypto.dart';
+
+class GenerateTestUserSig {
+  /*
+   * 腾讯云 SDKAppId,需要替换为您自己账号下的 SDKAppId。
+   *
+   * 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/trtc ) 创建应用,即可看到 SDKAppId,
+   * 它是腾讯云用于区分客户的唯一标识。
+   */
+  static int sdkAppId = 0;
+
+  /*
+   * 签名过期时间,建议不要设置的过短
+   * <p>
+   * 时间单位:秒
+   * 默认时间:7 x 24 x 60 x 60 = 604800 = 7 天
+   */
+  static int expireTime = 604800;
+
+  /*
+   * 计算签名用的加密密钥,获取步骤如下:
+   *
+   * step1. 进入腾讯云实时音视频[控制台](https://console.cloud.tencent.com/trtc ),如果还没有应用就创建一个,
+   * step2. 单击“应用配置”进入基础配置页面,并进一步找到“帐号体系集成”部分。
+   * step3. 点击“查看密钥”按钮,就可以看到计算 UserSig 使用的加密的密钥了,请将其拷贝并复制到如下的变量中
+   *
+   * 注意:该方案仅适用于调试Demo,正式上线前请将 UserSig 计算代码和密钥迁移到您的后台服务器上,以避免加密密钥泄露导致的流量盗用。
+   * 文档:https://cloud.tencent.com/document/product/647/17275#Server
+   */
+  static String secretKey = '';
+
+  ///生成UserSig
+  static genTestSig(String userId) {
+    int currTime = _getCurrentTime();
+    String sig = '';
+    Map<String, dynamic> sigDoc = new Map<String, dynamic>();
+    sigDoc.addAll({
+      "TLS.ver": "2.0",
+      "TLS.identifier": userId,
+      "TLS.sdkappid": sdkAppId,
+      "TLS.expire": expireTime,
+      "TLS.time": currTime,
+    });
+
+    sig = _hmacsha256(
+      identifier: userId,
+      currTime: currTime,
+      expire: expireTime,
+    );
+    sigDoc['TLS.sig'] = sig;
+    String jsonStr = json.encode(sigDoc);
+    List<int> compress = zlib.encode(utf8.encode(jsonStr));
+    return _escape(content: base64.encode(compress));
+  }
+
+  static int _getCurrentTime() {
+    return (new DateTime.now().millisecondsSinceEpoch / 1000).floor();
+  }
+
+  static String _hmacsha256({
+    required String identifier,
+    required int currTime,
+    required int expire,
+  }) {
+    int sdkappid = sdkAppId;
+    String contentToBeSigned =
+        "TLS.identifier:$identifier\nTLS.sdkappid:$sdkappid\nTLS.time:$currTime\nTLS.expire:$expire\n";
+    Hmac hmacSha256 = new Hmac(sha256, utf8.encode(secretKey));
+    Digest hmacSha256Digest =
+        hmacSha256.convert(utf8.encode(contentToBeSigned));
+    return base64.encode(hmacSha256Digest.bytes);
+  }
+
+  static String _escape({
+    required String content,
+  }) {
+    return content
+        .replaceAll('\+', '*')
+        .replaceAll('\/', '-')
+        .replaceAll('=', '_');
+  }
+}

+ 39 - 0
lib/common/trtc/ProfileManager_Mock.dart

@@ -0,0 +1,39 @@
+import 'package:ctjt_flutter/common/trtc/TxUtils.dart';
+
+class UserModel {
+  String phone;
+  String name;
+  String avatar;
+  String userId;
+  UserModel(
+      {this.phone = '', this.name = '', this.avatar = '', this.userId = ''});
+}
+
+class ProfileManager {
+  static ProfileManager? _instance;
+
+  static getInstance() {
+    if (_instance == null) {
+      _instance = new ProfileManager();
+    }
+    return _instance;
+  }
+
+  Future<List<UserModel>> queryUserInfo(String userId) {
+    return Future.value([
+      UserModel(
+          phone: userId,
+          name: userId,
+          avatar: TxUtils.getDefaltAvatarUrl(),
+          userId: userId)
+    ]);
+  }
+
+  Future<UserModel> querySingleUserInfo(String userId) {
+    return Future.value(UserModel(
+        phone: userId,
+        name: userId,
+        avatar: TxUtils.getDefaltAvatarUrl(),
+        userId: userId));
+  }
+}

+ 100 - 0
lib/common/trtc/TxUtils.dart

@@ -0,0 +1,100 @@
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:flutter_styled_toast/flutter_styled_toast.dart' as StyledToast;
+import 'package:shared_preferences/shared_preferences.dart';
+import 'dart:math';
+
+const USERID_KEY = "UserId";
+
+class TxUtils {
+  static String _loginUserId = '';
+  static() {
+    if (_loginUserId == '') {
+      getStorageByKey(USERID_KEY).then((value) {
+        _loginUserId = value;
+      });
+    }
+  }
+
+  static showErrorToast(text, context) {
+    Fluttertoast.showToast(
+      msg: text,
+      toastLength: Toast.LENGTH_SHORT,
+      gravity: ToastGravity.BOTTOM,
+      timeInSecForIosWeb: 3,
+      backgroundColor: Colors.red,
+      textColor: Colors.white,
+      fontSize: 16.0,
+    );
+  }
+
+  static getRandomNumber() {
+    Random rng = new Random();
+    //2147483647
+    String numStr = '';
+    for (var i = 0; i < 9; i++) {
+      numStr += rng.nextInt(9).toString();
+    }
+    return int.tryParse(numStr);
+  }
+
+  static List<String> _defaltUrlList = [
+    'https://imgcache.qq.com/operation/dianshi/other/7.157d962fa53be4107d6258af6e6d83f33d45fba4.png',
+    'https://imgcache.qq.com/operation/dianshi/other/5.ca48acfebc4dfb68c6c463c9f33e60cb8d7c9565.png',
+    'https://imgcache.qq.com/operation/dianshi/other/1.724142271f4e811457eee00763e63f454af52d13.png',
+    'https://imgcache.qq.com/operation/dianshi/other/4.67f22bd6d283d942d06e69c6b8a2c819c0e11af5.png',
+    'https://imgcache.qq.com/operation/dianshi/other/6.1b984e741cc2275cda3451fa44515e018cc49cb5.png',
+    //先不用这种图片,或者和白色字体不搭配
+    //'https://imgcache.qq.com/operation/dianshi/other/2.4c958e11852b2caa75da6c2726f9248108d6ec8a.png',
+  ];
+  static getRandoAvatarUrl() {
+    Random rng = new Random();
+    return _defaltUrlList[rng.nextInt(_defaltUrlList.length)];
+  }
+
+  static getDefaltAvatarUrl() {
+    return _defaltUrlList[0];
+  }
+
+  static showToast(String text, context) {
+    Fluttertoast.cancel();
+    Fluttertoast.showToast(
+      msg: text,
+      toastLength: Toast.LENGTH_SHORT,
+      gravity: ToastGravity.BOTTOM,
+      timeInSecForIosWeb: 3,
+      backgroundColor: Color.fromRGBO(192, 192, 192, 0.3),
+      textColor: Colors.black,
+      fontSize: 16.0,
+    );
+  }
+
+  static showStyledToast(String text, BuildContext context) {
+    StyledToast.showToast(
+      text,
+      context: context,
+      position: StyledToast.StyledToastPosition.center,
+    );
+  }
+
+  static setStorageByKey(key, value) async {
+    if (key == USERID_KEY) {
+      _loginUserId = value;
+    }
+    SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
+    sharedPreferences.setString(key, value);
+  }
+
+  static Future<String> getStorageByKey(key) async {
+    SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
+    String? rStr = sharedPreferences.getString(key);
+    return rStr == null ? Future.value('') : Future.value(rStr);
+  }
+
+  static Future<String> getLoginUserId() {
+    if (_loginUserId == '') {
+      return getStorageByKey(USERID_KEY);
+    }
+    return Future.value(_loginUserId);
+  }
+}

+ 164 - 0
lib/common/trtc/calling/model/TRTCCalling.dart

@@ -0,0 +1,164 @@
+import 'TRTCCallingDef.dart';
+import 'impl/TRTCCallingImpl.dart';
+
+abstract class TRTCCalling {
+  static int typeUnknow = 0;
+  static int typeAudioCall = 1; //纯音频通话
+  static int typeVideoCall = 2; //视频通话
+
+  /*
+  * 获取 TRTCCalling 单例对象
+  *
+  * @return TRTCCalling 实例
+  * @note 可以调用 {@link TRTCCalling.destroySharedInstance()} 销毁单例对象
+  */
+  static Future<TRTCCalling> sharedInstance() async {
+    return TRTCCallingImpl.sharedInstance();
+  }
+
+  /*
+  * 销毁 TRTCCalling 单例对象
+  *
+  * @note 销毁实例后,外部缓存的 TRTCCalling 实例不能再使用,需要重新调用 {@link TRTCCalling.sharedInstance()} 获取新实例
+  */
+  static void destroySharedInstance() async {
+    TRTCCallingImpl.destroySharedInstance();
+  }
+
+  /// 销毁函数,如果不需要再运行该实例,请调用该接口
+  void destroy();
+
+  //////////////////////////////////////////////////////////
+  //
+  //                 基础接口
+  //
+  //////////////////////////////////////////////////////////
+  /*
+  * 设置组件事件监听接口
+  *
+  * 您可以通过 registerListener 获得 TRTCCalling 的各种状态通知
+  *
+  * @param VoiceListenerFunc func 回调接口
+  */
+  void registerListener(VoiceListenerFunc func);
+
+  /*
+  * 移除组件事件监听接口
+  */
+  void unRegisterListener(VoiceListenerFunc func);
+
+  /*
+  * 登录
+  *
+  * @param sdkAppId 您可以在实时音视频控制台 >【[应用管理](https://console.cloud.tencent.com/trtc/app)】> 应用信息中查看 SDKAppID
+  * @param userId 当前用户的 ID,字符串类型,只允许包含英文字母(a-z 和 A-Z)、数字(0-9)、连词符(-)和下划线(\_)
+  * @param userSig 腾讯云设计的一种安全保护签名,获取方式请参考 [如何计算 UserSig](https://cloud.tencent.com/document/product/647/17275)。
+  * @param 返回值:成功时 code 为0
+  */
+  Future<ActionCallback> login(int sdkAppId, String userId, String userSig);
+
+  /*
+  * 退出登录
+  */
+  Future<ActionCallback> logout();
+
+  /*
+  * C2C邀请通话,被邀请方会收到 {@link TRTCCallingDelegate#onInvited } 的回调
+  * 如果当前处于通话中,可以调用该函数以邀请第三方进入通话
+  *
+  * @param userId 被邀请方
+  * @param type   1-语音通话,2-视频通话
+  */
+  Future<ActionCallback> call(String userId, int type);
+
+  /*
+  * IM群组邀请通话,被邀请方会收到 {@link TRTCCallingDelegate#onInvited } 的回调
+  * 如果当前处于通话中,可以继续调用该函数继续邀请他人进入通话,同时正在通话的用户会收到 {@link TRTCCallingDelegate#onGroupCallInviteeListUpdate(List)} 的回调
+  *
+  * @param userIdList 邀请列表
+  * @param type       1-语音通话,2-视频通话
+  * @param groupId    IM群组ID,选填。如果填写该参数,那么通话请求消息是通过群消息系统广播出去的,这种消息广播方式比较简单可靠。如果不填写,那么 TRTCCalling 组件会采用单发消息逐一通知。
+  */
+  Future<ActionCallback> groupCall(
+      List<String> userIdList, int type, String? groupId);
+
+  /*
+  * 当您作为被邀请方收到 {@link TRTCCallingDelegate#onInvited } 的回调时,可以调用该函数接听来电
+  */
+  Future<ActionCallback> accept();
+
+  /*
+  * 当您作为被邀请方收到 {@link TRTCCallingDelegate#onInvited } 的回调时,可以调用该函数拒绝来电
+  */
+  Future<ActionCallback> reject();
+
+  /*
+  * 当您处于通话中,可以调用该函数结束通话
+  */
+  Future<void> hangup();
+
+  /*
+  * 当您收到 onUserVideoAvailable 回调时,可以调用该函数将远端用户的摄像头数据渲染到指定的TRTCCloudVideoView中
+  *
+  * @param userId           远端用户id
+  * @param viewId 远端用户数据将渲染到该view中
+  */
+  Future<void> startRemoteView(String userId, int streamType, int viewId);
+
+  /*
+  * 当您收到 onUserVideoAvailable 回调为false时,可以停止渲染数据
+  *
+  * @param userId 远端用户id
+  */
+  Future<void> stopRemoteView(String userId, int streamType);
+
+  /*
+  * 更新远端视频画面的窗口,仅仅ios有效
+  *
+  * @param userId           远端用户id
+  * @param viewId 远端用户数据将渲染到该view中
+  */
+  Future<void> updateRemoteView(String userId, int streamType, int viewId);
+  /*
+  * 您可以调用该函数开启摄像头,并渲染在指定的TRTCCloudVideoView中
+  * 处于通话中的用户会收到 {@link TRTCCallingDelegate#onUserVideoAvailable(java.lang.String, boolean)} 回调
+  *
+  * @param isFrontCamera    是否开启前置摄像头
+  * @param viewId TRTCCloudVideoView生成的viewId
+  */
+  Future<void> openCamera(bool isFrontCamera, int viewId);
+
+  /// 更新本地视频预览画面的窗口,仅仅ios有效
+  ///
+  /// 参数:
+  ///
+  /// viewId	承载视频画面的控件
+  Future<void> updateLocalView(int viewId);
+
+  /*
+  * 您可以调用该函数关闭摄像头
+  * 处于通话中的用户会收到 {@link TRTCCallingDelegate#onUserVideoAvailable(java.lang.String, boolean)} 回调
+  */
+  Future<void> closeCamera();
+
+  /*
+  * 您可以调用该函数切换前后摄像头
+  *
+  * @param isFrontCamera true:切换前置摄像头 false:切换后置摄像头
+  */
+  Future<void> switchCamera(bool isFrontCamera);
+
+  /*
+  * 是否静音mic
+  *
+  * @param isMute true:麦克风关闭 false:麦克风打开
+  */
+  Future<void> setMicMute(bool isMute);
+
+  /*
+  * 是否开启免提
+  *
+  * @param isHandsFree true:开启免提 false:关闭免提
+  */
+  Future<void> setHandsFree(bool isHandsFree);
+}

+ 110 - 0
lib/common/trtc/calling/model/TRTCCallingDef.dart

@@ -0,0 +1,110 @@
+// 关键类定义
+
+class ActionCallback {
+  /// 错误码
+  int code;
+
+  /// 信息描述
+  String desc;
+
+  ActionCallback({this.code = 0, this.desc = ''});
+}
+
+class RoomInfo {
+  /// 【字段含义】房间唯一标识
+  int roomId;
+
+  /// 【字段含义】房间名称
+  String? roomName;
+
+  /// 【字段含义】房间封面图
+  String? coverUrl;
+
+  /// 【字段含义】房主id
+  String ownerId;
+
+  /// 【字段含义】房主昵称
+  String? ownerName;
+
+  /// 【字段含义】房间人数
+  int? memberCount;
+
+  RoomInfo(
+      {required this.roomId,
+      this.roomName,
+      this.coverUrl,
+      this.memberCount,
+      required this.ownerId,
+      this.ownerName});
+}
+
+class RoomInfoCallback {
+  /// 错误码
+  int code;
+
+  /// 信息描述
+  String desc;
+
+  List<RoomInfo>? list;
+
+  RoomInfoCallback({required this.code, required this.desc, this.list});
+}
+
+class RoomParam {
+  /// 房间名称
+  String? roomName;
+
+  /// 房间封面图
+  String? coverUrl;
+
+  RoomParam({this.roomName, this.coverUrl});
+}
+
+class MemberListCallback {
+  /// 错误码
+  int code;
+
+  /// 信息描述
+  String desc;
+
+  /// nextSeq	分页拉取标志,第一次拉取填0,回调成功如果 nextSeq 不为零,需要分页,传入再次拉取,直至为0。
+  int nextSeq;
+
+  List<UserInfo>? list;
+
+  MemberListCallback(
+      {this.code = 0, this.desc = '', this.nextSeq = 0, this.list});
+}
+
+class UserListCallback {
+  /// 错误码
+  int code;
+
+  /// 信息描述
+  String desc;
+
+  /// 用户信息列表
+  List<UserInfo>? list;
+
+  UserListCallback({this.code = 0, this.desc = '', this.list});
+}
+
+class UserInfo {
+  /// 用户唯一标识
+  String userId;
+
+  /// 用户昵称
+  String userName;
+
+  /// 用户头像
+  String userAvatar;
+
+  /// 主播是否开麦
+  bool mute;
+
+  UserInfo(
+      {required this.userId,
+      required this.userName,
+      required this.userAvatar,
+      required this.mute});
+}

+ 151 - 0
lib/common/trtc/calling/model/TRTCCallingDelegate.dart

@@ -0,0 +1,151 @@
+/// TRTCCallingDelegate回调事件
+enum TRTCCallingDelegate {
+  /// 错误回调,表示 SDK 不可恢复的错误,一定要监听并分情况给用户适当的界面提示
+  ///
+  /// 参数param:
+  ///
+  /// errCode	错误码
+  ///
+  /// errMsg	错误信息
+  ///
+  /// extraInfo	扩展信息字段,个别错误码可能会带额外的信息帮助定位问题
+  onError,
+
+  /// 警告回调,用于告知您一些非严重性问题,例如出现卡顿或者可恢复的解码失败。
+  ///
+  /// 参数param:
+  ///
+  /// warningCode	错误码
+  ///
+  /// warningMsg	警告信息
+  ///
+  /// extraInfo	扩展信息字段,个别警告码可能会带额外的信息帮助定位问题
+  onWarning,
+
+  ///本地进房
+  ///
+  /// 如果加入成功,result 会是一个正数(result > 0),代表加入房间的时间消耗,单位是毫秒(ms)。
+  ///
+  /// 如果加入失败,result 会是一个负数(result < 0),代表进房失败的错误码。
+  ///
+  /// 参数param:
+  ///
+  /// result > 0 时为进房耗时(ms),result < 0 时为进房错误码
+  onEnterRoom,
+
+  /// 有用户加入当前房间。
+  ///
+  /// 参数param:
+  ///
+  /// userId	用户标识
+  onUserEnter,
+
+  /// 有用户离开当前房间。
+  ///
+  /// 参数param:
+  ///
+  /// userId	用户标识
+  ///
+  /// reason	离开原因,0表示用户主动退出房间,1表示用户超时退出,2表示被踢出房间。
+  onUserLeave,
+
+  /*
+  * 正在IM群组通话时,如果其他与会者邀请他人,会收到此回调
+  * 例如 A-B-C 正在IM群组中,A邀请[D、E]进入通话,B、C会收到[D、E]的回调
+  * 如果此时 A 再邀请 F 进入群聊,那么B、C会收到[D、E、F]的回调
+  * @param userIdList 邀请群组
+  */
+  onGroupCallInviteeListUpdate,
+
+  /*
+  * 被邀请通话回调
+  * @param sponsor 邀请者
+  * @param userIdList 同时还被邀请的人
+  * @param isFromGroup 是否IM群组邀请
+  * @param callType 邀请类型 1-语音通话,2-视频通话
+  */
+  onInvited,
+
+  /*
+   * 1. 在C2C通话中,只有发起方会收到拒绝回调
+   * 例如 A 邀请 B、C 进入通话,B拒绝,A可以收到该回调,但C不行
+   *
+   * 2. 在IM群组通话中,所有被邀请人均能收到该回调
+   * 例如 A 邀请 B、C 进入通话,B拒绝,A、C均能收到该回调
+   * @param userId 拒绝通话的用户
+   */
+  onReject,
+
+  /*
+    * 1. 在C2C通话中,只有发起方会收到无人应答的回调
+    * 例如 A 邀请 B、C 进入通话,B不应答,A可以收到该回调,但C不行
+    *
+    * 2. 在IM群组通话中,所有被邀请人均能收到该回调
+    * 例如 A 邀请 B、C 进入通话,B不应答,A、C均能收到该回调
+    * @param userId
+    */
+  onNoResp,
+
+  /*
+   * 邀请方忙线
+   * @param userId 忙线用户
+   */
+  onLineBusy,
+
+  /*
+   * 作为被邀请方会收到,收到该回调说明本次通话被取消了
+   */
+  onCallingCancel,
+
+  /*
+  * 作为被邀请方会收到,收到该回调说明本次通话超时未应答
+  */
+  onCallingTimeout,
+
+  /*
+  * 收到该回调说明本次通话结束了
+  */
+  onCallEnd,
+
+  /// 远端用户是否存在可播放的主路画面(一般用于摄像头)
+  ///
+  /// 当您收到 onUserVideoAvailable(userId, true) 通知时,表示该路画面已经有可用的视频数据帧到达。 此时,您需要调用 startRemoteView(userid) 接口加载该用户的远程画面。 然后,您会收到名为 onFirstVideoFrame(userid) 的首帧画面渲染回调。
+  ///
+  /// 当您收到 onUserVideoAvailable(userId, false) 通知时,表示该路远程画面已经被关闭,可能由于该用户调用了 muteLocalVideo() 或 stopLocalPreview()。
+  ///
+  /// 参数param:
+  ///
+  /// userId	用户标识
+  ///
+  /// available	画面是否开启
+  onUserVideoAvailable,
+
+  /// 远端用户是否存在可播放的主路画面(一般用于摄像头)
+  ///
+  /// 当您收到 onUserVideoAvailable(userId, true) 通知时,表示该路画面已经有可用的视频数据帧到达。 此时,您需要调用 startRemoteView(userid) 接口加载该用户的远程画面。 然后,您会收到名为 onFirstVideoFrame(userid) 的首帧画面渲染回调。
+  ///
+  /// 当您收到 onUserVideoAvailable(userId, false) 通知时,表示该路远程画面已经被关闭,可能由于该用户调用了 muteLocalVideo() 或 stopLocalPreview()。
+  ///
+  /// 参数param:
+  ///
+  /// userId	用户标识
+  ///
+  /// available	画面是否开启
+  onUserAudioAvailable,
+
+  /// 用于提示音量大小的回调,包括每个 userId 的音量和远端总音量。
+  ///
+  /// 您可以通过调用 TRTCCloud 中的 enableAudioVolumeEvaluation 接口来开关这个回调或者设置它的触发间隔。 需要注意的是,调用 enableAudioVolumeEvaluation 开启音量回调后,无论频道内是否有人说话,都会按设置的时间间隔调用这个回调; 如果没有人说话,则 userVolumes 为空,totalVolume 为0。
+  ///
+  /// 注意:userId 为本地用户 ID 时表示自己的音量,userVolumes 内仅包含正在说话(音量不为0)的用户音量信息。
+  ///
+  /// 参数param:
+  ///
+  /// userVolumes	所有正在说话的房间成员的音量,取值范围0 - 100。
+  ///
+  /// totalVolume	所有远端成员的总音量, 取值范围0 - 100。
+  onUserVoiceVolume,
+
+  //其他用户登录了同一账号,被踢下线
+  onKickedOffline
+}

+ 656 - 0
lib/common/trtc/calling/model/impl/TRTCCallingImpl.dart

@@ -0,0 +1,656 @@
+import 'dart:convert';
+import 'dart:math';
+
+import 'package:tencent_im_sdk_plugin/enum/V2TimSDKListener.dart';
+import 'package:tencent_im_sdk_plugin/enum/V2TimSignalingListener.dart';
+import 'package:tencent_im_sdk_plugin/models/v2_tim_callback.dart';
+import 'package:tencent_trtc_cloud/tx_beauty_manager.dart';
+
+import '../TRTCCalling.dart';
+import '../TRTCCallingDef.dart';
+import '../TRTCCallingDelegate.dart';
+
+//trtc sdk
+import 'package:tencent_trtc_cloud/trtc_cloud.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud_def.dart';
+import 'package:tencent_trtc_cloud/tx_audio_effect_manager.dart';
+import 'package:tencent_trtc_cloud/tx_device_manager.dart';
+
+//im sdk
+import 'package:tencent_im_sdk_plugin/tencent_im_sdk_plugin.dart';
+import 'package:tencent_im_sdk_plugin/models/v2_tim_value_callback.dart';
+import 'package:tencent_im_sdk_plugin/enum/log_level.dart';
+import 'package:tencent_im_sdk_plugin/manager/v2_tim_manager.dart';
+
+class TRTCCallingImpl extends TRTCCalling {
+  String logTag = "TRTCCallingImpl";
+  static TRTCCallingImpl? sInstance;
+
+  int timeOutCount = 30; //超时时间,默认30s
+
+  int codeErr = -1;
+
+  // 是否首次邀请
+  bool isOnCalling = false;
+  bool mIsInRoom = false;
+  int mEnterRoomTime = 0;
+  String mCurCallID = "";
+  int mCurRoomID = 0;
+  String mCurGroupId = ""; //当前群组通话的群组ID
+  String? mNickName;
+  String? mFaceUrl;
+
+  /*
+   * 当前邀请列表
+   * C2C通话时会记录自己邀请的用户
+   * IM群组通话时会同步群组内邀请的用户
+   * 当用户接听、拒绝、忙线、超时会从列表中移除该用户
+   */
+  List<String> mCurInvitedList = [];
+
+  List<dynamic> mCurCallList = [];
+
+  //当前语音通话中的远端用户
+  Set mCurRoomRemoteUserSet = new Set();
+  /*
+  * C2C通话的邀请人
+  * 例如A邀请B,B存储的mCurSponsorForMe为A
+  */
+  String mCurSponsorForMe = "";
+  //当前通话的类型
+  int? mCurCallType;
+
+  late int mSdkAppId;
+  late String mCurUserId;
+  late String mCurUserSig;
+  String? mRoomId;
+  String? mOwnerUserId;
+  bool mIsInitIMSDK = false;
+  bool mIsLogin = false;
+  String mRole = "audience"; //默认为观众,archor为主播
+  late V2TIMManager timManager;
+  late TRTCCloud mTRTCCloud;
+  late TXAudioEffectManager txAudioManager;
+  late TXDeviceManager txDeviceManager;
+  Set<VoiceListenerFunc> listeners = Set();
+
+  TRTCCallingImpl() {
+    //获取腾讯即时通信IM manager
+    timManager = TencentImSDKPlugin.v2TIMManager;
+    initTRTC();
+  }
+
+  initTRTC() async {
+    mTRTCCloud = (await TRTCCloud.sharedInstance())!;
+    txDeviceManager = mTRTCCloud.getDeviceManager();
+    txAudioManager = mTRTCCloud.getAudioEffectManager();
+  }
+
+  static sharedInstance() {
+    if (sInstance == null) {
+      sInstance = new TRTCCallingImpl();
+    }
+    return sInstance;
+  }
+
+  static void destroySharedInstance() {
+    if (sInstance != null) {
+      sInstance = null;
+    }
+    TRTCCloud.destroySharedInstance();
+  }
+
+  @override
+  void destroy() {
+    mTRTCCloud.stopLocalPreview();
+    mTRTCCloud.stopLocalAudio();
+    mTRTCCloud.exitRoom();
+  }
+
+  @override
+  void registerListener(VoiceListenerFunc func) {
+    if (listeners.isEmpty) {
+      //监听im事件
+      timManager
+          .getSignalingManager()
+          .addSignalingListener(listener: signalingListener());
+      //监听trtc事件
+      mTRTCCloud.registerListener(rtcListener);
+    }
+    listeners.add(func);
+  }
+
+  @override
+  void unRegisterListener(VoiceListenerFunc func) {
+    listeners.remove(func);
+    if (listeners.isEmpty) {
+      mTRTCCloud.unRegisterListener(rtcListener);
+      timManager
+          .getSignalingManager()
+          .removeSignalingListener(listener: signalingListener);
+    }
+  }
+
+  emitEvent(type, params) {
+    for (var item in listeners) {
+      item(type, params);
+    }
+  }
+
+  signalingListener() {
+    TRTCCallingDelegate type;
+    return new V2TimSignalingListener(
+      onInvitationCancelled: (inviteID, inviter, data) {
+        if (!_isCallingData(data)) {
+          return;
+        }
+        if (inviteID == mCurCallID) {
+          _stopCall();
+          emitEvent(TRTCCallingDelegate.onCallingCancel, {});
+        }
+      },
+      onInvitationTimeout: (inviteID, inviteeList) {
+        //邀请者
+        String curGroupCallId = _getGroupCallId(mCurUserId);
+        if (!_isEmpty(mCurCallID) && inviteID != mCurCallID) {
+          return;
+        } else if (_isEmpty(mCurCallID) && inviteID != curGroupCallId) {
+          return;
+        }
+        if (mCurSponsorForMe.isEmpty) {
+          for (var i = 0; i < inviteeList.length; i++) {
+            emitEvent(TRTCCallingDelegate.onNoResp, inviteeList[i]);
+            mCurInvitedList.remove(inviteeList[i]);
+          }
+        } else {
+          //被邀请者
+          if (inviteeList.contains(mCurUserId)) {
+            _stopCall();
+            emitEvent(TRTCCallingDelegate.onCallingTimeout, {});
+          }
+          mCurInvitedList.remove(inviteeList);
+        }
+        // 每次超时都需要判断当前是否需要结束通话
+        _preExitRoom(null);
+      },
+      onInviteeAccepted: (inviteID, invitee, data) {
+        if (!_isCallingData(data)) {
+          return;
+        }
+        mCurInvitedList.remove(invitee);
+      },
+      onInviteeRejected: (inviteID, invitee, data) {
+        if (!_isCallingData(data)) {
+          return;
+        }
+        String curGroupCallId = _getGroupCallId(invitee);
+        if (mCurCallID == inviteID || curGroupCallId == inviteID) {
+          try {
+            Map<String, dynamic>? customMap = jsonDecode(data);
+            mCurInvitedList.remove(invitee);
+            if (customMap != null && customMap.containsKey('line_busy')) {
+              emitEvent(TRTCCallingDelegate.onLineBusy, invitee);
+            } else {
+              emitEvent(TRTCCallingDelegate.onReject, invitee);
+            }
+            _preExitRoom(null);
+          } catch (e) {
+            print(logTag +
+                "=onInviteeRejected JsonSyntaxException:" +
+                e.toString());
+          }
+        }
+      },
+      onReceiveNewInvitation:
+          (inviteID, inviter, groupID, inviteeList, data) async {
+        if (!_isCallingData(data)) {
+          return;
+        }
+        try {
+          Map<String, dynamic>? customMap = jsonDecode(data);
+          if (customMap == null) {
+            print(logTag + "onReceiveNewInvitation extraMap is null, ignore");
+            return;
+          }
+          if (customMap.containsKey('call_type')) {
+            mCurCallType = customMap['call_type'];
+          }
+          if (customMap.containsKey('call_end')) {
+            _preExitRoom(null);
+            return;
+          }
+          if (customMap.containsKey('room_id')) {
+            mCurRoomID = customMap['room_id'];
+          }
+        } catch (e) {
+          print(logTag +
+              "=onReceiveNewInvitation JsonSyntaxException:" +
+              e.toString());
+        }
+        if (isOnCalling && inviteeList.contains(mCurUserId)) {
+          // 正在通话时,收到了一个邀请我的通话请求,需要告诉对方忙线
+          Map<String, dynamic> busyMap = _getCustomMap();
+          busyMap['line_busy'] = 'line_busy';
+          await timManager
+              .getSignalingManager()
+              .reject(inviteID: inviteID, data: jsonEncode(busyMap));
+          return;
+        }
+        // 与对方处在同一个群中,此时收到了邀请群中其他人通话的请求,界面上展示连接动画
+        if (!_isEmpty(groupID) && !_isEmpty(mCurGroupId)) {
+          mCurInvitedList.addAll(inviteeList);
+          TRTCCallingDelegate type =
+              TRTCCallingDelegate.onGroupCallInviteeListUpdate;
+          emitEvent(type, mCurInvitedList);
+        }
+        if (!inviteeList.contains(mCurUserId)) {
+          return;
+        }
+
+        mCurSponsorForMe = inviter;
+        mCurCallID = inviteID;
+        mCurGroupId = groupID;
+        type = TRTCCallingDelegate.onInvited;
+        emitEvent(type, {
+          'sponsor': inviter,
+          'userIds': inviteeList.remove(mCurUserId),
+          'isFromGroup': !_isEmpty(groupID),
+          'type': mCurCallType
+        });
+      },
+    );
+  }
+
+  rtcListener(rtcType, param) {
+    String typeStr = rtcType.toString();
+    TRTCCallingDelegate type;
+    typeStr = typeStr.replaceFirst("TRTCCloudListener.", "");
+    if (typeStr == "onEnterRoom") {
+      if (param < 0) {
+        _stopCall();
+      } else {
+        mIsInRoom = true;
+      }
+    } else if (typeStr == "onError") {
+      type = TRTCCallingDelegate.onError;
+      emitEvent(type, param);
+    } else if (typeStr == "onUserVoiceVolume") {
+      type = TRTCCallingDelegate.onUserVoiceVolume;
+      emitEvent(type, param);
+    } else if (typeStr == "onUserVideoAvailable") {
+      type = TRTCCallingDelegate.onUserVideoAvailable;
+      emitEvent(type, param);
+    } else if (typeStr == "onUserAudioAvailable") {
+      type = TRTCCallingDelegate.onUserAudioAvailable;
+      emitEvent(type, param);
+    } else if (typeStr == "onRemoteUserEnterRoom") {
+      mCurRoomRemoteUserSet.add(param);
+      // 只有单聊这个时间才是正确的,因为单聊只会有一个用户进群,群聊这个时间会被后面的人重置
+      mEnterRoomTime = DateTime.now().millisecondsSinceEpoch;
+      type = TRTCCallingDelegate.onUserEnter;
+      emitEvent(type, param);
+    } else if (typeStr == "onRemoteUserLeaveRoom") {
+      mCurRoomRemoteUserSet.remove(param['userId']);
+      mCurInvitedList.remove(param['userId']);
+      type = TRTCCallingDelegate.onUserLeave;
+      emitEvent(type, param['userId']);
+      _preExitRoom(param['userId']);
+    }
+  }
+
+  // 多人通话时,根据userId找到对应的通话id
+  _getGroupCallId(String userId) {
+    for (int i = 0; i < mCurCallList.length; i++) {
+      if (mCurCallList[i]['userId'] == userId) {
+        return mCurCallList[i]['callId'];
+      }
+    }
+    return '';
+  }
+
+  /*
+  * 重要:用于判断是否需要结束本次通话
+  * 在用户超时、拒绝、忙线、有人退出房间时需要进行判断
+  */
+  _preExitRoom(String? leaveUser) {
+    if (mCurRoomRemoteUserSet.isEmpty && mCurInvitedList.isEmpty && mIsInRoom) {
+      // 当没有其他用户在房间里了,则结束通话。
+      if (!_isEmpty(leaveUser)) {
+        Map<String, dynamic> customMap = _getCustomMap();
+        //customMap['call_end'] = 'call_end';
+        customMap['call_end'] = 10;
+        if (_isEmpty(mCurGroupId)) {
+          timManager
+              .getSignalingManager()
+              .invite(invitee: leaveUser!, data: jsonEncode(customMap));
+        } else {
+          timManager.getSignalingManager().inviteInGroup(
+              groupID: mCurGroupId,
+              inviteeList: mCurInvitedList,
+              data: jsonEncode(customMap));
+        }
+      }
+      _exitRoom();
+      _stopCall();
+      emitEvent(TRTCCallingDelegate.onCallEnd, {});
+    }
+  }
+
+  @override
+  Future<ActionCallback> login(
+      int sdkAppId, String userId, String userSig) async {
+    mSdkAppId = sdkAppId;
+    mCurUserId = userId;
+    mCurUserSig = userSig;
+
+    if (!mIsInitIMSDK) {
+      //初始化SDK
+      V2TimValueCallback<bool> initRes = await timManager.initSDK(
+          sdkAppID: sdkAppId, //填入在控制台上申请的sdkappid
+          loglevel: LogLevel.V2TIM_LOG_ERROR,
+          listener: new V2TimSDKListener(onKickedOffline: () {
+            TRTCCallingDelegate type = TRTCCallingDelegate.onKickedOffline;
+            emitEvent(type, {});
+          }));
+      if (initRes.code != 0) {
+        //初始化sdk错误
+        return ActionCallback(code: 0, desc: 'init im sdk error');
+      }
+    }
+    mIsInitIMSDK = true;
+
+    // 登陆到 IM
+    String? loginedUserId = (await timManager.getLoginUser()).data;
+
+    if (loginedUserId != null && loginedUserId == userId) {
+      mIsLogin = true;
+      return ActionCallback(code: 0, desc: 'login im success');
+    }
+    V2TimCallback loginRes =
+        await timManager.login(userID: userId, userSig: userSig);
+    if (loginRes.code == 0) {
+      mIsLogin = true;
+      return ActionCallback(code: 0, desc: 'login im success');
+    } else {
+      return ActionCallback(code: codeErr, desc: loginRes.desc);
+    }
+  }
+
+  @override
+  Future<ActionCallback> logout() async {
+    V2TimCallback loginRes = await timManager.logout();
+    _stopCall();
+    _exitRoom();
+    mNickName = "";
+    mFaceUrl = "";
+    return ActionCallback(code: loginRes.code, desc: loginRes.desc);
+  }
+
+  @override
+  Future<ActionCallback> call(String userId, int type) async {
+    if (!isOnCalling) {
+      // 首次拨打电话,生成id,并进入trtc房间
+      mCurRoomID = _generateRoomID();
+      mCurCallType = type;
+      _enterTRTCRoom();
+    }
+    mCurInvitedList.add(userId);
+
+    V2TimValueCallback res = await timManager.getSignalingManager().invite(
+        invitee: userId,
+        data: jsonEncode(_getCustomMap()),
+        timeout: timeOutCount,
+        onlineUserOnly: false);
+    mCurCallID = res.data;
+    mCurCallList.add({'userId': userId, 'callId': mCurCallID});
+    return ActionCallback(code: res.code, desc: res.desc);
+  }
+
+  _isCallingData(String data) {
+    try {
+      Map<String, dynamic> customMap = jsonDecode(data);
+      if (customMap.containsKey('call_type')) {
+        return true;
+      }
+    } catch (e) {
+      print("isCallingData json parse error");
+      return false;
+    }
+    return false;
+  }
+
+  _getCustomMap() {
+    Map<String, dynamic> customMap = new Map<String, dynamic>();
+    customMap['version'] = 1;
+    customMap['call_type'] = mCurCallType;
+    customMap['room_id'] = mCurRoomID;
+    return customMap;
+  }
+
+  /*
+  * trtc 进房
+  */
+  _enterTRTCRoom() {
+    isOnCalling = true;
+    if (mCurCallType == TRTCCalling.typeVideoCall) {
+      // 开启基础美颜
+      TXBeautyManager txBeautyManager = mTRTCCloud.getBeautyManager();
+      // 自然美颜
+      txBeautyManager.setBeautyStyle(1);
+      txBeautyManager.setBeautyLevel(6);
+      // 进房前需要设置一下关键参数
+      TRTCVideoEncParam encParam = new TRTCVideoEncParam();
+      encParam.videoResolution = TRTCCloudDef.TRTC_VIDEO_RESOLUTION_960_540;
+      encParam.videoFps = 15;
+      encParam.videoBitrate = 1000;
+      encParam.videoResolutionMode =
+          TRTCCloudDef.TRTC_VIDEO_RESOLUTION_MODE_PORTRAIT;
+      encParam.enableAdjustRes = true;
+      mTRTCCloud.setVideoEncoderParam(encParam);
+    }
+
+    mTRTCCloud.enableAudioVolumeEvaluation(500);
+    txDeviceManager.setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_SPEAKER);
+    mTRTCCloud.muteLocalAudio(false);
+    mTRTCCloud.startLocalAudio(TRTCCloudDef.TRTC_AUDIO_QUALITY_DEFAULT);
+    mTRTCCloud.enterRoom(
+        TRTCParams(
+            sdkAppId: mSdkAppId,
+            userId: mCurUserId,
+            userSig: mCurUserSig,
+            roomId: mCurRoomID,
+            role: TRTCCloudDef.TRTCRoleAnchor),
+        mCurCallType == TRTCCalling.typeVideoCall
+            ? TRTCCloudDef.TRTC_APP_SCENE_VIDEOCALL
+            : TRTCCloudDef.TRTC_APP_SCENE_AUDIOCALL);
+  }
+
+  @override
+  Future<ActionCallback> groupCall(
+      List<String> userIdList, int type, String? groupId) async {
+    if (_isListEmpty(userIdList)) {
+      return ActionCallback(code: codeErr, desc: 'userIdList is empty');
+    }
+    if (!isOnCalling) {
+      // 首次拨打电话,生成id,并进入trtc房间
+      mCurRoomID = _generateRoomID();
+      mCurCallType = type;
+      _enterTRTCRoom();
+    }
+
+    // 过滤已经邀请的用户id
+    List<String> filterInvitedList = [];
+    for (var i = 0; i < userIdList.length; i++) {
+      if (!mCurInvitedList.contains(userIdList[i])) {
+        filterInvitedList.add(userIdList[i]);
+      }
+    }
+    if (_isListEmpty(filterInvitedList)) {
+      return ActionCallback(
+          code: codeErr, desc: 'the userIdList has been invited');
+    }
+    mCurInvitedList = filterInvitedList;
+    if (_isEmpty(groupId)) {
+      for (int i = 0; i < mCurInvitedList.length; i++) {
+        V2TimValueCallback res = await timManager.getSignalingManager().invite(
+            invitee: mCurInvitedList[i],
+            data: jsonEncode(_getCustomMap()),
+            timeout: timeOutCount,
+            onlineUserOnly: false);
+        mCurCallList.add({'userId': mCurInvitedList[i], 'callId': res.data});
+      }
+      return ActionCallback(code: 0, desc: '');
+    } else {
+      V2TimValueCallback res = await timManager
+          .getSignalingManager()
+          .inviteInGroup(
+              groupID: groupId!,
+              inviteeList: mCurInvitedList,
+              data: jsonEncode(_getCustomMap()),
+              timeout: timeOutCount,
+              onlineUserOnly: false);
+      mCurCallID = res.data;
+      return ActionCallback(code: res.code, desc: res.desc);
+    }
+  }
+
+  _isListEmpty(List? list) {
+    return list == null || list.length == 0;
+  }
+
+  _isEmpty(String? data) {
+    return data == null || data == "";
+  }
+
+  /*
+  * 停止此次通话,把所有的变量都会重置
+  */
+  _stopCall() {
+    isOnCalling = false;
+    mIsInRoom = false;
+    mEnterRoomTime = 0;
+    mCurCallID = "";
+    mCurRoomID = 0;
+    mCurInvitedList = [];
+    mCurCallList = [];
+    mCurRoomRemoteUserSet.clear();
+    mCurSponsorForMe = "";
+    mCurGroupId = "";
+    mCurCallType = TRTCCalling.typeUnknow;
+  }
+
+  @override
+  Future<ActionCallback> accept() async {
+    _enterTRTCRoom();
+    V2TimCallback res = await timManager
+        .getSignalingManager()
+        .accept(inviteID: mCurCallID, data: jsonEncode(_getCustomMap()));
+    return ActionCallback(code: res.code, desc: res.desc);
+  }
+
+  @override
+  Future<void> closeCamera() async {
+    return mTRTCCloud.stopLocalPreview();
+  }
+
+  @override
+  Future<void> hangup() async {
+    if (!isOnCalling) {
+      await reject();
+      return;
+    }
+    _exitRoom();
+    if (_isEmpty(mCurGroupId)) {
+      for (int i = 0; i < mCurInvitedList.length; i++) {
+        await timManager.getSignalingManager().cancel(
+            inviteID: _getGroupCallId(mCurInvitedList[i]),
+            data: jsonEncode(_getCustomMap()));
+      }
+    } else {
+      if (mCurRoomRemoteUserSet.isEmpty) {
+        await timManager.getSignalingManager().cancel(
+            inviteID: _getGroupCallId(mCurCallID),
+            data: jsonEncode(_getCustomMap()));
+      }
+    }
+    _stopCall();
+  }
+
+  /*
+  * trtc 退房
+  */
+  _exitRoom() {
+    mTRTCCloud.stopLocalPreview();
+    mTRTCCloud.stopLocalAudio();
+    mTRTCCloud.exitRoom();
+  }
+
+  @override
+  Future<void> openCamera(bool isFrontCamera, int viewId) {
+    return mTRTCCloud.startLocalPreview(isFrontCamera, viewId);
+  }
+
+  @override
+  Future<void> updateLocalView(int viewId) {
+    return mTRTCCloud.updateLocalView(viewId);
+  }
+
+  @override
+  Future<ActionCallback> reject() async {
+    V2TimCallback res = await timManager
+        .getSignalingManager()
+        .reject(inviteID: mCurCallID, data: jsonEncode(_getCustomMap()));
+    _stopCall();
+    return ActionCallback(code: res.code, desc: res.desc);
+  }
+
+  @override
+  Future<void> setHandsFree(bool isHandsFree) {
+    if (isHandsFree) {
+      return txDeviceManager
+          .setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_SPEAKER);
+    } else {
+      return txDeviceManager
+          .setAudioRoute(TRTCCloudDef.TRTC_AUDIO_ROUTE_EARPIECE);
+    }
+  }
+
+  @override
+  Future<void> setMicMute(bool isMute) {
+    return mTRTCCloud.muteLocalAudio(isMute);
+  }
+
+  @override
+  Future<void> startRemoteView(String userId, int streamType, int viewId) {
+    return mTRTCCloud.startRemoteView(userId, streamType, viewId);
+  }
+
+  @override
+  Future<void> stopRemoteView(String userId, int streamType) {
+    return mTRTCCloud.stopRemoteView(userId, streamType);
+  }
+
+  @override
+  Future<void> updateRemoteView(String userId, int streamType, int viewId) {
+    return mTRTCCloud.updateRemoteView(viewId, streamType, userId);
+  }
+
+  @override
+  Future<void> switchCamera(bool isFrontCamera) {
+    return txDeviceManager.switchCamera(isFrontCamera);
+  }
+
+  _generateRoomID() {
+    Random rng = new Random();
+    //2147483647
+    String numStr = '';
+    for (var i = 0; i < 9; i++) {
+      numStr += rng.nextInt(9).toString();
+    }
+    return int.tryParse(numStr);
+  }
+}
+
+/// @nodoc
+typedef VoiceListenerFunc<P> = void Function(
+    TRTCCallingDelegate type, P params);

+ 306 - 0
lib/common/trtc/calling/ui/TRTCCallingContact.dart

@@ -0,0 +1,306 @@
+import 'package:ctjt_flutter/common/trtc/GenerateTestUserSig.dart';
+import 'package:ctjt_flutter/common/trtc/ProfileManager_Mock.dart';
+import 'package:ctjt_flutter/common/trtc/TxUtils.dart';
+import 'package:ctjt_flutter/common/trtc/calling/model/TRTCCalling.dart';
+import 'package:ctjt_flutter/common/trtc/calling/model/TRTCCallingDelegate.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'base/CallTypes.dart';
+import 'base/CallingScenes.dart';
+
+class TRTCCallingContact extends StatefulWidget {
+  TRTCCallingContact(this.callingScenes, {Key? key}) : super(key: key);
+  final CallingScenes callingScenes;
+
+  @override
+  _TRTCCallingContactState createState() => _TRTCCallingContactState();
+}
+
+class _TRTCCallingContactState extends State<TRTCCallingContact> {
+  String searchText = '';
+  String myLoginInfoId = '';
+  List<UserModel> userList = [];
+  late ProfileManager _profileManager;
+  late TRTCCalling sInstance;
+  goIndex() {
+    Navigator.pushReplacementNamed(
+      context,
+      "/index",
+    );
+    return true;
+  }
+
+  goLoginPage() {
+    Navigator.pushReplacementNamed(
+      context,
+      "/login",
+    );
+    return true;
+  }
+
+  //搜索
+  onSearchClick() async {
+    List<UserModel> ls =
+        await ProfileManager.getInstance().queryUserInfo(searchText);
+
+    setState(() {
+      userList = ls;
+    });
+  }
+
+  //发起通话
+  onCallClick(UserModel userInfo) async {
+    if (userInfo.userId == myLoginInfoId) {
+      TxUtils.showErrorToast('不能呼叫自己', context);
+      return;
+    }
+    Navigator.pushReplacementNamed(
+      context,
+      "/calling/callingView",
+      arguments: {
+        "remoteUserInfo": userInfo,
+        "callType": CallTypes.Type_Call_Someone,
+        "callingScenes": widget.callingScenes
+      },
+    );
+  }
+
+  // 提示浮层
+  showToast(text) {
+    Fluttertoast.showToast(
+      msg: text,
+      toastLength: Toast.LENGTH_SHORT,
+      gravity: ToastGravity.CENTER,
+    );
+  }
+
+  initUserInfo() async {
+    _profileManager = await ProfileManager.getInstance();
+    sInstance = await TRTCCalling.sharedInstance();
+    String loginId = await TxUtils.getLoginUserId();
+    await sInstance.login(GenerateTestUserSig.sdkAppId, loginId,
+        await GenerateTestUserSig.genTestSig(loginId));
+    sInstance.unRegisterListener(onTrtcListener);
+    sInstance.registerListener(onTrtcListener);
+    if (loginId == '') {
+      TxUtils.showErrorToast("请先登录。", context);
+      goLoginPage();
+    } else {
+      setState(() {
+        myLoginInfoId = loginId;
+      });
+    }
+  }
+
+  onTrtcListener(type, params) async {
+    switch (type) {
+      case TRTCCallingDelegate.onInvited:
+        {
+          UserModel userInfo = await _profileManager
+              .querySingleUserInfo(params["sponsor"].toString());
+          Navigator.pushReplacementNamed(
+            context,
+            "/calling/callingView",
+            arguments: {
+              "remoteUserInfo": userInfo,
+              "callType": CallTypes.Type_Being_Called,
+              "callingScenes": params['type'] == TRTCCalling.typeVideoCall
+                  ? CallingScenes.VideoOneVOne
+                  : CallingScenes.AudioOneVOne
+            },
+          );
+        }
+        break;
+    }
+  }
+
+  @override
+  void initState() {
+    super.initState();
+    initUserInfo();
+  }
+
+  @override
+  void dispose() {
+    super.dispose();
+    sInstance.unRegisterListener(onTrtcListener);
+  }
+
+  getGuideSearchWidget() {
+    return Column(
+      mainAxisAlignment: MainAxisAlignment.center,
+      mainAxisSize: MainAxisSize.max,
+      children: [
+        Center(
+          child: Image.asset(
+            'assets/images/callingDemo/search.png',
+            height: 97,
+          ),
+        ),
+        Center(
+          child: Text('搜索添加已注册用户'),
+        ),
+        Center(
+          child: Text('以发起通话'),
+        ),
+      ],
+    );
+  }
+
+  getSearchResult() {
+    return CustomScrollView(
+      slivers: [
+        SliverFixedExtentList(
+          itemExtent: 55.0.h,
+          delegate: SliverChildBuilderDelegate(
+            (BuildContext context, int index) {
+              var userInfo = userList[index];
+              return Container(
+                alignment: Alignment.centerLeft,
+                margin: EdgeInsets.only(left: 20.w, right: 20.w),
+                child: Row(
+                  children: [
+                    Container(
+                      child: ClipRRect(
+                        borderRadius: BorderRadius.circular(44.r),
+                        child: Image.network(
+                          userInfo.avatar,
+                          height: 44.h,
+                          fit: BoxFit.fitHeight,
+                        ),
+                      ),
+                    ),
+                    Expanded(
+                      flex: 1,
+                      child: Padding(
+                        padding: EdgeInsets.fromLTRB(15.w, 0, 0, 0),
+                        child: Text(
+                          userInfo.name,
+                          style: TextStyle(
+                            color: Colors.black,
+                            fontSize: 16.sp,
+                          ),
+                        ),
+                      ),
+                    ),
+                    Container(
+                      // ignore: deprecated_member_use
+                      child: RaisedButton(
+                        color: Colors.green,
+                        onPressed: () {
+                          onCallClick(userInfo);
+                        },
+                        child: Text(
+                          '呼叫',
+                          style: TextStyle(fontSize: 16.0.sp, color: Colors.white),
+                        ),
+                        shape: RoundedRectangleBorder(
+                          side: BorderSide.none,
+                          borderRadius: BorderRadius.all(Radius.circular(20.r)),
+                        ),
+                      ),
+                    )
+                  ],
+                ),
+              );
+            },
+            childCount: userList.length,
+          ),
+        ),
+      ],
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    var searchBtn = Row(
+      children: [
+        Expanded(
+          flex: 1,
+          child: Container(
+            padding: EdgeInsets.only(left: 20.w, right: 20.w, bottom: 10.h),
+            decoration: BoxDecoration(
+              borderRadius: BorderRadius.all(
+                Radius.circular(19.0.r),
+              ),
+              color: Color.fromRGBO(244, 245, 249, 1.000),
+            ),
+            child: TextField(
+                style: TextStyle(color: Colors.black),
+                autofocus: true,
+                decoration: InputDecoration(
+                  hintText: "搜索用户ID",
+                  hintStyle:
+                      TextStyle(color: Color.fromRGBO(187, 187, 187, 1.000)),
+                  enabledBorder: UnderlineInputBorder(
+                    borderSide: BorderSide(color: Colors.white),
+                  ),
+                ),
+                keyboardType: TextInputType.number,
+                onChanged: (value) => this.searchText = value),
+          ),
+        ),
+        Container(
+          margin: EdgeInsets.only(right: 20.w),
+          // ignore: deprecated_member_use
+          child: RaisedButton(
+            color: Color.fromRGBO(0, 110, 255, 1.000),
+            shape: RoundedRectangleBorder(
+              side: BorderSide.none,
+              borderRadius: BorderRadius.all(Radius.circular(20.r)),
+            ),
+            onPressed: () {
+              onSearchClick();
+            },
+            child: Text(
+              '搜索',
+              style: TextStyle(color: Colors.white),
+            ),
+          ),
+        ),
+      ],
+    );
+    var myInfo = Row(
+      children: [
+        Container(
+          constraints: BoxConstraints(minHeight: 12.h, minWidth: 3.w),
+          margin: EdgeInsets.only(left: 20.w, right: 10.w),
+          color: Color.fromRGBO(153, 153, 153, 1.000),
+        ),
+        Text('您的用户ID是 $myLoginInfoId'),
+      ],
+    );
+    return Scaffold(
+      appBar: AppBar(
+        title: widget.callingScenes == CallingScenes.VideoOneVOne
+            ? Text('视频通话')
+            : Text('语音通话'),
+        leading: IconButton(
+          icon: Icon(Icons.arrow_back_ios), //color: Colors.black
+          onPressed: () async {
+            goIndex();
+          },
+        ),
+        centerTitle: true,
+        elevation: 0,
+      ),
+      body: WillPopScope(
+        onWillPop: () async {
+          return goIndex();
+        },
+        child: Column(
+          children: [
+            searchBtn,
+            myInfo,
+            Expanded(
+              flex: 1,
+              child: getSearchResult(), //getGuideSearchWidget(),
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 640 - 0
lib/common/trtc/calling/ui/VideoCall/TRTCCallingVideo.dart

@@ -0,0 +1,640 @@
+import 'dart:io';
+
+import 'package:ctjt_flutter/common/trtc/ProfileManager_Mock.dart';
+import 'package:ctjt_flutter/common/trtc/TxUtils.dart';
+import 'package:ctjt_flutter/common/trtc/calling/model/TRTCCalling.dart';
+import 'package:ctjt_flutter/common/trtc/calling/model/TRTCCallingDelegate.dart';
+import 'package:ctjt_flutter/common/trtc/calling/ui/base/CallTypes.dart';
+import 'package:ctjt_flutter/common/trtc/calling/ui/base/CallingScenes.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud_def.dart';
+import 'package:tencent_trtc_cloud/trtc_cloud_video_view.dart';
+import 'dart:async';
+import '../base/ExtendButton.dart';
+import '../base/CallStatus.dart';
+
+class TRTCCallingVideo extends StatefulWidget {
+  @override
+  _TRTCCallingVideoState createState() => _TRTCCallingVideoState();
+}
+
+class _TRTCCallingVideoState extends State<TRTCCallingVideo> {
+  CallStatus _currentCallStatus = CallStatus.calling;
+  CallTypes _currentCallType = CallTypes.Type_Call_Someone;
+  CallingScenes _callingScenes = CallingScenes.AudioOneVOne;
+  //已经通话时长
+  String _hadCallingTime = "00:00";
+  late DateTime _startAnswerTime;
+  bool _isCameraOff = false;
+  bool _isHandsFree = true;
+  bool _isMicrophoneOff = false;
+  bool _isFrontCamera = true;
+  int _bigVideoViewId = -1;
+  Timer? _hadCalledCalcTimer;
+
+  late int _smallVideoViewId;
+  double _smallViewTop = 64.h;
+  double _smallViewRight = 20.w;
+  //为false的时候,在已接听状态的时候。小画面显示本地视频,大画面显示远端视频。
+  bool isChangeBigSmallVideo = false;
+  UserModel? _remoteUserInfo;
+  //远端画面可见不可见
+  bool _remoteUserAvailable = true;
+
+  late TRTCCalling _tRTCCallingService;
+
+  @override
+  void initState() {
+    super.initState();
+
+    Future.delayed(Duration.zero, () {
+      this.initRemoteInfo();
+    });
+    initTrtc();
+  }
+
+  initTrtc() async {
+    _tRTCCallingService = await TRTCCalling.sharedInstance();
+    _tRTCCallingService.registerListener(onRtcListener);
+  }
+
+  onRtcListener(type, params) {
+    switch (type) {
+      case TRTCCallingDelegate.onError:
+        showMessageTips("发生错误:" + params['errCode'] + "," + params['errMsg'],
+            stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onWarning:
+        print('onWarning:warning code = ' +
+            params['warningCode'] +
+            " ,warning msg = " +
+            params['warningMsg']);
+        break;
+      case TRTCCallingDelegate.onUserEnter:
+        handleOnUserAnswer();
+        break;
+      case TRTCCallingDelegate.onUserLeave:
+        showMessageTips("用户离开了", stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onReject:
+        showMessageTips("拒绝通话", stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onNoResp:
+        showMessageTips("无响应", stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onLineBusy:
+        showMessageTips("忙线", stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onCallingCancel:
+        showMessageTips("取消了通话", stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onCallingTimeout:
+        showMessageTips("本次通话超时未应答", stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onCallEnd:
+        showMessageTips("结束通话", stopCameraAndFinish);
+        break;
+      case TRTCCallingDelegate.onUserVideoAvailable:
+        handleOnUserVideoAvailable(params);
+        break;
+      case TRTCCallingDelegate.onKickedOffline:
+        showMessageTips("你被踢下线了", stopCameraAndFinish);
+        break;
+    }
+  }
+
+  initRemoteInfo() async {
+    Map arguments = ModalRoute.of(context)!.settings.arguments! as Map;
+    safeSetState(() {
+      _remoteUserInfo = arguments['remoteUserInfo'] as UserModel;
+      _currentCallType = arguments["callType"] as CallTypes;
+      _callingScenes = arguments['callingScenes'] as CallingScenes;
+      Future.delayed(Duration(microseconds: 100), () {
+        if (_currentCallType == CallTypes.Type_Call_Someone) {
+          _tRTCCallingService.call(
+              _remoteUserInfo!.userId,
+              _callingScenes == CallingScenes.VideoOneVOne
+                  ? TRTCCalling.typeVideoCall
+                  : TRTCCalling.typeAudioCall);
+        }
+      });
+    });
+  }
+
+  //用户接听
+  handleOnUserAnswer() async {
+    if (_remoteUserInfo != null) {
+      _startAnswerTime = DateTime.now();
+      safeSetState(() async {
+        _currentCallStatus = CallStatus.answer;
+        _hadCallingTime = "00:00";
+        if (_bigVideoViewId != -1) {
+          await _tRTCCallingService.startRemoteView(
+            _remoteUserInfo!.userId,
+            TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SMALL,
+            _bigVideoViewId,
+          );
+        }
+      });
+      this._callIngTimeUpdate();
+    }
+  }
+
+  handleOnUserVideoAvailable(params) async {
+    if (_remoteUserInfo != null &&
+        params["userId"].toString() == _remoteUserInfo!.userId) {
+      safeSetState(() {
+        _remoteUserAvailable = params["available"] as bool;
+      });
+    }
+  }
+
+  showMessageTips(String msg, Function callback) {
+    TxUtils.showErrorToast(msg, context);
+    Future.delayed(Duration(seconds: 1), () {
+      callback();
+    });
+  }
+
+  stopCameraAndFinish() {
+    _tRTCCallingService.setMicMute(true);
+    _tRTCCallingService.closeCamera();
+    Future.delayed(Duration(seconds: 1), () {
+      if (mounted) {
+        Navigator.pushReplacementNamed(
+          context,
+          "/index",
+        );
+      }
+    });
+  }
+
+  String _twoDigits(int n) {
+    if (n >= 10) return "$n";
+    return "0$n";
+  }
+
+  _getDurationTimeString(Duration duration) {
+    String line = "";
+    if (duration.inHours != 0) {
+      line = _twoDigits(duration.inHours.remainder(24)) + ":";
+    }
+    line = line + _twoDigits(duration.inMinutes.remainder(60)) + ":";
+    line = line + _twoDigits(duration.inSeconds.remainder(60));
+    return line;
+  }
+
+  _callIngTimeUpdate() {
+    _hadCalledCalcTimer = Timer.periodic(Duration(seconds: 1), (Timer timer) {
+      DateTime now = DateTime.now();
+      Duration duration = now.difference(_startAnswerTime);
+      safeSetState(() {
+        _hadCallingTime = _getDurationTimeString(duration);
+      });
+    });
+  }
+
+  double _getOpacityByVis(bool vis) {
+    return vis ? 1.0 : 0;
+  }
+
+  safeSetState(callBack) {
+    setState(() {
+      if (mounted) {
+        callBack();
+      }
+    });
+  }
+
+  @override
+  dispose() {
+    if (_hadCalledCalcTimer != null) {
+      _hadCalledCalcTimer!.cancel();
+    }
+    _tRTCCallingService.unRegisterListener(onRtcListener);
+    super.dispose();
+  }
+
+  //前后摄像头切换
+  onSwitchCamera() {
+    _tRTCCallingService.switchCamera(!_isFrontCamera);
+    safeSetState(() {
+      _isFrontCamera = !_isFrontCamera;
+    });
+  }
+
+  //麦克风启用禁用
+  onMicrophoneTap() {
+    _tRTCCallingService.setMicMute(!_isMicrophoneOff);
+    setState(() {
+      _isMicrophoneOff = !_isMicrophoneOff;
+    });
+  }
+
+  //摄像头启用禁用
+  onCameraTap() async {
+    if (!_isCameraOff) {
+      await _tRTCCallingService.closeCamera();
+    } else {
+      //为false的时候,在已接听状态的时候。小画面显示本地视频,大画面显示远端视频。
+      if (isChangeBigSmallVideo) {
+        await _tRTCCallingService.openCamera(_isFrontCamera, _bigVideoViewId);
+      } else {
+        await _tRTCCallingService.openCamera(_isFrontCamera, _smallVideoViewId);
+      }
+    }
+    safeSetState(() {
+      _isCameraOff = !_isCameraOff;
+    });
+  }
+
+  //扬声器是否禁用
+  onHandsfreeTap() {
+    _tRTCCallingService.setHandsFree(!_isHandsFree);
+    setState(() {
+      _isHandsFree = !_isHandsFree;
+    });
+  }
+
+  onSwitchAudioTap() {
+    //先不支持切到语音通话
+    // _tRTCCallingService.closeCamera();
+    // safeSetState(() {
+    //   _callingScenes = CallingScenes.AudioOneVOne;
+    // });
+  }
+
+  //挂断
+  onHangUpCall() async {
+    _tRTCCallingService.closeCamera();
+    if (_currentCallType == CallTypes.Type_Being_Called &&
+        _currentCallStatus == CallStatus.calling) {
+      await _tRTCCallingService.reject();
+    } else {
+      await _tRTCCallingService.hangup();
+    }
+    Navigator.pushReplacementNamed(
+      context,
+      "/index",
+    );
+  }
+
+  //接听
+  onAcceptCall() async {
+    await _tRTCCallingService.accept();
+    safeSetState(() {
+      _currentCallStatus = CallStatus.answer;
+    });
+  }
+
+  getTopBarWidget() {
+    bool isCalling = _currentCallStatus == CallStatus.calling ? true : false;
+    var topWidget = Positioned(
+      left: 0,
+      top: _callingScenes == CallingScenes.VideoOneVOne ? 64.h : 185.h,
+      width: MediaQuery.of(context).size.width,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: isCalling
+            ? [
+                Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    Text(
+                      _remoteUserInfo != null ? _remoteUserInfo!.name : "--",
+                      style: TextStyle(
+                        fontSize: 24.sp,
+                        color: Colors.white,
+                        fontWeight: FontWeight.bold,
+                      ),
+                    ),
+                  ],
+                ),
+                Row(
+                  mainAxisAlignment: MainAxisAlignment.center,
+                  children: [
+                    Text(
+                      '正在等待对方接受邀请…',
+                      style: TextStyle(fontSize: 12.sp, color: Colors.white),
+                    ),
+                  ],
+                )
+              ]
+            : _callingScenes == CallingScenes.VideoOneVOne
+                ? [
+                    Row(
+                      mainAxisAlignment: MainAxisAlignment.start,
+                      children: [
+                        Container(
+                          margin: EdgeInsets.only(
+                            left: 20.w,
+                          ),
+                          decoration: BoxDecoration(),
+                          child: InkWell(
+                            onTap: () {
+                              onSwitchCamera();
+                            },
+                            child: Image.asset(
+                              'assets/images/callingDemo/switch-camera.png',
+                              height: 32.h,
+                              color: Color.fromRGBO(125, 123, 123, 1.0),
+                            ),
+                          ),
+                        )
+                      ],
+                    )
+                  ]
+                : [
+                    //1V1语音通话显示名字
+                    Row(
+                      mainAxisAlignment: MainAxisAlignment.center,
+                      children: [
+                        Text(
+                          _remoteUserInfo != null
+                              ? _remoteUserInfo!.name
+                              : "--",
+                          style: TextStyle(
+                            fontSize: 24.sp,
+                            color: Colors.white,
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                      ],
+                    ),
+                  ],
+      ),
+    );
+    return topWidget;
+  }
+
+  getButtomWidget() {
+    var callSomeBtnList = [
+      _currentCallStatus == CallStatus.answer
+          ? ExtendButton(
+              imgUrl: _isMicrophoneOff
+                  ? "assets/images/callingDemo/microphone-off.png"
+                  : "assets/images/callingDemo/microphone-on.png",
+              tips: "麦克风",
+              onTap: () {
+                onMicrophoneTap();
+              },
+            )
+          : SizedBox(
+              height: 0,
+              width: 0,
+            ),
+      ExtendButton(
+        imgUrl: "assets/images/callingDemo/hangup.png",
+        tips: "挂断",
+        onTap: () {
+          onHangUpCall();
+        },
+      ),
+      _currentCallStatus == CallStatus.answer
+          ? ExtendButton(
+              imgUrl: _callingScenes == CallingScenes.VideoOneVOne
+                  ? _isCameraOff
+                      ? "assets/images/callingDemo/camera-off.png"
+                      : "assets/images/callingDemo/camera-on.png"
+                  : _isHandsFree
+                      ? "assets/images/callingDemo/trtccalling_ic_handsfree_enable.png"
+                      : "assets/images/callingDemo/trtccalling_ic_handsfree_disable.png",
+              tips:
+                  _callingScenes == CallingScenes.VideoOneVOne ? "摄像头" : "扬声器",
+              onTap: () {
+                if (_callingScenes == CallingScenes.VideoOneVOne)
+                  onCameraTap();
+                else
+                  onHandsfreeTap();
+              },
+            )
+          : SizedBox(
+              height: 0,
+              width: 0,
+            ),
+    ];
+    if (_currentCallType == CallTypes.Type_Being_Called &&
+        _currentCallStatus == CallStatus.calling) {
+      callSomeBtnList.insert(
+        2,
+        SizedBox(
+          height: 0,
+          width: 0,
+        ),
+      );
+      callSomeBtnList.insert(
+        3,
+        ExtendButton(
+          imgUrl: "assets/images/callingDemo/trtccalling_ic_dialing.png",
+          tips: "接听",
+          onTap: () {
+            onAcceptCall();
+          },
+        ),
+      );
+    }
+    var buttomWidget = Positioned(
+      left: 0,
+      bottom: 50.h,
+      width: MediaQuery.of(context).size.width,
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Container(
+            margin: EdgeInsets.only(bottom: 20.h),
+            child: _currentCallStatus == CallStatus.answer
+                ? Text(
+                    '$_hadCallingTime',
+                    style: TextStyle(color: Colors.white),
+                  )
+                :
+                // _callingScenes == CallingScenes.VideoOneVOne
+                //     ? ExtendButton(
+                //         imgUrl: "assets/images/callingDemo/switchToAudio.png",
+                //         imgHieght: 18,
+                //         imgColor: Color.fromRGBO(125, 123, 123, 1.0),
+                //         tips: "切到语音通话",
+                //         onTap: () {
+                //           onSwitchAudioTap();
+                //         },
+                //       ):
+                SizedBox(
+                    height: 0,
+                    width: 0,
+                  ),
+          ),
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+            children: callSomeBtnList,
+          )
+        ],
+      ),
+    );
+    return buttomWidget;
+  }
+
+  getBigVideo() {
+    if (_callingScenes == CallingScenes.AudioOneVOne) return Container();
+    bool nowIsLocalView = true; //判断当前大窗口是否显示本地摄像头
+    if (_currentCallStatus == CallStatus.calling)
+      nowIsLocalView = true;
+    else {
+      //已经接听
+      if (isChangeBigSmallVideo) {
+        nowIsLocalView = true;
+      } else {
+        nowIsLocalView = false; //远端画面
+      }
+    }
+    var opacityVal = nowIsLocalView
+        ? _getOpacityByVis(!_isCameraOff)
+        : _getOpacityByVis(_remoteUserAvailable);
+    return _callingScenes == CallingScenes.VideoOneVOne
+        ? AnimatedOpacity(
+            duration: Duration(milliseconds: 100),
+            opacity: opacityVal,
+            child: TRTCCloudVideoView(
+              key: ValueKey("_bigVideoViewId"),
+              viewType: TRTCCloudDef.TRTC_VideoView_SurfaceView,
+              onViewCreated: (viewId) async {
+                _bigVideoViewId = viewId;
+                if (_callingScenes == CallingScenes.VideoOneVOne) {
+                  await _tRTCCallingService.openCamera(
+                      _isFrontCamera, _bigVideoViewId);
+                }
+              },
+            ),
+          )
+        : Container();
+  }
+
+  getSmallVideoContainer() {
+    if (_callingScenes == CallingScenes.AudioOneVOne) {
+      return Container(
+        height: 100.h,
+        width: 100.w,
+        child: Container(),
+        decoration: _remoteUserInfo != null
+            ? BoxDecoration(
+                image: DecorationImage(
+                  image: NetworkImage(_remoteUserInfo!.avatar),
+                  fit: BoxFit.cover,
+                ),
+              )
+            : BoxDecoration(),
+      );
+    }
+    bool nowIsRemoteView = false; //判断当前小窗口是否显示远端画面
+    if (_currentCallStatus == CallStatus.calling)
+      nowIsRemoteView = true;
+    else {
+      //已经接听
+      if (isChangeBigSmallVideo) {
+        nowIsRemoteView = true;
+      } else {
+        nowIsRemoteView = false; //本地摄像头
+      }
+    }
+    return Container(
+      height: _currentCallStatus == CallStatus.calling ? 100.h : 216.h,
+      width: 100.w,
+      child: _currentCallStatus == CallStatus.answer
+          ? AnimatedOpacity(
+              duration: Duration(milliseconds: 100),
+              opacity: nowIsRemoteView
+                  ? _getOpacityByVis(_remoteUserAvailable)
+                  : _getOpacityByVis(!_isCameraOff),
+              child: TRTCCloudVideoView(
+                key: ValueKey("_smallVideoViewId"),
+                viewType: TRTCCloudDef.TRTC_VideoView_SurfaceView,
+                onViewCreated: (viewId) async {
+                  _smallVideoViewId = viewId;
+                  if (Platform.isIOS) {
+                    await _tRTCCallingService
+                        .updateLocalView(_smallVideoViewId);
+                  } else {
+                    await _tRTCCallingService.openCamera(
+                        _isFrontCamera, _smallVideoViewId);
+                  }
+                },
+              ),
+            )
+          : Container(),
+      decoration:
+          _remoteUserInfo != null && _currentCallStatus == CallStatus.calling
+              ? BoxDecoration(
+                  image: DecorationImage(
+                    image: NetworkImage(_remoteUserInfo!.avatar),
+                    fit: BoxFit.cover,
+                  ),
+                )
+              : BoxDecoration(),
+    );
+  }
+
+  changeVideoView() {
+    if (_callingScenes == CallingScenes.AudioOneVOne ||
+        _currentCallStatus == CallStatus.calling) return;
+
+    setState(() async {
+      isChangeBigSmallVideo = !isChangeBigSmallVideo;
+      //为false的时候,在已接听状态的时候。小画面显示本地视频,大画面显示远端视频。
+      if (isChangeBigSmallVideo) {
+        await _tRTCCallingService.updateLocalView(_bigVideoViewId);
+        await _tRTCCallingService.updateRemoteView(_remoteUserInfo!.userId,
+            TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SMALL, _smallVideoViewId);
+      } else {
+        await _tRTCCallingService.updateLocalView(_smallVideoViewId);
+        await _tRTCCallingService.updateRemoteView(_remoteUserInfo!.userId,
+            TRTCCloudDef.TRTC_VIDEO_STREAM_TYPE_SMALL, _bigVideoViewId);
+      }
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    var remotePanel = Positioned(
+        top: _smallViewTop,
+        right: _callingScenes == CallingScenes.VideoOneVOne
+            ? _smallViewRight
+            : MediaQuery.of(context).size.width / 2 - 100 / 2,
+        child: GestureDetector(
+          onDoubleTap: () {
+            if (Platform.isIOS) changeVideoView();
+          },
+          onPanUpdate: (DragUpdateDetails e) {
+            //用户手指滑动时,更新偏移,重新构建
+            if (_callingScenes == CallingScenes.VideoOneVOne) {
+              safeSetState(() {
+                _smallViewRight -= e.delta.dx;
+                _smallViewTop += e.delta.dy;
+              });
+            }
+          },
+          child: getSmallVideoContainer(),
+        ));
+    return Scaffold(
+      body: WillPopScope(
+        onWillPop: () async {
+          return true;
+        },
+        child: Stack(
+          alignment: Alignment.topLeft,
+          fit: StackFit.expand,
+          children: [
+            Container(
+              color: Color.fromRGBO(
+                  93, 91, 90, 1), //Color.fromRGBO(242, 243, 248, 1),
+              child: getBigVideo(),
+            ),
+            remotePanel,
+            getTopBarWidget(),
+            getButtomWidget(),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 4 - 0
lib/common/trtc/calling/ui/base/CallStatus.dart

@@ -0,0 +1,4 @@
+enum CallStatus {
+  calling, //拨打中
+  answer, //已接听
+}

+ 4 - 0
lib/common/trtc/calling/ui/base/CallTypes.dart

@@ -0,0 +1,4 @@
+enum CallTypes {
+  Type_Call_Someone, //主动拨打某人
+  Type_Being_Called, //作为用户被叫
+}

+ 4 - 0
lib/common/trtc/calling/ui/base/CallingScenes.dart

@@ -0,0 +1,4 @@
+enum CallingScenes {
+  VideoOneVOne, //一对一视频通话
+  AudioOneVOne, //一对一语音通话
+}

+ 44 - 0
lib/common/trtc/calling/ui/base/ExtendButton.dart

@@ -0,0 +1,44 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+
+class ExtendButton extends StatelessWidget {
+  ExtendButton(
+      {this.imgUrl = "",
+      this.tips = "",
+      this.onTap,
+      this.imgHieght = 0,
+      this.imgColor,
+      Key? key})
+      : super(key: key);
+  final String imgUrl;
+  final double imgHieght;
+  final Color? imgColor;
+  final String tips;
+  final GestureTapCallback? onTap;
+
+  @override
+  Widget build(BuildContext context) {
+    return InkWell(
+      onTap: () {
+        this.onTap!();
+      },
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Image.asset(
+            imgUrl,
+            height: imgHieght > 0 ? this.imgHieght : 52.0,
+            color: imgColor != null ? imgColor : null,
+          ),
+          Container(
+            margin: EdgeInsets.only(top: 10),
+            child: Text(
+              tips,
+              style: TextStyle(fontSize: 12, color: Colors.white),
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 137 - 0
lib/common/utils.dart

@@ -0,0 +1,137 @@
+import 'package:permission_handler/permission_handler.dart';
+import 'package:vibration/vibration.dart';
+
+/// 通用工具类
+class Utils {
+  /// 静态常量定义区
+  static const int DefaultVibrateDuration = 70; // 默认震动时长
+  static const timeout = const Duration(seconds: 1);
+
+  static final RegExp ssidPattern = RegExp(r'^\"(.*)\"$');
+  static final RegExp cellNoPattern = RegExp(r'^1[3-9]\d{9}$');
+  static final RegExp numberPattern = RegExp('[0-9]');
+  static final RegExp namePattern = RegExp(r'[a-z,A-Z,0-9|\s]');
+  static final RegExp passwordPattern = RegExp(
+      '[a-z,A-Z,0-9|@|#|\$|%|&|-|+|(|)|.|,|\'|:|;|!|?|/|\||*|"|=|{|}|[|\\]|\\\\|_|`|~|\\^|<|\\>]');
+  static final RegExp versionPattern = RegExp(r'(\d+)\.*(\d*)\.*(\d*)');
+
+  /// 手机号码格式检查
+  static bool testCellNo(String value) {
+    if (value.isEmpty) {
+      return false;
+    }
+
+    return cellNoPattern.hasMatch(value);
+  }
+
+  /// 震动
+  static Future<Null> vibrate({duration = DefaultVibrateDuration}) async {
+    bool? hasVibrator = await Vibration.hasVibrator();
+    if (null != hasVibrator && !hasVibrator) {
+      return;
+    }
+
+    bool? hasAmplitudeControl = await Vibration.hasAmplitudeControl();
+    if (null != hasAmplitudeControl && !hasAmplitudeControl) {
+      Vibration.vibrate(duration: duration, amplitude: 128);
+    } else {
+      Vibration.vibrate(duration: duration);
+    }
+  }
+
+  /// 权限申请
+  static Future<bool> checkPermission(Permission permission,
+      {bool init = false}) async {
+    var permissionName = permission.toString();
+
+    PermissionStatus permissionStatus = await permission.status;
+    if (permissionStatus.isGranted) {
+      print('$permissionName 已有权限');
+      return true;
+    } else {
+      bool isShow = await permission.shouldShowRequestRationale;
+      if (init || isShow) {
+        // 申请权限
+        Map<Permission, PermissionStatus> statuses =
+            await [permission].request();
+
+        // 申请结果
+        if (statuses[permission] == PermissionStatus.granted) {
+          print('$permissionName 权限申请成功');
+          return true;
+        } else {
+          print('$permissionName 权限申请被拒绝 ${statuses[permission]}');
+          return false;
+        }
+      } else {
+        print('$permissionName 权限已被禁用,请在设置中手动开启重新进入程序');
+        openAppSettings();
+        return false;
+      }
+    }
+  }
+}
+
+/// 版本比较工具类
+class Version {
+  String origin = ""; // 原版本字符串
+  int major = -1; // 主版本
+  int secondary = -1; // 次要版本
+  int revision = -1; // 修订号
+
+  Version(String origin) {
+    this.origin = "";
+    this.major = -1;
+    this.secondary = -1;
+    this.revision = -1;
+
+    if (origin.isEmpty) {
+      return;
+    }
+    origin = origin.trim();
+    this.origin = origin;
+
+    var m = Utils.versionPattern.firstMatch(origin);
+    if (null == m || m.groupCount < 2) {
+      return;
+    }
+
+    var str = m.group(1);
+    var v = int.tryParse(str!);
+    if (null != v) {
+      this.major = v;
+    }
+
+    if (m.groupCount > 1) {
+      str = m.group(2);
+      v = int.tryParse(str!);
+      if (null != v) {
+        this.secondary = v;
+      }
+    }
+
+    if (m.groupCount > 2) {
+      str = m.group(3);
+      v = int.tryParse(str!);
+      if (null != v) {
+        this.revision = v;
+      }
+    }
+  }
+
+  bool moreThan(Version obj) {
+    if (obj.major < 0 || this.major < 0) {
+      return false;
+    }
+
+    if (obj.major != this.major) {
+      return this.major > obj.major;
+    }
+
+    if (obj.secondary != this.secondary) {
+      return this.secondary > obj.secondary;
+    }
+
+    return this.revision > obj.revision;
+  }
+}

+ 106 - 0
lib/main.dart

@@ -0,0 +1,106 @@
+import 'dart:io';
+
+import 'package:ctjt_flutter/common/states.dart';
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/common/trtc/calling/ui/TRTCCallingContact.dart';
+import 'package:ctjt_flutter/common/trtc/calling/ui/VideoCall/TRTCCallingVideo.dart';
+import 'package:ctjt_flutter/common/trtc/calling/ui/base/CallingScenes.dart';
+import 'package:ctjt_flutter/common/utils.dart';
+import 'package:ctjt_flutter/pages/about.dart';
+import 'package:ctjt_flutter/pages/home.dart';
+import 'package:ctjt_flutter/pages/remote_call.dart';
+import 'package:device_info/device_info.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:provider/provider.dart';
+
+// 注意,加入Bugly构建的时候需要使用命令指定目标架构,然后apk命令手工安装生成的包
+// 命令如:flutter build apk --release --target-platform android-arm64
+// void main() => FlutterBugly.postCatchedException(() async {
+//   runApp(MyApp());
+//   initScreen();
+//
+//   FlutterBugly.init(androidAppId: "xxxx", iOSAppId: "xxxx");
+// });
+
+void main() {
+  FlutterError.onError = (FlutterErrorDetails details) {
+    FlutterError.dumpErrorToConsole(details);
+  };
+  WidgetsFlutterBinding.ensureInitialized();
+
+  _initScreen().then((_) {
+    runApp(MyApp());
+  });
+}
+
+Future<void> _initScreen() async {
+  // 强制竖屏
+  await SystemChrome.setPreferredOrientations(
+      [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]);
+
+  var deviceInfo = DeviceInfoPlugin();
+
+  // 如果是安卓,设置状态栏背景色透明
+  if (Platform.isAndroid) {
+    SystemChrome.setSystemUIOverlayStyle(Styles.statusBarStyle);
+
+    var androidInfo = await deviceInfo.androidInfo;
+    print('Running on ${androidInfo.model}');
+  } else if (Platform.isIOS) {
+    var iosInfo = await deviceInfo.iosInfo;
+    print('Running on ${iosInfo.utsname.machine}');
+  }
+}
+
+class MyApp extends StatelessWidget {
+  @override
+  Widget build(BuildContext context) {
+    _requestPermissions(context); // 动态申请权限
+    return ScreenUtilInit( // 屏幕自适应
+        designSize: Size(1125, 2436), // 填入设计稿中设备的屏幕尺寸,单位dp
+        builder: () => MultiProvider( // 启动应用时需要初始化状态通知
+              providers: [
+                ChangeNotifierProvider(create: (_) => UserStatus()),
+                ChangeNotifierProvider(create: (_) => AppVersion()),
+                ChangeNotifierProvider(create: (_) => AppDownloadProcess()),
+              ],
+              child: MaterialApp(
+                title: 'Flutter项目原型',
+                debugShowCheckedModeBanner: false,
+                theme: ThemeData(
+                  primaryColor: Styles.primaryColor,
+                  //primarySwatch: Colors.blue,
+                  visualDensity: VisualDensity.adaptivePlatformDensity,
+                ),
+                builder: (context, widget) {
+                  return MediaQuery(
+                    // 设置文字大小不随系统设置改变
+                    data: MediaQuery.of(context).copyWith(textScaleFactor: 1.0),
+                    child: widget!,
+                  );
+                },
+                // 注册路由
+                home: HomePage(),
+                routes: {
+                  "/index": (context) => HomePage(),
+                  "/about": (context) => AboutPage(),
+                  "/remote_call": (context) => RemoteCallPage(),
+                  "/calling/audioContact": (context) =>
+                      TRTCCallingContact(CallingScenes.AudioOneVOne),
+                  "/calling/callingView": (context) => TRTCCallingVideo(),
+                },
+              ),
+            ));
+  }
+}
+
+// 动态申请权限
+Future<Null> _requestPermissions(BuildContext context) async {
+  await Utils.checkPermission(Permission.storage, init: true);
+  await Utils.checkPermission(Permission.microphone, init: true);
+  // await Utils.checkPermission(Permission.camera, init: true);
+  // await Utils.checkPermission(Permission.bluetooth, init: true);
+}

+ 80 - 0
lib/model/version_res.dart

@@ -0,0 +1,80 @@
+class VersionRes {
+  VersionResBody? body;
+  int? status;
+  String? msg;
+
+  VersionRes({this.body, this.status, this.msg});
+
+  VersionRes.fromJson(Map<String, dynamic> json) {
+    body = json['body'] != null ? new VersionResBody.fromJson(json['body']) : null;
+    status = json['status'];
+    msg = json['msg'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final Map<String, dynamic> data = new Map<String, dynamic>();
+    if (this.body != null) {
+      data['body'] = this.body!.toJson();
+    }
+    data['status'] = this.status;
+    data['msg'] = this.msg;
+    return data;
+  }
+}
+
+class VersionResBody {
+  String? id;
+  String? createdDate;
+  int? channelType;
+  String? url;
+  String? instructions;
+  String? md5;
+  String? targetVersion;
+  String? lastVersion;
+  int? isStrongUpdate;
+  String? lastTime;
+  int? status;
+
+  VersionResBody(
+      {this.id,
+        this.createdDate,
+        this.channelType,
+        this.url,
+        this.instructions,
+        this.md5,
+        this.targetVersion,
+        this.lastVersion,
+        this.isStrongUpdate,
+        this.lastTime,
+        this.status});
+
+  VersionResBody.fromJson(Map<String, dynamic> json) {
+    id = json['id'];
+    createdDate = json['createdDate'];
+    channelType = json['channelType'];
+    url = json['url'];
+    instructions = json['instructions'];
+    md5 = json['md5'];
+    targetVersion = json['targetVersion'];
+    lastVersion = json['lastVersion'];
+    isStrongUpdate = json['isStrongUpdate'];
+    lastTime = json['lastTime'];
+    status = json['status'];
+  }
+
+  Map<String, dynamic> toJson() {
+    final Map<String, dynamic> data = new Map<String, dynamic>();
+    data['id'] = this.id;
+    data['createdDate'] = this.createdDate;
+    data['channelType'] = this.channelType;
+    data['url'] = this.url;
+    data['instructions'] = this.instructions;
+    data['md5'] = this.md5;
+    data['targetVersion'] = this.targetVersion;
+    data['lastVersion'] = this.lastVersion;
+    data['isStrongUpdate'] = this.isStrongUpdate;
+    data['lastTime'] = this.lastTime;
+    data['status'] = this.status;
+    return data;
+  }
+}

+ 278 - 0
lib/pages/about.dart

@@ -0,0 +1,278 @@
+import 'dart:io';
+
+import 'package:ctjt_flutter/common/states.dart';
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/common/utils.dart';
+import 'package:ctjt_flutter/service/app_service.dart';
+import 'package:ctjt_flutter/widget/button.dart';
+import 'package:ctjt_flutter/widget/contact.dart';
+import 'package:ctjt_flutter/widget/title.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:provider/provider.dart';
+import 'package:r_upgrade/r_upgrade.dart';
+
+// 关于页(APP更新页)
+class AboutPage extends StatefulWidget {
+  AboutPage({Key? key}) : super(key: key);
+
+  @override
+  _AboutPageState createState() => _AboutPageState();
+}
+
+class _AboutPageState extends State<AboutPage> {
+  final _formKey = GlobalKey<FormState>();
+  late AppService _appService;
+  bool isLatest = false;
+
+  static const String LoadingTip = '正在获取...';
+  TextEditingController _latestVersionController =
+      TextEditingController(text: LoadingTip);
+  String _downloadUrl = "";
+  String _versionDescription = "";
+
+  @override
+  void initState() {
+    super.initState();
+    _appService = AppService.getInstance(this.context);
+    initVersion();
+  }
+
+  Future<Null> initVersion() async {
+    // 在common中初始化时,已获取当前APP版本并缓存,本地APP版本相对稳定,此处不再获取
+
+    // 从服务器获取手机APP新版本信息
+    var localVersion =
+        Provider.of<AppVersion>(context, listen: false).appVersion;
+    var resp = await _appService.fetchVersionInfo(localVersion);
+    if (null == resp ||
+        resp.status != 1 ||
+        (resp.targetVersion!.isNotEmpty &&
+            resp.targetVersion!.trim() != localVersion)) {
+      _latestVersionController.text = localVersion;
+      isLatest = true;
+      return;
+    }
+    setState(() {
+      _downloadUrl = resp.url!;
+      _versionDescription = resp.instructions!;
+
+      var lv = Version(localVersion);
+      var rv = Version(resp.lastVersion!);
+      if (rv.moreThan(lv)) {
+        _latestVersionController.text = '${lv.origin} -> ${rv.origin}';
+        isLatest = false;
+      } else {
+        _latestVersionController.text = lv.origin;
+        isLatest = true;
+      }
+    });
+  }
+
+  // 下载进度对话框
+  void _showDownloadDialog() {
+    showDialog(
+      // 下载对话框
+      context: context,
+      barrierDismissible: false, // 点击遮罩不关闭
+      builder: (context) {
+        return SimpleDialog(
+            title: Text('更新', style: TextStyle(color: Styles.primaryColor)),
+            children: <Widget>[
+              Offstage(
+                // 更新说明
+                offstage: _versionDescription.trim() != "",
+                child: Container(
+                    height: 600.h,
+                    child: SingleChildScrollView(
+                      child: Text(_versionDescription.trim()),
+                    )),
+              ),
+              Container(
+                // 进度条
+                height: 200.h,
+                alignment: Alignment.centerLeft,
+                padding: EdgeInsets.only(
+                    left: 60.w,
+                    right: 60.w),
+                child: Column(
+                  children: [
+                    Container(
+                      // 条状进度条
+                      margin: EdgeInsets.symmetric(
+                          horizontal: 50.w,
+                          vertical: 50.h),
+                      height: 25.h,
+                      child: LinearProgressIndicator(
+                        backgroundColor: Styles.primaryColor,
+                        value: Provider.of<AppDownloadProcess>(context)
+                            .process, //设置比例
+                        valueColor: AlwaysStoppedAnimation<Color>(Colors.black),
+                        // Navigator.of(mContext).pop(); // 下载完成关闭对话框
+                      ),
+                    ),
+                    Text('正在为您更新,请耐心等待。'),
+                  ],
+                ),
+              ),
+            ]);
+      },
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Form(
+      key: _formKey,
+      child: Scaffold(
+        backgroundColor: Colors.white,
+        appBar: PageHead(),
+        body: Container(
+          padding: Styles.primaryPadding,
+          child: Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: <Widget>[
+              // 标题
+              PageTitle(text: "关于"),
+              Padding(
+                padding: EdgeInsets.only(
+                    top: 75.h,
+                    bottom: 75.h),
+                child: Text('最新应用软件版本:', style: Styles.configDescStyle),
+              ),
+              TextFormField(
+                controller: _latestVersionController,
+                readOnly: true,
+                enabled: false,
+                decoration: InputDecoration(
+                  suffixIcon: isLatest
+                      ? Icon(Icons.done,
+                          color: Styles.primaryColor, size: Styles.iconSize)
+                      : Icon(Icons.rotate_right,
+                          color: Styles.primaryColor, size: Styles.iconSize),
+                ),
+              ),
+              Padding(
+                padding: EdgeInsets.only(top: 25.h),
+                child: Text(isLatest ? '您的APP是最新的' : '有新的APP版本,点击按钮立即更新',
+                    style: Styles.configDescStyle),
+              ),
+              // 更新按钮
+              Offstage(
+                offstage: isLatest,
+                child: BigSButton(
+                  text: '更新',
+                  onPressed: () async {
+                    if (Platform.isIOS) {
+                      _launchToAppStore();
+                    } else if (Platform.isAndroid) {
+                      var hasPermission =
+                          await Utils.checkPermission(Permission.storage);
+                      if (hasPermission) {
+                        _installApk(
+                            _downloadUrl, _latestVersionController.text);
+                        _showDownloadDialog();
+                      } else {
+                        Fluttertoast.showToast(
+                            msg: "安装更新需要存储权限,该权限未打开",
+                            toastLength: Toast.LENGTH_LONG,
+                            gravity: ToastGravity.CENTER,
+                            timeInSecForIosWeb: 3);
+                      }
+                    } else {
+                      print('A platform witch unsupported.' +
+                          Platform.operatingSystem +
+                          ' ' +
+                          Platform.operatingSystemVersion);
+                    }
+                  },
+                ),
+              ),
+              Offstage(
+                offstage: Provider.of<UserStatus>(context).userToken.isEmpty,
+                child: BigSButton(
+                  text: '退出登录',
+                  onPressed: () {
+                    States.reset(context);
+                  },
+                ),
+              ),
+              TButton(
+                child: Text("用户协议和隐私政策",
+                    style: TextStyle(color: Styles.linkColor)),
+                onPressed: () {
+                  Contact.showUserContactDialog(context);
+                },
+              ),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+
+  /// 跳转到App Store指定页,用于IOS
+  Future<Null> _launchToAppStore() async {
+    // 参数为APP Store中的应用ID,对应APP Store页面 https://apps.apple.com/cn/app/XXXX/id1234
+    RUpgrade.upgradeFromAppStore('id1234')
+        .then((result) {
+      print('install apk $result');
+    }).catchError((error) {
+      print('install apk error: $error');
+    });
+  }
+
+  /// 下载并安装APK,用于Android
+  Future<Null> _installApk(String url, String version) async {
+    Directory? storageDir = await getExternalStorageDirectory();
+    if (null == storageDir) {
+      print('can not get storage dir');
+      _showDownloadErrorDialog();
+      return;
+    }
+    String storagePath = storageDir.path;
+    String fileName = '$storagePath/lyiew_v$version.apk';
+
+    RUpgrade.stream.listen((DownloadInfo info) { // 下载进度监听,通过状态通知更新界面进度条
+      Provider.of<AppDownloadProcess>(context, listen: false).process =
+          info.percent!;
+    });
+    RUpgrade.upgrade(url, fileName: fileName, isAutoRequestInstall: true) // 下载并安装
+        .then((result) {
+      print('install apk $result');
+    }).catchError((error) {
+      print('install apk error: $error');
+    });
+  }
+
+  // 下载失败提示对话框
+  void _showDownloadErrorDialog() {
+    showDialog(
+        context: context,
+        builder: (context) {
+          return SimpleDialog(
+              title: Text('提示', style: TextStyle(color: Styles.primaryColor)),
+              children: <Widget>[
+                Container(
+                  height: 200.h,
+                  alignment: Alignment.centerLeft,
+                  padding: EdgeInsets.only(
+                      left: 60.h,
+                      right: 60.h),
+                  child: Text('下载失败。'),
+                ),
+                Divider(),
+                TButton(
+                    child: Text("确定",
+                        style: TextStyle(fontWeight: FontWeight.w700)),
+                    onPressed: () {
+                      // 关闭对话框
+                      Navigator.of(context).pop();
+                    })
+              ]);
+        });
+  }
+}

+ 91 - 0
lib/pages/home.dart

@@ -0,0 +1,91 @@
+import 'dart:io';
+
+import 'package:ctjt_flutter/common/states.dart';
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/common/time.dart';
+import 'package:ctjt_flutter/widget/button.dart';
+import 'package:ctjt_flutter/widget/contact.dart';
+import 'package:ctjt_flutter/widget/drawer.dart';
+import 'package:ctjt_flutter/widget/title.dart';
+import 'package:flutter/material.dart';
+import 'package:fluttertoast/fluttertoast.dart';
+import 'package:provider/provider.dart';
+
+// 首页
+class HomePage extends StatefulWidget {
+  HomePage({Key? key}) : super(key: key);
+
+  @override
+  _HomePageState createState() => _HomePageState();
+}
+
+class _HomePageState extends State<HomePage> {
+  int tts = 0;
+
+  @override
+  void initState() {
+    super.initState();
+
+    // 初始化状态
+    States.init(context);
+
+    // 显示隐私协议
+    Future.delayed(Duration(seconds: 1), () {
+      var flag = Provider.of<AppVersion>(context, listen: false).showContact;
+      if (flag) {
+        Contact.showUserContactDialog(context);
+      }
+    });
+  }
+
+  @override
+  void didChangeDependencies() {
+    super.didChangeDependencies();
+    Styles.initSize(); // 在首页中初始化公共样式变量的值
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return WillPopScope(
+      // 响应Android返回按钮,需要在首页最外层包装此组件
+      child: Scaffold(
+          backgroundColor: Colors.white,
+          appBar: PageHead(
+            leadingType: LeadingType.Menu,
+            title: Text("Test"),
+          ),
+          // 左侧边栏
+          drawer: DrawerMenu(),
+          body: Column(
+            children: [
+              TButton(
+                  onPressed: () {
+                    Navigator.pushNamed(context, '/remote_call');
+                  },
+                  child: Text("呼叫测试")),
+            ],
+          )),
+      onWillPop: () {
+        if (Navigator.canPop(context)) {
+          print('返回了 -- 回退一层');
+          Navigator.pop(context);
+        } else {
+          int ticks = TimeUtils.getDayNow();
+          if (ticks - tts <= 2000) {
+            print('返回了 -- 退出程序');
+            exit(0);
+          } else {
+            print('返回了 -- 再按一次退出程序');
+            tts = ticks;
+            Fluttertoast.showToast(
+                msg: "再按一次退出程序",
+                toastLength: Toast.LENGTH_LONG,
+                gravity: ToastGravity.CENTER,
+                timeInSecForIosWeb: 1);
+          }
+        }
+        return new Future.value(false);
+      },
+    );
+  }
+}

+ 36 - 0
lib/pages/remote_call.dart

@@ -0,0 +1,36 @@
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/widget/button.dart';
+import 'package:ctjt_flutter/widget/title.dart';
+import 'package:flutter/material.dart';
+
+// 呼叫测试
+class RemoteCallPage extends StatefulWidget {
+  RemoteCallPage({Key? key}) : super(key: key);
+
+  @override
+  _RemoteCallPageState createState() => _RemoteCallPageState();
+}
+
+class _RemoteCallPageState extends State<RemoteCallPage> {
+  final _formKey = GlobalKey<FormState>();
+
+  @override
+  Widget build(BuildContext context) {
+    return Form(
+      key: _formKey,
+      child: Scaffold(
+        backgroundColor: Colors.white,
+        appBar: PageHead(),
+        body: Container(
+          padding: Styles.primaryPadding,
+          child: BigSButton(
+            text: '测试呼叫',
+            onPressed: () async {
+              Navigator.pushNamed(context, '/calling/audioContact');
+            },
+          ),
+        ),
+      ),
+    );
+  }
+}

+ 70 - 0
lib/service/app_service.dart

@@ -0,0 +1,70 @@
+import 'dart:io';
+
+import 'package:ctjt_flutter/common/states.dart';
+import 'package:ctjt_flutter/model/version_res.dart';
+import 'package:ctjt_flutter/service/base_service.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:provider/provider.dart';
+
+class AppService extends BaseService {
+  late BuildContext mContext;
+
+  AppService._();
+
+  static final _instance = AppService._();
+
+  factory AppService.getInstance(BuildContext context) {
+    _instance.mContext = context;
+    return _instance;
+  }
+
+  /// 拉取版本号信息
+  Future<VersionResBody?> fetchVersionInfo(String v) async {
+    int channelType = HTTPConfig.getChannelType(); // 根据环境判断更新渠道类型
+    var uri = '/common/getAppVersion?currentVersion=' + v + '&channelType=' + channelType.toString();
+    var data = await request(uri);
+    if (null == data) {
+      return null;
+    }
+    var ver = VersionRes.fromJson(data);
+    print("Got version from remote:" + ver.body!.lastVersion!);
+    return ver.body;
+  }
+
+  /// 下载安卓更新包
+  Future<File?> downloadAndroid(String url, String version) async {
+    /// 创建存储文件
+    Directory? storageDir = await getExternalStorageDirectory();
+    if (null == storageDir) {
+      return null;
+    }
+    String storagePath = storageDir.path;
+    File file = new File('$storagePath/ctjt_flutter_install_$version.apk');
+
+    if (!file.existsSync()) {
+      file.createSync();
+    }
+
+    /// 发起下载请求
+    Response? response = await requestFile(url,
+        onReceiveProgress: showDownloadProgress);
+    if (null == response) {
+      return null;
+    }
+    file.writeAsBytesSync(response.data);
+    return file;
+  }
+
+  /// 展示下载进度
+  void showDownloadProgress(num received, num total) {
+    if (total != -1) {
+      double _progress =
+          double.parse('${(received / total).toStringAsFixed(2)}');
+      print('Download process is $_progress');
+      Provider.of<AppDownloadProcess>(mContext, listen: false).process =
+          _progress;
+    }
+  }
+}

+ 138 - 0
lib/service/base_service.dart

@@ -0,0 +1,138 @@
+import 'dart:io';
+
+import 'package:dio/dio.dart';
+import 'package:sprintf/sprintf.dart';
+
+const String BASE_URL = "https://www.test.com/api";
+
+/// 请求配置类
+class HTTPConfig {
+  static const baseURL = BASE_URL;
+  static const connectTimeout = 5000;
+  static const receiveTimeout = 10000;
+
+  static int _channelType = 0;
+
+  /// 获取平台信息
+  static int getChannelType() {
+    if (_channelType != 0) {
+      return _channelType;
+    }
+    if (Platform.isAndroid) {
+      _channelType = 1;
+    } else {
+      _channelType = 2;
+    }
+    return _channelType;
+  }
+
+  /// HTTP标准文件请求
+  static Dio fileDio() {
+    BaseOptions options =
+        BaseOptions(baseUrl: baseURL, connectTimeout: connectTimeout);
+    Dio dio = Dio(options);
+
+    Interceptor dInter = InterceptorsWrapper(
+        onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
+      print(sprintf("Dio request file to [%s].", [options.uri.toString()]));
+      return handler.next(options);
+    }, onResponse: (Response response, ResponseInterceptorHandler handler) {
+      print(sprintf("Dio request file to [%s] replied with http status %d.",
+          [response.requestOptions.uri.toString(), response.statusCode]));
+      return handler.next(response);
+    }, onError: (DioError error, ErrorInterceptorHandler handler) {
+      print(sprintf("Dio request file to [%s] failed. %s",
+          [error.requestOptions.uri.toString(), error.toString()]));
+      return handler.next(error);
+    });
+    dio.interceptors.addAll([dInter]);
+    return dio;
+  }
+
+  /// HTTP标准请求
+  static Dio defaultDio() {
+    BaseOptions options = BaseOptions(
+        baseUrl: baseURL,
+        connectTimeout: connectTimeout,
+        receiveTimeout: receiveTimeout);
+    Dio dio = Dio(options);
+
+    Interceptor dInter = InterceptorsWrapper(
+        onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
+      print(sprintf("Dio request to [%s].", [options.uri.toString()]));
+      return handler.next(options);
+    }, onResponse: (Response response, ResponseInterceptorHandler handler) {
+      print(sprintf("Dio request to [%s] replied with http status %d. %s", [
+        response.requestOptions.uri.toString(),
+        response.statusCode,
+        response.toString()
+      ]));
+      return handler.next(response);
+    }, onError: (DioError error, ErrorInterceptorHandler handler) {
+      print(sprintf("Dio request to [%s] failed. %s",
+          [error.requestOptions.uri.toString(), error.toString()]));
+      return handler.next(error);
+    });
+    dio.interceptors.addAll([dInter]);
+    return dio;
+  }
+}
+
+/// 数据请求基类
+class BaseService {
+  static final Dio dio = HTTPConfig.defaultDio();
+  static final Dio fileDio = HTTPConfig.fileDio();
+
+  /// HTTP请求数据
+  Future<T?> request<T>(String url,
+      {String method = 'get',
+      dynamic data,
+      Map<String, String>? headers}) async {
+    final options = Options(method: method);
+
+    try {
+      if (null != headers && headers.length > 0) {
+        if (null == options.headers) {
+          options.headers = {};
+        }
+        headers.forEach((key, value) {
+          options.headers![key] = value;
+        });
+      }
+      Response response =
+          await dio.request<T>(url, data: data, options: options);
+      return response.data;
+    } on DioError {
+      return null;
+    }
+  }
+
+  /// HTTP请求文件
+  Future<Response?> requestFile(String url,
+      {String method = 'get',
+      dynamic data,
+      ProgressCallback? onReceiveProgress,
+      Map<String, String>? headers}) async {
+    final options = Options(
+      method: method,
+      responseType: ResponseType.bytes,
+      followRedirects: false,
+    );
+
+    try {
+      if (null != headers && headers.length > 0) {
+        if (null == options.headers) {
+          options.headers = {};
+        }
+        headers.forEach((key, value) {
+          options.headers![key] = value;
+        });
+      }
+      Response response = await fileDio.request(url,
+          data: data, onReceiveProgress: onReceiveProgress, options: options);
+      return response;
+    } on DioError {
+      return null;
+    }
+  }
+}

+ 192 - 0
lib/widget/button.dart

@@ -0,0 +1,192 @@
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/common/utils.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+/* ---------------------------------大提交按钮-------------------------------- */
+class BigSButton extends StatelessWidget {
+  static var _defaultPadding = EdgeInsets.only(top: 75.h);
+
+  BigSButton(
+      {Key? key,
+      required this.text,
+      this.padding,
+      this.milliseconds = 500,
+      this.vibrateDuration = Utils.DefaultVibrateDuration,
+      required this.onPressed})
+      : super(key: key);
+
+  final String text;
+  final EdgeInsetsGeometry? padding;
+  final Function onPressed;
+  final int milliseconds;
+  final int vibrateDuration;
+
+  @override
+  Widget build(BuildContext context) {
+    bool _isCan = true;
+
+    return Container(
+      // 设定尺寸的容器
+      width: double.infinity, // 占满宽度
+      height: 230.0.h,
+      padding: null == this.padding ? _defaultPadding : this.padding,
+      child: ElevatedButton(
+        style: ButtonStyle(
+          backgroundColor: MaterialStateProperty.all(Colors.white),
+          foregroundColor: MaterialStateProperty.all(Colors.white),
+          shape: MaterialStateProperty.all(RoundedRectangleBorder(
+              borderRadius: BorderRadius.circular(ScreenUtil().radius(20)))),
+          elevation: MaterialStateProperty.all(ScreenUtil().radius(7.5)),
+        ),
+        child: Text(this.text, style: Styles.btnFontStyle),
+        // 圆角
+        onPressed: () {
+          Utils.vibrate(duration: this.vibrateDuration); // 震动
+          if (_isCan) {
+            this.onPressed();
+            _isCan = false;
+            // 不能多次点击
+            Future.delayed(Duration(milliseconds: this.milliseconds), () {
+              _isCan = true;
+            });
+          }
+        },
+      ),
+    );
+  }
+}
+
+/* --------------------------------大文本按钮--------------------------------- */
+class BigTButton extends StatelessWidget {
+  static var _defaultPadding = EdgeInsets.only(top: 25.h);
+
+  BigTButton(
+      {Key? key,
+      required this.text,
+      this.padding,
+      this.milliseconds = 500,
+      this.vibrateDuration = Utils.DefaultVibrateDuration,
+      required this.onPressed})
+      : super(key: key);
+
+  final String text;
+  final EdgeInsetsGeometry? padding;
+  final Function onPressed;
+  final int milliseconds;
+  final int vibrateDuration;
+
+  @override
+  Widget build(BuildContext context) {
+    bool _isCan = true;
+
+    return Padding(
+      padding: null == this.padding ? _defaultPadding : this.padding!,
+      child: OutlinedButton(
+        style: ButtonStyle(
+          padding: MaterialStateProperty.all(EdgeInsets.all(0)),
+          side: MaterialStateProperty.all(BorderSide.none),
+        ),
+        child: Text(this.text,
+            textAlign: TextAlign.left,
+            overflow: TextOverflow.ellipsis,
+            style: Styles.btnFontStyle),
+        //color: Colors.red,
+        onPressed: () {
+          Utils.vibrate(duration: this.vibrateDuration); // 震动
+          if (_isCan) {
+            this.onPressed();
+            _isCan = false;
+            // 不能多次点击
+            Future.delayed(Duration(milliseconds: this.milliseconds), () {
+              _isCan = true;
+            });
+          }
+        },
+      ),
+    );
+  }
+}
+
+/* ----------------------------------图标按钮--------------------------------- */
+class IButton extends IconButton {
+  IButton({
+    Key? key,
+    double iconSize = 24.0,
+    VisualDensity? visualDensity,
+    EdgeInsetsGeometry padding = const EdgeInsets.all(8.0),
+    AlignmentGeometry alignment = Alignment.center,
+    double? splashRadius,
+    required Widget icon,
+    Color? color,
+    Color? focusColor,
+    Color? hoverColor,
+    Color? highlightColor,
+    Color? splashColor,
+    Color? disabledColor,
+    required VoidCallback onPressed,
+    MouseCursor mouseCursor = SystemMouseCursors.click,
+    FocusNode? focusNode,
+    bool autofocus = false,
+    String? tooltip,
+    bool enableFeedback = true,
+    BoxConstraints? constraints,
+    this.vibrateDuration = Utils.DefaultVibrateDuration,
+  }) : super(
+          key: key,
+          iconSize: iconSize,
+          visualDensity: visualDensity,
+          padding: padding,
+          alignment: alignment,
+          splashRadius: splashRadius,
+          icon: icon,
+          color: color,
+          focusColor: focusColor,
+          hoverColor: hoverColor,
+          highlightColor: highlightColor,
+          splashColor: splashColor,
+          disabledColor: disabledColor,
+          onPressed: () {
+            Utils.vibrate(duration: vibrateDuration); // 震动
+            onPressed();
+          },
+          mouseCursor: mouseCursor,
+          focusNode: focusNode,
+          autofocus: autofocus,
+          tooltip: tooltip,
+          enableFeedback: enableFeedback,
+          constraints: constraints,
+        );
+
+  final int vibrateDuration;
+}
+
+/* ----------------------------------文本按钮--------------------------------- */
+class TButton extends TextButton {
+  TButton({
+    Key? key,
+    required VoidCallback onPressed,
+    VoidCallback? onLongPress,
+    ButtonStyle? style,
+    FocusNode? focusNode,
+    bool autofocus = false,
+    Clip clipBehavior = Clip.none,
+    required Widget child,
+    this.vibrateDuration = Utils.DefaultVibrateDuration,
+  }) : super(
+          key: key,
+          onPressed: () {
+            Utils.vibrate(duration: vibrateDuration); // 震动
+            onPressed();
+          },
+          onLongPress: onLongPress,
+          style: style,
+          focusNode: focusNode,
+          autofocus: autofocus,
+          clipBehavior: clipBehavior,
+          child: child,
+        );
+
+  final int vibrateDuration;
+}

+ 102 - 0
lib/widget/contact.dart

@@ -0,0 +1,102 @@
+import 'dart:io';
+
+import 'package:ctjt_flutter/common/states.dart';
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/widget/button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_html/flutter_html.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:provider/provider.dart';
+
+/// 用户协议工具类
+class Contact {
+  /// 显示用户协议对话框
+  static void showUserContactDialog(BuildContext context) {
+    showDialog(
+        context: context,
+        barrierDismissible: false,
+        builder: (context) {
+          return SimpleDialog(
+              title: Text('用户协议和隐私政策',
+                  textAlign: TextAlign.center,
+                  style: TextStyle(color: Styles.primaryColor)),
+              children: [
+                Container(
+                    height: 1400.h,
+                    child: SingleChildScrollView(
+                      child: Html(
+                        data: userContact,
+                      ),
+                    )),
+                Divider(),
+                Offstage(
+                  offstage: !Provider.of<AppVersion>(context).showContact,
+                  child: Column(
+                    children: [
+                      TButton(
+                          child: Text("不同意并退出",
+                              style: TextStyle(fontWeight: FontWeight.w400)),
+                          onPressed: () {
+                            exit(0);
+                          }),
+                      Divider(),
+                      TButton(
+                          child: Text("同意",
+                              style: TextStyle(fontWeight: FontWeight.w700)),
+                          onPressed: () {
+                            // 保存标记
+                            Provider.of<AppVersion>(context, listen: false)
+                                .showContact = false;
+                            // 关闭对话框
+                            Navigator.of(context).pop();
+                          }),
+                    ],
+                  ),
+                ),
+                Offstage(
+                  offstage: Provider.of<AppVersion>(context).showContact,
+                  child: TButton(
+                      child: Text("确定",
+                          style: TextStyle(fontWeight: FontWeight.w700)),
+                      onPressed: () {
+                        Navigator.of(context).pop();
+                      }),
+                ),
+              ]);
+        });
+  }
+
+  static const contactProduceName = "XXXX";
+  static const userContact =
+      """<p>$contactProduceName尊重并保护所有使用服务用户的个人隐私权。为了给您提供更准确、更有个性化的服务,$contactProduceName会按照本隐私权政策的规定使用和披露您的个人信息。但$contactProduceName将以高度的勤勉、审慎义务对待这些信息。除本隐私权政策另有规定外,在未征得您事先许可的情况下,$contactProduceName不会将这些信息对外披露或向第三方提供。$contactProduceName会不时更新本隐私权政策。 您在同意$contactProduceName服务使用协议之时,即视为您已经同意本隐私权政策全部内容。本隐私权政策属于$contactProduceName服务使用协议不可分割的一部分。</p>
+<p>1. 适用范围</p>
+<p>a) 在您注册$contactProduceName帐号时,您根据$contactProduceName要求提供的个人注册信息;</p>
+<p>b) 在您使用$contactProduceName网络服务,或访问$contactProduceName平台网页时,$contactProduceName自动接收并记录的您的浏览器和计算机上的信息,包括但不限于您的IP地址、浏览器的类型、使用的语言、访问日期和时间、软硬件特征信息及您需求的网页记录等数据;</p>
+<p>c) $contactProduceName通过合法途径从商业伙伴处取得的用户个人数据。</p>
+<p>您了解并同意,以下信息不适用本隐私权政策:</p>
+<p>a) 您在使用$contactProduceName平台提供的搜索服务时输入的关键字信息;</p>
+<p>b) 违反法律规定或违反$contactProduceName规则行为及$contactProduceName已对您采取的措施。</p>
+<p>2. 信息使用</p>
+<p>a) $contactProduceName不会向任何无关第三方提供、出售、出租、分享或交易您的个人信息,除非事先得到您的许可,或该第三方和$contactProduceName(含$contactProduceName关联公司)单独或共同为您提供服务,且在该服务结束后,其将被禁止访问包括其以前能够访问的所有这些资料。</p>
+<p>b) $contactProduceName亦不允许任何第三方以任何手段收集、编辑、出售或者无偿传播您的个人信息。任何$contactProduceName平台用户如从事上述活动,一经发现,$contactProduceName有权立即终止与该用户的服务协议。</p>
+<p>c) 为服务用户的目的,$contactProduceName可能通过使用您的个人信息,向您提供您感兴趣的信息,包括但不限于向您发出产品和服务信息,或者与$contactProduceName合作伙伴共享信息以便他们向您发送有关其产品和服务的信息(后者需要您的事先同意)。</p>
+<p>3. 信息披露</p>
+<p>在如下情况下,$contactProduceName将依据您的个人意愿或法律的规定全部或部分的披露您的个人信息:</p>
+<p>a) 经您事先同意,向第三方披露;</p>
+<p>b) 为提供您所要求的产品和服务,而必须和第三方分享您的个人信息;</p>
+<p>c) 根据法律的有关规定,或者行政或司法机构的要求,向第三方或者行政、司法机构披露;</p>
+<p>d) 如您出现违反中国有关法律、法规或者$contactProduceName服务协议或相关规则的情况,需要向第三方披露;</p>
+<p>e) 如您是适格的知识产权投诉人并已提起投诉,应被投诉人要求,向被投诉人披露,以便双方处理可能的权利纠纷;</p>
+<p>f) 在$contactProduceName平台上创建的某一交易中,如交易任何一方履行或部分履行了交易义务并提出信息披露请求的,$contactProduceName有权决定向该用户提供其交易对方的联络方式等必要信息,以促成交易的完成或纠纷的解决。</p>
+<p>g) 其它$contactProduceName根据法律、法规或者网站政策认为合适的披露。</p>
+<p>4. 信息存储和交换</p>
+<p>$contactProduceName收集的有关您的信息和资料将保存在$contactProduceName及(或)其关联公司的服务器上,这些信息和资料可能传送至您所在国家、地区或$contactProduceName收集信息和资料所在地的境外并在境外被访问、存储和展示。</p>
+<p>5. Cookie的使用</p>
+<p>a) 在您未拒绝接受cookies的情况下,$contactProduceName会在您的计算机上设定或取用cookies</p>
+<p>,以便您能登录或使用依赖于cookies的$contactProduceName平台服务或功能。$contactProduceName使用cookies可为您提供更加周到的个性化服务,包括推广服务。  b) 您有权选择接受或拒绝接受cookies。您可以通过修改浏览器设置的方式拒绝接受cookies。但如果您选择拒绝接受cookies,则您可能无法登录或使用依赖于cookies的$contactProduceName网络服务或功能。</p>
+<p>c) 通过$contactProduceName所设cookies所取得的有关信息,将适用本政策。</p>
+<p>6. 信息安全</p>
+<p>a) $contactProduceName帐号均有安全保护功能,请妥善保管您的用户名及密码信息。$contactProduceName将通过对用户密码进行加密等安全措施确保您的信息不丢失,不被滥用和变造。尽管有前述安全措施,但同时也请您注意在信息网络上不存在“完善的安全措施”。</p>
+<p>b) 在使用$contactProduceName网络服务进行网上交流时,您可能会向交流对方披露自己的个人信息,如联络方式或者邮政地址。请您妥善保护自己的个人信息,仅在必要的情形下向他人提供。如您发现自己的个人信息泄密,尤其是$contactProduceName用户名及密码发生泄露,请您立即更改密码以避免造成损失。</p>
+""";
+}

+ 100 - 0
lib/widget/drawer.dart

@@ -0,0 +1,100 @@
+import 'package:ctjt_flutter/common/states.dart';
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/common/utils.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:provider/provider.dart';
+
+/* ----------------------------------左侧抽屉--------------------------------- */
+class DrawerMenu extends StatelessWidget {
+  String getUserShowName(BuildContext context) {
+    String userName = Provider.of<UserStatus>(context, listen: false).userName;
+    if (null == userName || userName.isEmpty) {
+      return "";
+    }
+
+    userName = userName.trim();
+    if (Utils.testCellNo(userName)) {
+      userName = userName.substring(0, 3) + '****' + userName.substring(7);
+    }
+    return userName;
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return SizedBox(
+      width: 812.0.w,
+      child: Drawer(
+        child: Column(
+          children: <Widget>[
+            // 头部
+            Container(
+              width: double.infinity,
+              height: 355.h,
+              padding: EdgeInsets.fromLTRB(70.0.w,
+                  102.0.h, 0, 0),
+              child: Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text("Test",
+                      style: TextStyle(
+                          fontSize: 72.0.w,
+                          fontWeight: FontWeight.normal,
+                          color: Styles.primaryColor)),
+                  Padding(
+                    padding: EdgeInsets.only(top: 30.h),
+                  ),
+                  Text(Provider.of<UserStatus>(context).userName.isEmpty ? '用户未登录' : '用户:' + getUserShowName(context),
+                      style: TextStyle(
+                          fontSize: 36.0.w,
+                          fontWeight: FontWeight.normal,
+                          color: Styles.primaryColor)),
+                ],
+              ),
+              decoration: BoxDecoration(color: Colors.white, boxShadow: [
+                BoxShadow(
+                    color: Colors.black12,
+                    offset: Offset(0.0, 0.0), //阴影xy轴偏移量
+                    blurRadius: ScreenUtil().radius(16), //阴影模糊程度
+                    spreadRadius: ScreenUtil().radius(2) //阴影扩散程度
+                    )
+              ]),
+            ),
+            Padding(
+              padding:
+                  EdgeInsets.fromLTRB(0, 35.0.h, 0, 0),
+            ),
+            ListTile(
+              contentPadding: Styles.drawerTilePadding,
+              title: Text("关于", style: Styles.drawerTileStyle),
+              onTap: () {
+                Utils.vibrate();
+                Navigator.of(context).pop(); // 隐藏侧边栏
+                Navigator.pushNamed(context, '/about');
+              },
+            ),
+            Expanded( // 占满剩余空间
+              child: Offstage(
+                offstage: Provider.of<AppVersion>(context).appVersion.isEmpty,
+                child: Container(
+                  alignment: Alignment.bottomLeft,
+                  padding: EdgeInsets.fromLTRB(
+                      70.0.w,
+                      0,
+                      70.0.w,
+                      170.0.h),
+                  child: Text('应用版本 ' + Provider.of<AppVersion>(context).appVersion,
+                      style: TextStyle(
+                          fontSize: 36.0.sp,
+                          fontWeight: FontWeight.normal,
+                          color: Colors.grey)),
+                ),
+              )
+            ),
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 432 - 0
lib/widget/input.dart

@@ -0,0 +1,432 @@
+import 'dart:async';
+
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/common/utils.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+import 'button.dart';
+
+/* ---------------------------------密码输入框-------------------------------- */
+class PWDInputGroup extends StatelessWidget {
+  static var _defaultPadding = EdgeInsets.only(top: 75.h);
+
+  PWDInputGroup(
+      {Key? key,
+      this.label = '密码:',
+      this.padding,
+      required this.controller})
+      : super(key: key);
+
+  final String label;
+  final EdgeInsetsGeometry? padding;
+  final TextEditingController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: null == this.padding ? _defaultPadding : this.padding!,
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(this.label, style: Styles.formLabelStyle),
+          PWDInput(
+            controller: this.controller,
+          )
+        ],
+      ),
+    );
+  }
+}
+
+class PWDInput extends StatefulWidget {
+  PWDInput({
+    Key? key,
+    this.hintText = '6-16位数字、字母或符号',
+    this.minLength = 6,
+    this.maxLength = 16,
+    required this.controller
+  }) : super(key: key);
+
+  final hintText;
+  final minLength;
+  final maxLength;
+  final TextEditingController controller;
+
+  @override
+  _PWDInputState createState() => _PWDInputState();
+}
+
+class _PWDInputState extends State<PWDInput> {
+  var _isCanSee = false;
+  var _hideClear = true;
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      controller: widget.controller,
+      decoration: InputDecoration(
+        // 用于设置文本框右侧按钮的装饰器
+        hintText: widget.hintText,
+        suffixIcon: SizedBox(
+          // 设置按钮区域宽度
+          width: Styles.iconSize! * 4.0,
+          child: Row(
+            // 水平布局
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: <Widget>[
+              Expanded(
+                // 撑满剩余水平空间的布局容器
+                child: Offstage(
+                  // 显示隐藏控制
+                  offstage: _hideClear,
+                  child: IButton(
+                      // 文本框清理按钮
+                      iconSize: Styles.iconSize!,
+                      icon: Icon(Icons.cancel, color: Styles.primaryColor),
+                      onPressed: () {
+                        widget.controller.clear();
+                        setState(() {
+                          _hideClear = true;
+                        });
+                      }),
+                ),
+              ),
+              IButton(
+                  // 查看密码按钮
+                  iconSize: Styles.iconSize!,
+                  icon: _isCanSee
+                      ? Icon(Icons.visibility, color: Styles.primaryColor)
+                      : Icon(Icons.visibility_off, color: Colors.black54),
+                  onPressed: () {
+                    setState(() {
+                      _isCanSee = !_isCanSee;
+                    });
+                  }),
+            ],
+          ),
+        ),
+      ),
+      maxLength: widget.maxLength,
+      validator: (value) {
+        // 校验
+        return value!.isEmpty || value.length > widget.maxLength || value.length < widget.minLength
+            ? '请输入由${widget.hintText}组成的密码'
+            : null;
+      },
+      // 密码键盘
+      keyboardType: TextInputType.visiblePassword,
+      // 允许输入的字符
+      inputFormatters: [
+        FilteringTextInputFormatter.allow(Utils.passwordPattern)
+      ],
+      // 密码是否可见
+      obscureText: !_isCanSee,
+      // 每次输入改变时
+      onChanged: (value) {
+        setState(() {
+          _hideClear = value.isEmpty;
+        });
+      },
+    );
+  }
+}
+
+/* --------------------------------验证码输入框------------------------------- */
+class VCodeInputGroup extends StatelessWidget {
+  static var _defaultPadding = EdgeInsets.only(top: 75.h);
+
+  VCodeInputGroup(
+      {Key? key,
+      this.label = '验证码:',
+      this.padding,
+      required this.controller,
+      this.onPressed})
+      : super(key: key);
+
+  final String label;
+  final EdgeInsetsGeometry? padding;
+  final TextEditingController controller;
+  final Function? onPressed;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: null == this.padding ? _defaultPadding : this.padding!,
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(this.label, style: Styles.formLabelStyle),
+          VCodeInput(
+            controller: this.controller,
+            onPressed: this.onPressed,
+          )
+        ],
+      ),
+    );
+  }
+}
+
+class VCodeInput extends StatefulWidget {
+  VCodeInput({Key? key, required this.controller, this.onPressed}) : super(key: key);
+
+  final TextEditingController controller;
+  final Function? onPressed;
+
+  @override
+  _VCodeInputState createState() => _VCodeInputState();
+}
+
+class _VCodeInputState extends State<VCodeInput> {
+  var _text = '发送验证码';
+  var _seconds = 60;
+  var _isDisabled = false;
+  var _hideClear = true;
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      controller: widget.controller,
+      decoration: InputDecoration(
+        hintText: '4位验证码',
+        suffixIcon: SizedBox(
+          width: Styles.buttonFontSize! * 5 + Styles.iconSize! * 3,
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: <Widget>[
+              Expanded(
+                child: Offstage(
+                  offstage: _hideClear,
+                  child: IButton(
+                      iconSize: Styles.iconSize!,
+                      icon: Icon(Icons.cancel, color: Styles.primaryColor),
+                      onPressed: () {
+                        widget.controller.clear();
+                        setState(() {
+                          _hideClear = true;
+                        });
+                      }),
+                ),
+              ),
+              TButton(
+                  child: Text(_text, style: Styles.btnFontStyle),
+                  onPressed: _onPressed()),
+            ],
+          ),
+        ),
+      ),
+      maxLength: 4,
+      validator: (value) {
+        return value!.isEmpty ? '请输入验证码' : null;
+      },
+      keyboardType: TextInputType.number,
+      inputFormatters: [FilteringTextInputFormatter.allow(Utils.numberPattern)],
+      onChanged: (value) {
+        setState(() {
+          _hideClear = value.isEmpty;
+        });
+      },
+    );
+  }
+
+  VoidCallback _onPressed() {
+    if (_isDisabled) {
+      return () {};
+    } else {
+      return () {
+        if (null != widget.onPressed) {
+          widget.onPressed!();
+        }
+        _isDisabled = true;
+        _seconds = 60;
+        Timer.periodic(Utils.timeout, (timer) {
+          setState(() {
+            _text = _seconds.toString().padLeft(2, '0') + "秒后重试";
+          });
+          _seconds--;
+
+          if (_seconds < 0) {
+            setState(() {
+              _text = '发送验证码';
+            });
+            timer.cancel(); // 取消定时器
+            _isDisabled = false;
+          }
+        });
+      };
+    }
+  }
+}
+
+/* --------------------------------手机号输入框------------------------------- */
+class CellInputGroup extends StatelessWidget {
+  static var _defaultPadding = EdgeInsets.only(top: 75.h);
+
+  CellInputGroup(
+      {Key? key,
+      this.label = '手机号:',
+      this.padding,
+      required this.controller})
+      : super(key: key);
+
+  final String label;
+  final EdgeInsetsGeometry? padding;
+  final TextEditingController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: null == this.padding ? _defaultPadding : this.padding!,
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(this.label, style: Styles.formLabelStyle),
+          CellInput(
+            controller: this.controller,
+          )
+        ],
+      ),
+    );
+  }
+}
+
+class CellInput extends StatefulWidget {
+  CellInput({Key? key, required this.controller}) : super(key: key);
+
+  final TextEditingController controller;
+
+  @override
+  _CellInputState createState() => _CellInputState();
+}
+
+class _CellInputState extends State<CellInput> {
+  var _hideClear = true;
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      controller: widget.controller,
+      decoration: InputDecoration(
+          hintText: '11位手机号码',
+          suffixIcon: Offstage(
+            offstage: _hideClear,
+            child: IButton(
+                iconSize: Styles.iconSize!,
+                icon: Icon(Icons.cancel, color: Styles.primaryColor),
+                onPressed: () {
+                  widget.controller.clear();
+                  setState(() {
+                    _hideClear = true;
+                  });
+                }),
+          )),
+      maxLength: 11,
+      validator: (value) {
+        return Utils.testCellNo(value!) ? null : '请输入有效的手机号';
+      },
+      keyboardType: TextInputType.phone,
+      inputFormatters: [FilteringTextInputFormatter.allow(Utils.numberPattern)],
+      onChanged: (value) {
+        setState(() {
+          _hideClear = value.isEmpty;
+        });
+      },
+    );
+  }
+}
+
+/* ---------------------------------文本输入框-------------------------------- */
+class StrInputGroup extends StatelessWidget {
+  static var _defaultPadding = EdgeInsets.only(top: 75.h);
+
+  StrInputGroup(
+      {Key? key,
+      this.label = '名称:',
+      this.hintText,
+      this.maxLength,
+      this.padding,
+      required this.controller})
+      : super(key: key);
+
+  final String label;
+  final String? hintText;
+  final int? maxLength;
+  final EdgeInsetsGeometry? padding;
+  final TextEditingController controller;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: null == this.padding ? _defaultPadding : this.padding!,
+      child: Column(
+        mainAxisSize: MainAxisSize.min,
+        crossAxisAlignment: CrossAxisAlignment.start,
+        children: [
+          Text(this.label, style: Styles.formLabelStyle),
+          StrInput(
+            hintText: this.hintText,
+            maxLength: this.maxLength,
+            controller: this.controller,
+          )
+        ],
+      ),
+    );
+  }
+}
+
+class StrInput extends StatefulWidget {
+  StrInput({
+    Key? key,
+    this.hintText = '最长16个字符',
+    this.maxLength = 16,
+    this.inputFormatters,
+    required this.controller,
+  }) : super(key: key);
+
+  final hintText;
+  final maxLength;
+  final List<TextInputFormatter>? inputFormatters;
+  final TextEditingController controller;
+
+  @override
+  _StrInputState createState() => _StrInputState();
+}
+
+class _StrInputState extends State<StrInput> {
+  var _hideClear = true;
+
+  @override
+  Widget build(BuildContext context) {
+    return TextFormField(
+      controller: widget.controller,
+      decoration: InputDecoration(
+          hintText: widget.hintText,
+          suffixIcon: Offstage(
+            offstage: _hideClear,
+            child: IButton(
+                iconSize: Styles.iconSize!,
+                icon: Icon(Icons.cancel, color: Styles.primaryColor),
+                onPressed: () {
+                  widget.controller.clear();
+                  setState(() {
+                    _hideClear = true;
+                  });
+                }),
+          )),
+      maxLength: widget.maxLength,
+      validator: (value) {
+        return value!.isNotEmpty ? null : '不能为空值';
+      },
+      inputFormatters: widget.inputFormatters,
+      onChanged: (value) {
+        setState(() {
+          _hideClear = value.isEmpty;
+        });
+      },
+    );
+  }
+}

+ 91 - 0
lib/widget/title.dart

@@ -0,0 +1,91 @@
+import 'package:ctjt_flutter/common/styles.dart';
+import 'package:ctjt_flutter/widget/button.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+
+/* ---------------------------------页面内标题-------------------------------- */
+class PageTitle extends StatelessWidget {
+  static var _defaultPadding = EdgeInsets.only(
+      top: 75.h, bottom: 75.h);
+
+  PageTitle({Key? key, required this.text, this.padding}) : super(key: key);
+
+  final String text;
+  final EdgeInsetsGeometry? padding;
+
+  @override
+  Widget build(BuildContext context) {
+    return Padding(
+      padding: null == this.padding ? _defaultPadding : this.padding!,
+      child: Text(this.text, style: Styles.titleStyle),
+    );
+  }
+}
+
+/* ----------------------------------状态栏---------------------------------- */
+// 预设的几种常见左上角按钮类型枚举
+enum LeadingType { Back, Menu, None }
+
+class PageHead extends StatelessWidget implements PreferredSizeWidget {
+  static var _defaultSize = Size.fromHeight(125.h);
+
+  PageHead(
+      {Key? key, this.leadingType = LeadingType.Back, this.leading, this.title})
+      : super(key: key);
+
+  final LeadingType leadingType;
+  final Widget? leading;
+  final Widget? title;
+
+  @override
+  Widget build(BuildContext context) {
+    Widget? leading;
+    if (null != this.leading) {
+      leading = this.leading!;
+    } else {
+      switch (this.leadingType) {
+        case LeadingType.Menu:
+          {
+            leading = IButton(
+                iconSize: Styles.iconBigSize!,
+                icon: const Icon(Icons.menu, color: Styles.primaryColor),
+                onPressed: () {
+                  Scaffold.of(context).openDrawer();
+                });
+          }
+          break;
+        case LeadingType.Back:
+          {
+            leading = IButton(
+                iconSize: Styles.iconBigSize!,
+                icon: const Icon(Icons.arrow_back_ios,
+                    color: Styles.primaryColor),
+                onPressed: () {
+                  Navigator.of(context).pop();
+                });
+          }
+          break;
+        default:
+          break;
+      }
+    }
+    return PreferredSize(
+      // 设定状态栏大小的容器
+      child: AppBar(
+        centerTitle: true,
+        leading: leading,
+        title: this.title,
+        elevation: 0,
+        systemOverlayStyle:
+            SystemUiOverlayStyle(statusBarBrightness: Brightness.light),
+        // 是否夜览
+        backgroundColor: Colors.transparent, // 状态栏设置为透明底色
+      ),
+      preferredSize: _defaultSize,
+    );
+  }
+
+  @override
+  Size get preferredSize => _defaultSize;
+}

+ 97 - 0
pubspec.yaml

@@ -0,0 +1,97 @@
+name: ctjt_flutter
+description: Flutter项目原型
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+# The following defines the version and build number for your application.
+# A version number is three numbers separated by dots, like 1.2.43
+# followed by an optional build number separated by a +.
+# Both the version and the builder number may be overridden in flutter
+# build by specifying --build-name and --build-number, respectively.
+# In Android, build-name is used as versionName while build-number used as versionCode.
+# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
+# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
+# Read more about iOS versioning at
+# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
+version: 1.0.0+1
+
+environment:
+  sdk: ">=2.12.0 <3.0.0"
+
+dependencies:
+  flutter:
+    sdk: flutter
+
+
+  # The following adds the Cupertino Icons font to your application.
+  # Use with the CupertinoIcons class for iOS style icons.
+  cupertino_icons: ^1.0.3                   # 图标库
+  provider: ^6.0.1                          # 状态通知
+  package_info: ^2.0.2                      # APP包信息
+  device_info: ^2.0.2                       # 设备信息
+  sprintf: ^6.0.0                           # 字符串格式化
+  flutter_screenutil: ^5.0.0+2              # 屏幕工具库
+  dio: ^4.0.0                               # 网络请求
+  fluttertoast: ^8.0.8                      # Toast
+  flutter_html: ^2.1.5                      # HTML组件
+  permission_handler: ^8.1.6                # 权限管理
+  shared_preferences: ^2.0.8                # 本地储存
+  vibration: 1.7.4-nullsafety.0             # 震动
+  r_upgrade: ^0.3.5                         # 安装插件
+#  url_launcher: ^6.0.12                     # 超链接跳转
+  path_provider: ^2.0.5                     # 目录信息
+#  flutter_bugly: ^0.3.3                     # Bugly
+  tencent_trtc_cloud: ^1.2.4                # 腾讯云实时音视频,用于实时音视频互通
+  tencent_im_sdk_plugin: ^3.5.0             # 腾讯云IM,也是实时语音通话需要依赖,用于串联接听和拨打信令
+  flutter_styled_toast: ^2.0.0              # 丰富样式的Toast
+  crypto: ^3.0.1                            # 加解密算法库
+  synchronized: ^3.0.0                      # 同步锁
+
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+
+  # The following line ensures that the Material Icons font is
+  # included with your application, so that you can use the icons in
+  # the material Icons class.
+  uses-material-design: true
+
+  # To add assets to your application, add an assets section, like this:
+  # assets:
+  #   - images/a_dot_burr.jpeg
+  #   - images/a_dot_ham.jpeg
+
+  # An image asset can refer to one or more resolution-specific "variants", see
+  # https://flutter.dev/assets-and-images/#resolution-aware.
+
+  # For details regarding adding assets from package dependencies, see
+  # https://flutter.dev/assets-and-images/#from-packages
+
+  # To add custom fonts to your application, add a fonts section here,
+  # in this "flutter" section. Each entry in this list should have a
+  # "family" key with the font family name, and a "fonts" key with a
+  # list giving the asset and other descriptors for the font. For
+  # example:
+  # fonts:
+  #   - family: Schyler
+  #     fonts:
+  #       - asset: fonts/Schyler-Regular.ttf
+  #       - asset: fonts/Schyler-Italic.ttf
+  #         style: italic
+  #   - family: Trajan Pro
+  #     fonts:
+  #       - asset: fonts/TrajanPro.ttf
+  #       - asset: fonts/TrajanPro_Bold.ttf
+  #         weight: 700
+  #
+  # For details regarding fonts from package dependencies,
+  # see https://flutter.dev/custom-fonts/#from-packages

+ 30 - 0
test/widget_test.dart

@@ -0,0 +1,30 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility that Flutter provides. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:ctjt_flutter/main.dart';
+
+void main() {
+  testWidgets('Counter increments smoke test', (WidgetTester tester) async {
+    // Build our app and trigger a frame.
+    await tester.pumpWidget(MyApp());
+
+    // Verify that our counter starts at 0.
+    expect(find.text('0'), findsOneWidget);
+    expect(find.text('1'), findsNothing);
+
+    // Tap the '+' icon and trigger a frame.
+    await tester.tap(find.byIcon(Icons.add));
+    await tester.pump();
+
+    // Verify that our counter has incremented.
+    expect(find.text('0'), findsNothing);
+    expect(find.text('1'), findsOneWidget);
+  });
+}