Kaynağa Gözat

1. 添加登录界面
2. 添加好友列表界面
3. 添加搜索好友

ignatz 3 yıl önce
ebeveyn
işleme
8c877a6895
40 değiştirilmiş dosya ile 3254 ekleme ve 91 silme
  1. 5 2
      android/app/build.gradle
  2. 21 0
      android/app/src/main/AndroidManifest.xml
  3. 64 0
      lib/assets/android.svg
  4. 1 0
      lib/assets/apple.svg
  5. BIN
      lib/assets/avatar.jpg
  6. 23 0
      lib/assets/books.svg
  7. BIN
      lib/assets/error.png
  8. 1 0
      lib/assets/gamepad.svg
  9. 1 0
      lib/assets/github.svg
  10. BIN
      lib/assets/init.jpg
  11. 1 0
      lib/assets/random.svg
  12. 78 0
      lib/assets/rankings.svg
  13. 6 0
      lib/assets/unknown.svg
  14. 271 0
      lib/basic/Common.dart
  15. 50 0
      lib/basic/Entities.dart
  16. 235 0
      lib/basic/Method.dart
  17. 31 0
      lib/basic/enum/ErrorTypes.dart
  18. 16 88
      lib/main.dart
  19. 78 0
      lib/screens/AboutScreen.dart
  20. 175 0
      lib/screens/AccountScreen.dart
  21. 31 0
      lib/screens/AddFriendScreen.dart
  22. 89 0
      lib/screens/AppScreen.dart
  23. 204 0
      lib/screens/ContactsScreen.dart
  24. 59 0
      lib/screens/FilePhotoViewScreen.dart
  25. 90 0
      lib/screens/InitScreen.dart
  26. 67 0
      lib/screens/LoginScreen.dart
  27. 80 0
      lib/screens/NewFriendScreen.dart
  28. 82 0
      lib/screens/ProfileScreen.dart
  29. 134 0
      lib/screens/SearchScreen.dart
  30. 76 0
      lib/screens/SpaceScreen.dart
  31. 38 0
      lib/screens/components/Avatar.dart
  32. 43 0
      lib/screens/components/Badge.dart
  33. 39 0
      lib/screens/components/ContentLoading.dart
  34. 356 0
      lib/screens/components/Images.dart
  35. 62 0
      lib/screens/components/ItemBuilder.dart
  36. 28 0
      lib/screens/components/ProgressDialog.dart
  37. 15 0
      lib/screens/components/RightClickPop.dart
  38. 138 0
      lib/screens/components/UserProfileCard.dart
  39. 550 0
      pubspec.lock
  40. 16 1
      pubspec.yaml

+ 5 - 2
android/app/build.gradle

@@ -26,7 +26,9 @@ apply plugin: 'kotlin-android'
 apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
 
 android {
-    compileSdkVersion flutter.compileSdkVersion
+//    compileSdkVersion flutter.compileSdkVersion
+    compileSdkVersion 33
+//    minSdkVersion 21
     ndkVersion flutter.ndkVersion
 
     compileOptions {
@@ -47,10 +49,11 @@ android {
         applicationId "xyz.ignatz.prime_chat"
         // You can update the following values to match your application needs.
         // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
-        minSdkVersion flutter.minSdkVersion
+        minSdkVersion 21
         targetSdkVersion flutter.targetSdkVersion
         versionCode flutterVersionCode.toInteger()
         versionName flutterVersionName
+        multiDexEnabled true
     }
 
     buildTypes {

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

@@ -25,10 +25,31 @@
                 <category android:name="android.intent.category.LAUNCHER"/>
             </intent-filter>
         </activity>
+
+<!--       <activity android:name="com.android.camera.CropImage"/>-->
+       <activity
+           android:name="com.yalantis.ucrop.UCropActivity"
+           android:screenOrientation="portrait"
+           android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
+
         <!-- 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>
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
+    <!-- Remove READ_MEDIA_IMAGES if you don't need to load photos. -->
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
+    <!-- Remove READ_MEDIA_VIDEO if you don't need to load videos. -->
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO"/>
+    <!-- Remove READ_MEDIA_AUDIO if you don't need to load audios. -->
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO"/>
 </manifest>

+ 64 - 0
lib/assets/android.svg

@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 width="553.048px" height="553.048px" viewBox="0 0 553.048 553.048" style="enable-background:new 0 0 553.048 553.048;"
+	 xml:space="preserve">
+<g>
+	<g>
+		<path d="M76.774,179.141c-9.529,0-17.614,3.323-24.26,9.969c-6.646,6.646-9.97,14.621-9.97,23.929v142.914
+			c0,9.541,3.323,17.619,9.97,24.266c6.646,6.646,14.731,9.97,24.26,9.97c9.522,0,17.558-3.323,24.101-9.97
+			c6.53-6.646,9.804-14.725,9.804-24.266V213.039c0-9.309-3.323-17.283-9.97-23.929C94.062,182.464,86.082,179.141,76.774,179.141z"
+			/>
+		<path d="M351.972,50.847L375.57,7.315c1.549-2.882,0.998-5.092-1.658-6.646c-2.883-1.34-5.098-0.661-6.646,1.989l-23.928,43.88
+			c-21.055-9.309-43.324-13.972-66.807-13.972c-23.488,0-45.759,4.664-66.806,13.972l-23.929-43.88
+			c-1.555-2.65-3.77-3.323-6.646-1.989c-2.662,1.561-3.213,3.764-1.658,6.646l23.599,43.532
+			c-23.929,12.203-42.987,29.198-57.167,51.022c-14.18,21.836-21.273,45.698-21.273,71.628h307.426
+			c0-25.924-7.094-49.787-21.273-71.628C394.623,80.045,375.675,63.05,351.972,50.847z M215.539,114.165
+			c-2.552,2.558-5.6,3.831-9.143,3.831c-3.55,0-6.536-1.273-8.972-3.831c-2.436-2.546-3.654-5.582-3.654-9.137
+			c0-3.543,1.218-6.585,3.654-9.137c2.436-2.546,5.429-3.819,8.972-3.819s6.591,1.273,9.143,3.819
+			c2.546,2.558,3.825,5.594,3.825,9.137C219.357,108.577,218.079,111.619,215.539,114.165z M355.625,114.165
+			c-2.441,2.558-5.434,3.831-8.971,3.831c-3.551,0-6.598-1.273-9.145-3.831c-2.551-2.546-3.824-5.582-3.824-9.137
+			c0-3.543,1.273-6.585,3.824-9.137c2.547-2.546,5.594-3.819,9.145-3.819c3.543,0,6.529,1.273,8.971,3.819
+			c2.438,2.558,3.654,5.594,3.654,9.137C359.279,108.577,358.062,111.619,355.625,114.165z"/>
+		<path d="M123.971,406.804c0,10.202,3.543,18.838,10.63,25.925c7.093,7.087,15.729,10.63,25.924,10.63h24.596l0.337,75.454
+			c0,9.528,3.323,17.619,9.969,24.266s14.627,9.97,23.929,9.97c9.523,0,17.613-3.323,24.26-9.97s9.97-14.737,9.97-24.266v-75.447
+			h45.864v75.447c0,9.528,3.322,17.619,9.969,24.266s14.73,9.97,24.26,9.97c9.523,0,17.613-3.323,24.26-9.97
+			s9.969-14.737,9.969-24.266v-75.447h24.928c9.969,0,18.494-3.544,25.594-10.631c7.086-7.087,10.631-15.723,10.631-25.924V185.45
+			H123.971V406.804z"/>
+		<path d="M476.275,179.141c-9.309,0-17.283,3.274-23.93,9.804c-6.646,6.542-9.969,14.578-9.969,24.094v142.914
+			c0,9.541,3.322,17.619,9.969,24.266s14.627,9.97,23.93,9.97c9.523,0,17.613-3.323,24.26-9.97s9.969-14.725,9.969-24.266V213.039
+			c0-9.517-3.322-17.552-9.969-24.094C493.888,182.415,485.798,179.141,476.275,179.141z"/>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 1 - 0
lib/assets/apple.svg

@@ -0,0 +1 @@
+<svg width="842" height="1e3" xmlns="http://www.w3.org/2000/svg"><path d="M702 960c-54.2 52.6-114 44.4-171 19.6-60.6-25.3-116-26.9-180 0-79.7 34.4-122 24.4-170-19.6-271-279-231-704 77-720 74.7 4 127 41.3 171 44.4 65.4-13.3 128-51.4 198-46.4 84.1 6.8 147 40 189 99.7-173 104-132 332 26.9 396-31.8 83.5-72.6 166-141 227zM423 237C414.9 113 515.4 11 631 1c15.9 143-130 250-208 236z"/></svg>

BIN
lib/assets/avatar.jpg


Dosya farkı çok büyük olduğundan ihmal edildi
+ 23 - 0
lib/assets/books.svg


BIN
lib/assets/error.png


+ 1 - 0
lib/assets/gamepad.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M8 10.5c0 .827-.673 1.5-1.5 1.5s-1.5-.673-1.5-1.5.673-1.5 1.5-1.5 1.5.673 1.5 1.5zm16 6.216c0 1.587-.56 2.591-1.749 3.179-.143.071-.296.105-.449.105-.242 0-.482-.087-.672-.255l-3.109-2.745c-.558-.494-1.044-1.004-2.404-1.004h-7.233c-1.36 0-1.847.51-2.404 1.004l-3.11 2.745c-.188.168-.429.255-.672.255-.152 0-.307-.034-.449-.105-1.189-.588-1.749-1.592-1.749-3.178 0-2.062.945-5.461 2.681-9.857.822-2.083 2.292-2.86 3.695-2.86.656 0 1.298.17 1.853.456 2.424 1.249 5.17 1.223 7.544 0 .553-.286 1.195-.456 1.851-.456 1.403 0 2.874.777 3.696 2.86 1.735 4.395 2.68 7.794 2.68 9.856zm-15-6.216c0-1.381-1.119-2.5-2.5-2.5s-2.5 1.119-2.5 2.5 1.119 2.5 2.5 2.5 2.5-1.119 2.5-2.5zm4-2.5c0-.552-.447-1-1-1s-1 .448-1 1 .447 1 1 1 1-.448 1-1zm3.5 2.5c0-.415-.336-.75-.75-.75s-.75.335-.75.75.336.75.75.75.75-.335.75-.75zm1.75 1.75c0-.415-.336-.75-.75-.75s-.75.335-.75.75.336.75.75.75.75-.335.75-.75zm0-3.531c0-.415-.336-.75-.75-.75s-.75.335-.75.75.336.75.75.75.75-.336.75-.75zm1.734 1.781c0-.415-.336-.75-.75-.75s-.75.335-.75.75.336.75.75.75.75-.335.75-.75z"/></svg>

+ 1 - 0
lib/assets/github.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>

BIN
lib/assets/init.jpg


+ 1 - 0
lib/assets/random.svg

@@ -0,0 +1 @@
+<?xml version="1.0" encoding="utf-8"?><svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 122.88 105.71" style="enable-background:new 0 0 122.88 105.71" xml:space="preserve"><style type="text/css">.st0{fill-rule:evenodd;clip-rule:evenodd;}</style><g><path class="st0" d="M0,79.45c-0.02-1.95,0.76-3.06,2.51-3.18h14.08c5.98,0,8.89,0.16,13.98-3.91c1.08-0.86,2.1-1.86,3.06-3 c4.55-5.41,6.17-11.96,7.87-18.9C44.79,37,50.03,22.78,63.98,17.15c7.94-3.2,18.82-2.59,27.41-2.59h5.27l0.01-10.05 c0-5.01,1.18-5.88,4.79-2.45l19.55,18.58c2.36,2.24,2.03,3.7-0.22,5.86L101.49,45c-3.37,3.41-4.89,2.45-4.82-2.26v-11.8 c-34-0.52-32.57,1.67-42.05,34.09c-3.5,10.04-8.81,17.08-15.59,21.69c-7.09,4.82-13.68,6.39-22.02,6.39H6.65 C0.71,93.11,0,92.83,0,86.75V79.45L0,79.45z M0.23,26.26c-0.02,1.95,0.76,3.06,2.51,3.18h14.7c5.98,0,8.89-0.16,13.98,3.91 c1.08,0.86,2.1,1.86,3.06,3c1.16,1.38,2.13,2.84,2.96,4.35c1.5-4.69,3.36-9.29,5.82-13.5c0.7-1.19,1.44-2.35,2.23-3.48 c-1.74-1.8-3.61-3.37-5.61-4.73c-7.09-4.82-13.68-6.39-22.02-6.39H6.88c-5.94,0-6.65,0.28-6.65,6.36V26.26L0.23,26.26z M53.57,80.45c2.96,3.42,6.63,6.24,11.27,8.11c7.94,3.2,18.21,2.59,26.8,2.59h5.27l0.01,10.05c0,5.01,1.18,5.88,4.79,2.45 l19.55-18.58c2.36-2.24,2.03-3.7-0.22-5.86l-19.3-18.5c-3.37-3.41-4.89-2.45-4.82,2.26v11.8c-24.78,0.38-30.42-0.69-35.32-13.84 c-0.27,0.94-0.64,2.23-1.93,6.65c-0.03,0.1-0.06,0.19-0.09,0.28l0,0C57.91,72.62,55.9,76.79,53.57,80.45L53.57,80.45z"/></g></svg>

+ 78 - 0
lib/assets/rankings.svg

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="iso-8859-1"?>
+<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 502.664 502.664" style="enable-background:new 0 0 502.664 502.664;" xml:space="preserve">
+<g>
+	<g>
+		<rect y="289.793" style="fill:#010002;" width="148.666" height="199.638"/>
+		<rect x="353.998" y="238.497" style="fill:#010002;" width="148.666" height="250.933"/>
+		<rect x="176.988" y="164.057" style="fill:#010002;" width="148.709" height="325.374"/>
+		<path style="fill:#010002;" d="M429.474,87.243l21.053,42.71l47.154,6.859l-34.082,33.241l8.024,46.96l-42.149-22.175
+			l-42.149,22.175l8.024-46.96l-34.082-33.241l47.111-6.86L429.474,87.243z"/>
+		<path style="fill:#010002;" d="M252.141,13.234l21.075,42.732l47.154,6.86l-34.082,33.262l8.046,46.916l-42.171-22.153
+			l-42.171,22.153l8.024-46.916l-34.082-33.262l47.132-6.86L252.141,13.234z"/>
+		<path style="fill:#010002;" d="M71.744,137.05l21.053,42.732l47.154,6.881l-34.06,33.219l8.024,46.938l-42.171-22.153
+			l-42.171,22.175l8.046-46.938L3.538,186.684l47.132-6.881L71.744,137.05z"/>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+	<g>
+	</g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+<g>
+</g>
+</svg>

+ 6 - 0
lib/assets/unknown.svg

@@ -0,0 +1,6 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+        "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg class="icon" style="width: 5em; height: 5em;vertical-align: middle;fill: #000000;overflow: hidden;"
+     viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg">
+    <path d="M512 51.2c254.08 0 460.8 206.72 460.8 460.8s-206.72 460.8-460.8 460.8S51.2 766.08 51.2 512 257.92 51.2 512 51.2M512 0C229.248 0 0 229.248 0 512s229.248 512 512 512 512-229.248 512-512S794.752 0 512 0L512 0zM470.976 642.624C470.72 633.6 470.656 626.88 470.656 622.4c0-26.496 3.776-49.344 11.264-68.608 5.504-14.528 14.4-29.12 26.624-43.904 9.024-10.752 25.216-26.432 48.576-47.04s38.592-37.056 45.568-49.344 10.496-25.6 10.496-40.128c0-26.24-10.24-49.344-30.72-69.184S536.768 274.368 507.008 274.368c-28.736 0-52.736 9.024-72 27.008S403.136 347.52 397.12 385.728L327.744 377.472c6.272-51.264 24.832-90.496 55.68-117.76S455.104 218.88 505.856 218.88c53.76 0 96.64 14.656 128.64 43.904s48 64.64 48 106.112c0 24-5.632 46.144-16.896 66.368s-33.28 44.864-65.984 73.856C577.6 528.64 563.264 542.976 556.48 552.256S544.768 572.096 541.504 584.128s-5.12 31.488-5.632 58.496L470.976 642.624zM466.88 777.984l0-76.864 76.864 0 0 76.864L466.88 777.984z"/>
+</svg>

+ 271 - 0
lib/basic/Common.dart

@@ -0,0 +1,271 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_styled_toast/flutter_styled_toast.dart';
+
+/// 默认的图片尺寸
+double coverWidth = 210;
+double coverHeight = 315;
+
+String categoryTitle(String? categoryTitle) {
+  return categoryTitle ?? "全分类";
+}
+
+/// 显示一个toast
+void defaultToast(BuildContext context, String title) {
+  showToast(
+    title,
+    context: context,
+    position: StyledToastPosition.center,
+    animation: StyledToastAnimation.scale,
+    reverseAnimation: StyledToastAnimation.fade,
+    duration: const Duration(seconds: 4),
+    animDuration: const Duration(seconds: 1),
+    curve: Curves.elasticOut,
+    reverseCurve: Curves.linear,
+  );
+}
+
+/// 显示一个确认框, 用户关闭弹窗以及选择否都会返回false, 仅当用户选择确定时返回true
+Future<bool> confirmDialog(
+    BuildContext context, String title, String content) async {
+  return await showDialog(
+          context: context,
+          builder: (context) => AlertDialog(
+                title: Text(title),
+                content: SingleChildScrollView(
+                  child: ListBody(
+                    children: <Widget>[Text(content)],
+                  ),
+                ),
+                actions: <Widget>[
+                  MaterialButton(
+                    child: const Text('取消'),
+                    onPressed: () {
+                      Navigator.of(context).pop(false);
+                    },
+                  ),
+                  MaterialButton(
+                    child: const Text('确定'),
+                    onPressed: () {
+                      Navigator.of(context).pop(true);
+                    },
+                  ),
+                ],
+              )) ??
+      false;
+}
+
+/// 显示一个消息提示框
+Future alertDialog(BuildContext context, String title, String content) {
+  return showDialog(
+    context: context,
+    builder: (context) => AlertDialog(
+      title: Text(title),
+      content: SingleChildScrollView(
+        child: ListBody(
+          children: <Widget>[
+            Text(content),
+          ],
+        ),
+      ),
+      actions: <Widget>[
+        MaterialButton(
+          child: const Text('确定'),
+          onPressed: () {
+            Navigator.of(context).pop();
+          },
+        ),
+      ],
+    ),
+  );
+}
+
+/// stream-filter的替代方法
+List<T> filteredList<T>(List<T> list, bool Function(T) filter) {
+  List<T> result = [];
+  for (var element in list) {
+    if (filter(element)) {
+      result.add(element);
+    }
+  }
+  return result;
+}
+
+/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容
+Future<T?> chooseListDialog<T>(
+    BuildContext context, String title, List<T> items,
+    {String? tips}) async {
+  return showDialog<T>(
+    context: context,
+    builder: (BuildContext context) {
+      return SimpleDialog(
+        title: Text(title),
+        children: [
+          ...items.map((e) => SimpleDialogOption(
+                onPressed: () {
+                  Navigator.of(context).pop(e);
+                },
+                child: Text('$e'),
+              )),
+          ...tips != null
+              ? [
+                  Container(
+                    padding: const EdgeInsets.fromLTRB(15, 5, 15, 15),
+                    child: Text(tips),
+                  ),
+                ]
+              : [],
+        ],
+      );
+    },
+  );
+}
+
+/// 创建一个单选对话框, 用户取消选择返回null, 否则返回所选内容(value)
+Future<T?> chooseMapDialog<T>(
+    BuildContext buildContext, Map<String, T> values, String title) async {
+  return await showDialog<T>(
+    context: buildContext,
+    builder: (BuildContext context) {
+      return SimpleDialog(
+        title: Text(title),
+        children: values.entries
+            .map((e) => SimpleDialogOption(
+                  child: Text(e.key),
+                  onPressed: () {
+                    Navigator.of(context).pop(e.value);
+                  },
+                ))
+            .toList(),
+      );
+    },
+  );
+}
+
+/// 输入对话框1
+
+var _controller =
+    TextEditingController.fromValue(const TextEditingValue(text: ''));
+
+Future<String?> displayTextInputDialog(BuildContext context,
+    {String? title,
+    String src = "",
+    String? hint,
+    String? desc,
+    bool isPasswd = false}) {
+  _controller.text = src;
+  return showDialog(
+    context: context,
+    builder: (context) {
+      return AlertDialog(
+        title: title == null ? null : Text(title),
+        content: SingleChildScrollView(
+          child: ListBody(
+            children: [
+              TextField(
+                controller: _controller,
+                decoration: InputDecoration(hintText: hint),
+                obscureText: isPasswd,
+                obscuringCharacter: '\u2022',
+              ),
+              ...(desc == null
+                  ? []
+                  : [
+                      Container(
+                        padding: const EdgeInsets.only(top: 20, bottom: 10),
+                        child: Text(
+                          desc,
+                          style: TextStyle(
+                              fontSize: 12,
+                              color: Theme.of(context)
+                                  .textTheme
+                                  .bodyText1
+                                  ?.color
+                                  ?.withOpacity(.5)),
+                        ),
+                      )
+                    ]),
+            ],
+          ),
+        ),
+        actions: <Widget>[
+          MaterialButton(
+            child: const Text('取消'),
+            onPressed: () {
+              Navigator.of(context).pop();
+            },
+          ),
+          MaterialButton(
+            child: const Text('确认'),
+            onPressed: () {
+              Navigator.of(context).pop(_controller.text);
+            },
+          ),
+        ],
+      );
+    },
+  );
+}
+
+/// 将字符串前面加0直至满足len位
+String add0(int num, int len) {
+  var rsp = "$num";
+  while (rsp.length < len) {
+    rsp = "0$rsp";
+  }
+  return rsp;
+}
+
+
+
+/// 输入对话框2
+
+final TextEditingController _textEditController =
+    TextEditingController(text: '');
+
+Future<String?> inputString(BuildContext context, String title,
+    {String hint = "", String? defaultValue}) async {
+  if (defaultValue != null) {
+    _textEditController.text = defaultValue;
+  } else {
+    _textEditController.clear();
+  }
+  return showDialog(
+    context: context,
+    builder: (context) {
+      return AlertDialog(
+        content: Card(
+          child: SingleChildScrollView(
+            child: ListBody(
+              children: [
+                Text(title),
+                TextField(
+                  controller: _textEditController,
+                  decoration: InputDecoration(
+                    labelText: hint,
+                  ),
+                ),
+              ],
+            ),
+          ),
+        ),
+        actions: <Widget>[
+          MaterialButton(
+            onPressed: () {
+              Navigator.pop(context);
+            },
+            child: const Text('取消'),
+          ),
+          MaterialButton(
+            onPressed: () {
+              Navigator.pop(context, _textEditController.text);
+            },
+            child: const Text('确定'),
+          ),
+        ],
+      );
+    },
+  );
+}

+ 50 - 0
lib/basic/Entities.dart

@@ -0,0 +1,50 @@
+import 'dart:convert';
+
+import 'package:azlistview/azlistview.dart';
+import 'package:prime_chat/basic/Method.dart';
+
+/// 图片
+class RemoteImageInfo {
+  // String originalName;
+  String path;
+  String fileServer;
+
+  RemoteImageInfo(
+      {required this.path, this.fileServer = "$api/file/"});
+
+  RemoteImageInfo.mock() : path = "", fileServer = "";
+
+  RemoteImageInfo.fromJson(Map<String, dynamic> json) :
+    // this.originalName = json["originalName"],
+    path = json["path"],
+    fileServer = json["fileServer"];
+}
+
+class FriendInfo extends UserProfile with ISuspensionBean {
+  String? tagIndex;
+
+  FriendInfo({required super.name, super.avatar, this.tagIndex});
+
+
+  @override
+  String getSuspensionTag() => tagIndex ?? name.substring(0, 1);
+
+  // toJson() => {"username": username, "uid": uid};
+
+  // @override
+  // String toString() => jsonEncode(this);
+}
+
+/// 用户自己的信息
+class UserProfile {
+  String name;
+  final RemoteImageInfo? _avatar;
+
+  UserProfile({required this.name, RemoteImageInfo? avatar}): _avatar = avatar;
+
+  RemoteImageInfo get avatar => _avatar ?? RemoteImageInfo.mock();
+
+  UserProfile.fromJson(Map<String, dynamic> json)
+      : name = json["Name"],
+        _avatar = RemoteImageInfo(path: json["Avatar"]);
+}

+ 235 - 0
lib/basic/Method.dart

@@ -0,0 +1,235 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
+import 'package:flutter_styled_toast/flutter_styled_toast.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:permission_handler/permission_handler.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'Entities.dart';
+
+
+const api = "http://api.ignatz.xyz";
+
+final method = Method._();
+
+class Method {
+  Method._();
+
+  var dio = Dio();
+
+  var options = Options();
+
+  Future<int> home() async {
+    try {
+      final res = await dio.get("$api/home");
+      if (res.statusCode == HttpStatus.ok) {
+        var data = jsonDecode(res.toString());
+        final uid = data["uid"];
+        if (await setUID(uid)) return uid;
+      }
+    } on Exception catch(e) {
+      debugPrint("home 网络错误");
+      // debugPrint(e.toString());
+      return 0;
+    }
+    return 0;
+  }
+
+  Future<bool> login() async {
+    final username = await getUsername();
+    final password = await getPassword();
+    debugPrint('login: username=$username password=$password');
+    if (username.isEmpty || password.isEmpty) return false;
+    try {
+      final res = await dio.post("$api/token", data: {
+        "username": username, "password": password});
+      if (res.statusCode == HttpStatus.ok) {
+        var data = jsonDecode(res.toString());
+        final token = data['token'];
+        dio = Dio(BaseOptions(headers: {
+          HttpHeaders.authorizationHeader: "Bearer $token"
+        }));
+
+        debugPrint('token=$token');
+        setToken(token);
+        return true;
+      }
+    } on Exception catch(e) {
+      debugPrint("login 网络错误");
+      return false;
+    }
+    return false;
+  }
+
+  Future<List<UserProfile>> getFriends({int depth=0}) async {
+    try {
+      final res = await dio.get('$api/friends');
+      switch (res.statusCode) {
+        case HttpStatus.ok:
+          var data = jsonDecode(res.toString());
+          List<UserProfile> friends = [];
+          data["friends"].forEach((v) => friends.add(UserProfile.fromJson(v)));
+          debugPrint(friends.toString());
+          return friends;
+        case HttpStatus.unauthorized:
+          if (depth < 3) {
+            login();
+            return getFriends(depth: depth+1);
+          }
+      }
+    } on Exception catch (e) {
+      debugPrint("网络错误");
+      return [];
+    }
+    return [];
+  }
+
+  Future<String> getUsername() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.getString("username") ?? "";
+  }
+  Future<String> getPassword() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.getString("password") ?? "";
+  }
+
+  Future<bool> setUsername(String username) async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.setString("username", username);
+  }
+  Future<bool> setUID(int uid) async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.setInt("uid", uid);
+  }
+
+  Future<int> getUID() async {
+    final prefs = await SharedPreferences.getInstance();
+    int uid = prefs.getInt("uid") ?? 0;
+    if (uid == 0) uid = await home();
+    print('uid=$uid');
+    return uid;
+  }
+
+  Future<bool> setPassword(String password) async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.setString("password", password);
+  }
+
+  Future<bool> clearPassword() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.remove("password");
+  }
+
+  Future<bool> clearToken() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.remove("token");
+  }
+  Future<bool> clearUID() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.remove("uid");
+  }
+
+  Future<String> getToken() async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.getString("token") ?? "";
+  }
+
+  Future<bool> setToken(String token) async {
+    final prefs = await SharedPreferences.getInstance();
+    return prefs.setString("token", token);
+  }
+
+  Future<bool> preLogin() async {
+    if (await home() > 0) {
+      return true;
+    }
+    return login();
+  }
+
+  logout() async {
+    await method.clearToken();
+    await method.clearPassword();
+    await method.clearUID();
+    dio = Dio();
+  }
+
+  Future<UserProfile?> getProfile([String? name]) async {
+    name = name ?? await getUsername();
+    debugPrint("get profile of $name");
+    final res = await dio.get("$api/profile/$name");
+    if (res.statusCode == HttpStatus.ok) {
+      final data = jsonDecode(res.toString());
+      final profile = UserProfile.fromJson(data["profile"]);
+      debugPrint("profile get, name=${profile.name}");
+      return profile;
+    }
+    return null;
+  }
+
+  Future download(
+      String url,
+      String savePath, {
+        Map<String, dynamic>? queryParams,
+        CancelToken? cancelToken,
+        dynamic data,
+        Options? options,
+        void Function(int, int)? onReceiveProgress,
+      }) async {
+    try {
+      return await dio.download(
+        url,
+        savePath,
+        queryParameters: queryParams,
+        cancelToken: cancelToken,
+        onReceiveProgress: onReceiveProgress,
+      );
+    } on DioError catch (e) {
+      print(e);
+      if (CancelToken.isCancel(e)) {
+        EasyLoading.showInfo('下载已取消!');
+      } else {
+        if (e.response != null) {
+          // _handleErrorResponse(e.response);
+        } else {
+          EasyLoading.showError(e.message);
+        }
+      }
+    } on Exception catch (e) {
+      EasyLoading.showError(e.toString());
+    }
+  }
+
+  Future<String?> downloadImage(String path, String fileServer) async {
+    Directory dir = await getApplicationDocumentsDirectory();
+    String dirPath = dir.path;
+    final savePath = "$dirPath/$path";
+    File file = File(savePath);
+    debugPrint("savePath=$savePath");
+    if (file.existsSync()) return savePath;
+
+    try {
+      final res = await dio.download('$fileServer/$path', savePath);
+      debugPrint(res.toString());
+    } on Exception catch (e) {
+      debugPrint("文件下载失败");
+      return null;
+    }
+    return savePath;
+  }
+
+  Future<bool> updateAvatar(Uint8List buff, String suf) async {
+    final file = MultipartFile.fromBytes(buff, filename: 'avatar$suf');
+    final res = await dio.post("$api/avatar", data: FormData.fromMap({"file": file}));
+    if (res.statusCode == HttpStatus.ok) {
+      return true;
+    }
+    return false;
+  }
+
+
+}

+ 31 - 0
lib/basic/enum/ErrorTypes.dart

@@ -0,0 +1,31 @@
+const ERROR_TYPE_NETWORK = "NETWORK_ERROR";
+const ERROR_TYPE_PERMISSION = "PERMISSION_ERROR";
+const ERROR_TYPE_TIME = "TIME_ERROR";
+const ERROR_TYPE_UNDER_REVIEW = "UNDER_VIEW_ERROR";
+
+// 错误的类型, 方便照展示和谐的提示
+String errorType(String error) {
+  // EXCEPTION
+  // Get "https://picaapi.picacomic.com/categories": net/http: TLS handshake timeout
+  // Get "https://picaapi.picacomic.com/comics?c=%E9%95%B7%E7%AF%87&s=ua&page=1": proxyconnect tcp: dial tcp 192.168.123.217:1080: connect: connection refused
+  // Get "https://picaapi.picacomic.com/comics?c=%E5%85%A8%E5%BD%A9&s=ua&page=1": context deadline exceeded (Client.Timeout exceeded while awaiting headers)
+  if (error.contains("timeout") ||
+      error.contains("connection refused") ||
+      error.contains("deadline") ||
+      error.contains("connection abort") ||
+      error.contains("certificate") ||
+      error.contains("x509") ||
+      error.contains("ssl")) {
+    return ERROR_TYPE_NETWORK;
+  }
+  if (error.contains("permission denied")) {
+    return ERROR_TYPE_PERMISSION;
+  }
+  if (error.contains("time is not synchronize")) {
+    return ERROR_TYPE_TIME;
+  }
+  if (error.contains("under review")) {
+    return ERROR_TYPE_UNDER_REVIEW;
+  }
+  return "";
+}

+ 16 - 88
lib/main.dart

@@ -1,6 +1,15 @@
 import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
+import 'package:prime_chat/screens/InitScreen.dart';
 
 void main() {
+  WidgetsFlutterBinding.ensureInitialized();
+  // 强制竖屏
+  SystemChrome.setPreferredOrientations([
+    DeviceOrientation.portraitUp,
+    DeviceOrientation.portraitDown
+  ]);
   runApp(const MyApp());
 }
 
@@ -13,95 +22,14 @@ class MyApp extends StatelessWidget {
     return MaterialApp(
       title: 'Flutter Demo',
       theme: ThemeData(
-        // This is the theme of your application.
-        //
-        // Try running your application with "flutter run". You'll see the
-        // application has a blue toolbar. Then, without quitting the app, try
-        // changing the primarySwatch below to Colors.green and then invoke
-        // "hot reload" (press "r" in the console where you ran "flutter run",
-        // or simply save your changes to "hot reload" in a Flutter IDE).
-        // Notice that the counter didn't reset back to zero; the application
-        // is not restarted.
-        primarySwatch: Colors.blue,
+          primarySwatch: Colors.blue,
+          appBarTheme: const AppBarTheme(
+            color: Colors.black12,
+            titleTextStyle: TextStyle(color: Colors.black, fontSize: 18, fontWeight: FontWeight.w500),
+            elevation: 0,
+          )
       ),
-      home: const MyHomePage(title: 'Flutter Demo Home Page'),
-    );
-  }
-}
-
-class MyHomePage extends StatefulWidget {
-  const MyHomePage({super.key, required this.title});
-
-
-  final String title;
-
-  @override
-  State<MyHomePage> createState() => _MyHomePageState();
-}
-
-class _MyHomePageState extends State<MyHomePage> {
-  int _counter = 0;
-
-  void _incrementCounter() {
-    setState(() {
-      // This call to setState tells the Flutter framework that something has
-      // changed in this State, which causes it to rerun the build method below
-      // so that the display can reflect the updated values. If we changed
-      // _counter without calling setState(), then the build method would not be
-      // called again, and so nothing would appear to happen.
-      _counter++;
-    });
-  }
-
-  @override
-  Widget build(BuildContext context) {
-    // This method is rerun every time setState is called, for instance as done
-    // by the _incrementCounter method above.
-    //
-    // The Flutter framework has been optimized to make rerunning build methods
-    // fast, so that you can just rebuild anything that needs updating rather
-    // than having to individually change instances of widgets.
-    return Scaffold(
-      appBar: AppBar(
-        // Here we take the value from the MyHomePage object that was created by
-        // the App.build method, and use it to set our appbar title.
-        title: Text(widget.title),
-      ),
-      body: Center(
-        // Center is a layout widget. It takes a single child and positions it
-        // in the middle of the parent.
-        child: Column(
-          // Column is also a layout widget. It takes a list of children and
-          // arranges them vertically. By default, it sizes itself to fit its
-          // children horizontally, and tries to be as tall as its parent.
-          //
-          // Invoke "debug painting" (press "p" in the console, choose the
-          // "Toggle Debug Paint" action from the Flutter Inspector in Android
-          // Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
-          // to see the wireframe for each widget.
-          //
-          // Column has various properties to control how it sizes itself and
-          // how it positions its children. Here we use mainAxisAlignment to
-          // center the children vertically; the main axis here is the vertical
-          // axis because Columns are vertical (the cross axis would be
-          // horizontal).
-          mainAxisAlignment: MainAxisAlignment.center,
-          children: <Widget>[
-            const Text(
-              'You have pushed the button this many times:',
-            ),
-            Text(
-              '$_counter',
-              style: Theme.of(context).textTheme.headline4,
-            ),
-          ],
-        ),
-      ),
-      floatingActionButton: FloatingActionButton(
-        onPressed: _incrementCounter,
-        tooltip: 'Increment',
-        child: const Icon(Icons.add),
-      ), // This trailing comma makes auto-formatting nicer for build methods.
+      home: const InitScreen(),
     );
   }
 }

+ 78 - 0
lib/screens/AboutScreen.dart

@@ -0,0 +1,78 @@
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+
+import 'components/RightClickPop.dart';
+
+const _releasesUrl = "https://github.com/niuhuan/pikapika/releases";
+
+// 关于
+class AboutScreen extends StatelessWidget {
+
+  const AboutScreen({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return rightClickPop(
+      child: buildScreen(context),
+      context: context,
+      canPop: true,
+    );
+  }
+
+  Widget buildScreen(BuildContext context) {
+    var size = MediaQuery.of(context).size;
+    var min = size.width < size.height ? size.width : size.height;
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('关于'),
+      ),
+      body: ListView(
+        children: [
+          SizedBox(
+            width: min / 2,
+            height: min / 2,
+            child: Center(
+              child: SvgPicture.asset(
+                'lib/assets/github.svg',
+                width: min / 3,
+                height: min / 3,
+                color: Colors.grey.shade500,
+              ),
+            ),
+          ),
+          Container(height: 20),
+          const Divider(),
+          Container(
+            padding: const EdgeInsets.only(left: 20, right: 20),
+            child: Column(
+              crossAxisAlignment: CrossAxisAlignment.start,
+              children: const [
+                Text(
+                  '软件版本 : v0.1',
+                  style: TextStyle(
+                    height: 1.3,
+                  ),
+                ),
+              ],
+            ),
+          ),
+          const Divider(),
+          Container(
+            padding: const EdgeInsets.all(20),
+            child: const SelectableText(
+              "提示 : \n"
+              "1. 提示一\n"
+              "2. 提示二\n",
+              style: TextStyle(
+                height: 1.3,
+              ),
+            ),
+          ),
+          const Divider(),
+        ],
+      ),
+    );
+  }
+
+}

+ 175 - 0
lib/screens/AccountScreen.dart

@@ -0,0 +1,175 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import '../basic/Common.dart';
+import '../basic/Method.dart';
+import '../basic/enum/ErrorTypes.dart';
+import 'AboutScreen.dart';
+import 'AppScreen.dart';
+import 'components/ContentLoading.dart';
+
+// 账户设置
+class AccountScreen extends StatefulWidget {
+  const AccountScreen({Key? key}) : super(key: key);
+
+  @override
+  _AccountScreenState createState() => _AccountScreenState();
+}
+
+class _AccountScreenState extends State<AccountScreen> {
+  late bool _logging = false;
+  late String _username = "";
+  late String _password = "";
+  late StreamSubscription<String?> _linkSubscription;
+
+  @override
+  void initState() {
+    // _linkSubscription = linkSubscript(context);
+    _loadProperties();
+    super.initState();
+    // Future.delayed(Duration.zero, () async {
+    //   versionPop(context);
+    //   versionEvent.subscribe(_versionSub);
+    // });
+  }
+
+  // @override
+  // void dispose() {
+    // _linkSubscription.cancel();
+    // versionEvent.unsubscribe(_versionSub);
+    // super.dispose();
+  // }
+
+  _versionSub(_) {
+    // versionPop(context);
+  }
+
+
+  Future _loadProperties() async {
+    final username = await method.getUsername();
+    final password = await method.getPassword();
+    setState(() {
+      _username = username;
+      _password = password;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_logging) {
+      return _buildLogging();
+    }
+    return _buildGui();
+  }
+
+  Widget _buildLogging() {
+    return const Scaffold(
+      body: ContentLoading(label: '登录中'),
+    );
+  }
+
+  Widget _buildGui() {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('登录'),
+        actions: [
+          IconButton(
+            onPressed: () {
+              Navigator.push(
+                context,
+                MaterialPageRoute(
+                  builder: (context) => const AboutScreen(
+                  ),
+                ),
+              );
+            },
+            icon: const Text('关于'),
+          ),
+          IconButton(
+            onPressed: _logIn,
+            icon: const Icon(Icons.save),
+          ),
+        ],
+      ),
+      body: ListView(
+        children: [
+          ListTile(
+            title: const Text("账号"),
+            subtitle: Text(_username == "" ? "未设置" : _username),
+            onTap: () async {
+              String? input = await displayTextInputDialog(
+                context,
+                src: _username,
+                title: '账号',
+                hint: '请输入账号',
+              );
+              if (input != null) {
+                await method.setUsername(input);
+                setState(() {
+                  _username = input;
+                });
+              }
+            },
+          ),
+          ListTile(
+            title: const Text("密码"),
+            subtitle: Text(_password == "" ? "未设置" : '\u2022' * 10),
+            onTap: () async {
+              String? input = await displayTextInputDialog(
+                context,
+                src: _password,
+                title: '密码',
+                hint: '请输入密码',
+                isPasswd: true,
+              );
+              if (input != null) {
+                await method.setPassword(input);
+                setState(() {
+                  _password = input;
+                });
+              }
+            },
+          ),
+        ],
+      ),
+    );
+  }
+
+  _logIn() async {
+    setState(() {
+      _logging = true;
+    });
+    try {
+      await method.login();
+      if (!mounted) return; // don't use context after async if you're not sure your widget is mounted.
+      Navigator.pushReplacement(
+        context,
+        MaterialPageRoute(builder: (context) => const AppScreen()),
+      );
+    } catch (e, s) {
+      print("$e\n$s");
+      setState(() {
+        _logging = false;
+      });
+      var message = "请检查账号密码或网络环境";
+      switch (errorType("$e")) {
+        case ERROR_TYPE_NETWORK:
+          message = "网络不通";
+          break;
+        case ERROR_TYPE_TIME:
+          message = "请检查设备时间";
+          break;
+      }
+      if ("$e".contains("email") && "$e".contains("password")) {
+        message = "请检查账号密码";
+      }
+      alertDialog(
+        context,
+        '登录失败',
+        "$message\n$e",
+      );
+    }
+  }
+}

+ 31 - 0
lib/screens/AddFriendScreen.dart

@@ -0,0 +1,31 @@
+import 'package:flutter/material.dart';
+import 'package:prime_chat/basic/Entities.dart';
+
+class AddFriendScreen extends StatefulWidget {
+  const AddFriendScreen(this.profile, {Key? key}) : super(key: key);
+
+  final UserProfile profile;
+
+  @override
+  State<AddFriendScreen> createState() => _AddFriendScreenState();
+}
+
+class _AddFriendScreenState extends State<AddFriendScreen> {
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text("添加好友"),
+      ),
+      body: Column(
+        children: const [
+          TextField(
+            decoration: InputDecoration(
+              hintText: "消息"
+            ),
+          )
+        ],
+      ),
+    );
+  }
+}

+ 89 - 0
lib/screens/AppScreen.dart

@@ -0,0 +1,89 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_styled_toast/flutter_styled_toast.dart';
+import 'package:prime_chat/screens/ContactsScreen.dart';
+import 'package:prime_chat/screens/SpaceScreen.dart';
+
+// MAIN UI 底部导航栏
+class AppScreen extends StatefulWidget {
+  const AppScreen({Key? key}) : super(key: key);
+
+  @override
+  State<AppScreen> createState() => _AppScreenState();
+}
+
+class _AppScreenState extends State<AppScreen> {
+  late StreamSubscription<String?> _linkSubscription;
+
+
+  static const List<Widget> _widgetOptions = <Widget>[
+    ContactsScreen(),
+    SpaceScreen(),
+  ];
+
+  late int _selectedIndex = 0;
+
+  void _onItemTapped(int index) {
+    setState(() {
+      _selectedIndex = index;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    final body = Scaffold(
+      body: IndexedStack(
+        index: _selectedIndex,
+        children: _widgetOptions,
+      ),
+      bottomNavigationBar: BottomNavigationBar(
+        items: const <BottomNavigationBarItem>[
+          BottomNavigationBarItem(
+            icon: Icon(Icons.people),
+            label: '通讯录',
+          ),
+          BottomNavigationBarItem(
+            icon: Icon(Icons.face),
+            label: '我的',
+          ),
+        ],
+        currentIndex: _selectedIndex,
+        iconSize: 20,
+        selectedFontSize: 12,
+        unselectedFontSize: 12,
+        onTap: _onItemTapped,
+      ),
+    );
+    return willPop(body);
+  }
+
+  int _noticeTime = 0;
+
+  Widget willPop(Scaffold body) {
+    return WillPopScope(
+      child: body,
+      onWillPop: () async {
+        final now = DateTime.now().millisecondsSinceEpoch;
+        if (_noticeTime + 3000 > now) {
+          return true;
+        } else {
+          _noticeTime = now;
+          showToast(
+            "再次返回将会退出应用程序",
+            context: context,
+            position: StyledToastPosition.center,
+            animation: StyledToastAnimation.scale,
+            reverseAnimation: StyledToastAnimation.fade,
+            duration: const Duration(seconds: 3),
+            animDuration: const Duration(milliseconds: 300),
+            curve: Curves.elasticOut,
+            reverseCurve: Curves.linear,
+          );
+          return false;
+        }
+      },
+    );
+  }
+}

+ 204 - 0
lib/screens/ContactsScreen.dart

@@ -0,0 +1,204 @@
+import 'package:azlistview/azlistview.dart';
+import 'package:flutter/material.dart';
+import 'package:lpinyin/lpinyin.dart';
+import 'package:prime_chat/screens/ProfileScreen.dart';
+import '../basic/Common.dart';
+
+import '../basic/Entities.dart';
+import '../basic/Method.dart';
+import 'NewFriendScreen.dart';
+import 'components/Avatar.dart';
+
+// 好友列表页面
+class ContactsScreen extends StatefulWidget {
+  const ContactsScreen({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _ContactsScreenState();
+}
+
+class _ContactsScreenState extends State<ContactsScreen> {
+
+  List<FriendInfo> _friends = [];
+  double susItemHeight = 40;
+
+  @override
+  void initState() {
+    super.initState();
+    _loadFriends();
+  }
+
+  void _loadFriends() {
+    method.getFriends().then((value) {
+      _friends = [];
+      for (var e in value) {
+        _friends.add(FriendInfo(name: e.name, avatar: e.avatar));
+      }
+      _handleList(_friends);
+    });
+  }
+
+  void _handleList(List<FriendInfo> list) {
+    for (int i = 0, length = list.length; i < length; i++) {
+      String pinyin = PinyinHelper.getPinyinE(list[i].name);
+      String tag = pinyin.substring(0, 1).toUpperCase();
+      // list[i].namePinyin = pinyin;
+      if (RegExp("[A-Z]").hasMatch(tag)) {
+        list[i].tagIndex = tag;
+      } else {
+        list[i].tagIndex = "#";
+      }
+    }
+    // A-Z sort.
+    SuspensionUtil.sortListBySuspensionTag(_friends);
+
+    // show sus tag.
+    SuspensionUtil.setShowSuspensionStatus(_friends);
+
+    // add header.
+    _friends.insert(0, FriendInfo(name: 'header', tagIndex: '↑'));
+
+    setState(() {});
+  }
+
+  Widget _buildHeader() {
+    return Container(
+      padding: const EdgeInsets.all(20),
+      alignment: Alignment.center,
+      child: Column(
+        // mainAxisAlignment: MainAxisAlignment.center,
+        children: <Widget>[
+
+          ListTile(
+            leading: ClipRRect(
+              borderRadius: BorderRadius.circular(10.0),
+              child: Container(
+                color: Colors.orange,
+                height: 40.0,
+                width: 40.0,
+                child: const Icon(Icons.person_add, color: Colors.white, size: 30.0,),
+              ),
+            ),
+            title: const Text("新的好友"),
+            onTap: () {
+              Navigator.push(
+                context,
+                MaterialPageRoute(builder: (context) => const NewFriendScreen()),
+              );
+            },
+          )
+        ],
+      ),
+    );
+  }
+
+  Widget _buildSusWidget(String susTag) {
+    return Container(
+      padding: const EdgeInsets.symmetric(horizontal: 15.0),
+      height: susItemHeight,
+      width: double.infinity,
+      alignment: Alignment.centerLeft,
+      child: Row(
+        children: <Widget>[
+          Text(
+            susTag,
+            textScaleFactor: 1.2,
+          ),
+          const Expanded(
+              child: Divider(
+                height: .0,
+                indent: 10.0,
+              ))
+        ],
+      ),
+    );
+  }
+
+  Widget _buildListItem(FriendInfo model) {
+    String susTag = model.getSuspensionTag();
+    return Column(
+      children: <Widget>[
+        Offstage(
+          offstage: model.isShowSuspension != true,
+          child: _buildSusWidget(susTag),
+        ),
+        ListTile(
+          leading: Avatar(model.avatar),
+          title: Text(model.name),
+          onTap: () {
+            Navigator.push(
+                context, MaterialPageRoute(
+                builder: (context) => ProfileScreen(
+                    profile: model, isFriend: true)));
+          },
+        )
+      ],
+    );
+  }
+
+  Decoration getIndexBarDecoration(Color color) {
+    return BoxDecoration(
+        color: color,
+        borderRadius: BorderRadius.circular(20.0),
+        border: Border.all(color: Colors.grey[300]!, width: .5));
+  }
+
+  Widget _buildList(BuildContext context) {
+    return AzListView(
+      data: _friends,
+      itemCount: _friends.length,
+      itemBuilder: (BuildContext context, int index) {
+        if (index == 0) return _buildHeader();
+        final model = _friends[index];
+        return _buildListItem(model);
+      },
+      physics: const BouncingScrollPhysics(),
+      indexBarData: SuspensionUtil.getTagIndexList(_friends),
+      indexHintBuilder: (context, hint) {
+        return Container(
+          alignment: Alignment.center,
+          width: 60.0,
+          height: 60.0,
+          decoration: BoxDecoration(
+            color: Colors.blue[700]!.withAlpha(200),
+            shape: BoxShape.circle,
+          ),
+          child:
+          Text(hint, style: const TextStyle(color: Colors.white, fontSize: 30.0)),
+        );
+      },
+      indexBarMargin: const EdgeInsets.all(10),
+      indexBarOptions: IndexBarOptions(
+        needRebuild: true,
+        decoration: getIndexBarDecoration(Colors.grey[50]!),
+        downDecoration: getIndexBarDecoration(Colors.grey[200]!),
+      ),
+    );
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('好友'),
+        actions: [
+          IconButton(
+            onPressed: () async {
+              // Navigator.push(
+              //   context,
+              //   MaterialPageRoute(builder: (context) => const AddFriendScreen()),
+              // );
+            },
+            icon: const Icon(Icons.add),
+          ),
+        ],
+      ),
+      body: RefreshIndicator(
+        onRefresh: () async {
+          _loadFriends();
+        },
+        child: _buildList(context),
+      ),
+    );
+  }
+}

+ 59 - 0
lib/screens/FilePhotoViewScreen.dart

@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+
+import '../basic/Common.dart';
+import 'components/Images.dart';
+import 'components/RightClickPop.dart';
+import 'package:photo_view/photo_view.dart';
+
+// 预览图片
+class FilePhotoViewScreen extends StatelessWidget {
+  final String filePath;
+
+  const FilePhotoViewScreen(this.filePath, {Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context){
+    return rightClickPop(
+      child: buildScreen(context),
+      context: context,
+      canPop: true,
+    );
+  }
+
+  Widget buildScreen(BuildContext context) => Scaffold(
+        body: Stack(
+          children: [
+            GestureDetector(
+              onLongPress: () async {
+                String? choose =
+                    await chooseListDialog(context, '请选择', ['保存图片']);
+                switch (choose) {
+                  case '保存图片':
+                    // TODO: saveImage
+                    // saveImage(filePath, context);
+                    break;
+                }
+              },
+              child: PhotoView(
+                imageProvider: ResourceFileImageProvider(filePath),
+              ),
+            ),
+            InkWell(
+              onTap: () => Navigator.of(context).pop(),
+              child: Container(
+                margin: const EdgeInsets.only(top: 30),
+                padding: const EdgeInsets.only(left: 4, right: 4),
+                decoration: BoxDecoration(
+                  color: Colors.black.withOpacity(.75),
+                  borderRadius: const BorderRadius.only(
+                    topRight: Radius.circular(8),
+                    bottomRight: Radius.circular(8),
+                  ),
+                ),
+                child: const Icon(Icons.keyboard_backspace, color: Colors.white),
+              ),
+            ),
+          ],
+        ),
+      );
+}

+ 90 - 0
lib/screens/InitScreen.dart

@@ -0,0 +1,90 @@
+
+// 初始化界面
+import 'package:flutter/material.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
+import '../basic/Method.dart';
+import 'AccountScreen.dart';
+import 'AppScreen.dart';
+import 'LoginScreen.dart';
+
+class InitScreen extends StatefulWidget {
+  const InitScreen({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _InitScreenState();
+}
+
+class _InitScreenState extends State<InitScreen> {
+  var _authenticating = false;
+
+  @override
+  initState() {
+    _init();
+    super.initState();
+  }
+
+  Future<dynamic> _init() async {
+
+    if (_authenticating) {
+      _goAuthentication();
+    } else {
+      _goApplication();
+    }
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    if (_authenticating) {
+      return Scaffold(
+        appBar: AppBar(
+          title: const Text("身份验证"),
+        ),
+        body: Center(
+          child: Container(
+            padding: const EdgeInsets.all(20),
+            child: MaterialButton(
+              onPressed: () {
+                _goAuthentication();
+              },
+              child:
+              const Text('您在之前使用APP时开启了身份验证, 请点这段文字进行身份核查, 核查通过后将会进入APP'),
+            ),
+          ),
+        ),
+      );
+    }
+    return const Scaffold(
+      backgroundColor: Colors.white,
+      /*body: ConstrainedBox(
+        constraints: const BoxConstraints.expand(),
+        child: Image.asset(
+          "lib/assets/init.jpg",
+          fit: BoxFit.contain,
+        ),
+      ),*/
+    );
+  }
+
+  Future _goApplication() async {
+    // 登录, 如果token失效重新登录, 网络不好的时候可能需要1分钟
+    if (await method.preLogin()) {
+      // 如果token或username+password有效则直接进入登录好的界面
+      Navigator.pushReplacement(
+        context,
+        MaterialPageRoute(builder: (context) => const AppScreen()),
+      );
+    } else {
+      // 否则跳转到登录页
+      Navigator.pushReplacement(
+        context,
+        MaterialPageRoute(builder: (context) => LoginScreen()),
+      );
+    }
+  }
+
+  Future _goAuthentication() async {
+    _goApplication();
+  }
+}

+ 67 - 0
lib/screens/LoginScreen.dart

@@ -0,0 +1,67 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_login/flutter_login.dart';
+
+import '../basic/Method.dart';
+import 'AppScreen.dart';
+// import 'dashboard_screen.dart';
+
+const users = {
+  'dribbble@gmail.com': '12345',
+  'hunter@gmail.com': 'hunter',
+};
+
+class LoginScreen extends StatelessWidget {
+  Duration get loginTime => Duration(milliseconds: 2250);
+
+  _authUser(BuildContext context) {
+    return (LoginData data) {
+      debugPrint('Name: ${data.name}, Password: ${data.password}');
+      method.setUsername(data.name);
+      method.setPassword(data.password);
+      method.login().then((value) {
+        if (!value) return false;
+        Navigator.pushReplacement(
+          context,
+          MaterialPageRoute(builder: (context) => const AppScreen()),
+        );
+        return true;
+      });
+      // if (!widget.mounted) return; // don't use context after async if you're not sure your widget is mounted.
+    };
+  }
+
+  Future<String?> _signupUser(SignupData data) {
+    debugPrint('Signup Name: ${data.name}, Password: ${data.password}');
+    return Future.delayed(loginTime).then((_) {
+      return null;
+    });
+  }
+
+  Future<String?> _recoverPassword(String name) {
+    debugPrint('Name: $name');
+    return Future.delayed(loginTime).then((_) {
+      if (!users.containsKey(name)) {
+        return 'User not exists';
+      }
+      return null;
+    });
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    return FlutterLogin(
+      title: 'PrimeChat',
+      logo: const AssetImage('lib/assets/avatar.jpg'),
+      userType: LoginUserType.name,
+      userValidator: (_) => null,
+      onLogin: _authUser(context),
+      onSignup: _signupUser,
+      onSubmitAnimationCompleted: () {
+        // Navigator.of(context).pushReplacement(MaterialPageRoute(
+        //   builder: (context) => DashboardScreen(),
+        // ));
+      },
+      onRecoverPassword: _recoverPassword,
+    );
+  }
+}

+ 80 - 0
lib/screens/NewFriendScreen.dart

@@ -0,0 +1,80 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:prime_chat/screens/components/Images.dart';
+
+import 'SearchScreen.dart';
+import 'components/RightClickPop.dart';
+
+class NewFriendScreen extends StatefulWidget {
+  const NewFriendScreen({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _NewFriendScreenState();
+}
+
+class _NewFriendScreenState extends State<NewFriendScreen> {
+  @override
+  Widget build(BuildContext context) {
+    return rightClickPop(
+      child: buildScreen(context),
+      context: context,
+      canPop: true,
+    );
+  }
+
+  buildScreen(BuildContext context) {
+    return Scaffold(
+        appBar: AppBar(
+          title: const Text('新的朋友'),
+        ),
+        body: ListView.builder(
+            itemCount: 1,
+            itemBuilder: (context, index) {
+              if (index == 0) return _buildHeader(context);
+              return const Placeholder();
+            }));
+  }
+
+  Widget _buildHeader(BuildContext context) {
+    final theme = Theme.of(context);
+    return Container(
+      height: 60,
+      color: theme.appBarTheme.backgroundColor,
+      child: _buildSearchBar(context),
+    );
+  }
+
+  _buildSearchBar(context) {
+    final child = Container(
+      margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 20),
+      padding: const EdgeInsets.all(5),
+      decoration: BoxDecoration(
+        color: Colors.white,
+        borderRadius: BorderRadius.circular(6.0),
+      ),
+      child: Center(
+          child: FittedBox(
+              child: Row(
+                children: const [
+                  Icon(Icons.search, color: Colors.grey,),
+                  Text(
+                    "用户名",
+                    style: TextStyle(color: Colors.grey),
+                  )
+                ],
+              ))),
+    );
+    return GestureDetector(
+      onTap: () =>
+          Navigator.push(
+              context,
+              MaterialPageRoute(builder: (context) => const SearchScreen())),
+      child: child,
+    );
+  }
+
+  void _search(String value) {
+    debugPrint("搜索好友 $value");
+  }
+}

+ 82 - 0
lib/screens/ProfileScreen.dart

@@ -0,0 +1,82 @@
+import 'package:flutter/material.dart';
+import 'package:prime_chat/basic/Entities.dart';
+import 'package:prime_chat/screens/AddFriendScreen.dart';
+import 'package:prime_chat/screens/components/Avatar.dart';
+import 'package:prime_chat/screens/components/UserProfileCard.dart';
+
+class ProfileScreen extends StatelessWidget {
+  const ProfileScreen({Key? key, required this.profile, required this.isFriend})
+      : super(key: key);
+
+  final bool isFriend;
+  final UserProfile profile;
+
+  @override
+  Widget build(BuildContext context) {
+    debugPrint("build profile screen of ${profile.name}");
+    List<Widget> children = [
+      UserProfileCard(load: () async => profile),
+      const Divider(),
+    ];
+
+    if (isFriend) {
+      children
+          .add(_buildItem(context, Icons.messenger_outline, "发消息", onTap: () {
+        // Navigator.push(context,
+        //     MaterialPageRoute(builder: (context) => AddFriendScreen(profile)));
+      }));
+    } else {
+      children.add(_buildItem(context, Icons.person_add_alt_outlined, "加好友", onTap: () {
+        Navigator.push(context,
+            MaterialPageRoute(builder: (context) => AddFriendScreen(profile)));
+      }));
+    }
+
+    return Scaffold(
+      appBar: AppBar(),
+      body: Column(children: children),
+    );
+  }
+
+  Widget _buildAddFriend(BuildContext context) {
+    return GestureDetector(
+      child: const Text("添加好友"),
+      onTap: () {
+        Navigator.push(context,
+            MaterialPageRoute(builder: (context) => AddFriendScreen(profile)));
+      },
+    );
+  }
+
+  Widget _buildItem(BuildContext context, IconData icon, String title,
+      {void Function()? onTap}) {
+    return InkWell(
+      onTap: onTap,
+      child: Center(
+          child: Row(
+              mainAxisSize: MainAxisSize.min,
+              crossAxisAlignment: CrossAxisAlignment.center,
+              children: [
+            Icon(
+              icon,
+            ),
+            const SizedBox(
+              width: 10,
+              height: 50,
+            ),
+            Text(
+              title,
+              style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w300),
+            ),
+          ])),
+    );
+  }
+
+  Widget _buildInfo(BuildContext context) {
+    return Column(
+      children: [
+        Text(profile.name),
+      ],
+    );
+  }
+}

+ 134 - 0
lib/screens/SearchScreen.dart

@@ -0,0 +1,134 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_easyloading/flutter_easyloading.dart';
+import 'package:flutter_styled_toast/flutter_styled_toast.dart';
+import 'package:prime_chat/screens/ProfileScreen.dart';
+import 'package:prime_chat/screens/components/Avatar.dart';
+import 'package:prime_chat/screens/components/ItemBuilder.dart';
+
+import '../basic/Method.dart';
+import 'components/ProgressDialog.dart';
+
+class SearchScreen extends StatefulWidget {
+  const SearchScreen({Key? key}) : super(key: key);
+
+  @override
+  State<SearchScreen> createState() => _SearchScreenState();
+}
+
+class _SearchScreenState extends State<SearchScreen> {
+  bool loading = false;
+  List<Widget> children = [];
+
+  @override
+  Widget build(BuildContext context) {
+    return ProgressDialog(
+        loading: loading,
+        label: "搜索中...",
+        child: Scaffold(
+          body: Column(
+            children: [
+              SearchBar(
+                onSubmitted: (value) => _search(value),
+              ),
+              ...children
+            ],
+          ),
+        ));
+  }
+
+  _search(String value) async {
+    setState(() {
+      loading = true;
+    });
+    final profile = await method.getProfile(value);
+    debugPrint("get profile = ${profile?.name}");
+    setState(() {
+      loading = false;
+      if (profile != null) {
+        children.add(ListTile(
+          leading: Avatar(profile.avatar),
+          title: Text(profile.name),
+          onTap: () {
+            Navigator.push(
+                context,
+                MaterialPageRoute(
+                    builder: (context) =>
+                        ProfileScreen(profile: profile, isFriend: false)));
+          },
+        ));
+      }
+    });
+  }
+}
+
+class SearchBar extends StatelessWidget {
+  const SearchBar({Key? key, this.onSubmitted, this.onChanged})
+      : super(key: key);
+
+  final void Function(String)? onSubmitted;
+  final void Function(String)? onChanged;
+
+  @override
+  Widget build(BuildContext context) {
+    final theme = Theme.of(context);
+    return Container(
+      height: 80,
+      color: theme.appBarTheme.backgroundColor,
+      child: Column(
+        children: [
+          const SizedBox(
+            height: 30,
+          ),
+          SizedBox(
+            height: 40,
+            child: Row(
+              children: [
+                Expanded(
+                    child: Container(
+                  // color: Colors.red,
+                  // width: MediaQuery.of(context).size.width - 50,
+                  margin: const EdgeInsets.symmetric(horizontal: 10),
+                  padding: const EdgeInsets.symmetric(horizontal: 10),
+                  decoration: BoxDecoration(
+                    color: Colors.white,
+                    borderRadius: BorderRadius.circular(6.0),
+                  ),
+                  child: TextField(
+                    cursorColor: Colors.green,
+                    autofocus: true,
+                    style: const TextStyle(
+                      fontSize: 18.0,
+                      color: Colors.black,
+                      fontWeight: FontWeight.w300,
+                    ),
+                    decoration: const InputDecoration(
+                      contentPadding: EdgeInsets.only(bottom: 10),
+                      icon: Icon(
+                        Icons.search,
+                        size: 20.0,
+                        color: Colors.grey,
+                      ),
+                      border: InputBorder.none,
+                      hintText: '搜索',
+                      hintStyle: TextStyle(color: Colors.grey),
+                    ),
+                    onSubmitted: onSubmitted,
+                    onChanged: onChanged,
+                  ),
+                )),
+                SizedBox(
+                    width: 40,
+                    // height: 34,
+                    child: GestureDetector(
+                      child: const Text("取消"),
+                      onTap: () => Navigator.pop(context),
+                    ))
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
+}

+ 76 - 0
lib/screens/SpaceScreen.dart

@@ -0,0 +1,76 @@
+import 'package:flutter/material.dart';
+import '../basic/Common.dart';
+
+import '../basic/Method.dart';
+import 'AboutScreen.dart';
+import 'AccountScreen.dart';
+import 'LoginScreen.dart';
+import 'components/Badge.dart';
+import 'components/UserProfileCard.dart';
+
+// 个人空间页面
+class SpaceScreen extends StatefulWidget {
+  const SpaceScreen({Key? key}) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _SpaceScreenState();
+}
+
+class _SpaceScreenState extends State<SpaceScreen> {
+
+
+  @override
+  Widget build(BuildContext context) {
+    return Scaffold(
+      appBar: AppBar(
+        title: const Text('我的'),
+        actions: [
+          IconButton(
+            onPressed: () async {
+              bool result =
+                  await confirmDialog(context, '退出登录', '您确认要退出当前账号吗?');
+              if (result) {
+                await method.logout();
+                if (!mounted) return;
+                Navigator.pushReplacement(
+                  context,
+                  MaterialPageRoute(
+                      builder: (context) => LoginScreen()),
+                );
+              }
+            },
+            icon: const Icon(Icons.exit_to_app),
+          ),
+          IconButton(
+            onPressed: () {
+              Navigator.push(
+                context,
+                MaterialPageRoute(builder: (context) => const AboutScreen()),
+              );
+            },
+            icon: const Badged(
+              badge: null,
+              child: Icon(Icons.info_outline),
+            ),
+          ),
+        ],
+      ),
+      body: ListView(
+        children: [
+          const Divider(),
+          UserProfileCard(load: () async => method.getProfile(),),
+          const Divider(),
+          GestureDetector(
+              onTap: () {
+                Navigator.push(
+                  context,
+                  MaterialPageRoute(builder: (context) => const AboutScreen()),
+                );
+              },
+              child: const Text('关于'),),
+          const Divider(),
+        ],
+      ),
+    );
+  }
+}

+ 38 - 0
lib/screens/components/Avatar.dart

@@ -0,0 +1,38 @@
+import 'package:flutter/material.dart';
+import '../../basic/Entities.dart';
+import 'Images.dart';
+
+const double _avatarMargin = 5;
+const double _avatarBorderSize = 1.5;
+
+// 头像
+class Avatar extends StatelessWidget {
+  final RemoteImageInfo avatarImage;
+  final double size;
+
+  const Avatar(this.avatarImage, {this.size = 50, Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    var theme = Theme.of(context);
+    return Container(
+      margin: const EdgeInsets.all(_avatarMargin),
+      decoration: BoxDecoration(
+          shape: BoxShape.circle,
+          border: Border.all(
+            color: theme.colorScheme.secondary,
+            style: BorderStyle.solid,
+            width: _avatarBorderSize,
+          )),
+      child: ClipRRect(
+        borderRadius: BorderRadius.all(Radius.circular(size)),
+        child: RemoteImage(
+          fileServer: avatarImage.fileServer,
+          path: avatarImage.path,
+          width: size,
+          height: size,
+        ),
+      ),
+    );
+  }
+}

+ 43 - 0
lib/screens/components/Badge.dart

@@ -0,0 +1,43 @@
+import 'package:flutter/material.dart';
+
+// 提示信息, 组件右上角的小红点
+class Badged extends StatelessWidget {
+  final String? badge;
+  final Widget child;
+
+  const Badged({Key? key, required this.child, this.badge}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    if (badge == null) {
+      return child;
+    }
+    return Stack(
+      children: [
+        child,
+        Positioned(
+          right: 0,
+          child: Container(
+            padding: const EdgeInsets.all(1),
+            decoration: BoxDecoration(
+              color: Colors.red,
+              borderRadius: BorderRadius.circular(6),
+            ),
+            constraints: const BoxConstraints(
+              minWidth: 12,
+              minHeight: 12,
+            ),
+            child: Text(
+              badge!,
+              style: const TextStyle(
+                color: Colors.white,
+                fontSize: 8,
+              ),
+              textAlign: TextAlign.center,
+            ),
+          ),
+        ),
+      ],
+    );
+  }
+}

+ 39 - 0
lib/screens/components/ContentLoading.dart

@@ -0,0 +1,39 @@
+import 'package:flutter/material.dart';
+
+class ContentLoading extends StatelessWidget {
+  final String label;
+
+  const ContentLoading({Key? key, required this.label}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) {
+        var width = constraints.maxWidth;
+        var height = constraints.maxHeight;
+        var min = width < height ? width : height;
+        var theme = Theme.of(context);
+        return Center(
+          child: Column(
+            children: [
+              Expanded(child: Container()),
+              SizedBox(
+                width: min / 2,
+                height: min / 2,
+                child: false
+                    ? Icon(Icons.refresh, color: Colors.grey[100])
+                    : CircularProgressIndicator(
+                        color: theme.colorScheme.secondary,
+                        backgroundColor: Colors.grey[100],
+                      ),
+              ),
+              Container(height: min / 10),
+              Text(label, style: TextStyle(fontSize: min / 15)),
+              Expanded(child: Container()),
+            ],
+          ),
+        );
+      },
+    );
+  }
+}

+ 356 - 0
lib/screens/components/Images.dart

@@ -0,0 +1,356 @@
+import 'dart:typed_data';
+import 'dart:ui';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:flutter_svg/svg.dart';
+import 'dart:io';
+import 'dart:ui' as ui show Codec;
+
+import '../../basic/Common.dart';
+import '../../basic/Method.dart';
+import '../FilePhotoViewScreen.dart';
+
+// 从本地加载图片
+class ResourceFileImageProvider
+    extends ImageProvider<ResourceFileImageProvider> {
+  final String path;
+  final double scale;
+
+  ResourceFileImageProvider(this.path, {this.scale = 1.0});
+
+  @override
+  ImageStreamCompleter loadBuffer(
+      ResourceFileImageProvider key, DecoderBufferCallback decode) {
+    return MultiFrameImageStreamCompleter(
+      codec: _loadAsync(key),
+      scale: key.scale,
+    );
+  }
+
+  @override
+  Future<ResourceFileImageProvider> obtainKey(
+      ImageConfiguration configuration) {
+    return SynchronousFuture<ResourceFileImageProvider>(this);
+  }
+
+  Future<ui.Codec> _loadAsync(ResourceFileImageProvider key) async {
+    assert(key == this);
+    return PaintingBinding.instance
+        .instantiateImageCodecFromBuffer(await ImmutableBuffer.fromUint8List(await File(path).readAsBytes()));
+  }
+
+  @override
+  bool operator ==(dynamic other) {
+    if (other.runtimeType != runtimeType) return false;
+    final ResourceFileImageProvider typedOther = other;
+    return path == typedOther.path && scale == typedOther.scale;
+  }
+
+  @override
+  int get hashCode => Object.hash(path, scale);
+
+  @override
+  String toString() => '$runtimeType('
+      'path: ${describeIdentity(path)},'
+      ' scale: $scale'
+      ')';
+}
+
+// 从本地加载图片
+// class ResourceDownloadFileImageProvider
+//     extends ImageProvider<ResourceDownloadFileImageProvider> {
+//   final String path;
+//   final double scale;
+//
+//   ResourceDownloadFileImageProvider(this.path, {this.scale = 1.0});
+//
+//   @override
+//   ImageStreamCompleter load(
+//       ResourceDownloadFileImageProvider key, DecoderCallback decode) {
+//     return MultiFrameImageStreamCompleter(
+//       codec: _loadAsync(key),
+//       scale: key.scale,
+//     );
+//   }
+//
+//   @override
+//   Future<ResourceDownloadFileImageProvider> obtainKey(
+//       ImageConfiguration configuration) {
+//     return SynchronousFuture<ResourceDownloadFileImageProvider>(this);
+//   }
+//
+//   Future<ui.Codec> _loadAsync(ResourceDownloadFileImageProvider key) async {
+//     assert(key == this);
+//     return PaintingBinding.instance!.instantiateImageCodec(
+//         await File(await method.downloadImagePath(path)).readAsBytes());
+//   }
+//
+//   @override
+//   bool operator ==(dynamic other) {
+//     if (other.runtimeType != runtimeType) return false;
+//     final ResourceDownloadFileImageProvider typedOther = other;
+//     return path == typedOther.path && scale == typedOther.scale;
+//   }
+//
+//   @override
+//   int get hashCode => hashValues(path, scale);
+//
+//   @override
+//   String toString() => '$runtimeType('
+//       'path: ${describeIdentity(path)},'
+//       ' scale: $scale'
+//       ')';
+// }
+
+// 从远端加载图片
+// class ResourceRemoteImageProvider
+//     extends ImageProvider<ResourceRemoteImageProvider> {
+//   final String fileServer;
+//   final String path;
+//   final double scale;
+//
+//   ResourceRemoteImageProvider(this.fileServer, this.path, {this.scale = 1.0});
+//
+//   @override
+//   ImageStreamCompleter load(
+//       ResourceRemoteImageProvider key, DecoderCallback decode) {
+//     return MultiFrameImageStreamCompleter(
+//       codec: _loadAsync(key),
+//       scale: key.scale,
+//     );
+//   }
+//
+//   @override
+//   Future<ResourceRemoteImageProvider> obtainKey(
+//       ImageConfiguration configuration) {
+//     return SynchronousFuture<ResourceRemoteImageProvider>(this);
+//   }
+//
+//   Future<ui.Codec> _loadAsync(ResourceRemoteImageProvider key) async {
+//     assert(key == this);
+//     var downloadTo = await method.remoteImageData(fileServer, path);
+//     return PaintingBinding.instance!
+//         .instantiateImageCodec(await File(downloadTo.finalPath).readAsBytes());
+//   }
+//
+//   @override
+//   bool operator ==(dynamic other) {
+//     if (other.runtimeType != runtimeType) return false;
+//     final ResourceRemoteImageProvider typedOther = other;
+//     return fileServer == typedOther.fileServer &&
+//         path == typedOther.path &&
+//         scale == typedOther.scale;
+//   }
+//
+//   @override
+//   int get hashCode => hashValues(fileServer, path, scale);
+//
+//   @override
+//   String toString() => '$runtimeType('
+//       'fileServer: ${describeIdentity(fileServer)},'
+//       ' path: ${describeIdentity(path)},'
+//       ' scale: $scale'
+//       ')';
+// }
+
+// 下载的图片
+// class DownloadImage extends StatefulWidget {
+//   final String path;
+//   final double? width;
+//   final double? height;
+//
+//   const DownloadImage({
+//     Key? key,
+//     required this.path,
+//     this.width,
+//     this.height,
+//   }) : super(key: key);
+//
+//   @override
+//   State<StatefulWidget> createState() => _DownloadImageState();
+// }
+//
+// class _DownloadImageState extends State<DownloadImage> {
+//   late final Future<String> _future = method.downloadImagePath(widget.path);
+//
+//   @override
+//   Widget build(BuildContext context) {
+//     return pathFutureImage(
+//       _future,
+//       widget.width,
+//       widget.height,
+//       context: context,
+//     );
+//   }
+// }
+
+// 远端图片
+class RemoteImage extends StatefulWidget {
+  final String fileServer;
+  final String path;
+  final double? width;
+  final double? height;
+  final BoxFit fit;
+
+  const RemoteImage({
+    Key? key,
+    required this.fileServer,
+    required this.path,
+    this.width,
+    this.height,
+    this.fit = BoxFit.cover,
+  }) : super(key: key);
+
+  @override
+  State<StatefulWidget> createState() => _RemoteImageState();
+}
+
+class _RemoteImageState extends State<RemoteImage> {
+  late bool _mock;
+  late Future<String> _future;
+
+  @override
+  void initState() {
+    debugPrint("path=${widget.path}");
+    _mock = widget.fileServer == "" || widget.path == "";
+    if (!_mock) {
+      _future = method.downloadImage(widget.path, widget.fileServer).then((value) => value!);
+    }
+    super.initState();
+  }
+
+  @override
+  Widget build(BuildContext context) {
+    // return buildMock(widget.width, widget.height);
+    if (_mock) {
+      return buildMock(widget.width, widget.height);
+    }
+    return pathFutureImage(
+      _future,
+      widget.width,
+      widget.height,
+      fit: widget.fit,
+      context: context,
+    );
+  }
+}
+
+Widget pathFutureImage(Future<String> future, double? width, double? height,
+    {BoxFit fit = BoxFit.cover, BuildContext? context}) {
+  return FutureBuilder(
+      future: future,
+      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
+        if (snapshot.hasError) {
+          print("${snapshot.error}");
+          print("${snapshot.stackTrace}");
+          return buildError(width, height);
+        }
+        if (snapshot.connectionState != ConnectionState.done) {
+          return buildLoading(width, height);
+        }
+        return buildFile(
+          snapshot.data!,
+          width,
+          height,
+          fit: fit,
+          context: context,
+        );
+      });
+}
+
+// 通用方法
+
+Widget buildSvg(String source, double? width, double? height,
+    {Color? color, double? margin}) {
+  var widget = Container(
+    width: width,
+    height: height,
+    padding: margin != null ? const EdgeInsets.all(10) : null,
+    child: Center(
+      child: SvgPicture.asset(
+        source,
+        width: width,
+        height: height,
+        color: color,
+      ),
+    ),
+  );
+  return GestureDetector(onLongPress: () {}, child: widget);
+}
+
+Widget buildMock(double? width, double? height) {
+  var widget = Container(
+    width: width,
+    height: height,
+    padding: const EdgeInsets.all(10),
+    child: Center(
+      child: SvgPicture.asset(
+        'lib/assets/unknown.svg',
+        width: width,
+        height: height,
+        color: Colors.grey.shade600,
+      ),
+    ),
+  );
+  return GestureDetector(onLongPress: () {}, child: widget);
+}
+
+Widget buildError(double? width, double? height) {
+  return Image(
+    image: const AssetImage('lib/assets/error.png'),
+    width: width,
+    height: height,
+  );
+}
+
+Widget buildLoading(double? width, double? height) {
+  double? size;
+  if (width != null && height != null) {
+    size = width < height ? width : height;
+  }
+  return SizedBox(
+    width: width,
+    height: height,
+    child: Center(
+      child: Icon(
+        Icons.downloading,
+        size: size,
+        color: Colors.black12,
+      ),
+    ),
+  );
+}
+
+Widget buildFile(String file, double? width, double? height,
+    {BoxFit fit = BoxFit.cover, BuildContext? context}) {
+  var image = Image(
+    image: ResourceFileImageProvider(file),
+    width: width,
+    height: height,
+    errorBuilder: (a, b, c) {
+      print("$b");
+      print("$c");
+      return buildError(width, height);
+    },
+    fit: fit,
+  );
+  if (context == null) return image;
+  return GestureDetector(
+    onLongPress: () async {
+      String? choose = await chooseListDialog(context, '请选择', ['预览图片', '保存图片']);
+      switch (choose) {
+        case '预览图片':
+          Navigator.of(context).push(MaterialPageRoute(
+            builder: (context) => FilePhotoViewScreen(file),
+          ));
+          break;
+        case '保存图片':
+          //TODO: SAVE image
+          // saveImage(file, context);
+          break;
+      }
+    },
+    child: image,
+  );
+}

+ 62 - 0
lib/screens/components/ItemBuilder.dart

@@ -0,0 +1,62 @@
+import 'package:flutter/material.dart';
+
+// 非全屏FutureBuilder封装
+class ItemBuilder<T> extends StatelessWidget {
+  final Future<T> future;
+  final AsyncWidgetBuilder<T> successBuilder;
+  final Future<dynamic> Function() onRefresh;
+  final double? loadingHeight;
+  final double? height;
+
+  const ItemBuilder({
+    Key? key,
+    required this.future,
+    required this.successBuilder,
+    required this.onRefresh,
+    this.height,
+    this.loadingHeight,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return LayoutBuilder(
+      builder: (BuildContext context, BoxConstraints constraints) {
+        var _maxWidth = constraints.maxWidth;
+        var _loadingHeight = height ?? loadingHeight ?? _maxWidth / 2;
+        return FutureBuilder(
+            future: future,
+            builder: (BuildContext context, AsyncSnapshot<T> snapshot) {
+              if (snapshot.hasError) {
+                print("${snapshot.error}");
+                print("${snapshot.stackTrace}");
+                return InkWell(
+                  onTap: onRefresh,
+                  child: SizedBox(
+                    width: _maxWidth,
+                    height: _loadingHeight,
+                    child: Center(
+                      child:
+                          Icon(Icons.sync_problem, size: _loadingHeight / 1.5),
+                    ),
+                  ),
+                );
+              }
+              if (snapshot.connectionState != ConnectionState.done) {
+                return SizedBox(
+                  width: _maxWidth,
+                  height: _loadingHeight,
+                  child: Center(
+                    child: Icon(Icons.sync, size: _loadingHeight / 1.5),
+                  ),
+                );
+              }
+              return SizedBox(
+                width: _maxWidth,
+                height: height,
+                child: successBuilder(context, snapshot),
+              );
+            });
+      },
+    );
+  }
+}

+ 28 - 0
lib/screens/components/ProgressDialog.dart

@@ -0,0 +1,28 @@
+import 'package:flutter/material.dart';
+import 'package:prime_chat/screens/components/ContentLoading.dart';
+
+class ProgressDialog extends StatelessWidget {
+  final bool loading;
+  final Widget child;
+  final String label;
+
+  const ProgressDialog(
+      {Key? key,
+      required this.loading,
+      required this.label,
+      required this.child})
+      : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    List<Widget> widgetList = [];
+    widgetList.add(child);
+    //如果正在加载,则显示加载添加加载中布局
+    if (loading) {
+      widgetList.add(ContentLoading(label: label));
+    }
+    return Stack(
+      children: widgetList,
+    );
+  }
+}

+ 15 - 0
lib/screens/components/RightClickPop.dart

@@ -0,0 +1,15 @@
+import 'package:flutter/material.dart';
+
+Widget rightClickPop({
+  required Widget child,
+  required BuildContext context,
+  bool canPop = true,
+}) =>
+    GestureDetector(
+      onSecondaryTap: () {
+        if (canPop) {
+          Navigator.of(context).pop();
+        }
+      },
+      child: child,
+    );

+ 138 - 0
lib/screens/components/UserProfileCard.dart

@@ -0,0 +1,138 @@
+import 'dart:convert';
+import 'dart:io';
+import 'dart:ui';
+
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:image_cropper/image_cropper.dart';
+import 'package:wechat_assets_picker/wechat_assets_picker.dart';
+
+import '../../basic/Common.dart';
+import '../../basic/Entities.dart';
+import '../../basic/Method.dart';
+import 'Avatar.dart';
+import 'Images.dart';
+import 'ItemBuilder.dart';
+
+const double _cardHeight = 180;
+
+// 用户信息卡
+class UserProfileCard extends StatefulWidget {
+  const UserProfileCard({Key? key, required this.load}) : super(key: key);
+
+  final Future<UserProfile?> Function() load;
+
+  @override
+  State<StatefulWidget> createState() => _UserProfileCardState();
+}
+
+class _UserProfileCardState extends State<UserProfileCard> {
+  late Future<UserProfile?> _future = widget.load();
+
+  // Future<UserProfile?> _load() async {
+  //   var profile = await method.getProfile(widget.name);
+  //   return profile;
+  // }
+
+  @override
+  Widget build(BuildContext context) {
+    var nameStyle = const TextStyle(
+      fontSize: 14,
+      fontWeight: FontWeight.bold,
+    );
+    var nameStrutStyle = const StrutStyle(
+      fontSize: 14,
+      forceStrutHeight: true,
+      fontWeight: FontWeight.bold,
+    );
+    return ItemBuilder(
+      future: _future,
+      onRefresh: () async {
+        setState(() => _future = widget.load());
+      },
+      height: _cardHeight,
+      successBuilder:
+          (BuildContext context, AsyncSnapshot<UserProfile?> snapshot) {
+        UserProfile profile = snapshot.data!;
+        return Stack(
+          children: [
+            SizedBox(
+              height: _cardHeight,
+              child: Column(
+                children: [
+                  Expanded(child: Container()),
+                  GestureDetector(
+                    onTap: () async {
+                      if (Platform.isAndroid || Platform.isIOS) {
+                        await _updateAvatarPhone();
+                      }
+                    },
+                    child: Avatar(profile.avatar, size: 65),
+                  ),
+                  Container(height: 5),
+                  Text(
+                    profile.name,
+                    style: nameStyle,
+                    strutStyle: nameStrutStyle,
+                  ),
+                  Container(height: 8),
+                  Expanded(child: Container()),
+                ],
+              ),
+            )
+          ],
+        );
+      },
+    );
+  }
+
+  void _reload() {
+    setState(() {
+      _future = widget.load();
+    });
+  }
+
+  Future _updateAvatarPhone() async {
+    final assets = await AssetPicker.pickAssets(context,
+        pickerConfig: const AssetPickerConfig(maxAssets: 1));
+    if (assets == null || assets.isEmpty) return;
+    final image = await assets.first.file;
+    // final ImagePicker _picker = ImagePicker();
+    // final XFile? image = await _picker.pickImage(source: ImageSource.gallery);
+    if (image != null) {
+      final cropper = ImageCropper();
+      final croppedFile = await cropper.cropImage(
+        sourcePath: image.path,
+        aspectRatioPresets: [
+          CropAspectRatioPreset.square,
+        ],
+        aspectRatio: const CropAspectRatio(ratioX: 200, ratioY: 200),
+        maxWidth: 200,
+        maxHeight: 200,
+        uiSettings: [AndroidUiSettings(
+          toolbarTitle: "修改头像",
+          toolbarColor: Colors.blue,
+          toolbarWidgetColor: Colors.white,
+          initAspectRatio: CropAspectRatioPreset.original,
+          lockAspectRatio: true,
+        ),
+        IOSUiSettings(
+          resetAspectRatioEnabled: true,
+          aspectRatioLockEnabled: true,
+          title: "修改头像",
+        ),]
+      );
+      if (croppedFile != null) {
+        final path = image.path;
+        final suf = path.substring(path.lastIndexOf('.'));
+        var buff = await croppedFile.readAsBytes();
+        // var data = base64Encode(buff);
+        await method.updateAvatar(buff, suf);
+        _reload();
+      }
+      // final buff = image.readAsBytesSync();
+      // await method.updateAvatar(buff);
+      // _reload();
+    }
+  }
+}

+ 550 - 0
pubspec.lock

@@ -1,6 +1,20 @@
 # Generated by pub
 # See https://dart.dev/tools/pub/glossary#lockfile
 packages:
+  another_flushbar:
+    dependency: transitive
+    description:
+      name: another_flushbar
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.12.29"
+  another_transformer_page_view:
+    dependency: transitive
+    description:
+      name: another_transformer_page_view
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
   async:
     dependency: transitive
     description:
@@ -8,6 +22,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.9.0"
+  azlistview:
+    dependency: "direct main"
+    description:
+      name: azlistview
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.0"
   boolean_selector:
     dependency: transitive
     description:
@@ -36,6 +57,20 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.16.0"
+  crypto:
+    dependency: transitive
+    description:
+      name: crypto
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.2"
+  csslib:
+    dependency: transitive
+    description:
+      name: csslib
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.17.2"
   cupertino_icons:
     dependency: "direct main"
     description:
@@ -43,6 +78,27 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.0.5"
+  dio:
+    dependency: "direct main"
+    description:
+      name: dio
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.0.6"
+  extended_image:
+    dependency: transitive
+    description:
+      name: extended_image
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.3.4"
+  extended_image_library:
+    dependency: transitive
+    description:
+      name: extended_image_library
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.4.1"
   fake_async:
     dependency: transitive
     description:
@@ -50,11 +106,32 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.3.1"
+  ffi:
+    dependency: transitive
+    description:
+      name: ffi
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.1"
+  file:
+    dependency: transitive
+    description:
+      name: file
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.4"
   flutter:
     dependency: "direct main"
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_easyloading:
+    dependency: "direct main"
+    description:
+      name: flutter_easyloading
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.5"
   flutter_lints:
     dependency: "direct dev"
     description:
@@ -62,11 +139,119 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.0.1"
+  flutter_localizations:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  flutter_login:
+    dependency: "direct main"
+    description:
+      name: flutter_login
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.1.1"
+  flutter_spinkit:
+    dependency: transitive
+    description:
+      name: flutter_spinkit
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.0"
+  flutter_styled_toast:
+    dependency: "direct main"
+    description:
+      name: flutter_styled_toast
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.3"
+  flutter_svg:
+    dependency: "direct main"
+    description:
+      name: flutter_svg
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.1.6"
   flutter_test:
     dependency: "direct dev"
     description: flutter
     source: sdk
     version: "0.0.0"
+  flutter_web_plugins:
+    dependency: transitive
+    description: flutter
+    source: sdk
+    version: "0.0.0"
+  font_awesome_flutter:
+    dependency: transitive
+    description:
+      name: font_awesome_flutter
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "10.3.0"
+  html:
+    dependency: transitive
+    description:
+      name: html
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.15.1"
+  http:
+    dependency: transitive
+    description:
+      name: http
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.13.5"
+  http_client_helper:
+    dependency: transitive
+    description:
+      name: http_client_helper
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
+  http_parser:
+    dependency: transitive
+    description:
+      name: http_parser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.0.2"
+  image_cropper:
+    dependency: "direct main"
+    description:
+      name: image_cropper
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  image_cropper_for_web:
+    dependency: transitive
+    description:
+      name: image_cropper_for_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.3"
+  image_cropper_platform_interface:
+    dependency: transitive
+    description:
+      name: image_cropper_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.3"
+  intl:
+    dependency: transitive
+    description:
+      name: intl
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.17.0"
+  js:
+    dependency: transitive
+    description:
+      name: js
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.6.4"
   lints:
     dependency: transitive
     description:
@@ -74,6 +259,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.0.1"
+  lpinyin:
+    dependency: "direct main"
+    description:
+      name: lpinyin
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.3"
   matcher:
     dependency: transitive
     description:
@@ -95,6 +287,13 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.0"
+  nested:
+    dependency: transitive
+    description:
+      name: nested
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.0"
   path:
     dependency: transitive
     description:
@@ -102,6 +301,230 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "1.8.2"
+  path_drawing:
+    dependency: transitive
+    description:
+      name: path_drawing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
+  path_parsing:
+    dependency: transitive
+    description:
+      name: path_parsing
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.0.1"
+  path_provider:
+    dependency: "direct main"
+    description:
+      name: path_provider
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.11"
+  path_provider_android:
+    dependency: transitive
+    description:
+      name: path_provider_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.22"
+  path_provider_ios:
+    dependency: transitive
+    description:
+      name: path_provider_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.11"
+  path_provider_linux:
+    dependency: transitive
+    description:
+      name: path_provider_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.7"
+  path_provider_macos:
+    dependency: transitive
+    description:
+      name: path_provider_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.6"
+  path_provider_platform_interface:
+    dependency: transitive
+    description:
+      name: path_provider_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.5"
+  path_provider_windows:
+    dependency: transitive
+    description:
+      name: path_provider_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.3"
+  permission_handler:
+    dependency: "direct main"
+    description:
+      name: permission_handler
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "10.2.0"
+  permission_handler_android:
+    dependency: transitive
+    description:
+      name: permission_handler_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "10.2.0"
+  permission_handler_apple:
+    dependency: transitive
+    description:
+      name: permission_handler_apple
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "9.0.7"
+  permission_handler_platform_interface:
+    dependency: transitive
+    description:
+      name: permission_handler_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.9.0"
+  permission_handler_windows:
+    dependency: transitive
+    description:
+      name: permission_handler_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.1.2"
+  petitparser:
+    dependency: transitive
+    description:
+      name: petitparser
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "5.1.0"
+  photo_manager:
+    dependency: transitive
+    description:
+      name: photo_manager
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.5.2"
+  photo_view:
+    dependency: "direct main"
+    description:
+      name: photo_view
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.14.0"
+  platform:
+    dependency: transitive
+    description:
+      name: platform
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
+  plugin_platform_interface:
+    dependency: transitive
+    description:
+      name: plugin_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.3"
+  process:
+    dependency: transitive
+    description:
+      name: process
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "4.2.4"
+  provider:
+    dependency: transitive
+    description:
+      name: provider
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.5"
+  quiver:
+    dependency: transitive
+    description:
+      name: quiver
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.2.1"
+  scrollable_positioned_list:
+    dependency: transitive
+    description:
+      name: scrollable_positioned_list
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.3"
+  shared_preferences:
+    dependency: "direct main"
+    description:
+      name: shared_preferences
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.15"
+  shared_preferences_android:
+    dependency: transitive
+    description:
+      name: shared_preferences_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.14"
+  shared_preferences_ios:
+    dependency: transitive
+    description:
+      name: shared_preferences_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  shared_preferences_linux:
+    dependency: transitive
+    description:
+      name: shared_preferences_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  shared_preferences_macos:
+    dependency: transitive
+    description:
+      name: shared_preferences_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  shared_preferences_platform_interface:
+    dependency: transitive
+    description:
+      name: shared_preferences_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.0"
+  shared_preferences_web:
+    dependency: transitive
+    description:
+      name: shared_preferences_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.4"
+  shared_preferences_windows:
+    dependency: transitive
+    description:
+      name: shared_preferences_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.2"
+  sign_in_button:
+    dependency: transitive
+    description:
+      name: sign_in_button
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.0"
   sky_engine:
     dependency: transitive
     description: flutter
@@ -149,6 +572,69 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "0.4.12"
+  typed_data:
+    dependency: transitive
+    description:
+      name: typed_data
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "1.3.1"
+  url_launcher:
+    dependency: transitive
+    description:
+      name: url_launcher
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.7"
+  url_launcher_android:
+    dependency: transitive
+    description:
+      name: url_launcher_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.22"
+  url_launcher_ios:
+    dependency: transitive
+    description:
+      name: url_launcher_ios
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.17"
+  url_launcher_linux:
+    dependency: transitive
+    description:
+      name: url_launcher_linux
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  url_launcher_macos:
+    dependency: transitive
+    description:
+      name: url_launcher_macos
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
+  url_launcher_platform_interface:
+    dependency: transitive
+    description:
+      name: url_launcher_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.1.1"
+  url_launcher_web:
+    dependency: transitive
+    description:
+      name: url_launcher_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.13"
+  url_launcher_windows:
+    dependency: transitive
+    description:
+      name: url_launcher_windows
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.0.1"
   vector_math:
     dependency: transitive
     description:
@@ -156,5 +642,69 @@ packages:
       url: "https://pub.dartlang.org"
     source: hosted
     version: "2.1.2"
+  video_player:
+    dependency: transitive
+    description:
+      name: video_player
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.4.10"
+  video_player_android:
+    dependency: transitive
+    description:
+      name: video_player_android
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.10"
+  video_player_avfoundation:
+    dependency: transitive
+    description:
+      name: video_player_avfoundation
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.3.8"
+  video_player_platform_interface:
+    dependency: transitive
+    description:
+      name: video_player_platform_interface
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.0.1"
+  video_player_web:
+    dependency: transitive
+    description:
+      name: video_player_web
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "2.0.13"
+  wechat_assets_picker:
+    dependency: "direct main"
+    description:
+      name: wechat_assets_picker
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "8.3.0"
+  win32:
+    dependency: transitive
+    description:
+      name: win32
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "3.1.3"
+  xdg_directories:
+    dependency: transitive
+    description:
+      name: xdg_directories
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "0.2.0+2"
+  xml:
+    dependency: transitive
+    description:
+      name: xml
+      url: "https://pub.dartlang.org"
+    source: hosted
+    version: "6.1.0"
 sdks:
   dart: ">=2.18.5 <3.0.0"
+  flutter: ">=3.3.0"

+ 16 - 1
pubspec.yaml

@@ -29,6 +29,7 @@ environment:
 # the latest version available on pub.dev. To see which dependencies have newer
 # versions available, run `flutter pub outdated`.
 dependencies:
+
   flutter:
     sdk: flutter
 
@@ -36,6 +37,19 @@ dependencies:
   # The following adds the Cupertino Icons font to your application.
   # Use with the CupertinoIcons class for iOS style icons.
   cupertino_icons: ^1.0.2
+  shared_preferences: ^2.0.15
+  dio: ^4.0.6
+  flutter_styled_toast: ^2.1.3
+  flutter_svg: ^1.1.6
+  azlistview: ^2.0.0
+  flutter_easyloading: ^3.0.5
+  path_provider: ^2.0.11
+  permission_handler: ^10.2.0
+  image_cropper: ^3.0.1
+  wechat_assets_picker: ^8.3.0
+  photo_view: ^0.14.0
+  flutter_login: ^4.1.1
+  lpinyin: ^2.0.3
 
 dev_dependencies:
   flutter_test:
@@ -60,7 +74,8 @@ flutter:
   uses-material-design: true
 
   # To add assets to your application, add an assets section, like this:
-  # assets:
+  assets:
+    - lib/assets/
   #   - images/a_dot_burr.jpeg
   #   - images/a_dot_ham.jpeg
 

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor