Browse Source

Merge branch 'wr_202303' of http://192.168.10.200:3000/fmk/huixiang_app into wr_202303

# Conflicts:
#	pubspec.lock
dart3_last
fff 1 month ago
parent
commit
d43bbd90ee
  1. 6
      android/app/build.gradle
  2. 4
      android/build.gradle
  3. 4
      android/gradle.properties
  4. BIN
      assets/image/2x/chat_more.webp
  5. BIN
      assets/image/2x/fa_bu.webp
  6. BIN
      assets/image/2x/icon_gz.webp
  7. BIN
      assets/image/2x/icon_pl.webp
  8. BIN
      assets/image/2x/icon_system_message_new.webp
  9. BIN
      assets/image/2x/icon_z.webp
  10. BIN
      assets/image/3x/chat_more.webp
  11. BIN
      assets/image/3x/fa_bu.webp
  12. BIN
      assets/image/3x/icon_gz.webp
  13. BIN
      assets/image/3x/icon_pl.webp
  14. BIN
      assets/image/3x/icon_system_message_new.webp
  15. BIN
      assets/image/3x/icon_z.webp
  16. BIN
      assets/image/chat_more.webp
  17. BIN
      assets/image/fa_bu.webp
  18. BIN
      assets/image/icon_gz.webp
  19. BIN
      assets/image/icon_pl.webp
  20. BIN
      assets/image/icon_system_message_new.webp
  21. BIN
      assets/image/icon_z.webp
  22. 11
      assets/svg/shequn.svg
  23. 27
      ios/Runner.xcodeproj/project.pbxproj
  24. 2
      lib/community/community_view/community_dynamic.dart
  25. 88
      lib/constant.dart
  26. 1
      lib/home/home_page.dart
  27. 210
      lib/im/SocketClient.dart
  28. 150
      lib/im/add_friend.dart
  29. 1172
      lib/im/chat_details_page.dart
  30. 98
      lib/im/chat_friend_group.dart
  31. 51
      lib/im/chat_setting.dart
  32. 10
      lib/im/contact_share.dart
  33. 228
      lib/im/database/hx_database.dart
  34. 290
      lib/im/database/hx_database.g.dart
  35. 57
      lib/im/database/message.dart
  36. 34
      lib/im/database/message_dao.dart
  37. 41
      lib/im/database/migration.dart
  38. 365
      lib/im/im_search.dart
  39. 2
      lib/im/im_view/custom_underline_tabIndicator.dart
  40. 204
      lib/im/im_view/friend_groip_list.dart
  41. 723
      lib/im/im_view/im_page.dart
  42. 17
      lib/im/out/auth.pb.dart
  43. 8
      lib/im/out/auth.pbjson.dart
  44. 18
      lib/im/out/message.pb.dart
  45. 25
      lib/im/out/message.pbenum.dart
  46. 22
      lib/im/out/message.pbjson.dart
  47. 28
      lib/main.dart
  48. 10
      lib/main_page.dart
  49. 2
      lib/message/system_message.dart
  50. 1
      lib/mine/edit_signature.dart
  51. 1
      lib/mine/fans_page.dart
  52. 1
      lib/mine/follow_page.dart
  53. 1
      lib/mine/mine_page.dart
  54. 880
      lib/mine/personal_page.dart
  55. 4
      lib/retrofit/data/address.dart
  56. 73
      lib/retrofit/data/im_user.dart
  57. 12
      lib/retrofit/data/social_info.dart
  58. 11
      lib/retrofit/min_api.dart
  59. 4
      lib/retrofit/min_api.g.dart
  60. 31
      lib/retrofit/retrofit_api.dart
  61. 97
      lib/retrofit/retrofit_api.g.dart
  62. 13
      lib/setting/about_page.dart
  63. 14
      lib/store/store_view/shop_goods.dart
  64. 2
      lib/store/store_view/store_order_list.dart
  65. 1
      lib/union/union_list.dart
  66. 65
      lib/utils/flutter_utils.dart
  67. 85
      lib/utils/qiniu.dart
  68. 7
      lib/utils/upload_async.dart
  69. 1
      lib/view_widget/classic_header.dart
  70. 1
      lib/web/web_view/comment_list.dart
  71. 11
      pubspec.yaml
  72. 58
      qiniu-dart-sdk/.travis.yml
  73. 76
      qiniu-dart-sdk/CODE_OF_CONDUCT.md
  74. 15
      qiniu-dart-sdk/README.md
  75. 12
      qiniu-dart-sdk/base/CHANGELOG.md
  76. 201
      qiniu-dart-sdk/base/LICENSE
  77. 29
      qiniu-dart-sdk/base/README.md
  78. 90
      qiniu-dart-sdk/base/analysis_options.yaml
  79. 5
      qiniu-dart-sdk/base/lib/qiniu_sdk_base.dart
  80. 125
      qiniu-dart-sdk/base/lib/src/auth/auth.dart
  81. 209
      qiniu-dart-sdk/base/lib/src/auth/put_policy.dart
  82. 12
      qiniu-dart-sdk/base/lib/src/error/error.dart
  83. 39
      qiniu-dart-sdk/base/lib/src/storage/config/cache.dart
  84. 28
      qiniu-dart-sdk/base/lib/src/storage/config/config.dart
  85. 124
      qiniu-dart-sdk/base/lib/src/storage/config/host.dart
  86. 11
      qiniu-dart-sdk/base/lib/src/storage/config/protocol.dart
  87. 75
      qiniu-dart-sdk/base/lib/src/storage/error/error.dart
  88. 20
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/cache_mixin.dart
  89. 51
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/complete_parts_task.dart
  90. 92
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/init_parts_task.dart
  91. 26
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/part.dart
  92. 25
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_by_part_options.dart
  93. 187
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_parts_task.dart
  94. 112
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_part_task.dart
  95. 260
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_parts_task.dart
  96. 13
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_options.dart
  97. 60
      qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_task.dart
  98. 5
      qiniu-dart-sdk/base/lib/src/storage/methods/put/put.dart
  99. 3
      qiniu-dart-sdk/base/lib/src/storage/methods/put/put_controller.dart
  100. 28
      qiniu-dart-sdk/base/lib/src/storage/methods/put/put_options.dart
  101. Some files were not shown because too many files have changed in this diff Show More

6
android/app/build.gradle

@ -27,7 +27,7 @@ apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
apply plugin: 'com.mob.sdk' apply plugin: 'com.mob.sdk'
apply plugin: 'com.huawei.agconnect' //apply plugin: 'com.huawei.agconnect'
MobSDK { MobSDK {
appKey "m33ee7650da86a" appKey "m33ee7650da86a"
@ -203,7 +203,7 @@ dependencies {
implementation 'com.tencent.tpns:xiaomi:1.2.7.1-release' implementation 'com.tencent.tpns:xiaomi:1.2.7.1-release'
implementation 'com.tencent.tpns:huawei:1.2.6.0-release' // implementation 'com.tencent.tpns:huawei:1.2.6.0-release'
// HMS Core Push // HMS Core Push
implementation 'com.huawei.hms:push:5.3.0.304' // implementation 'com.huawei.hms:push:5.3.0.304'
} }

4
android/build.gradle

@ -1,5 +1,5 @@
buildscript { buildscript {
ext.kotlin_version = '1.7.10' ext.kotlin_version = '1.8.10'
repositories { repositories {
maven { url "https://www.jitpack.io" } maven { url "https://www.jitpack.io" }
maven {url 'https://developer.huawei.com/repo/'} maven {url 'https://developer.huawei.com/repo/'}
@ -23,7 +23,7 @@ buildscript {
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.4.2' classpath 'com.android.tools.build:gradle:7.4.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.huawei.agconnect:agcp:1.4.1.300' // classpath 'com.huawei.agconnect:agcp:1.4.1.300'
classpath 'com.mob.sdk:MobSDK:+' classpath 'com.mob.sdk:MobSDK:+'
classpath fileTree(include:['*.jar'], dir:'libs') classpath fileTree(include:['*.jar'], dir:'libs')
classpath 'com.umeng.umsdk:common:9.4.7' classpath 'com.umeng.umsdk:common:9.4.7'

4
android/gradle.properties

@ -1,5 +1,5 @@
org.gradle.jvmargs=-Xmx1536M #org.gradle.jvmargs=-Xmx1536M
#org.gradle.jvmargs=-Xmx4096m org.gradle.jvmargs=-Xmx4096m
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=true
MobSDK.mobEnv=x MobSDK.mobEnv=x

BIN
assets/image/2x/chat_more.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 868 B

BIN
assets/image/2x/fa_bu.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/image/2x/icon_gz.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/image/2x/icon_pl.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
assets/image/2x/icon_system_message_new.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

BIN
assets/image/2x/icon_z.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

BIN
assets/image/3x/chat_more.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/image/3x/fa_bu.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
assets/image/3x/icon_gz.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
assets/image/3x/icon_pl.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
assets/image/3x/icon_system_message_new.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

BIN
assets/image/3x/icon_z.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
assets/image/chat_more.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 B

BIN
assets/image/fa_bu.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 479 B

After

Width:  |  Height:  |  Size: 974 B

BIN
assets/image/icon_gz.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 822 B

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
assets/image/icon_pl.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 762 B

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
assets/image/icon_system_message_new.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
assets/image/icon_z.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 806 B

After

Width:  |  Height:  |  Size: 1.1 KiB

11
assets/svg/shequn.svg

@ -1,14 +1,13 @@
<svg width="22.000000" height="22.000000" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <svg width="30.000000" height="30.000000" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<desc> <desc>
Created with Pixso. Created with Pixso.
</desc> </desc>
<defs> <defs>
<clipPath id="clip114_923"> <clipPath id="clip244_618">
<rect id="shequn" width="22.000000" height="22.000000" fill="white" fill-opacity="0"/> <rect id="shequn-2" width="23.000000" height="23.000000" transform="translate(4.000000 4.000000)" fill="white" fill-opacity="0"/>
</clipPath> </clipPath>
</defs> </defs>
<g clip-path="url(#clip114_923)"> <g clip-path="url(#clip244_618)">
<path id="path" d="M13.69 14.75C12.4 15.71 11.05 16.56 9.63 17.32C8.21 18.07 6.75 18.72 5.24 19.26C5.45 19.41 5.67 19.55 5.89 19.68C6.11 19.81 6.34 19.93 6.57 20.04C6.8 20.16 7.04 20.26 7.28 20.36C7.52 20.45 7.77 20.54 8.01 20.61C8.26 20.69 8.51 20.76 8.76 20.82C9.01 20.87 9.27 20.92 9.52 20.96C9.78 21 10.03 21.02 10.29 21.04C10.55 21.06 10.81 21.07 11.07 21.07C11.33 21.06 11.58 21.05 11.84 21.03C12.1 21.01 12.35 20.98 12.61 20.94C12.87 20.89 13.12 20.84 13.37 20.78C13.62 20.72 13.87 20.65 14.11 20.57C14.36 20.49 14.6 20.4 14.84 20.3C15.08 20.2 15.32 20.1 15.55 19.98C15.78 19.86 16 19.74 16.22 19.6C16.44 19.47 16.66 19.33 16.87 19.18C17.08 19.02 17.28 18.87 17.48 18.7C17.68 18.53 17.87 18.36 18.05 18.18C18.24 18 18.42 17.81 18.59 17.61C18.76 17.42 18.92 17.22 19.07 17.01C19.23 16.8 19.37 16.59 19.51 16.37C19.65 16.16 19.78 15.93 19.9 15.7C20.02 15.47 20.13 15.24 20.24 15.01C20.34 14.77 20.43 14.53 20.52 14.28C20.6 14.04 20.68 13.79 20.74 13.54C20.81 13.29 20.87 13.04 20.91 12.79C20.96 12.53 20.99 12.28 21.02 12.02C21.05 11.76 21.06 11.5 21.07 11.24C21.08 10.99 21.07 10.73 21.06 10.47C21.05 10.21 21.02 9.95 20.99 9.7C20.96 9.44 20.92 9.19 20.86 8.93C20.81 8.68 20.75 8.43 20.68 8.18C19.68 9.44 18.59 10.62 17.42 11.72C16.25 12.82 15.01 13.83 13.69 14.75Z" fill="#32A060" fill-opacity="1.000000" fill-rule="nonzero"/> <path id="path" d="M25.61 12.46C26.85 16.9 25.25 21.67 21.64 24.34C18 27.04 13.15 27.07 9.48 24.44C12.63 23.27 15.61 21.63 18.31 19.56C21.06 17.57 23.52 15.18 25.61 12.46ZM9.36 6.64C13.29 3.73 18.61 3.95 22.3 7.18C24.55 6.38 26.21 6.27 26.78 7.12C28 8.88 23.93 14.03 17.7 18.67C11.46 23.3 5.43 25.62 4.21 23.87C3.63 23.04 4.24 21.45 5.7 19.5C3.93 14.84 5.45 9.53 9.36 6.64ZM13.38 9.61C12.11 9.61 11.08 10.69 11.08 12.01C11.08 13.33 12.11 14.41 13.38 14.41C14.64 14.41 15.67 13.33 15.67 12.01C15.67 10.69 14.64 9.61 13.38 9.61Z" fill="#D8D8D8" fill-opacity="1.000000" fill-rule="nonzero"/>
<path id="path" d="M21.79 3.25C21.24 2.46 19.65 2.56 17.51 3.31C17.3 3.13 17.08 2.96 16.85 2.8C16.63 2.64 16.39 2.49 16.15 2.34C15.91 2.2 15.67 2.07 15.42 1.95C15.17 1.82 14.92 1.71 14.66 1.61C14.4 1.51 14.13 1.42 13.87 1.34C13.6 1.26 13.33 1.19 13.06 1.14C12.79 1.08 12.51 1.03 12.24 1C11.96 0.97 11.68 0.94 11.41 0.93C11.13 0.92 10.85 0.92 10.57 0.93C10.29 0.95 10.02 0.97 9.74 1C9.47 1.04 9.19 1.08 8.92 1.14C8.65 1.2 8.38 1.27 8.11 1.35C7.84 1.43 7.58 1.52 7.32 1.62C7.06 1.72 6.81 1.83 6.56 1.96C6.31 2.08 6.07 2.21 5.83 2.35C5.59 2.5 5.36 2.65 5.13 2.81C4.9 2.97 4.69 3.14 4.47 3.32C4.26 3.5 4.06 3.69 3.86 3.89C3.66 4.09 3.48 4.29 3.3 4.51C3.12 4.72 2.95 4.94 2.79 5.16C2.63 5.39 2.47 5.62 2.33 5.86C2.19 6.1 2.06 6.35 1.94 6.6C1.82 6.85 1.71 7.1 1.61 7.36C1.51 7.62 1.42 7.88 1.34 8.15C1.26 8.42 1.19 8.69 1.13 8.96C1.08 9.23 1.03 9.51 1 9.78C0.97 10.06 0.94 10.34 0.93 10.61C0.92 10.89 0.92 11.17 0.94 11.45C0.95 11.73 0.97 12 1.01 12.28C1.04 12.55 1.09 12.83 1.15 13.1C1.21 13.37 1.28 13.64 1.36 13.91C1.44 14.17 1.53 14.44 1.63 14.7C0.23 16.5 -0.35 17.97 0.2 18.73C1.36 20.36 7.14 18.21 13.1 13.93C19.07 9.64 22.95 4.87 21.79 3.25ZM10.15 9.83L9.18 11.07L8.64 9.6L7.44 8.64L8.91 8.12L9.87 6.89L10.39 8.36L11.62 9.33L10.15 9.83Z" fill="#32A060" fill-opacity="1.000000" fill-rule="nonzero"/>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

27
ios/Runner.xcodeproj/project.pbxproj

@ -602,7 +602,7 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = YF3Q8DVP52; DEVELOPMENT_TEAM = YF3Q8DVP52;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -653,7 +653,7 @@
"$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/baidu",
"$(PROJECT_DIR)/Runner/baidu", "$(PROJECT_DIR)/Runner/baidu",
); );
MARKETING_VERSION = 3.2.31; MARKETING_VERSION = 3.2.33;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
@ -667,11 +667,12 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx; PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Profile; name = Profile;
@ -801,7 +802,7 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = YF3Q8DVP52; DEVELOPMENT_TEAM = YF3Q8DVP52;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -852,7 +853,7 @@
"$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/baidu",
"$(PROJECT_DIR)/Runner/baidu", "$(PROJECT_DIR)/Runner/baidu",
); );
MARKETING_VERSION = 3.2.31; MARKETING_VERSION = 3.2.33;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
@ -866,11 +867,13 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx; PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Debug; name = Debug;
@ -886,7 +889,8 @@
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO; CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = NO;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 8; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 10;
DEVELOPMENT_TEAM = YF3Q8DVP52; DEVELOPMENT_TEAM = YF3Q8DVP52;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = ( FRAMEWORK_SEARCH_PATHS = (
@ -937,7 +941,7 @@
"$(PROJECT_DIR)/baidu", "$(PROJECT_DIR)/baidu",
"$(PROJECT_DIR)/Runner/baidu", "$(PROJECT_DIR)/Runner/baidu",
); );
MARKETING_VERSION = 3.2.31; MARKETING_VERSION = 3.2.33;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = ( OTHER_LDFLAGS = (
"$(inherited)", "$(inherited)",
@ -951,11 +955,12 @@
); );
PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx; PRODUCT_BUNDLE_IDENTIFIER = com.zsw.hx;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = 1;
VERSIONING_SYSTEM = "apple-generic"; VERSIONING_SYSTEM = "apple-generic";
}; };
name = Release; name = Release;

2
lib/community/community_view/community_dynamic.dart

@ -174,6 +174,7 @@ class _CommunityDynamic extends State<CommunityDynamic> {
"memberId": (widget.article.author == widget.userId) "memberId": (widget.article.author == widget.userId)
? "0" ? "0"
: widget.article.author, : widget.article.author,
"inletType":0
}); });
widget.exitFull(); widget.exitFull();
} }
@ -560,6 +561,7 @@ class _CommunityDynamic extends State<CommunityDynamic> {
arguments: {"articleId": widget.article.id,"shareUrl":buildShareUrl()}); arguments: {"articleId": widget.article.id,"shareUrl":buildShareUrl()});
return; return;
} else if (platform == ShareSDKPlatforms.facebook) { } else if (platform == ShareSDKPlatforms.facebook) {
Navigator.of(context).popAndPushNamed('/router/chat_details_page', Navigator.of(context).popAndPushNamed('/router/chat_details_page',
arguments: {"articleId": widget.article.id, arguments: {"articleId": widget.article.id,
"shareUrl":buildShareUrl(),}); "shareUrl":buildShareUrl(),});

88
lib/constant.dart

@ -0,0 +1,88 @@
import 'package:flutter/foundation.dart';
/// URL的HOST
const String chatImageHost = "http://skk8mlm5b.hn-bkt.clouddn.com/";
/// socket的host和port端口
//47.93.216.24:9090 线 192.168.10.200/192.168.10.129:9090
const String socketHost = kDebugMode ? '192.168.10.200' : '47.93.216.24';
const num socketPort = 9090;
const ipBaseUrl = "http://whois.pconline.com.cn";
///
const localMiniBaseUrl = "http://192.168.10.54:8765/app/";///
const serviceMiniBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线
/// app接口的请求地址
const localBaseUrl = "http://192.168.10.54:8766/app/";///
const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线
/// list进行分组
Map<S, List<T>> groupBy<S, T>(Iterable<T> values, S Function(T) key) {
var map = <S, List<T>>{};
for (var element in values) {
(map[key(element)] ??= []).add(element);
}
return map;
}
/// list进行分组计数
Map<String, int> groupCount<S, T>(Map<S, List<T>> values) {
var map = <String, int>{};
for (var element in values.keys) {
map["$element"] = values[element]?.length ?? 0;
}
return map;
}
/// list进行分组并取最大值
Map<String, T> groupItem<S, T>(Map<S, List<T>> values, {int Function(T) key}) {
var map = <String, T>{};
for (var element in values.keys) {
if (values[element] == null) {
continue;
}
map["$element"] = key == null ? values[element].first : values[element].lMax(key);
}
return map;
}
///
T max<T>(Iterable<T> list, int Function(T) key) {
T tt;
for (T t in list) {
if (tt == null) {
tt = t;
}
if (key(tt) < key(t)) {
tt = t;
}
}
return tt;
}
extension ListExtension<S, T> on Iterable<T> {
Map<S, List<T>> lGroupBy(S Function(T) key) {
return groupBy(this, key);
}
T lMax(int Function(T) key) {
return max(this, key);
}
}
extension MapExtension<S, T> on Map<S, List<T>> {
Map<String, int> get mGroupCount => groupCount(this);
Map<String, T> mGroupItem({int Function(T) key}) {
return groupItem(this, key: key);
}
}

1
lib/home/home_page.dart

@ -855,4 +855,5 @@ class HomePageState extends State<HomePage> with AutomaticKeepAliveClientMixin {
@override @override
bool get wantKeepAlive => true; bool get wantKeepAlive => true;
} }

210
lib/im/SocketClient.dart

@ -1,103 +1,245 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:core';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:huixiang/constant.dart';
import 'package:huixiang/im/Proto.dart'; import 'package:huixiang/im/Proto.dart';
import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/message.dart';
import 'package:huixiang/im/out/auth.pb.dart'; import 'package:huixiang/im/out/auth.pb.dart';
import 'package:huixiang/im/out/message.pb.dart'; import 'package:huixiang/im/out/message.pb.dart';
import 'package:huixiang/main.dart'; import 'package:huixiang/main.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
class SocketClient { class SocketClient {
Socket _socket; Socket _socket;
SharedPreferences shared; SharedPreferences _shared;
Timer timer;
bool get heartbeatActive => timer != null && timer.isActive;
connect() async { connect() async {
shared = await SharedPreferences.getInstance(); _shared = await SharedPreferences.getInstance();
await Socket.connect('192.168.10.129', 9090).then((value) { if (_socket != null) {
debugPrint("socket-connect"); reconnect();
return;
}
showDebugToast("socket-connect .... ");
await Socket.connect(socketHost, socketPort).then((value) {
debugPrint("socket-connect-$socketHost");
_socket = value; _socket = value;
_socket.listen((data) { _socket.listen((data) {
print(data); print(data);
print("socket-listen"); print("socket-listen");
Proto proto = Proto.fromBytes(data); Proto proto = Proto.fromBytes(data);
print("socket-listen: $proto"); MsgData dataResult = MsgData.fromBuffer(proto.body);
MsgData data1 = MsgData.fromBuffer(proto.body); print('收到来自:${dataResult.from},消息内容: ${utf8.decode(dataResult.data)} ');
print('收到来自:${data1.from},消息内容: ${utf8.decode(data1.data)} ');
hxDatabase.messageDao.insertMessage(createMessage(mobile, utf8.decode(data1.data), userId: data1.from));
callbacks.forEach((callback) { receiveInsertMessage(dataResult).then((messageMap) {
callback.call(data1); if (callbacks[dataResult.from] != null) {
messageMap["state"] = 1;
}
hxDatabase.insert(messageMap).then((value) {
messageMap["id"] = value;
Message message = Message.fromJson(messageMap);
if (callbacks[dataResult.from] != null) {
callbacks[dataResult.from].call(message); /// user conversation callback
}
callbacks[userId]?.call(message); /// user self conversation list callback
});
}); });
}, onError: (Object error, StackTrace stackTrace) { }, onError: (Object error, StackTrace stackTrace) {
debugPrint("socket-error: $error, stackTrace: ${stackTrace}"); debugPrint("socket-listen-error: $error, stackTrace: $stackTrace");
showDebugToast("socket-listen-error: $error, stackTrace: $stackTrace");
reconnect();
}, onDone: () {
debugPrint("socket-listen-down: down");
}); });
authRequest(shared.getString("token")); authRequest(_shared.getString("token"));
heartbeat();
}).catchError((error) { }).catchError((error) {
debugPrint("socket-connect-error: $error"); debugPrint("socket-connect-error: $error");
showDebugToast("socket-connect-error: $error");
_socket = null;
reconnect();
});
}
Future<Map<String, dynamic>> receiveInsertMessage(MsgData dataResult) async {
Uint8List dataBytes = dataResult.data;
String content = utf8.decode(dataBytes);
String attach = "";
MsgType type = MsgType.values[dataResult.type.value];
if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) {
String filePath = await qiniu.downloadFile(content);
Map<String, dynamic> result = await ImageGallerySaver.saveFile(filePath).catchError((error){});
bool isSuccess = result["isSuccess"] != null && result["isSuccess"];
attach = filePath;
}
Map<String, dynamic> messageMap = createMessage(userId, content, attach: attach, msgType: type.value, fromId: dataResult.from);
return messageMap;
}
showDebugToast(text) {
if (kDebugMode) {
SmartDialog.showToast(text, alignment: Alignment.center);
}
}
Proto heartbeatData() {
DateTime dateTime = DateTime.now();
int millisecondsTime = dateTime.millisecondsSinceEpoch;
Uint8List data = utf8.encode(jsonEncode({"heartbeat": millisecondsTime}));
MsgData msgData = MsgData(from: userId, type: MsgType.TEXT, data: data);
final proto2 = Proto(3, 1, msgData.writeToBuffer());
debugPrint("heartbeat: ${dateTime.toString()}");
return proto2;
}
heartbeat() {
cancelTimer();
timer = Timer.periodic(const Duration(milliseconds: 30000), (timer) {
if(!checkSocket()) {
timer.cancel();
return;
}
sendHeartBeatAndCheckSocket();
}); });
} }
List<Function> callbacks = []; /// send heartBeat and check socket is connected
/// send error: reconnect,
/// else check Timer.periodic isActive ,
/// if not active: Timer.periodic send heartBeat
sendHeartBeatAndCheckSocket() {
final proto2 = heartbeatData();
try {
_socket.add(proto2.toBytes());
if (!socketClient.heartbeatActive) {
heartbeat();
debugPrint("socket-periodic-send-heart-beat");
}
} catch (e) {
debugPrint("socket-send-error: ${e.toString()}");
showDebugToast("socket-send-error: ${e.toString()}");
reconnect();
}
}
addCallback(Function callback) { cancelTimer() {
callbacks.add(callback); if (timer != null && timer.isActive) {
timer.cancel();
timer = null;
}
} }
removeCallback(Function callback) { reconnect() {
callbacks.remove(callback); dispose();
Future.delayed(Duration(milliseconds: 3000), () {
connect();
});
}
Map<String, Function(Message message)> callbacks = <String, Function(Message message)>{};
addCallback(String userId, callback) {
callbacks[userId] = callback;
}
removeCallback(String userId) {
callbacks.remove(userId);
} }
dispose() { dispose() {
if (_socket != null) {
_socket.close(); _socket.close();
_socket = null;
}
} }
authRequest(String token) { authRequest(String token) {
if (!checkSocket()) { if (!checkSocket()) {
return; return;
} }
debugPrint("socket-authRequest: request");
final authReq = AuthReq() final authReq = AuthReq()
..uid = mobile ..uid = userId
..token = token; ..token = token;
final authReqBytes = authReq.writeToBuffer(); final authReqBytes = authReq.writeToBuffer();
final proto = Proto(1, 1, authReqBytes); // operation seqId 1 final proto = Proto(1, 1, authReqBytes); // operation seqId 1
final protoBytes = proto.toBytes(); final protoBytes = proto.toBytes();
try {
_socket.add(protoBytes); _socket.add(protoBytes);
} catch (e) {
debugPrint("socket-authRequest: $e");
Future.delayed(const Duration(milliseconds: 1000), () {
authRequest(token);
});
}
} }
sendMessage(int toId, String content) { Future<Message> sendMessage(String toId, String content, {String attach, int msgType = 1, replyId}) async {
MsgType type = MsgType.values[msgType];
Uint8List data = utf8.encode(content);
if (type == MsgType.IMAGE || type == MsgType.VIDEO || type == MsgType.AUDIO) {
File file = File(attach);
Directory dir = await getTemporaryDirectory();
File newFile = await file.copy("${dir.path}/hx_${attach.split('/').last}");
attach = newFile.path;
}
Map<String, dynamic> message = createMessage(toId, content, fromId: userId, attach: attach, msgType: msgType, replyId: replyId);
message["state"] = 1;
int id = await hxDatabase.insert(message).catchError((error) {
debugPrint("insertMessage: $error");
});
if (!checkSocket()) { if (!checkSocket()) {
return; hxDatabase.update({"id": id, "state": 3}).catchError((error) {
debugPrint("insertMessage: ${error.toString()}");
});
message["id"] = id;
message["state"] = 3;
return Message.fromJson(message);
} }
Uint8List data = utf8.encode(content); message["id"] = id;
MsgData msgData = MsgData(to: toId, from: mobile, type: MsgType.SINGLE_TEXT,data: data);
MsgData msgData = MsgData(to: toId, from: userId, type: type, data: data);
final proto2 = Proto(5, 1, msgData.writeToBuffer()); final proto2 = Proto(5, 1, msgData.writeToBuffer());
try {
_socket.add(proto2.toBytes()); _socket.add(proto2.toBytes());
hxDatabase.messageDao.insertMessage(createMessage(toId, content, userId: mobile)).catchError((error) { debugPrint("socket-send-success:");
debugPrint("insertMessage: $error"); } catch (e) {
hxDatabase.update({"id": id, "state": 3}).catchError((error) {
debugPrint("insertMessage: ${error.toString()}");
}); });
debugPrint("insertMessage: end"); debugPrint("socket-send-error: ${e.toString()}");
showDebugToast("socket-send-error: ${e.toString()}");
reconnect();
}
return Message.fromJson(message);
} }
checkSocket() { checkSocket() {
if (_socket == null) { if (_socket == null) {
connect(); reconnect();
return false; return false;
} }
return true; return true;
} }
get mobile => 123456; String get userId => _shared.getString("userId");
} }

150
lib/im/add_friend.dart

@ -18,8 +18,6 @@ class AddFriend extends StatefulWidget {
class _AddFriend extends State<AddFriend> { class _AddFriend extends State<AddFriend> {
ApiService apiService; ApiService apiService;
final TextEditingController editingController = TextEditingController();
FocusNode _focusNode = FocusNode();
@override @override
void initState() { void initState() {
@ -29,7 +27,6 @@ class _AddFriend extends State<AddFriend> {
/// ///
@override @override
void dispose() { void dispose() {
_focusNode.unfocus();
super.dispose(); super.dispose();
} }
@ -70,7 +67,7 @@ class _AddFriend extends State<AddFriend> {
) )
], ],
),), ),),
friendGroupSearch(), addFriendSearch(),
Padding( Padding(
padding: EdgeInsets.only(left:18.w,bottom: 16.h), padding: EdgeInsets.only(left:18.w,bottom: 16.h),
child: Text( child: Text(
@ -96,7 +93,7 @@ class _AddFriend extends State<AddFriend> {
); );
} }
/// ///
Widget addFriendItem() { Widget addFriendItem() {
return Container( return Container(
margin: EdgeInsets.only(left:16.w,right:16.w,bottom: 24.h), margin: EdgeInsets.only(left:16.w,right:16.w,bottom: 24.h),
@ -122,7 +119,12 @@ class _AddFriend extends State<AddFriend> {
fontWeight: FontWeight.w500), fontWeight: FontWeight.w500),
), ),
), ),
Container( GestureDetector(
behavior: HitTestBehavior.opaque,
onTap:(){
showFriendVerificationDialog();
},
child: Container(
padding: EdgeInsets.symmetric(horizontal:18.w,vertical:6.h), padding: EdgeInsets.symmetric(horizontal:18.w,vertical:6.h),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26.5.r), borderRadius: BorderRadius.circular(26.5.r),
@ -143,48 +145,140 @@ class _AddFriend extends State<AddFriend> {
color: Colors.white, color: Colors.white,
fontWeight:MyFontWeight.regular), fontWeight:MyFontWeight.regular),
), ),
),
) )
]), ]),
); );
} }
/// ///
Widget friendGroupSearch() { showFriendVerificationDialog() {
return Container( showDialog(
margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w,29.h), context: context,
padding: EdgeInsets.symmetric(vertical: 13.h), builder: (context) {
return AlertDialog(
contentPadding: EdgeInsets.zero, //
content: Container(
// width: MediaQuery.of(context).size.width - 84,
height: 130.h,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFFDFCFC),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: TextField( child: Column(
textInputAction: TextInputAction.search, mainAxisAlignment: MainAxisAlignment.center,
onEditingComplete: () { crossAxisAlignment: CrossAxisAlignment.center,
FocusScope.of(context).requestFocus(FocusNode()); children: [
Padding(padding:EdgeInsets.symmetric(vertical:16.h),
child: Text(
"好友验证",
style: TextStyle(
color: Color(0xFF060606),
fontSize: 16.sp,
fontWeight: MyFontWeight.bold,
),
),),
Expanded(child:Text(
"我是秀才8856",
style: TextStyle(
color: Color(0xFF060606),
fontSize: 12.sp,
fontWeight: MyFontWeight.regular,
),
)),
// Spacer(),
Container(
margin:EdgeInsets.only(top:18.h),
height:1.h,
width: double.infinity,
color:Color(0xFFEDEDED),
),
Row(
children: [
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
}, },
controller: editingController, child: Container(
// height: 38.h,
child: Text(
"取消",
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 16.sp,
color: Color(0xFF060606),
)
)
)
)
), ),
decoration: InputDecoration( Container(
hintText: "搜索", height:45,
hintStyle: TextStyle( width: 1.w,
fontSize: 14.sp, color: Color(0xFFEDEDED),
color: Color(0xFFA29E9E), ),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
},
child: Container(
// height: 38.h,
child: Text(
"确认",
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF060606),
)
)
)
)
)
],
)
],
),
),
);
},
);
}
///
Widget addFriendSearch() {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap:(){Navigator.of(context).pushNamed('/router/im_search');},
child: Container(
margin: EdgeInsets.fromLTRB(16.w,22.h, 16.w, 24.h),
padding: EdgeInsets.symmetric(vertical: 13.h),
decoration: BoxDecoration(
color: Color(0xFFFDFCFC),
borderRadius: BorderRadius.circular(4),
), ),
isCollapsed: true, child: Row(
prefixIcon: Padding( children: [
Padding(
padding: EdgeInsets.only(left: 15.w, right: 5.w), padding: EdgeInsets.only(left: 15.w, right: 5.w),
child: Image.asset( child: Image.asset(
"assets/image/icon_search.webp", "assets/image/icon_search.webp",
width: 14.h, width: 14.h,
height: 14.h, height: 14.h,
color: Color(0xFFB3B3B3), color: Color(0xFF353535),
),
), ),
Text(
"搜索",
style: TextStyle(
color: Color(0xFFA29E9E),
fontSize: 16.sp,
fontWeight: MyFontWeight.regular,
), ),
prefixIconConstraints: BoxConstraints(),
border: InputBorder.none,
), ),
],
)
), ),
); );
} }

1172
lib/im/chat_details_page.dart

File diff suppressed because it is too large Load Diff

98
lib/im/chat_friend_group.dart

@ -1,17 +1,17 @@
import 'dart:ui'; import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/retrofit/retrofit_api.dart';
import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:huixiang/view_widget/my_appbar.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../generated/l10n.dart'; import '../../generated/l10n.dart';
import '../../utils/font_weight.dart'; import '../retrofit/data/base_data.dart';
import '../main.dart'; import '../retrofit/data/social_info.dart';
import '../utils/event_type.dart';
import '../view_widget/my_tab.dart'; import '../view_widget/my_tab.dart';
import 'im_view/custom_underline_tabIndicator.dart'; import 'im_view/custom_underline_tabIndicator.dart';
import 'im_view/friend_groip_list.dart'; import 'im_view/friend_groip_list.dart';
@ -28,56 +28,84 @@ class _ChatFriendGroup extends State<ChatFriendGroup>
ApiService apiService; ApiService apiService;
TabController tabController; TabController tabController;
List<GlobalKey> _allKey = []; List<GlobalKey> _allKey = [];
SocialInfo infoNumber;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
tabController = TabController(length: 3, vsync: this, initialIndex: 0); tabController = TabController(length: 3, vsync: this, initialIndex: 0);
tabController.addListener(() {
if(!tabController.indexIsChanging)
setState(() {});
});
loadFinish(); loadFinish();
querySocialInfo();
}
@override
void dispose() {
super.dispose();
tabController.dispose();
} }
loadFinish() { loadFinish() {
_allKey = [GlobalKey(), GlobalKey(), GlobalKey(), GlobalKey()]; _allKey = [GlobalKey(), GlobalKey(), GlobalKey()];
setState(() {}); setState(() {});
} }
///(///)
querySocialInfo() async {
SharedPreferences value = await SharedPreferences.getInstance();
apiService = ApiService(Dio(),
context: context, token: value.getString("token"), showLoading: false);
BaseData<SocialInfo> baseData =
await apiService.socialInfo().catchError((onError) {});
if (baseData != null && baseData.isSuccess) {
setState(() {
infoNumber = baseData.data;
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color(0xFFFFFFFF), backgroundColor: Color(0xFFFFFFFF),
appBar: MyAppBar( appBar: MyAppBar(
title: "好友(2)", title: tabController.index == 0 ? "${S.of(context).haoyou} (${infoNumber?.mutualFollowCount ?? "0"})" :
(tabController.index == 1 ? "${S.of(context).guanzhu} (${infoNumber?.follow ?? "0"})" : "${S.of(context).fensi} (${infoNumber?.fans ?? "0"})"),
titleColor: Color(0xFF0D0D0D), titleColor: Color(0xFF0D0D0D),
titleSize: 17.sp, titleSize: 17.sp,
leading: true, leading: true,
leadingColor: Colors.black, leadingColor: Colors.black,
background: Color(0xFFFFFFFF), background: Color(0xFFFFFFFF),
action: GestureDetector( // action: GestureDetector(
behavior: HitTestBehavior.opaque, // behavior: HitTestBehavior.opaque,
onTap: () { // onTap: () {
Navigator.of(context).pushNamed('/router/add_friend'); // Navigator.of(context).pushNamed('/router/add_friend');
}, // },
child: Container( // child: Container(
padding: EdgeInsets.all(12), // padding: EdgeInsets.all(12),
decoration: BoxDecoration( // decoration: BoxDecoration(
color: Color(0xFFFFFFFF), // color: Color(0xFFFFFFFF),
borderRadius: BorderRadius.circular(20.r), // borderRadius: BorderRadius.circular(20.r),
boxShadow: [ // boxShadow: [
BoxShadow( // BoxShadow(
color: Color(0xFF000000).withAlpha(25), // color: Color(0xFF000000).withAlpha(25),
offset: Offset(0, 0), // offset: Offset(0, 0),
blurRadius: 4, // blurRadius: 4,
spreadRadius: 0, // spreadRadius: 0,
) // )
], // ],
), // ),
child: Image.asset( // child: Image.asset(
"assets/image/add_friend.webp", // "assets/image/add_friend.webp",
fit: BoxFit.fill, // fit: BoxFit.fill,
height: 14.h,width: 14.h, // height: 14.h,width: 14.h,
), // ),
), // ),
), // ),
), ),
body: Container( body: Container(
child: Column( child: Column(
@ -120,9 +148,9 @@ class _ChatFriendGroup extends State<ChatFriendGroup>
child: TabBarView( child: TabBarView(
controller: tabController, controller: tabController,
children: [ children: [
FriendGroupList(_allKey[0]), FriendGroupList(_allKey[0],"","好友"),
FriendGroupList(_allKey[1]), FriendGroupList(_allKey[1],"false", "关注"),
FriendGroupList(_allKey[2]), FriendGroupList(_allKey[2],"true","粉丝"),
], ],
), ),
) )

51
lib/im/chat_setting.dart

@ -4,13 +4,18 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/im/database/message.dart';
import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:huixiang/view_widget/my_appbar.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import '../../generated/l10n.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../utils/font_weight.dart'; import '../../utils/font_weight.dart';
import '../main.dart';
import '../retrofit/data/im_user.dart';
class ChatSetting extends StatefulWidget { class ChatSetting extends StatefulWidget {
final Map<String, dynamic> arguments;
ChatSetting({this.arguments});
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
@ -19,12 +24,21 @@ class ChatSetting extends StatefulWidget {
} }
class _ChatSetting extends State<ChatSetting> { class _ChatSetting extends State<ChatSetting> {
ApiService apiService; ImUser imUser;
bool topSetting = false; String selfUserId = "";
@override @override
void initState() { void initState() {
super.initState(); super.initState();
initData();
}
void initData() async {
selfUserId = (await SharedPreferences.getInstance()).getString("userId");
imUser = await hxDatabase.queryImUserById(widget.arguments["userId"]);
setState(() {
});
} }
@override @override
@ -61,18 +75,23 @@ class _ChatSetting extends State<ChatSetting>{
), ),
), ),
CupertinoSwitch( CupertinoSwitch(
value: (topSetting), value: ((imUser?.isTop ?? 0) == 1),
activeColor: Color(0xFF32A060), activeColor: Color(0xFF32A060),
onChanged: (bool value) { onChanged: (bool value) async {
setState((){ if (imUser == null) return;
topSetting = !topSetting; imUser.isTop = value ? 1 : 0;
}); await hxDatabase.insertOrUpdateImUser(imUser.toJson());
setState(() {});
}, },
), ),
], ],
), ),
SizedBox(height:31.h,), SizedBox(
Row( height: 31.h,
),
GestureDetector(
behavior: HitTestBehavior.opaque,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
@ -91,12 +110,18 @@ class _ChatSetting extends State<ChatSetting>{
color: Colors.black, color: Colors.black,
), ),
], ],
),
onTap: () async {
await hxDatabase.deleteByUser(conversationId(widget.arguments["userId"] ?? "", selfUserId));
// SmartDialog.showToast("删除成功",
// alignment: Alignment.center);
},
) )
], ],
), ),
), ),
Expanded(child: Expanded(
Container( child: Container(
color: Colors.white, color: Colors.white,
)) ))
], ],

10
lib/im/contact_share.dart

@ -8,8 +8,6 @@ import 'package:huixiang/retrofit/retrofit_api.dart';
import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:huixiang/view_widget/my_appbar.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import '../../generated/l10n.dart';
import '../../retrofit/data/base_data.dart';
import '../../utils/font_weight.dart'; import '../../utils/font_weight.dart';
class ContactsShare extends StatefulWidget { class ContactsShare extends StatefulWidget {
@ -160,8 +158,8 @@ class _ContactsShare extends State<ContactsShare> {
// width: 44, // width: 44,
// height: 44, // height: 44,
// fit: BoxFit.cover, // fit: BoxFit.cover,
// errorSrc: "assets/image/default_user.webp", // errorSrc: "assets/image/default_1.webp",
// fadeSrc: "assets/image/default_user.webp", // fadeSrc: "assets/image/default_1.webp",
// ), // ),
Image.asset( Image.asset(
"assets/image/fuka_zj.webp", "assets/image/fuka_zj.webp",
@ -215,8 +213,8 @@ class _ContactsShare extends State<ContactsShare> {
// width: 44, // width: 44,
// height: 44, // height: 44,
// fit: BoxFit.cover, // fit: BoxFit.cover,
// errorSrc: "assets/image/default_user.webp", // errorSrc: "assets/image/default_1.webp",
// fadeSrc: "assets/image/default_user.webp", // fadeSrc: "assets/image/default_1.webp",
// ), // ),
Image.asset( Image.asset(
"assets/image/fuka_zj.webp", "assets/image/fuka_zj.webp",

228
lib/im/database/hx_database.dart

@ -1,18 +1,226 @@
import 'package:flutter/cupertino.dart';
import 'package:huixiang/constant.dart';
import 'package:huixiang/im/database/message.dart';
import 'package:huixiang/im/database/migration.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sqflite/sqflite.dart';
import '../../retrofit/data/im_user.dart';
import 'dart:async'; class HxDatabase {
Database db;
import 'package:floor/floor.dart'; void open({String key}) async {
import 'package:flutter/cupertino.dart'; // _migrations.add(Migration(3, 4, (Database database) async {
import 'package:huixiang/im/database/message_dao.dart'; // await database.execute('ALTER TABLE ImUser ADD COLUMN IF NOT EXISTS `isTop` INTEGER DEFAULT 0');
import 'package:huixiang/im/database/message.dart'; // }));
import 'package:sqflite/sqflite.dart' as sqflite;
String databaseName = 'hx.db';
if (key?.isNotEmpty ?? false) {
databaseName = 'hx_$key.db';
}
await openDatabase(databaseName, version: 1, onCreate: (Database db, int version) async {
db.execute('CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `conversationId` VARCHAR(40), `fromId` VARCHAR(20), `toId` VARCHAR(20), `replyId` VARCHAR(20), `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` VARCHAR(20), `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))');
db.execute('CREATE TABLE IF NOT EXISTS `ImUser` (`id` INTEGER, `mid` VARCHAR(20), `nickname` VARCHAR(20), `avatar` VARCHAR(200), `phone` VARCHAR(200), `isDelete` INTEGER, `isTop` INTEGER, PRIMARY KEY (`id`))');
}, onConfigure: (database) async {
await database.execute('PRAGMA foreign_keys = ON');
debugPrint("database-version: ${await database.getVersion()}");
}, onUpgrade: (database, startVersion, endVersion) async {
await runMigrations(database, startVersion, endVersion, _migrations);
}, onOpen: (Database db) {
this.db = db;
});
}
void close() {
db.close();
}
_dbIsOpen() async {
if (db == null || !db.isOpen) {
var sp = await SharedPreferences.getInstance();
open(key: sp.getString("userId"));
}
}
part 'hx_database.g.dart'; Future<Message> lastMessage(String conversationId) async {
await _dbIsOpen();
String sql = 'SELECT * FROM Message WHERE conversationId = ? ORDER BY time DESC LIMIT 1';
List<Message> messages = await db.rawQuery(sql, [conversationId]).then((value) {
return value.map((e) {
debugPrint("Message: $e");
return Message.fromJson(e);
}).toList();
}, onError: (error) {
debugPrint("Message_error: $error");
});
return (messages?.isNotEmpty ?? false) ? messages.first : null;
}
Future<List<Message>> queryList() async{
await _dbIsOpen();
String sql = '''SELECT *
FROM `Message`
WHERE ROWID IN (
SELECT ROWID
FROM (
SELECT ROWID, conversationId, MAX(`time`) AS max_time
FROM `Message`
GROUP BY conversationId
) AS grouped_messages
WHERE max_time = (
SELECT MAX(`time`)
FROM `Message`
WHERE conversationId = grouped_messages.conversationId
)
)
ORDER BY `time` DESC;''';
return db.rawQuery(sql).then((value) {
return value.map((e) {
debugPrint("Message: $e");
return Message.fromJson(e);
}).toList();
}, onError: (error) {
debugPrint("Message-error: $error");
});
}
Future<List<Message>> queryUList(conversationId, {int page = 1, int pageSize = 10}) async{
await _dbIsOpen();
int start = (page - 1) * pageSize;
String sql = 'SELECT * FROM Message WHERE conversationId = ? ORDER BY time DESC LIMIT ?, ?';
return db.rawQuery(sql, [conversationId, start, pageSize]).then((value) {
return value.map((e) => Message.fromJson(e)).toList();
}, onError: (error) {
debugPrint("Message-error: $error");
});
}
Future<List<Message>> queryTList(conversationId) async{
await _dbIsOpen();
String sql = 'SELECT *, time / 300000 * 300000 AS time_interval FROM Message WHERE conversationId = ? GROUP BY time_interval ORDER BY time DESC';
return db.rawQuery(sql, [conversationId]).then((value) {
return value.map((e) => Message.fromJson(e)).toList();
}, onError: (error) {
debugPrint("Message-error: $error");
});
}
Future<Map<String, int>> messageUnreadCount(List<String> conversationIds) async {
await _dbIsOpen();
String userStr = conversationIds.join("','");
debugPrint("userStr: $userStr");
List<Message> messages = await db.query("Message",
where: "conversationId IN ('$userStr') AND state = 0 AND isDelete = 0",
whereArgs: [],
).then((value) {
return value.map((e) => Message.fromJson(e)).toList();
}, onError: (error) {
debugPrint("Message-error: $error");
});
return (messages??[]).lGroupBy((p) => p.conversationId).mGroupCount;
}
@Database(version: 1, entities: [Message]) Future<List<Map>> queryListAll() async{
abstract class HxDatabase extends FloorDatabase { await _dbIsOpen();
String sql = 'SELECT * FROM Message ORDER BY time DESC';
return db.rawQuery(sql);
}
MessageDao get messageDao; Future<int> deleteByUser(String conversationId) async {
return db.delete("Message",where: "conversationId = ?", whereArgs: [conversationId]);
}
Future<int> deleteByMsgId(String id) async {
return db.delete("Message",where: "id = ?", whereArgs: [id]);
}
Future<int> deleteAll() async {
return db.delete("Message");
}
update(Map<dynamic, dynamic> message) async{
await _dbIsOpen();
debugPrint("Message_insert: $message");
return db.update("Message", message,
where: 'id = ?', whereArgs: [message['id']]);
}
Future<int> insert(Map message) async {
await _dbIsOpen();
debugPrint("Message_insert: $message");
return db.insert("Message", message);
}
/// update message read state
readMessage(String conversationId) async{
await _dbIsOpen();
db.update("Message", {"state": 1},
where: "conversationId = ? AND state = 0 AND isDelete = 0",
whereArgs: [conversationId]);
}
Future<int> insertOrUpdateImUser(Map imUserMap) async {
await _dbIsOpen();
debugPrint("imUser_insert: $imUserMap");
if ((await queryImUserById(imUserMap['mid'])) == null)
return db.insert("ImUser", imUserMap);
else
return db.update("ImUser", imUserMap,
where: 'mid = ?', whereArgs: [imUserMap['mid']]);
}
Future<List<ImUser>> queryImUser(List<String> userIds) async {
await _dbIsOpen();
String query =
'SELECT * FROM ImUser WHERE mid IN (${userIds.map((mid) => "'$mid'").join(',')})';
return db.rawQuery(query).then((value) {
return value.map((e) => ImUser.fromJson(e)).toList();
}, onError: (error) {
debugPrint("ImUser_error: $error");
});
}
Future<ImUser> queryImUserById(String userId) async {
await _dbIsOpen();
List<ImUser> imUser = await db.query("ImUser",
distinct: true, where: "mid = ?", whereArgs: [userId]).then((value) {
return value.map((e) => ImUser.fromJson(e)).toList();
}, onError: (error) {
debugPrint("ImUser_error: $error");
});
return (imUser?.isNotEmpty ?? false) ? imUser.first : null;
}
final List<Migration> _migrations = [];
addMigrations(List<Migration> migrations) {
_migrations.addAll(migrations);
return this;
}
Future<void> runMigrations(
final Database migrationDatabase,
final int startVersion,
final int endVersion,
final List<Migration> migrations,
) async {
final relevantMigrations = migrations
.where((migration) => migration.startVersion >= startVersion)
.toList()
..sort(
(first, second) => first.startVersion.compareTo(second.startVersion));
if (relevantMigrations.isEmpty ||
relevantMigrations.last.endVersion != endVersion) {
throw StateError(
'There is no migration supplied to update the database to the current version.'
' Aborting the migration.',
);
}
for (final migration in relevantMigrations) {
await migration.migrate(migrationDatabase);
}
}
} }

290
lib/im/database/hx_database.g.dart

@ -1,144 +1,146 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // // GENERATED CODE - DO NOT MODIFY BY HAND
//
part of 'hx_database.dart'; // part of 'hx_database.dart';
//
// ************************************************************************** // // **************************************************************************
// FloorGenerator // // FloorGenerator
// ************************************************************************** // // **************************************************************************
//
// ignore: avoid_classes_with_only_static_members // // ignore: avoid_classes_with_only_static_members
class $FloorHxDatabase { // import 'package:floor/floor.dart';
/// Creates a database builder for a persistent database. //
/// Once a database is built, you should keep a reference to it and re-use it. // class $FloorHxDatabase {
static _$HxDatabaseBuilder databaseBuilder(String name) => // /// Creates a database builder for a persistent database.
_$HxDatabaseBuilder(name); // /// Once a database is built, you should keep a reference to it and re-use it.
// static _$HxDatabaseBuilder databaseBuilder(String name) =>
/// Creates a database builder for an in memory database. // _$HxDatabaseBuilder(name);
/// Information stored in an in memory database disappears when the process is killed. //
/// Once a database is built, you should keep a reference to it and re-use it. // /// Creates a database builder for an in memory database.
static _$HxDatabaseBuilder inMemoryDatabaseBuilder() => // /// Information stored in an in memory database disappears when the process is killed.
_$HxDatabaseBuilder(null); // /// Once a database is built, you should keep a reference to it and re-use it.
} // static _$HxDatabaseBuilder inMemoryDatabaseBuilder() =>
// _$HxDatabaseBuilder(null);
class _$HxDatabaseBuilder { // }
_$HxDatabaseBuilder(this.name); //
// class _$HxDatabaseBuilder {
final String name; // _$HxDatabaseBuilder(this.name);
//
final List<Migration> _migrations = []; // final String name;
//
Callback _callback; // final List<Migration> _migrations = [];
//
/// Adds migrations to the builder. // Callback _callback;
_$HxDatabaseBuilder addMigrations(List<Migration> migrations) { //
_migrations.addAll(migrations); // /// Adds migrations to the builder.
return this; // _$HxDatabaseBuilder addMigrations(List<Migration> migrations) {
} // _migrations.addAll(migrations);
// return this;
/// Adds a database [Callback] to the builder. // }
_$HxDatabaseBuilder addCallback(Callback callback) { //
_callback = callback; // /// Adds a database [Callback] to the builder.
return this; // _$HxDatabaseBuilder addCallback(Callback callback) {
} // _callback = callback;
// return this;
/// Creates the database and initializes it. // }
Future<HxDatabase> build() async { //
final path = name != null // /// Creates the database and initializes it.
? await sqfliteDatabaseFactory.getDatabasePath(name) // Future<HxDatabase> build() async {
: ':memory:'; // final path = name != null
final database = _$HxDatabase(); // ? await sqfliteDatabaseFactory.getDatabasePath(name)
database.database = await database.open( // : ':memory:';
path, // final database = _$HxDatabase();
_migrations, // database.database = await database.open(
_callback, // path,
); // _migrations,
return database; // _callback,
} // );
} // return database;
// }
class _$HxDatabase extends HxDatabase { // }
_$HxDatabase([StreamController<String> listener]) { //
changeListener = listener ?? StreamController<String>.broadcast(); // class _$HxDatabase extends HxDatabase {
} // _$HxDatabase([StreamController<String> listener]) {
// changeListener = listener ?? StreamController<String>.broadcast();
MessageDao _messageDaoInstance; // }
//
Future<sqflite.Database> open( // MessageDao _messageDaoInstance;
String path, //
List<Migration> migrations, [ // Future<sqflite.Database> open(
Callback callback, // String path,
]) async { // List<Migration> migrations, [
final databaseOptions = sqflite.OpenDatabaseOptions( // Callback callback,
version: 1, // ]) async {
onConfigure: (database) async { // final databaseOptions = sqflite.OpenDatabaseOptions(
await database.execute('PRAGMA foreign_keys = ON'); // version: 1,
await callback?.onConfigure?.call(database); // onConfigure: (database) async {
}, // await database.execute('PRAGMA foreign_keys = ON');
onOpen: (database) async { // await callback?.onConfigure?.call(database);
await callback?.onOpen?.call(database); // },
}, // onOpen: (database) async {
onUpgrade: (database, startVersion, endVersion) async { // await callback?.onOpen?.call(database);
await MigrationAdapter.runMigrations( // },
database, startVersion, endVersion, migrations); // onUpgrade: (database, startVersion, endVersion) async {
// await MigrationAdapter.runMigrations(
await callback?.onUpgrade?.call(database, startVersion, endVersion); // database, startVersion, endVersion, migrations);
}, //
onCreate: (database, version) async { // await callback?.onUpgrade?.call(database, startVersion, endVersion);
await database.execute( // },
'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` INTEGER, `toId` INTEGER, `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` INTEGER, `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))'); // onCreate: (database, version) async {
// await database.execute(
await callback?.onCreate?.call(database, version); // 'CREATE TABLE IF NOT EXISTS `Message` (`id` INTEGER, `fromId` INTEGER, `toId` INTEGER, `content` TEXT, `attach` TEXT, `msgType` INTEGER, `time` INTEGER, `state` INTEGER, `isDelete` INTEGER, PRIMARY KEY (`id`))');
}, //
); // await callback?.onCreate?.call(database, version);
return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions); // },
} // );
// return sqfliteDatabaseFactory.openDatabase(path, options: databaseOptions);
@override // }
MessageDao get messageDao { //
return _messageDaoInstance ??= _$MessageDao(database, changeListener); // @override
} // MessageDao get messageDao {
} // return _messageDaoInstance ??= _$MessageDao(database, changeListener);
// }
class _$MessageDao extends MessageDao { // }
_$MessageDao( //
this.database, // class _$MessageDao extends MessageDao {
this.changeListener, // _$MessageDao(
) : _queryAdapter = QueryAdapter(database, changeListener), // this.database,
_messageInsertionAdapter = InsertionAdapter( // this.changeListener,
database, // ) : _queryAdapter = QueryAdapter(database, changeListener),
'Message', // _messageInsertionAdapter = InsertionAdapter(
(Message item) => item.toJson(), // database,
changeListener); // 'Message',
// (Message item) => item.toJson(),
final sqflite.DatabaseExecutor database; // changeListener);
//
final StreamController<String> changeListener; // final sqflite.DatabaseExecutor database;
//
final QueryAdapter _queryAdapter; // final StreamController<String> changeListener;
//
final InsertionAdapter<Message> _messageInsertionAdapter; // final QueryAdapter _queryAdapter;
//
@override // final InsertionAdapter<Message> _messageInsertionAdapter;
Stream<List<Message>> findMessageByToId(int toId) { //
return _queryAdapter.queryListStream( // @override
'SELECT * FROM Message WHERE toId = ?1', // Stream<List<Message>> findMessageByToId(int toId) {
mapper: (Map<String, Object> row) => Message.fromJson(row), // return _queryAdapter.queryListStream(
arguments: [toId], // 'SELECT * FROM Message WHERE toId = ?1',
queryableName: 'Message', // mapper: (Map<String, Object> row) => Message.fromJson(row),
isView: false); // arguments: [toId],
} // queryableName: 'Message',
// isView: false);
@override // }
Future<List<Message>> findMessageByGroup(int userId) { //
debugPrint("findMessageByGroup: $userId"); // @override
return _queryAdapter.queryList( // Future<List<Message>> findMessageByGroup(int userId) {
'SELECT * FROM Message WHERE toId = ?1 OR fromId = ?2 GROUP BY toId,fromId ORDER BY time DESC', // debugPrint("findMessageByGroup: $userId");
mapper: (Map<String, Object> row) => Message.fromJson(row), // return _queryAdapter.queryList(
arguments: [userId, userId]); // 'SELECT * FROM Message WHERE toId = ?1 OR fromId = ?2 GROUP BY toId,fromId ORDER BY time DESC',
} // mapper: (Map<String, Object> row) => Message.fromJson(row),
// arguments: [userId, userId]);
@override // }
Future<void> insertMessage(Message message) async { //
await _messageInsertionAdapter.insert(message, OnConflictStrategy.abort); // @override
} // Future<void> insertMessage(Message message) async {
} // await _messageInsertionAdapter.insert(message, OnConflictStrategy.abort);
// }
// }

57
lib/im/database/message.dart

@ -1,13 +1,25 @@
import 'package:floor/floor.dart';
@entity import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/cupertino.dart';
import 'package:huixiang/im/out/message.pb.dart';
import 'package:huixiang/im/out/message.pbenum.dart';
import 'package:image_gallery_saver/image_gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
class Message { class Message {
@primaryKey
int id; int id;
int fromId; String conversationId;
String fromId;
int toId; String toId;
String replyId;
String content; String content;
@ -15,18 +27,23 @@ class Message {
int msgType; int msgType;
int time; String time;
/// 0 unread, 1 read, 2
int state; int state;
int isDelete; int isDelete;
Message(id, fromId, toId, content, attach, msgType, time, state, isDelete); bool showTime = false;
Message(this.id, this.conversationId, this.fromId, this.toId, this.replyId, this.content, this.attach, this.msgType, this.time, this.state, this.isDelete);
factory Message.fromJson(Map<String, dynamic> json) => Message( factory Message.fromJson(Map<String, dynamic> json) => Message(
json["id"], json["id"],
json["conversationId"],
json["fromId"], json["fromId"],
json["toId"], json["toId"],
json["replyId"],
json["content"], json["content"],
json["attach"], json["attach"],
json["msgType"], json["msgType"],
@ -36,8 +53,10 @@ class Message {
Map<String, dynamic> toJson() => <String, dynamic>{ Map<String, dynamic> toJson() => <String, dynamic>{
"id": id, "id": id,
"conversationId": conversationId,
"fromId": fromId, "fromId": fromId,
"toId": toId, "toId": toId,
"replyId": replyId,
"content": content, "content": content,
"attach": attach, "attach": attach,
"msgType": msgType, "msgType": msgType,
@ -47,15 +66,27 @@ class Message {
}; };
} }
createMessage(var toId, String content, {String attach, int msgType, userId}) { createMessage(var toId, String content, {String attach, int msgType, fromId, replyId}) {
return Message.fromJson(<String, dynamic>{ return <String, dynamic>{
"fromId": userId, "conversationId": conversationId(fromId, toId),
"fromId": fromId,
"toId": toId, "toId": toId,
"replyId": replyId,
"content": content, "content": content,
"attach": attach, "attach": attach,
"msgType": msgType ?? 0, "msgType": msgType ?? 1,
"time": DateTime.now().millisecondsSinceEpoch, "time": "${DateTime.now().millisecondsSinceEpoch}",
"state": 0, "state": 0,
"isDelete": 0 "isDelete": 0
}); };
}
conversationId(tid, fid) {
num itid = num.parse(tid);
num ifid = num.parse(fid);
if (itid > ifid) {
return "$ifid-$itid";
}
return "$itid-$ifid";
} }

34
lib/im/database/message_dao.dart

@ -1,17 +1,17 @@
import 'package:floor/floor.dart'; // import 'package:floor/floor.dart';
import 'package:huixiang/im/database/message.dart'; // import 'package:huixiang/im/database/message.dart';
//
//
@dao // @dao
abstract class MessageDao { // abstract class MessageDao {
//
@Query('SELECT * FROM Message WHERE toId = :toId') // @Query('SELECT * FROM Message WHERE toId = :toId')
Stream<List<Message>> findMessageByToId(int toId); // Stream<List<Message>> findMessageByToId(int toId);
//
@insert // @insert
Future<void> insertMessage(Message message); // Future<void> insertMessage(Message message);
//
@Query('SELECT * FROM Message WHERE toId = :userId OR fromId = :userId GROUP BY toId,fromId ORDER BY time DESC') // @Query('SELECT * FROM Message WHERE toId = :userId OR fromId = :userId GROUP BY toId,fromId ORDER BY time DESC')
Future<List<Message>> findMessageByGroup(int userId); // Future<List<Message>> findMessageByGroup(int userId);
//
} // }

41
lib/im/database/migration.dart

@ -0,0 +1,41 @@
import 'package:sqflite/sqflite.dart' as sqflite;
/// Base class for a database migration.
///
/// Each migration can move between 2 versions that are defined by
/// [startVersion] and [endVersion].
class Migration {
/// The start version of the database.
final int startVersion;
/// The start version of the database.
final int endVersion;
/// Function that performs the migration.
final Future<void> Function(sqflite.Database database) migrate;
/// Creates a new migration between [startVersion] and [endVersion].
/// [migrate] will be called by the database and performs the actual
/// migration.
Migration(this.startVersion, this.endVersion, this.migrate)
: assert(startVersion > 0),
assert(startVersion < endVersion);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Migration &&
runtimeType == other.runtimeType &&
startVersion == other.startVersion &&
endVersion == other.endVersion &&
migrate == other.migrate;
@override
int get hashCode =>
startVersion.hashCode ^ endVersion.hashCode ^ migrate.hashCode;
@override
String toString() {
return 'Migration{startVersion: $startVersion, endVersion: $endVersion, migrate: $migrate}';
}
}

365
lib/im/im_search.dart

@ -0,0 +1,365 @@
import 'dart:ui';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../generated/l10n.dart';
import '../retrofit/data/base_data.dart';
import '../retrofit/data/im_user.dart';
import '../retrofit/retrofit_api.dart';
import '../utils/font_weight.dart';
import '../view_widget/custom_image.dart';
import '../view_widget/my_appbar.dart';
import '../view_widget/settlement_tips_dialog.dart';
class ImSearch extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _ImSearch();
}
}
class _ImSearch extends State<ImSearch> {
ApiService apiService;
final TextEditingController editingController = TextEditingController();
FocusNode _focusNode = FocusNode();
List<ImUser> searchUser = [];
int searchState = 0;
int textType = 0;
int searchUserIndex = 0;
String selfUserId = "";
@override
void initState() {
super.initState();
SharedPreferences.getInstance().then((value) {
selfUserId = value.getString("userId");
});
}
///
@override
void dispose() {
_focusNode.unfocus();
super.dispose();
}
///
queryImSearch(keyword) async {
if (apiService == null) {
SharedPreferences value = await SharedPreferences.getInstance();
apiService = ApiService(Dio(),
context: context,
token: value.getString("token"),
showLoading:false
);
}
BaseData<List<ImUser>> baseData =
await apiService.memberSearch(keyword).catchError((onError) {});
if (baseData != null && baseData.isSuccess) {
searchUser.clear();
baseData.data.forEach((element) {
if (element.phone != "" && element.nickname != "") {
searchUser.add(element);
}
});
searchState = 1;
if (baseData.data.length == 0) {
searchState = 2;
}
setState(() {});
}
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Scaffold(
backgroundColor: Color(0xFFFFFFFF),
resizeToAvoidBottomInset: false,
appBar: MyAppBar(
title: "搜索",
leadingColor: Colors.black,
background: Color(0xFFFFFFFF),
),
body: Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(child: imSearch()),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
},
child: Container(
margin: EdgeInsets.only(top: 8.h, bottom: 29.h),
alignment: Alignment.center,
padding: EdgeInsets.only(right: 16.w),
child: Text(
S.of(context).quxiao,
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFA29E9E),
fontSize: 14.sp,
fontWeight: MyFontWeight.medium,
),
)),
)
],
),
searchState == 2
? Center(
child: Text(
"未找到该用户",
style: TextStyle(
fontSize: 14.sp,
color: Color(0xFFA29E9E),
),
),
)
: Expanded(
child: Column(
children: [
Padding(
padding: EdgeInsets.only(bottom: 19.h),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (editingController.text != "" &&
searchState == 1)
Padding(
padding: EdgeInsets.only(left: 16.w),
child: Text(
"搜索用户:",
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF060606),
fontSize: 16.sp,
fontWeight: MyFontWeight.medium,
),
),
),
if (editingController.text != "" &&
searchState == 1)
Expanded(
child: Text(
editingController?.text ?? "",
style: TextStyle(
fontSize: 16.sp,
color: Color(0xFF32A060),
fontWeight: MyFontWeight.regular),
))
],
),
),
if (editingController.text != "" && searchState == 1)
Expanded(
child: ListView.builder(
itemCount: searchUser?.length ?? 0,
physics: BouncingScrollPhysics(),
shrinkWrap: true,
itemBuilder: (context, position) {
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
setState(() {
searchUserIndex = position;
});
Navigator.of(context).pushNamed(
'/router/personal_page',
arguments: {
"memberId": (searchUser[searchUserIndex]
.mid ??
"") ==
selfUserId
? "0"
: searchUser[searchUserIndex].mid,
"inletType": 0
});
FocusScope.of(context)
.requestFocus(FocusNode());
},
child: imSearchItem(searchUser[position]),
);
},
))
],
)),
],
),
),
),
);
}
///
Widget imSearch() {
return Container(
margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w, 29.h),
padding: EdgeInsets.symmetric(vertical: 13.h),
decoration: BoxDecoration(
color: Color(0xFFFDFCFC),
borderRadius: BorderRadius.circular(4),
),
child: TextField(
textInputAction: TextInputAction.search,
onChanged: (value) {
setState(() {
searchState = 3;
});
if (isNumeric(value)) {
textType = 1;
} else {
textType = 2;
}
if (editingController.text == null || editingController.text == "") {
return;
} else {
queryImSearch(editingController.text ?? "");
}
},
onEditingComplete: () {
FocusScope.of(context).requestFocus(FocusNode());
if (editingController.text == null || editingController.text == "") {
SmartDialog.show(
widget: SettlementTips(
() {},
text: "请输入姓名或手机号搜索",
));
} else {
queryImSearch(editingController.text ?? "");
}
},
controller: editingController,
style: TextStyle(
fontSize: 14.sp,
),
decoration: InputDecoration(
hintText: "输入姓名或手机号搜索",
hintStyle: TextStyle(
fontSize: 14.sp,
color: Color(0xFFA29E9E),
),
isCollapsed: true,
prefixIcon: Padding(
padding: EdgeInsets.only(left: 15.w, right: 5.w),
child: Image.asset(
"assets/image/icon_search.webp",
width: 14.h,
height: 14.h,
color: Color(0xFFB3B3B3),
),
),
prefixIconConstraints: BoxConstraints(),
border: InputBorder.none,
),
),
);
}
///
Widget imSearchItem(ImUser searchUser) {
return Container(
padding: EdgeInsets.only(left: 10.w, right: 16.w, bottom: 15.h),
child: Row(
children: [
MImage(
searchUser?.avatar ?? "",
isCircle: true,
height: 54.h,
width: 54.h,
fit: BoxFit.cover,
errorSrc: "assets/image/default_1.webp",
fadeSrc: "assets/image/default_1.webp",
),
SizedBox(
width: 12.w,
),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
RichText(
text: TextSpan(
style: TextStyle(color: Colors.black), //
children: _splitText(searchUser?.nickname ?? "",
editingController.text)
.map((part) {
return TextSpan(
text: part,
style: part == editingController.text
? TextStyle(color: Colors.green) // 绿
: null,
);
}).toList(),
),
),
SizedBox(
height: 7.h,
),
RichText(
text: TextSpan(
style: TextStyle(color: Colors.black), //
children: _splitText(
searchUser?.phone ?? "", editingController.text)
.map((part) {
return TextSpan(
text: part,
style: part == editingController.text
? TextStyle(color: Colors.green) // 绿
: null,
);
}).toList(),
),
),
],
),
),
],
));
}
///
List<String> _splitText(String text, String search) {
if (text == null || text.isEmpty || search == null || search.isEmpty) {
throw ArgumentError('text and search must not be null or empty');
}
final List<String> parts = [];
int start = 0;
int index = text.indexOf(search);
while (index != -1) {
if (index > start) {
parts.add(text.substring(start, index));
}
parts.add(text.substring(index, index + search.length));
start = index + search.length;
index = text.indexOf(search, start);
}
if (start < text.length) {
parts.add(text.substring(start));
}
return parts;
}
///
bool isNumeric(String str) {
RegExp regExp = RegExp(r'^\d+$');
return regExp.hasMatch(str);
}
}

2
lib/im/im_view/custom_underline_tabIndicator.dart

@ -89,7 +89,7 @@ class _UnderlinePainter extends BoxPainter {
final Rect indicator = decoration._indicatorRectFor(rect, textDirection) final Rect indicator = decoration._indicatorRectFor(rect, textDirection)
.deflate(decoration.borderSide.width / 2.0); .deflate(decoration.borderSide.width / 2.0);
final Paint paint = decoration.borderSide.toPaint(); final Paint paint = decoration.borderSide.toPaint();
paint.strokeWidth = 5; paint.strokeWidth = 4;
paint.strokeCap = StrokeCap.round; // paint.strokeCap = StrokeCap.round; //
canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint); canvas.drawLine(indicator.bottomLeft, indicator.bottomRight, paint);
} }

204
lib/im/im_view/friend_groip_list.dart

@ -1,13 +1,27 @@
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:huixiang/main.dart';
import 'package:huixiang/retrofit/data/im_user.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../retrofit/data/base_data.dart';
import '../../retrofit/data/follow_list.dart';
import '../../retrofit/data/page.dart';
import '../../retrofit/retrofit_api.dart'; import '../../retrofit/retrofit_api.dart';
import 'package:huixiang/im/database/message.dart';
import '../../utils/font_weight.dart';
import '../../view_widget/custom_image.dart';
import '../../view_widget/no_data_view.dart';
class FriendGroupList extends StatefulWidget { class FriendGroupList extends StatefulWidget {
FriendGroupList( final String isMyFans;
Key key, final String title;
) : super(key: key);
FriendGroupList(Key key, this.isMyFans, this.title) : super(key: key);
@override @override
State<StatefulWidget> createState() { State<StatefulWidget> createState() {
@ -17,21 +31,102 @@ class FriendGroupList extends StatefulWidget {
class _FriendGroupList extends State<FriendGroupList> { class _FriendGroupList extends State<FriendGroupList> {
ApiService apiService; ApiService apiService;
final TextEditingController editingController = TextEditingController(); RefreshController _refreshController;
FocusNode _focusNode = FocusNode(); List<String> userIds = [];
List<Message> messages = [];
int pageNum = 1;
List<ListData> list = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
// loadMessageList();
if (widget.isMyFans == "") {
queryMutualFollowList();
} else {
queryFollowList();
}
} }
/// ///
@override @override
void dispose() { void dispose() {
_focusNode.unfocus();
super.dispose(); super.dispose();
} }
// loadMessageList() async {
// SharedPreferences shared = await SharedPreferences.getInstance();
// String userId = shared.getString("userId");
// messages = await hxDatabase.queryList(userId);
// messages.forEach((element) {
// debugPrint("messages: ${element.toJson()}");
// });
// userIds = messages
// .map((e) => e.toId != userId ? e.toId : e.fromId)
// .toSet().where((element) => element != userId)
// .toList();
// if (mounted) {
// setState(() {});
// }
//
// }
///
queryFollowList() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
if (apiService == null)
apiService = ApiService(
Dio(),
context: context,
token: sharedPreferences.getString("token"),
showLoading: false,
);
BaseData<PageInfo<ListData>> baseData = await apiService.followList({
"isMyFans": widget.isMyFans,
"pageNum": pageNum,
"pageSize": 100,
}).catchError((error) {
_refreshController.refreshFailed();
_refreshController.loadFailed();
});
if (baseData != null && baseData.isSuccess) {
if (pageNum == 1) {
list.clear();
}
list.addAll(baseData.data.list);
if (!mounted) return;
setState(() {});
}
}
///
queryMutualFollowList() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
if (apiService == null)
apiService = ApiService(
Dio(),
context: context,
token: sharedPreferences.getString("token"),
showLoading: false,
);
BaseData<PageInfo<ListData>> baseData = await apiService.mutualFollowList({
"isMyFans": widget.isMyFans,
"pageNum": 1,
"pageSize": 100,
}).catchError((error) {
_refreshController.refreshFailed();
_refreshController.loadFailed();
});
if (baseData != null && baseData.isSuccess) {
if (pageNum == 1) {
list.clear();
}
list.addAll(baseData.data.list);
if (!mounted) return;
setState(() {});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
@ -39,39 +134,72 @@ class _FriendGroupList extends State<FriendGroupList> {
child: Column( child: Column(
children: [ children: [
friendGroupSearch(), friendGroupSearch(),
Expanded( (list == null || list.length == 0)
? NoDataView(
src: "assets/image/guan_zhu.webp",
isShowBtn: false,
text: widget.title == "好友"
? "目前暂无${widget?.title ?? ""}"
: ("目前暂无${widget?.title ?? ""}${widget?.title == "粉丝" ? "听说多发动态可以涨粉哦" : "可以在社群广场中关注自己喜欢的人哦"}~"),
fontSize: 16.sp,
margin: EdgeInsets.only(top: 120.h, left: 60.w, right: 60.w),
)
: Expanded(
child: ListView.builder( child: ListView.builder(
itemCount: 10, itemCount: list.length ?? 0,
physics: BouncingScrollPhysics(), physics: BouncingScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, position) { itemBuilder: (context, position) {
return friendGroupItem(); return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (list[position].mid == sharedPreferences.getString("userId")) {
SmartDialog.showToast("不能跟自己聊天", alignment: Alignment.center);
return;
}
Navigator.of(context).pushNamed(
'/router/chat_details_page',
arguments: {
"toUser": ImUser(
avatar: list[position].avatar,
mid: list[position].mid,
nickname: list[position].nickname,
),
},
);
},
child: friendGroupItem(list[position]),
);
}, },
)), ),
),
], ],
), ),
); );
} }
Widget friendGroupItem() { Widget friendGroupItem(ListData list) {
return Container( return Container(
margin: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 24.h), margin: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 24.h),
child: Row(children: [ child: Row(children: [
Container( Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26.5.r), borderRadius: BorderRadius.circular(100),
), ),
child: Image.asset( child: MImage(
"assets/image/bs_mine_heading.webp", list?.avatar ?? "",
isCircle: true,
width: 54.h, width: 54.h,
height: 54.h, height: 54.h,
fit: BoxFit.fill, fit: BoxFit.cover,
errorSrc: "assets/image/default_1.webp",
fadeSrc: "assets/image/default_1.webp",
), ),
), ),
Padding( Padding(
padding: EdgeInsets.only(left: 4.w), padding: EdgeInsets.only(left:12.w),
child: Text( child: Text(
"哈喽喽哈", list?.nickname ?? "",
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
color: Color(0xFF060606), color: Color(0xFF060606),
@ -84,41 +212,37 @@ class _FriendGroupList extends State<FriendGroupList> {
/// ///
Widget friendGroupSearch() { Widget friendGroupSearch() {
return Container( return GestureDetector(
margin: EdgeInsets.fromLTRB(16.w, 8.h, 16.w,24.h), behavior: HitTestBehavior.opaque,
onTap:(){Navigator.of(context).pushNamed('/router/im_search');},
child: Container(
margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 24.h),
padding: EdgeInsets.symmetric(vertical: 13.h), padding: EdgeInsets.symmetric(vertical: 13.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFFDFCFC), color: Color(0xFFFDFCFC),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
child: TextField( child: Row(
textInputAction: TextInputAction.search, children: [
onEditingComplete: () { Padding(
FocusScope.of(context).requestFocus(FocusNode());
},
controller: editingController,
style: TextStyle(
fontSize: 14.sp,
),
decoration: InputDecoration(
hintText: "搜索",
hintStyle: TextStyle(
fontSize: 14.sp,
color: Color(0xFFA29E9E),
),
isCollapsed: true,
prefixIcon: Padding(
padding: EdgeInsets.only(left: 15.w, right: 5.w), padding: EdgeInsets.only(left: 15.w, right: 5.w),
child: Image.asset( child: Image.asset(
"assets/image/icon_search.webp", "assets/image/icon_search.webp",
width: 14.h, width: 14.h,
height: 14.h, height: 14.h,
color: Color(0xFFB3B3B3), color: Color(0xFF353535),
),
), ),
Text(
"搜索",
style: TextStyle(
color: Color(0xFFA29E9E),
fontSize: 16.sp,
fontWeight: MyFontWeight.regular,
), ),
prefixIconConstraints: BoxConstraints(),
border: InputBorder.none,
), ),
],
)
), ),
); );
} }

723
lib/im/im_view/im_page.dart

@ -1,14 +1,13 @@
import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:huixiang/constant.dart';
import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/generated/l10n.dart';
import 'package:huixiang/im/database/message.dart'; import 'package:huixiang/im/database/message.dart';
import 'package:huixiang/retrofit/data/msg_stats.dart';
import 'package:huixiang/main.dart'; import 'package:huixiang/main.dart';
import 'package:huixiang/retrofit/data/base_data.dart'; import 'package:huixiang/retrofit/data/base_data.dart';
import 'package:huixiang/retrofit/data/msg_stats.dart';
import 'package:huixiang/retrofit/data/page.dart';
import 'package:huixiang/retrofit/retrofit_api.dart'; import 'package:huixiang/retrofit/retrofit_api.dart';
import 'package:huixiang/utils/font_weight.dart'; import 'package:huixiang/utils/font_weight.dart';
import 'package:huixiang/view_widget/classic_header.dart'; import 'package:huixiang/view_widget/classic_header.dart';
@ -17,6 +16,9 @@ import 'package:huixiang/view_widget/round_button.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import '../../retrofit/data/im_user.dart';
import '../../utils/flutter_utils.dart';
import '../../view_widget/custom_image.dart';
import 'on_chat_message.dart'; import 'on_chat_message.dart';
import 'on_chat_msg_instance.dart'; import 'on_chat_msg_instance.dart';
@ -42,7 +44,13 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
"6": 0, "6": 0,
}; };
int state = 0; int state = 0;
final TextEditingController imEditingController = TextEditingController(); List<String> conversationIds = [];
Map<String, Message> lastMessageMap = {};
Map<String, int> unreadCountMap = {};
Map<String, ImUser> contactMap = {};
int insertIndex = 0;
String selfUserId;
final RefreshController _refreshController = RefreshController();
@override @override
void onMessage(txt) { void onMessage(txt) {
@ -53,6 +61,8 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
void dispose() { void dispose() {
super.dispose(); super.dispose();
OnChatMsgInstance.instance.onChatMessage = null; OnChatMsgInstance.instance.onChatMessage = null;
socketClient.removeCallback(socketClient.userId);
} }
@override @override
@ -60,7 +70,8 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
super.initState(); super.initState();
OnChatMsgInstance.instance.onChatMessage = this; OnChatMsgInstance.instance.onChatMessage = this;
loadMessageList(); initSocketClient();
SharedPreferences.getInstance().then((value) { SharedPreferences.getInstance().then((value) {
apiService = apiService =
ApiService(Dio(), token: value.getString("token"), context: context); ApiService(Dio(), token: value.getString("token"), context: context);
@ -68,79 +79,156 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
}); });
} }
initSocketClient() async {
SharedPreferences shared = await SharedPreferences.getInstance();
selfUserId = shared.getString("userId");
socketClient.addCallback(selfUserId, (Message message) {
if (conversationIds.contains(message.conversationId)) {
conversationIds.remove(message.conversationId);
}
conversationIds.insert(insertIndex, message.conversationId);
lastMessageMap[message.conversationId] = message;
listenerRefresh(message);
});
loadMessageList();
}
_refresh() { _refresh() {
pageNum = 1; pageNum = 1;
loadMessageList(); loadMessageList();
queryMsgStats(); queryMsgStats();
} }
List<int> userIds = []; listenerRefresh(Message message) async {
await sortConversation(lastMessageMap);
Stream streamSubscription ; await queryUnreadCount(conversationIds);
debugPrint("messages_records : ${message.toJson()}");
if (contactMap[message.fromId] == null) {
queryMemberInfo([message.fromId]);
return;
}
refreshState();
}
loadMessageList() async { loadMessageList() async {
int userId = 123456; messages = await hxDatabase.queryList();
lastMessageMap = messages
.lGroupBy((p0) => p0.conversationId)
.mGroupItem(key: (p1) => num.parse(p1.time));
await queryImUserInfo(
messages.map((e) => e.toId != selfUserId ? e.toId : e.fromId).toList());
hxDatabase.changeListener.stream.listen((event) { await sortConversation(lastMessageMap);
debugPrint("messages: 1111");
}, onError: (Object error, stackTrace) { await queryUnreadCount(conversationIds);
debugPrint("messages: 3333");
refreshState();
}
/// update conversation by time sort
sortConversation(lastMessageMap) async {
List<Message> sortMessages = lastMessageMap.values.toList();
sortMessages
.sort((a, b) => (num.parse(b.time)).compareTo(num.parse(a.time)));
conversationIds =
sortMessages.map((e) => e.conversationId).toSet().toList();
}
/// update conversation unreadcount
queryUnreadCount(conversationIds) async {
unreadCountMap = await hxDatabase.messageUnreadCount(conversationIds);
debugPrint("unreadCountMap: $unreadCountMap");
}
/// update imuser info by mids
queryImUserInfo(userIds) async {
List<ImUser> contacts = (await hxDatabase.queryImUser(userIds)) ?? [];
if (contacts?.isEmpty ?? true) {
await queryMemberInfo(userIds);
return;
} else {
List<String> queryUserIds = userIds
.where((u) => contacts.where((c) => c.mid == u).isEmpty)
.toList();
if (queryUserIds.isNotEmpty) {
await queryMemberInfo(queryUserIds);
return;
}
}
contactMap = contacts
.lGroupBy((p0) => conversationId(p0.mid, selfUserId))
.mGroupItem();
List<String> topConversationIds = [], notTopUserIds = [];
contactMap.forEach((key, value) {
if (value.isTop == 1)
topConversationIds.add(key);
else
notTopUserIds.add(key);
}); });
hxDatabase.changeListener.onListen = () { insertIndex = topConversationIds.length;
debugPrint("messages: 2222"); this.conversationIds = topConversationIds..addAll(notTopUserIds);
}; }
messages = await hxDatabase.messageDao.findMessageByGroup(userId); /// update one conversation last message ,and update conversation sort
messages.forEach((element) { void updateLastMessage(String conversationId) async {
debugPrint("messages: $element"); Message message = await hxDatabase.lastMessage(conversationId);
debugPrint(
"lastmessage: $conversationId ${message?.content} ${message?.toJson()}");
if (message != null) {
lastMessageMap[conversationId] = message;
await sortConversation(lastMessageMap);
refreshState();
}
}
void updateUnreadCount() async {
unreadCountMap = await hxDatabase.messageUnreadCount(conversationIds);
refreshState();
}
refreshState() {
if (_refreshController.isRefresh) _refreshController.refreshCompleted();
if (mounted) setState(() {});
}
///
queryMemberInfo(List<String> mids) async {
if (mids.isEmpty) {
return;
}
BaseData<List<ImUser>> baseData = await apiService.memberInfoByIds({
"mids": mids,
}).catchError((error) {
SmartDialog.showToast(
AppUtils.dioErrorTypeToString(error.type),
alignment: Alignment.center,
);
}); });
userIds = messages if (baseData != null && baseData.isSuccess) {
.map((e) => e.toId != userId ? e.toId : e.fromId) if (baseData.data.isNotEmpty) {
.toSet() baseData.data.forEach((element) async {
.toList(); await hxDatabase.insertOrUpdateImUser(element.toJson());
if (mounted) { });
setState(() {}); baseData.data.forEach((element) {
} if (contactMap[conversationId(element.mid, selfUserId)] == null) {
contactMap[conversationId(element.mid, selfUserId)] = element;
} }
});
// queryMessage() async { refreshState();
// BaseData<PageInfo<Message>> baseData = await apiService.msgList({ }
// "pageNum": pageNum, } else {
// "pageSize": 10, SmartDialog.showToast(baseData.msg, alignment: Alignment.center);
// "searchKey": "", }
// "state": "", }
// "typed": ""
// }).catchError((onError) {
// _refreshController.loadFailed();
// _refreshController.refreshFailed();
// });
//
// if (baseData != null && baseData.isSuccess) {
// if (pageNum == 1) {
// messages.clear();
// }
// List<Message> message = [];
// message.addAll(baseData.data.list);
// message.forEach((element) {
// if (element.typed == 2 || element.typed == 3) {
// messages.add(element);
// }
// });
// _refreshController.loadComplete();
// _refreshController.refreshCompleted();
// if (mounted) setState(() {});
// if (pageNum * 10 > int.tryParse(baseData.data.total)) {
// _refreshController.loadNoData();
// } else {
// pageNum += 1;
// }
// } else {
// _refreshController.loadFailed();
// _refreshController.refreshFailed();
// }
// }
///App消息
queryMsgStats() async { queryMsgStats() async {
if (apiService == null) { if (apiService == null) {
SharedPreferences value = await SharedPreferences.getInstance(); SharedPreferences value = await SharedPreferences.getInstance();
@ -169,32 +257,11 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
EasyLoading.dismiss(); EasyLoading.dismiss();
} }
RefreshController _refreshController = RefreshController();
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Color(0xFFFFFFFF), backgroundColor: Color(0xFFFFFFFF),
body: SmartRefresher( body: Container(
enablePullDown: true,
enablePullUp: false,
header: MyHeader(),
physics: BouncingScrollPhysics(),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
builder: (BuildContext context, LoadStatus mode) {
return (messages.length == 0) ? Container() : MyFooter(mode);
},
),
controller: _refreshController,
onRefresh: _refresh,
onLoading: () {
setState(() {
_refresh();
});
},
child: Container(
// color: Colors.white,
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
@ -208,10 +275,6 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
stops: [0, 0.2, 0.4, 1], stops: [0, 0.2, 0.4, 1],
), ),
), ),
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Container(
padding: EdgeInsets.only(bottom: 30.h),
child: Column( child: Column(
children: [ children: [
Container( Container(
@ -219,7 +282,8 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
top: MediaQuery.of(context).padding.top + 12.h, top: MediaQuery.of(context).padding.top + 12.h,
bottom: 15.h, bottom: 15.h,
right: 16.w, right: 16.w,
left: 16.w), left: 16.w,
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -238,7 +302,10 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.of(context) Navigator.of(context)
.pushNamed('/router/chat_friend_group'); .pushNamed('/router/chat_friend_group')
.then((value) {
_refresh();
});
}, },
child: Container( child: Container(
padding: EdgeInsets.all(12), padding: EdgeInsets.all(12),
@ -257,53 +324,121 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
], ],
), ),
), ),
imSearchItem(), imPageSearch(),
Expanded(
child: SmartRefresher(
enablePullDown: true,
enablePullUp: false,
header: MyHeader(),
physics: BouncingScrollPhysics(),
footer: CustomFooter(
loadStyle: LoadStyle.ShowWhenLoading,
builder: (BuildContext context, LoadStatus mode) {
return (messages.length == 0)
? Container()
: MyFooter(mode);
},
),
controller: _refreshController,
onRefresh: _refresh,
onLoading: () {
_refresh();
},
child: SingleChildScrollView(
physics: BouncingScrollPhysics(),
child: Column(
children: [
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context)
.pushNamed('/router/system_notice')
.then((value) {
setState(() {
msgNumber["2"] = 0;
msgNumber["3"] = 0;
});
});
},
child: messageItem(
"assets/image/icon_system_message_new.webp",
S.of(context).xitongxiaoxi,
(msgNumber["2"] + msgNumber["3"]).toString()),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pushNamed(
'/router/system_details',
arguments: {"msgType": 4}).then((value) {
setState(() {
msgNumber["4"] = 0;
});
});
},
child: messageItem("assets/image/icon_gz.webp",
S.of(context).guanzhu, msgNumber["4"].toString()),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pushNamed(
'/router/system_details',
arguments: {"msgType": 6}).then((value) {
setState(() {
msgNumber["6"] = 0;
});
});
},
child: messageItem("assets/image/icon_pl.webp",
S.of(context).pinglun, msgNumber["6"].toString()),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pushNamed(
'/router/system_details',
arguments: {"msgType": 5}).then((value) {
setState(() {
msgNumber["5"] = 0;
});
});
},
child: messageItem("assets/image/icon_z.webp",
S.of(context).dianzan, msgNumber["5"].toString()),
),
chatList(), chatList(),
// buildMessage(),fgg SizedBox(height: 100.h)
], ],
), ),
), ),
), ),
), ),
],
),
), ),
); );
} }
/// ///
Widget imSearchItem() { Widget imPageSearch() {
return Container( return GestureDetector(
margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 0), behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pushNamed('/router/im_search').then((value) {
_refresh();
});
},
child: Container(
margin: EdgeInsets.fromLTRB(16.w, 0, 16.w, 16.h),
padding: EdgeInsets.symmetric(vertical: 13.h), padding: EdgeInsets.symmetric(vertical: 13.h),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Color(0xFFFFFFFF), color: Color(0xFFFDFCFC),
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
boxShadow: [
BoxShadow(
color: Colors.black.withAlpha(12),
offset: Offset(0, 3),
blurRadius: 14,
spreadRadius: 0,
),
],
),
child: TextField(
textInputAction: TextInputAction.search,
onEditingComplete: () {
socketClient.sendMessage(654321, "hello~");
FocusScope.of(context).requestFocus(FocusNode());
},
controller: imEditingController,
style: TextStyle(
fontSize: 14.sp,
), ),
decoration: InputDecoration( child: Row(
hintText: "搜索", children: [
hintStyle: TextStyle( Padding(
fontSize: 14.sp,
color: Color(0xFFA29E9E),
),
isCollapsed: true,
prefixIcon: Padding(
padding: EdgeInsets.only(left: 15.w, right: 5.w), padding: EdgeInsets.only(left: 15.w, right: 5.w),
child: Image.asset( child: Image.asset(
"assets/image/icon_search.webp", "assets/image/icon_search.webp",
@ -312,52 +447,154 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
color: Color(0xFF353535), color: Color(0xFF353535),
), ),
), ),
prefixIconConstraints: BoxConstraints(), Text(
border: InputBorder.none, "搜索",
style: TextStyle(
color: Color(0xFFA29E9E),
fontSize: 16.sp,
fontWeight: MyFontWeight.regular,
), ),
), ),
],
)),
); );
} }
bool _isDelEnter = false;
Map<String, ScrollController> idsController = {};
String lastScrollId;
double lastScrollOffset = 0;
bool _isScrollOpen = false;
/// ///
Widget chatList() { Widget chatList() {
return Container( return ListView(
child: ListView.builder( padding: EdgeInsets.only(top: 8.h),
padding: EdgeInsets.only(top: 16),
itemCount: userIds.length,
shrinkWrap: true, shrinkWrap: true,
physics: NeverScrollableScrollPhysics(), physics: NeverScrollableScrollPhysics(),
itemBuilder: (context, position) { children: conversationIds.map((e) {
return GestureDetector( ScrollController scrollController;
if (idsController.containsKey(e))
scrollController = idsController[e];
else {
scrollController = ScrollController();
idsController[e] = scrollController;
}
scrollController.addListener(() {
if (scrollController.offset > 0) {
if (lastScrollId != null && lastScrollId != e)
idsController[lastScrollId].jumpTo(0);
if (lastScrollOffset < scrollController.offset) {
scrollController
.jumpTo(scrollController.position.maxScrollExtent);
_isScrollOpen = true;
} else if (lastScrollOffset > scrollController.offset &&
_isScrollOpen) {
scrollController.jumpTo(0);
}
lastScrollId = e;
// scrollController.animateTo(
// lastScrollOffset == scrollController.position.maxScrollExtent
// ? 0
// : scrollController.position.maxScrollExtent,
// duration: Duration(milliseconds: 100),
// curve: Curves.ease);
lastScrollOffset = scrollController.offset;
} else {
if (lastScrollId == e) {
setState(() {
_isDelEnter = false;
_isScrollOpen = false;
});
}
}
});
int position = conversationIds.indexOf(e);
return SingleChildScrollView(
physics: BouncingScrollPhysics(),
controller: scrollController,
scrollDirection: Axis.horizontal,
child: Row(
children: [
GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
Navigator.of(context).pushNamed('/router/chat_details_page'); Navigator.of(context).pushNamed(
'/router/chat_details_page',
arguments: {
"toUser": contactMap[conversationIds[position]],
}, },
child: chatItem(userIds[position]), ).then((value) {
); unreadCountMap[conversationIds[position]] = 0;
updateLastMessage(conversationIds[position]);
_refresh();
});
},
child: chatItem(conversationIds[position]),
),
GestureDetector(
child: Container(
color: Colors.red,
alignment: Alignment.center,
padding:
EdgeInsets.symmetric(vertical: 25.h, horizontal: 14.w),
child: Text(
_isDelEnter && lastScrollId == e
? "删除并清空"
: S.of(context).shanchu,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: MyFontWeight.regular,
),
),
),
onTap: () async {
// showDelDialog(conversationIds[position]);
if (_isDelEnter) {
await hxDatabase.deleteByUser(conversationIds[position]);
lastScrollId = null;
_refresh();
} else {
Future.delayed(Duration(milliseconds: 100), () {
idsController[lastScrollId].animateTo(
idsController[lastScrollId].position.maxScrollExtent,
duration: Duration(milliseconds: 100),
curve: Curves.ease);
});
}
setState(() {
_isDelEnter = !_isDelEnter;
});
}, },
), ),
],
),
);
}).toList(),
); );
} }
Widget chatItem(userId) { Widget chatItem(conversationId) {
return Container( return Container(
padding: EdgeInsets.only(left: 16.w, right: 17.w, bottom: 18.h), padding: EdgeInsets.only(
left: 16.w,
right: 17.w,
bottom: 18.h,
),
width: MediaQuery.of(context).size.width,
child: Row( child: Row(
children: [ children: [
// MImage( MImage(
// "", !contactMap.containsKey(conversationId)
// isCircle: true, ? null
// width: 40, : contactMap[conversationId]?.avatar ?? "",
// height: 40, isCircle: true,
// fit: BoxFit.cover,
// errorSrc: "assets/image/default_user.webp",
// fadeSrc: "assets/image/default_user.webp",
// ),
Image.asset(
"assets/image/fuka_zj.webp",
height: 54.h, height: 54.h,
width: 54.h, width: 54.h,
fit: BoxFit.cover,
errorSrc: "assets/image/default_1.webp",
fadeSrc: "assets/image/default_1.webp",
), ),
SizedBox( SizedBox(
width: 12.w, width: 12.w,
@ -370,8 +607,10 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
"喽哈$userId", !contactMap.containsKey(conversationId)
overflow: TextOverflow.ellipsis, ? ""
: contactMap[conversationId]?.nickname ?? "",
// overflow: TextOverflow.fade,
maxLines: 1, maxLines: 1,
style: TextStyle( style: TextStyle(
fontSize: 16.sp, fontSize: 16.sp,
@ -381,7 +620,11 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
), ),
), ),
Text( Text(
"2021.03.08 13:22", lastMessageMap[conversationId]?.time != null
? AppUtils.timeFormatter(
DateTime.fromMillisecondsSinceEpoch(num.parse(
lastMessageMap[conversationId]?.time ?? "")))
: "",
style: TextStyle( style: TextStyle(
fontSize: 12.sp, fontSize: 12.sp,
color: Color(0xFFA29E9E), color: Color(0xFFA29E9E),
@ -397,7 +640,7 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
"新开的火锅店好吃得很", messageContent(lastMessageMap[conversationId]),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
@ -407,6 +650,8 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
), ),
), ),
), ),
if (unreadCountMap[conversationId] != null &&
unreadCountMap[conversationId] > 0)
Container( Container(
width: 16, width: 16,
height: 16, height: 16,
@ -415,7 +660,7 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
color: Color(0xFFFF441A), color: Color(0xFFFF441A),
), ),
child: RoundButton( child: RoundButton(
text: "99", text: "${unreadCountMap[conversationId]}",
textColor: Colors.white, textColor: Colors.white,
fontWeight: MyFontWeight.regular, fontWeight: MyFontWeight.regular,
backgroup: Color(0xFFFF441A), backgroup: Color(0xFFFF441A),
@ -432,4 +677,174 @@ class _IMPage extends State<IMPage> implements OnChatMessage {
), ),
); );
} }
String messageContent(Message message) {
if (message.msgType == 1) {
return message.content ?? "";
} else if (message.msgType == 2) {
return "【图片】";
} else if (message.msgType == 3) {
return "【语音】";
} else if (message.msgType == 4) {
return "【视频】";
} else if (message.msgType == 5) {
return "【红包】";
} else if (message.msgType == 6) {
return "【转账】";
} else if (message.msgType == 7) {
return "【位置】";
} else {
return "【未知的消息类型】";
}
}
Widget messageItem(img, title, messageNum) {
return Container(
padding: EdgeInsets.only(
top: 8.h,
bottom: 8.h,
left: 16.w,
right: 15.w,
),
child: Column(
children: [
Row(
children: [
Image.asset(
img,
fit: BoxFit.fill,
),
SizedBox(
width: 12.w,
),
Text(
title,
style: TextStyle(
fontSize: 14.sp,
color: Color(0xFF060606),
fontWeight: MyFontWeight.semi_bold,
),
),
Spacer(),
if (messageNum != "0")
((double.tryParse(messageNum) < 100)
? Container(
width: 16,
height: 16,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: Color(0xFFFF441A),
),
child: RoundButton(
text: messageNum,
textColor: Colors.white,
fontWeight: MyFontWeight.regular,
backgroup: Color(0xFFFF441A),
fontSize: 10.sp,
radius: 100,
))
: Container(
padding: EdgeInsets.symmetric(
horizontal: 4.w, vertical: 2.h),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(100),
color: Color(0xFFFF441A),
),
child: RoundButton(
text: "99+",
textColor: Colors.white,
fontWeight: MyFontWeight.regular,
backgroup: Color(0xFFFF441A),
fontSize: 10.sp,
radius: 100,
))),
],
),
],
),
);
}
///
showDelDialog(conversationId) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
contentPadding: EdgeInsets.zero, //
content: Container(
// width: MediaQuery.of(context).size.width - 84,
height: 130.h,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Container(
alignment: Alignment.center,
child: Text(
"删除并清空聊天记录",
style: TextStyle(
color: Color(0xFF060606),
fontSize: 16.sp,
fontWeight: MyFontWeight.bold,
),
),
),
),
// Spacer(),
Container(
height: 1.h,
width: double.infinity,
color: Color(0xFFEDEDED),
),
Row(
children: [
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Navigator.of(context).pop();
},
child: Container(
child: Text("取消",
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16.sp,
color: Color(0xFF060606),
))))),
Container(
height: 45,
width: 1.w,
color: Color(0xFFEDEDED),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () async {
await hxDatabase.deleteByUser(conversationId);
_refresh();
Navigator.of(context).pop();
},
child: Text(
"确认",
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFFFF370A),
),
),
),
),
],
)
],
),
),
);
},
);
}
} }

17
lib/im/out/auth.pb.dart

@ -14,8 +14,9 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
class AuthReq extends $pb.GeneratedMessage { class AuthReq extends $pb.GeneratedMessage {
factory AuthReq({ factory AuthReq({
$core.int? uid, $core.String? uid,
$core.String? token, $core.String? token,
}) { }) {
final $result = create(); final $result = create();
@ -32,7 +33,7 @@ class AuthReq extends $pb.GeneratedMessage {
factory AuthReq.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); factory AuthReq.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AuthReq', createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AuthReq', createEmptyInstance: create)
..a<$core.int>(1, _omitFieldNames ? '' : 'uid', $pb.PbFieldType.OU3) ..aOS(1, _omitFieldNames ? '' : 'uid')
..aOS(2, _omitFieldNames ? '' : 'token') ..aOS(2, _omitFieldNames ? '' : 'token')
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -59,9 +60,9 @@ class AuthReq extends $pb.GeneratedMessage {
static AuthReq? _defaultInstance; static AuthReq? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.int get uid => $_getIZ(0); $core.String get uid => $_getSZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set uid($core.int v) { $_setUnsignedInt32(0, v); } set uid($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasUid() => $_has(0); $core.bool hasUid() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
@ -79,7 +80,7 @@ class AuthReq extends $pb.GeneratedMessage {
class AuthResp extends $pb.GeneratedMessage { class AuthResp extends $pb.GeneratedMessage {
factory AuthResp({ factory AuthResp({
$core.int? uid, $core.String? uid,
$core.int? code, $core.int? code,
$core.String? message, $core.String? message,
}) { }) {
@ -100,7 +101,7 @@ class AuthResp extends $pb.GeneratedMessage {
factory AuthResp.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); factory AuthResp.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AuthResp', createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'AuthResp', createEmptyInstance: create)
..a<$core.int>(1, _omitFieldNames ? '' : 'uid', $pb.PbFieldType.OU3) ..aOS(1, _omitFieldNames ? '' : 'uid')
..a<$core.int>(2, _omitFieldNames ? '' : 'code', $pb.PbFieldType.OU3) ..a<$core.int>(2, _omitFieldNames ? '' : 'code', $pb.PbFieldType.OU3)
..aOS(3, _omitFieldNames ? '' : 'message') ..aOS(3, _omitFieldNames ? '' : 'message')
..hasRequiredFields = false ..hasRequiredFields = false
@ -128,9 +129,9 @@ class AuthResp extends $pb.GeneratedMessage {
static AuthResp? _defaultInstance; static AuthResp? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.int get uid => $_getIZ(0); $core.String get uid => $_getSZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set uid($core.int v) { $_setUnsignedInt32(0, v); } set uid($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasUid() => $_has(0); $core.bool hasUid() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)

8
lib/im/out/auth.pbjson.dart

@ -17,20 +17,20 @@ import 'dart:typed_data' as $typed_data;
const AuthReq$json = { const AuthReq$json = {
'1': 'AuthReq', '1': 'AuthReq',
'2': [ '2': [
{'1': 'uid', '3': 1, '4': 1, '5': 13, '10': 'uid'}, {'1': 'uid', '3': 1, '4': 1, '5': 9, '10': 'uid'},
{'1': 'token', '3': 2, '4': 1, '5': 9, '10': 'token'}, {'1': 'token', '3': 2, '4': 1, '5': 9, '10': 'token'},
], ],
}; };
/// Descriptor for `AuthReq`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `AuthReq`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authReqDescriptor = $convert.base64Decode( final $typed_data.Uint8List authReqDescriptor = $convert.base64Decode(
'CgdBdXRoUmVxEhAKA3VpZBgBIAEoDVIDdWlkEhQKBXRva2VuGAIgASgJUgV0b2tlbg=='); 'CgdBdXRoUmVxEhAKA3VpZBgBIAEoCVIDdWlkEhQKBXRva2VuGAIgASgJUgV0b2tlbg==');
@$core.Deprecated('Use authRespDescriptor instead') @$core.Deprecated('Use authRespDescriptor instead')
const AuthResp$json = { const AuthResp$json = {
'1': 'AuthResp', '1': 'AuthResp',
'2': [ '2': [
{'1': 'uid', '3': 1, '4': 1, '5': 13, '10': 'uid'}, {'1': 'uid', '3': 1, '4': 1, '5': 9, '10': 'uid'},
{'1': 'code', '3': 2, '4': 1, '5': 13, '10': 'code'}, {'1': 'code', '3': 2, '4': 1, '5': 13, '10': 'code'},
{'1': 'message', '3': 3, '4': 1, '5': 9, '10': 'message'}, {'1': 'message', '3': 3, '4': 1, '5': 9, '10': 'message'},
], ],
@ -38,6 +38,6 @@ const AuthResp$json = {
/// Descriptor for `AuthResp`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `AuthResp`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List authRespDescriptor = $convert.base64Decode( final $typed_data.Uint8List authRespDescriptor = $convert.base64Decode(
'CghBdXRoUmVzcBIQCgN1aWQYASABKA1SA3VpZBISCgRjb2RlGAIgASgNUgRjb2RlEhgKB21lc3' 'CghBdXRoUmVzcBIQCgN1aWQYASABKAlSA3VpZBISCgRjb2RlGAIgASgNUgRjb2RlEhgKB21lc3'
'NhZ2UYAyABKAlSB21lc3NhZ2U='); 'NhZ2UYAyABKAlSB21lc3NhZ2U=');

18
lib/im/out/message.pb.dart

@ -19,8 +19,8 @@ export 'message.pbenum.dart';
class MsgData extends $pb.GeneratedMessage { class MsgData extends $pb.GeneratedMessage {
factory MsgData({ factory MsgData({
$core.int? to, $core.String? to,
$core.int? from, $core.String? from,
$core.int? ctime, $core.int? ctime,
MsgType? type, MsgType? type,
$core.List<$core.int>? data, $core.List<$core.int>? data,
@ -48,10 +48,10 @@ class MsgData extends $pb.GeneratedMessage {
factory MsgData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r); factory MsgData.fromJson($core.String i, [$pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY]) => create()..mergeFromJson(i, r);
static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'MsgData', createEmptyInstance: create) static final $pb.BuilderInfo _i = $pb.BuilderInfo(_omitMessageNames ? '' : 'MsgData', createEmptyInstance: create)
..a<$core.int>(1, _omitFieldNames ? '' : 'to', $pb.PbFieldType.OU3) ..aOS(1, _omitFieldNames ? '' : 'to')
..a<$core.int>(2, _omitFieldNames ? '' : 'from', $pb.PbFieldType.OU3) ..aOS(2, _omitFieldNames ? '' : 'from')
..a<$core.int>(3, _omitFieldNames ? '' : 'ctime', $pb.PbFieldType.OU3) ..a<$core.int>(3, _omitFieldNames ? '' : 'ctime', $pb.PbFieldType.OU3)
..e<MsgType>(4, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: MsgType.SINGLE_TEXT, valueOf: MsgType.valueOf, enumValues: MsgType.values) ..e<MsgType>(4, _omitFieldNames ? '' : 'type', $pb.PbFieldType.OE, defaultOrMaker: MsgType.COMMAND, valueOf: MsgType.valueOf, enumValues: MsgType.values)
..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY) ..a<$core.List<$core.int>>(5, _omitFieldNames ? '' : 'data', $pb.PbFieldType.OY)
..hasRequiredFields = false ..hasRequiredFields = false
; ;
@ -78,18 +78,18 @@ class MsgData extends $pb.GeneratedMessage {
static MsgData? _defaultInstance; static MsgData? _defaultInstance;
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.int get to => $_getIZ(0); $core.String get to => $_getSZ(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
set to($core.int v) { $_setUnsignedInt32(0, v); } set to($core.String v) { $_setString(0, v); }
@$pb.TagNumber(1) @$pb.TagNumber(1)
$core.bool hasTo() => $_has(0); $core.bool hasTo() => $_has(0);
@$pb.TagNumber(1) @$pb.TagNumber(1)
void clearTo() => clearField(1); void clearTo() => clearField(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.int get from => $_getIZ(1); $core.String get from => $_getSZ(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)
set from($core.int v) { $_setUnsignedInt32(1, v); } set from($core.String v) { $_setString(1, v); }
@$pb.TagNumber(2) @$pb.TagNumber(2)
$core.bool hasFrom() => $_has(1); $core.bool hasFrom() => $_has(1);
@$pb.TagNumber(2) @$pb.TagNumber(2)

25
lib/im/out/message.pbenum.dart

@ -14,16 +14,25 @@ import 'dart:core' as $core;
import 'package:protobuf/protobuf.dart' as $pb; import 'package:protobuf/protobuf.dart' as $pb;
class MsgType extends $pb.ProtobufEnum { class MsgType extends $pb.ProtobufEnum {
static const MsgType SINGLE_TEXT = MsgType._(0, _omitEnumNames ? '' : 'SINGLE_TEXT');
static const MsgType SINGLE_AUDIO = MsgType._(1, _omitEnumNames ? '' : 'SINGLE_AUDIO'); static const MsgType COMMAND = MsgType._(0, _omitEnumNames ? '' : 'COMMAND');
static const MsgType GROUP_TEXT = MsgType._(2, _omitEnumNames ? '' : 'GROUP_TEXT'); static const MsgType TEXT = MsgType._(1, _omitEnumNames ? '' : 'TEXT');
static const MsgType GROUP_AUDIO = MsgType._(3, _omitEnumNames ? '' : 'GROUP_AUDIO'); static const MsgType IMAGE = MsgType._(2, _omitEnumNames ? '' : 'IMAGE');
static const MsgType AUDIO = MsgType._(3, _omitEnumNames ? '' : 'AUDIO');
static const MsgType VIDEO = MsgType._(4, _omitEnumNames ? '' : 'VIDEO');
static const MsgType PACKET = MsgType._(5, _omitEnumNames ? '' : 'PACKET');
static const MsgType TRANSFER = MsgType._(6, _omitEnumNames ? '' : 'TRANSFER');
static const MsgType LOCATION = MsgType._(7, _omitEnumNames ? '' : 'LOCATION');
static const $core.List<MsgType> values = <MsgType> [ static const $core.List<MsgType> values = <MsgType> [
SINGLE_TEXT, COMMAND,
SINGLE_AUDIO, TEXT,
GROUP_TEXT, IMAGE,
GROUP_AUDIO, AUDIO,
VIDEO,
PACKET,
TRANSFER,
LOCATION,
]; ];
static final $core.Map<$core.int, MsgType> _byValue = $pb.ProtobufEnum.initByValue(values); static final $core.Map<$core.int, MsgType> _byValue = $pb.ProtobufEnum.initByValue(values);

22
lib/im/out/message.pbjson.dart

@ -17,24 +17,28 @@ import 'dart:typed_data' as $typed_data;
const MsgType$json = { const MsgType$json = {
'1': 'MsgType', '1': 'MsgType',
'2': [ '2': [
{'1': 'SINGLE_TEXT', '2': 0}, {'1': 'COMMAND', '2': 0},
{'1': 'SINGLE_AUDIO', '2': 1}, {'1': 'TEXT', '2': 1},
{'1': 'GROUP_TEXT', '2': 2}, {'1': 'IMAGE', '2': 2},
{'1': 'GROUP_AUDIO', '2': 3}, {'1': 'AUDIO', '2': 3},
{'1': 'VIDEO', '2': 4},
{'1': 'PACKET', '2': 5},
{'1': 'TRANSFER', '2': 6},
{'1': 'LOCATION', '2': 7},
], ],
}; };
/// Descriptor for `MsgType`. Decode as a `google.protobuf.EnumDescriptorProto`. /// Descriptor for `MsgType`. Decode as a `google.protobuf.EnumDescriptorProto`.
final $typed_data.Uint8List msgTypeDescriptor = $convert.base64Decode( final $typed_data.Uint8List msgTypeDescriptor = $convert.base64Decode(
'CgdNc2dUeXBlEg8KC1NJTkdMRV9URVhUEAASEAoMU0lOR0xFX0FVRElPEAESDgoKR1JPVVBfVE' 'CgdNc2dUeXBlEgsKB0NPTU1BTkQQABIICgRURVhUEAESCQoFSU1BR0UQAhIJCgVBVURJTxADEg'
'VYVBACEg8KC0dST1VQX0FVRElPEAM='); 'kKBVZJREVPEAQSCgoGUEFDS0VUEAUSDAoIVFJBTlNGRVIQBhIMCghMT0NBVElPThAH');
@$core.Deprecated('Use msgDataDescriptor instead') @$core.Deprecated('Use msgDataDescriptor instead')
const MsgData$json = { const MsgData$json = {
'1': 'MsgData', '1': 'MsgData',
'2': [ '2': [
{'1': 'to', '3': 1, '4': 1, '5': 13, '10': 'to'}, {'1': 'to', '3': 1, '4': 1, '5': 9, '10': 'to'},
{'1': 'from', '3': 2, '4': 1, '5': 13, '10': 'from'}, {'1': 'from', '3': 2, '4': 1, '5': 9, '10': 'from'},
{'1': 'ctime', '3': 3, '4': 1, '5': 13, '10': 'ctime'}, {'1': 'ctime', '3': 3, '4': 1, '5': 13, '10': 'ctime'},
{'1': 'type', '3': 4, '4': 1, '5': 14, '6': '.MsgType', '10': 'type'}, {'1': 'type', '3': 4, '4': 1, '5': 14, '6': '.MsgType', '10': 'type'},
{'1': 'data', '3': 5, '4': 1, '5': 12, '10': 'data'}, {'1': 'data', '3': 5, '4': 1, '5': 12, '10': 'data'},
@ -43,7 +47,7 @@ const MsgData$json = {
/// Descriptor for `MsgData`. Decode as a `google.protobuf.DescriptorProto`. /// Descriptor for `MsgData`. Decode as a `google.protobuf.DescriptorProto`.
final $typed_data.Uint8List msgDataDescriptor = $convert.base64Decode( final $typed_data.Uint8List msgDataDescriptor = $convert.base64Decode(
'CgdNc2dEYXRhEg4KAnRvGAEgASgNUgJ0bxISCgRmcm9tGAIgASgNUgRmcm9tEhQKBWN0aW1lGA' 'CgdNc2dEYXRhEg4KAnRvGAEgASgJUgJ0bxISCgRmcm9tGAIgASgJUgRmcm9tEhQKBWN0aW1lGA'
'MgASgNUgVjdGltZRIcCgR0eXBlGAQgASgOMgguTXNnVHlwZVIEdHlwZRISCgRkYXRhGAUgASgM' 'MgASgNUgVjdGltZRIcCgR0eXBlGAQgASgOMgguTXNnVHlwZVIEdHlwZRISCgRkYXRhGAUgASgM'
'UgRkYXRh'); 'UgRkYXRh');

28
lib/main.dart

@ -78,6 +78,7 @@ import 'package:huixiang/test_page.dart';
import 'package:huixiang/union/location_map_page.dart'; import 'package:huixiang/union/location_map_page.dart';
import 'package:huixiang/union/union_select_city.dart'; import 'package:huixiang/union/union_select_city.dart';
import 'package:huixiang/utils/ImgCachePath.dart'; import 'package:huixiang/utils/ImgCachePath.dart';
import 'package:huixiang/utils/qiniu.dart';
import 'package:huixiang/vip/user_vip_service_page.dart'; import 'package:huixiang/vip/user_vip_service_page.dart';
import 'package:huixiang/web/web_page.dart'; import 'package:huixiang/web/web_page.dart';
@ -152,6 +153,7 @@ import 'im/chat_details_page.dart';
import 'im/chat_friend_group.dart'; import 'im/chat_friend_group.dart';
import 'im/chat_setting.dart'; import 'im/chat_setting.dart';
import 'im/contact_share.dart'; import 'im/contact_share.dart';
import 'im/im_search.dart';
import 'login/login_store_select.dart'; import 'login/login_store_select.dart';
import 'login/new_login_page.dart'; import 'login/new_login_page.dart';
import 'login/phone_address_page.dart'; import 'login/phone_address_page.dart';
@ -207,8 +209,7 @@ void main() async {
ImgCachePath(); ImgCachePath();
// initSdk(); // initSdk();
bool isFirst = sharedPreferences.getBool("isFirst"); bool isFirst = sharedPreferences.getBool("isFirst");
initShared();
initDatabase();
runApp(MyApp(locale, isFirst)); runApp(MyApp(locale, isFirst));
// FlutterBugly.postCatchedException((){ // FlutterBugly.postCatchedException((){
@ -219,13 +220,24 @@ void main() async {
// final XgFlutterPlugin xgFlutterPlugin = XgFlutterPlugin(); // final XgFlutterPlugin xgFlutterPlugin = XgFlutterPlugin();
HxDatabase hxDatabase; HxDatabase hxDatabase;
SharedPreferences sharedPreferences;
initShared() async {
sharedPreferences = await SharedPreferences.getInstance();
}
initDatabase(String userId) async {
hxDatabase = HxDatabase();
hxDatabase.open(key: userId);
}
initDatabase() async { closeDatabase() async {
hxDatabase = await $FloorHxDatabase.databaseBuilder('huixiang_database.db').build(); hxDatabase.close();
} }
final SocketClient socketClient = new SocketClient(); final SocketClient socketClient = new SocketClient();
EventBus eventBus = EventBus(sync: true); final Qiniu qiniu = Qiniu();
final EventBus eventBus = EventBus(sync: true);
Route lastRoutePage; Route lastRoutePage;
class MyApp extends StatelessWidget { class MyApp extends StatelessWidget {
@ -505,9 +517,9 @@ Map<String, WidgetBuilder> routers = <String, WidgetBuilder>{
'/router/contacts_share': (context, {arguments}) => '/router/contacts_share': (context, {arguments}) =>
ContactsShare(arguments:arguments), ContactsShare(arguments:arguments),
'/router/chat_details_page': (context, {arguments}) => '/router/chat_details_page': (context, {arguments}) =>
ChatDetailsPage(), ChatDetailsPage(arguments: arguments),
'/router/chat_setting': (context, {arguments}) => '/router/chat_setting': (context, {arguments}) =>
ChatSetting(), ChatSetting(arguments:arguments),
'/router/chat_friend_group': (context, {arguments}) => '/router/chat_friend_group': (context, {arguments}) =>
ChatFriendGroup(), ChatFriendGroup(),
'/router/add_friend': (context, {arguments}) => '/router/add_friend': (context, {arguments}) =>
@ -626,5 +638,7 @@ Map<String, WidgetBuilder> routers = <String, WidgetBuilder>{
EditInvoicesInfo(arguments:arguments), EditInvoicesInfo(arguments:arguments),
'/router/invoices_detail_page': (context, {arguments}) => '/router/invoices_detail_page': (context, {arguments}) =>
InvoicesDetailPage(arguments:arguments), InvoicesDetailPage(arguments:arguments),
'/router/im_search': (context, {arguments}) =>
ImSearch(),
}; };

10
lib/main_page.dart

@ -57,6 +57,7 @@ class _MainPage extends State<MainPage> with WidgetsBindingObserver {
final GlobalKey homePageKey = GlobalKey(); final GlobalKey homePageKey = GlobalKey();
final GlobalKey minePageKey = GlobalKey(); final GlobalKey minePageKey = GlobalKey();
final GlobalKey unionPageKey = GlobalKey(); final GlobalKey unionPageKey = GlobalKey();
// final GlobalKey vipPageKey = GlobalKey(); // final GlobalKey vipPageKey = GlobalKey();
final GlobalKey imPageKey = GlobalKey(); final GlobalKey imPageKey = GlobalKey();
@ -69,6 +70,7 @@ class _MainPage extends State<MainPage> with WidgetsBindingObserver {
void dispose() { void dispose() {
super.dispose(); super.dispose();
socketClient.dispose(); socketClient.dispose();
closeDatabase();
WidgetsBinding.instance.removeObserver(this); WidgetsBinding.instance.removeObserver(this);
} }
@ -80,6 +82,7 @@ class _MainPage extends State<MainPage> with WidgetsBindingObserver {
break; break;
case AppLifecycleState.resumed: // case AppLifecycleState.resumed: //
pushRoute(); pushRoute();
socketClient.sendHeartBeatAndCheckSocket();
if (DateTime.now().millisecondsSinceEpoch - lastTime > 420000) if (DateTime.now().millisecondsSinceEpoch - lastTime > 420000)
//** //**
// Navigator.of(context).popAndPushNamed('/router/start_page'); // Navigator.of(context).popAndPushNamed('/router/start_page');
@ -102,7 +105,11 @@ class _MainPage extends State<MainPage> with WidgetsBindingObserver {
void initState() { void initState() {
super.initState(); super.initState();
SharedPreferences.getInstance().then((value) {
String userId = value.getString("userId");
initDatabase(userId);
connectSocket(); connectSocket();
});
pageController = PageController( pageController = PageController(
initialPage: initialPage:
@ -125,7 +132,8 @@ class _MainPage extends State<MainPage> with WidgetsBindingObserver {
..dismissOnTap = false; ..dismissOnTap = false;
initSdk(); initSdk();
UmengCommonSdk.initCommon('6491509087568a379b5a1345', '6491509087568a379b5a1345', 'Umeng'); UmengCommonSdk.initCommon(
'6491509087568a379b5a1345', '6491509087568a379b5a1345', 'Umeng');
UmengCommonSdk.setPageCollectionModeManual(); UmengCommonSdk.setPageCollectionModeManual();
initPlatformState(); initPlatformState();

2
lib/message/system_message.dart

@ -287,7 +287,7 @@ class _SystemMessagePage extends State<SystemMessagePage>{
}); });
}); });
}, },
child:messageItem("assets/image/icon_system_message.webp", S.of(context).xitongxiaoxi, (msgNumber["2"]+msgNumber["3"]).toString()), child:messageItem("assets/image/icon_system_message_new.webp", S.of(context).xitongxiaoxi, (msgNumber["2"]+msgNumber["3"]).toString()),
), ),
// newsSurvey(), // newsSurvey(),
// SizedBox( // SizedBox(

1
lib/mine/edit_signature.dart

@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/generated/l10n.dart';
import 'package:huixiang/utils/font_weight.dart';
import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:huixiang/view_widget/my_appbar.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';

1
lib/mine/fans_page.dart

@ -154,6 +154,7 @@ class _FansPage extends State<FansPage>
Navigator.of(context) Navigator.of(context)
.pushNamed('/router/personal_page', arguments: { .pushNamed('/router/personal_page', arguments: {
"memberId": list.mid ?? "", "memberId": list.mid ?? "",
"inletType":0
});}, });},
child: Padding( child: Padding(
padding: EdgeInsets.only(right: 8.w), padding: EdgeInsets.only(right: 8.w),

1
lib/mine/follow_page.dart

@ -148,6 +148,7 @@ class _FollowPage extends State<FollowPage> with SingleTickerProviderStateMixin,
Navigator.of(context) Navigator.of(context)
.pushNamed('/router/personal_page', arguments: { .pushNamed('/router/personal_page', arguments: {
"memberId": list.mid ?? "", "memberId": list.mid ?? "",
"inletType":0
});}, });},
child: Padding( child: Padding(
padding: EdgeInsets.only(right: 8.w), padding: EdgeInsets.only(right: 8.w),

1
lib/mine/mine_page.dart

@ -65,6 +65,7 @@ class MinePageState extends State<MinePage> with AutomaticKeepAliveClientMixin {
: await Navigator.of(context) : await Navigator.of(context)
.pushNamed('/router/personal_page', arguments: { .pushNamed('/router/personal_page', arguments: {
"memberId": "0", "memberId": "0",
"inletType":0
}); });
setState(() {}); setState(() {});
} }

880
lib/mine/personal_page.dart

File diff suppressed because it is too large Load Diff

4
lib/retrofit/data/address.dart

@ -1,5 +1,7 @@
import 'dart:convert';
class Address { class Address {
Address(); Address();
@ -20,7 +22,7 @@ class Address {
factory Address.fromJson(Map<String, dynamic> json) => Address() factory Address.fromJson(Map<String, dynamic> json) => Address()
..address = json['address'] as String ..address = json['address'] as String
..area = json['area'] as String ..area = json['area'] as String
..city = json['city'] as String ..city = (json['city'] is List<dynamic>) ? jsonEncode(json['city']) : json['city'] as String
..cityInfo = json['cityInfo'] as String ..cityInfo = json['cityInfo'] as String
..id = json['id'] as String ..id = json['id'] as String
..isDefault = json['isDefault'] as bool ..isDefault = json['isDefault'] as bool

73
lib/retrofit/data/im_user.dart

@ -0,0 +1,73 @@
/// mid : "1379254113602109440"
/// nickname : "哈哈"
/// avatar : "https://pos.upload.lotus-wallet.com/admin/2022/11/d501d2cd-ffc0-49f2-967c-2e463462f500.jpeg"
/// phone : "13052919193"
/// isFollow : null
/// createTime : null
class ImUser {
ImUser({
String mid,
String nickname,
num isDelete,
num isTop,
String avatar,
String phone, }){
_mid = mid;
_nickname = nickname;
_isDelete = isDelete;
_isTop = isTop;
_avatar = avatar;
_phone = phone;
}
ImUser.fromJson(dynamic json) {
_mid = json['mid'];
_nickname = json['nickname'];
_isDelete = json['isDelete'];
_isTop = json['isTop'];
_avatar = json['avatar'];
_phone = json['phone'];
}
String _mid;
String _nickname;
num _isDelete;
num _isTop;
String _avatar;
String _phone;
ImUser copyWith({ String mid,
String nickname,
num isDelete,
num isTop,
String avatar,
String phone,
}) => ImUser( mid: mid ?? _mid,
nickname: nickname ?? _nickname,
isDelete: isDelete ?? _isDelete,
isTop: isTop ?? _isTop,
avatar: avatar ?? _avatar,
phone: phone ?? _phone,
);
String get mid => _mid;
String get nickname => _nickname;
num get isDelete => _isDelete;
num get isTop => _isTop;
String get avatar => _avatar;
String get phone => _phone;
set isTop(num value) {
_isTop = value;
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['mid'] = _mid;
map['nickname'] = _nickname;
map['isDelete'] = _isDelete;
map['isTop'] = _isTop;
map['avatar'] = _avatar;
map['phone'] = _phone;
return map;
}
}

12
lib/retrofit/data/social_info.dart

@ -5,24 +5,29 @@ class SocialInfo {
SocialInfo({ SocialInfo({
dynamic fans, dynamic fans,
dynamic follow, dynamic follow,
dynamic achievementNumber}){ dynamic achievementNumber,
dynamic mutualFollowCount}){
_fans = fans; _fans = fans;
_follow = follow; _follow = follow;
_achievementNumber = achievementNumber; _achievementNumber = achievementNumber;
_mutualFollowCount = mutualFollowCount;
} }
SocialInfo.fromJson(dynamic json) { SocialInfo.fromJson(dynamic json) {
_fans = json['fans']; _fans = json['fans'];
_follow = json['follow']; _follow = json['follow'];
_achievementNumber = json['achievementNumber']; _achievementNumber = json['achievementNumber'];
_mutualFollowCount = json['mutualFollowCount'];
} }
dynamic _fans; dynamic _fans;
dynamic _follow; dynamic _follow;
dynamic _achievementNumber; dynamic _achievementNumber;
dynamic _mutualFollowCount;
dynamic get fans => _fans; dynamic get fans => _fans;
dynamic get follow => _follow; dynamic get follow => _follow;
dynamic get achievementNumber => _achievementNumber; dynamic get achievementNumber => _achievementNumber;
dynamic get mutualFollowCount => _mutualFollowCount;
set fans(int value) { set fans(int value) {
@ -34,6 +39,7 @@ class SocialInfo {
map['fans'] = _fans; map['fans'] = _fans;
map['follow'] = _follow; map['follow'] = _follow;
map['achievementNumber'] = _achievementNumber; map['achievementNumber'] = _achievementNumber;
map['mutualFollowCount'] = _mutualFollowCount;
return map; return map;
} }
@ -44,4 +50,8 @@ class SocialInfo {
set achievementNumber(int value) { set achievementNumber(int value) {
_achievementNumber = value; _achievementNumber = value;
} }
set mutualFollowCount(int value) {
_mutualFollowCount = value;
}
} }

11
lib/retrofit/min_api.dart

@ -4,6 +4,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:huixiang/constant.dart';
import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/generated/l10n.dart';
import 'package:huixiang/retrofit/data/address.dart'; import 'package:huixiang/retrofit/data/address.dart';
import 'package:huixiang/retrofit/data/base_data.dart'; import 'package:huixiang/retrofit/data/base_data.dart';
@ -27,13 +28,10 @@ import 'data/shopping_home_config.dart';
part 'min_api.g.dart'; part 'min_api.g.dart';
const localBaseUrl = "http://192.168.10.54:8765/app/";///
// const localBaseUrl = "http://pos-test.api.lotus-wallet.com/app/";///
const serviceBaseUrl = "https://pos.api.lotus-wallet.com/app/";///线
/// ///
@RestApi(baseUrl: localBaseUrl) @RestApi(baseUrl: kReleaseMode ? serviceMiniBaseUrl : localMiniBaseUrl)
abstract class MinApiService { abstract class MinApiService {
factory MinApiService( factory MinApiService(
Dio dio, { Dio dio, {
@ -44,8 +42,7 @@ abstract class MinApiService {
String storeId, String storeId,
bool showLoading = false, bool showLoading = false,
}) { }) {
Map<String, dynamic> headers = Map<String, dynamic> headers = (token == null || token == "") ? {} : {'token': "Bearer $token"};
(token == null || token == "") ? {} : {'token': "Bearer $token"};
if (tenant != null && tenant != "") { if (tenant != null && tenant != "") {
headers["tenant"] = tenant; headers["tenant"] = tenant;
} }
@ -115,7 +112,7 @@ abstract class MinApiService {
); );
if (kReleaseMode) { if (kReleaseMode) {
baseUrl = serviceBaseUrl; baseUrl = serviceMiniBaseUrl;
} }
return _MinApiService(dio, baseUrl: baseUrl); return _MinApiService(dio, baseUrl: baseUrl);
} }

4
lib/retrofit/min_api.g.dart

@ -9,7 +9,7 @@ part of 'min_api.dart';
class _MinApiService implements MinApiService { class _MinApiService implements MinApiService {
_MinApiService(this._dio, {this.baseUrl}) { _MinApiService(this._dio, {this.baseUrl}) {
ArgumentError.checkNotNull(_dio, '_dio'); ArgumentError.checkNotNull(_dio, '_dio');
baseUrl ??= kReleaseMode?serviceBaseUrl:localBaseUrl; baseUrl ??= kReleaseMode ? serviceMiniBaseUrl : localMiniBaseUrl;
} }
final Dio _dio; final Dio _dio;
@ -113,7 +113,7 @@ class _MinApiService implements MinApiService {
const _extra = <String, dynamic>{}; const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{'tableId': tableId}; final queryParameters = <String, dynamic>{'tableId': tableId};
final _data = <String, dynamic>{}; final _data = <String, dynamic>{};
final _result = await _dio.request<Map<String, dynamic>>('shoppingcart', final _result = await _dio.request<Map<String, dynamic>>('/shoppingcart',
queryParameters: queryParameters, queryParameters: queryParameters,
options: RequestOptions( options: RequestOptions(
method: 'GET', method: 'GET',

31
lib/retrofit/retrofit_api.dart

@ -7,6 +7,7 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart'; import 'package:flutter_easyloading/flutter_easyloading.dart';
import 'package:huixiang/constant.dart';
import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/generated/l10n.dart';
import 'package:huixiang/retrofit/data/activity.dart'; import 'package:huixiang/retrofit/data/activity.dart';
import 'package:huixiang/retrofit/data/article.dart'; import 'package:huixiang/retrofit/data/article.dart';
@ -39,6 +40,7 @@ import 'data/goods_category.dart';
import 'data/headlines_list.dart'; import 'data/headlines_list.dart';
import 'data/headlines_list_details.dart'; import 'data/headlines_list_details.dart';
import 'data/home_rank.dart'; import 'data/home_rank.dart';
import 'data/im_user.dart';
import 'data/invitation_list.dart'; import 'data/invitation_list.dart';
import 'data/invoice_list.dart'; import 'data/invoice_list.dart';
import 'data/invoices_detail_info.dart'; import 'data/invoices_detail_info.dart';
@ -65,19 +67,12 @@ import 'data/vip_benefit_list.dart';
import 'data/vip_card.dart'; import 'data/vip_card.dart';
import 'data/vip_card_home.dart'; import 'data/vip_card_home.dart';
import 'data/vip_rule_details.dart'; import 'data/vip_rule_details.dart';
import 'data/wx_pay.dart';
part 'retrofit_api.g.dart'; part 'retrofit_api.g.dart';
const localBaseUrl = "http://192.168.10.54:8766/app/";///
// const localBaseUrl = "http://platform.test.api.lotus-wallet.com/app/";///
const serviceBaseUrl = "https://pos.platform.lotus-wallet.com/app/";///线
const ipBaseUrl = "http://whois.pconline.com.cn";
///ip @RestApi(baseUrl: kReleaseMode ? serviceBaseUrl : localBaseUrl)
@RestApi(baseUrl: localBaseUrl)
abstract class ApiService { abstract class ApiService {
factory ApiService( factory ApiService(
Dio dio, { Dio dio, {
@ -445,6 +440,11 @@ abstract class ApiService {
Future<BaseData<PageInfo<ListData>>> followList( Future<BaseData<PageInfo<ListData>>> followList(
@Body() Map<String, dynamic> map); @Body() Map<String, dynamic> map);
///
@POST("/member/mutualFollow/list")
Future<BaseData<PageInfo<ListData>>> mutualFollowList(
@Body() Map<String, dynamic> map);
//// ////
@PUT("/member/follow/{followId}") @PUT("/member/follow/{followId}")
Future<BaseData> follow(@Path("followId") String followId); Future<BaseData> follow(@Path("followId") String followId);
@ -453,7 +453,7 @@ abstract class ApiService {
@DELETE("/information/trend/{id}") @DELETE("/information/trend/{id}")
Future<BaseData> deleteTrend(@Path("id") String id); Future<BaseData> deleteTrend(@Path("id") String id);
///(//) ///(///)
@GET("/member/socialInfo") @GET("/member/socialInfo")
Future<BaseData<SocialInfo>> socialInfo(); Future<BaseData<SocialInfo>> socialInfo();
@ -648,4 +648,17 @@ abstract class ApiService {
/// ///
@GET("invoice/detail{id}") @GET("invoice/detail{id}")
Future<BaseData<InvoicesDetailInfo>> invoiceDetail(@Path("id") String id); Future<BaseData<InvoicesDetailInfo>> invoiceDetail(@Path("id") String id);
///
@POST("/member/memberInfoByIds")
Future<BaseData<List<ImUser>>> memberInfoByIds(@Body() Map<String, dynamic> param);
///Im关键字搜索
@GET("/member/memberSearch?keyword={keyword}")
Future<BaseData<List<ImUser>>> memberSearch(
@Path("keyword") String keyword);
@GET("/config/qiniuToken")
Future<BaseData<String>> getQiniuToken();
} }

97
lib/retrofit/retrofit_api.g.dart

@ -1412,6 +1412,32 @@ class _ApiService implements ApiService {
return value; return value;
} }
@override
Future<BaseData<PageInfo<ListData>>> mutualFollowList(map) async {
ArgumentError.checkNotNull(map, 'map');
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(map ?? <String, dynamic>{});
final _result = await _dio.request<Map<String, dynamic>>(
'/member/mutualFollow/list',
queryParameters: queryParameters,
options: RequestOptions(
method: 'POST',
headers: <String, dynamic>{},
extra: _extra,
baseUrl: baseUrl),
data: _data);
final value = BaseData<PageInfo<ListData>>.fromJson(
_result.data,
(json) => PageInfo<ListData>.fromJson(
json,
(json) => ListData.fromJson(json),
),
);
return value;
}
@override @override
Future<BaseData<dynamic>> follow(followId) async { Future<BaseData<dynamic>> follow(followId) async {
ArgumentError.checkNotNull(followId, 'followId'); ArgumentError.checkNotNull(followId, 'followId');
@ -2502,4 +2528,75 @@ class _ApiService implements ApiService {
); );
return value; return value;
} }
@override
Future<BaseData<List<ImUser>>> memberInfoByIds(param) async {
ArgumentError.checkNotNull(param, 'param');
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _data = <String, dynamic>{};
_data.addAll(param ?? <String, dynamic>{});
final _result = await _dio.request<Map<String, dynamic>>(
'/member/memberInfoByIds',
queryParameters: queryParameters,
options: RequestOptions(
method: 'POST',
headers: <String, dynamic>{},
extra: _extra,
baseUrl: baseUrl),
data: _data);
final value = BaseData<List<ImUser>>.fromJson(
_result.data,
(json) => (json as List<dynamic>)
.map<ImUser>((i) => ImUser.fromJson(i as Map<String, dynamic>))
.toList());
return value;
}
@override
Future<BaseData<List<ImUser>>> memberSearch(keyword) async {
ArgumentError.checkNotNull(keyword, 'keyword');
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.request<Map<String, dynamic>>(
'/member/memberSearch?keyword=$keyword',
queryParameters: queryParameters,
options: RequestOptions(
method: 'GET',
headers: <String, dynamic>{},
extra: _extra,
baseUrl: baseUrl),
data: _data);
final value = BaseData<List<ImUser>>.fromJson(
_result.data,
(json) => (json as List<dynamic>)
.map<ImUser>(
(i) => ImUser.fromJson(i as Map<String, dynamic>))
.toList());
return value;
}
@override
Future<BaseData<String>> getQiniuToken() async {
const _extra = <String, dynamic>{};
final queryParameters = <String, dynamic>{};
final _data = <String, dynamic>{};
final _result = await _dio.request<Map<String, dynamic>>(
'/config/qiniuToken',
queryParameters: queryParameters,
options: RequestOptions(
method: 'GET',
headers: <String, dynamic>{},
extra: _extra,
baseUrl: baseUrl,
),
data: _data,
);
final value = BaseData<String>.fromJson(
_result.data,
(json) => json);
return value;
}
} }

13
lib/setting/about_page.dart

@ -1,10 +1,13 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:huixiang/constant.dart';
import 'package:huixiang/generated/l10n.dart'; import 'package:huixiang/generated/l10n.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:huixiang/main.dart';
import 'package:huixiang/utils/bridge.dart'; import 'package:huixiang/utils/bridge.dart';
import 'package:huixiang/utils/font_weight.dart'; import 'package:huixiang/utils/font_weight.dart';
import 'package:huixiang/view_widget/my_appbar.dart'; import 'package:huixiang/view_widget/my_appbar.dart';
@ -46,7 +49,10 @@ class _AboutPage extends State<AboutPage> {
Container( Container(
decoration: new BoxDecoration( decoration: new BoxDecoration(
border: Border( border: Border(
bottom: BorderSide(color: Color(0xffF7F7F7), width: 0.0)), bottom: BorderSide(
color: Color(0xffF7F7F7), width: 0.0,
),
),
color: Color(0xffF7F7F7), color: Color(0xffF7F7F7),
), ),
padding: EdgeInsets.only(top: 25.h), padding: EdgeInsets.only(top: 25.h),
@ -81,6 +87,8 @@ class _AboutPage extends State<AboutPage> {
SizedBox( SizedBox(
height: 20.h, height: 20.h,
), ),
if (kDebugMode)
textItem("ip: $socketHost\nport: $socketPort\nid: ${socketClient.userId}"),
GestureDetector( GestureDetector(
onTap: (){ onTap: (){
// updateApp(); // updateApp();
@ -220,7 +228,8 @@ class _AboutPage extends State<AboutPage> {
size: 24, size: 24,
), ),
], ],
)), ),
),
], ],
), ),
); );

14
lib/store/store_view/shop_goods.dart

@ -132,7 +132,7 @@ class _ShopGoods extends State<ShopGoods> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children:[ children:[
Padding( Padding(
padding: EdgeInsets.only(right: 16.w,bottom:7.h), padding: EdgeInsets.only(right: 16.w, bottom:7),
child: Row( child: Row(
children: [ children: [
Expanded( Expanded(
@ -167,7 +167,7 @@ class _ShopGoods extends State<ShopGoods> {
), ),
), ),
Padding( Padding(
padding: EdgeInsets.only(right: 16.w,bottom:7.h), padding: EdgeInsets.only(right: 16.w,bottom:7),
child:Text( child:Text(
(widget.productListBean != null (widget.productListBean != null
? widget.productListBean.shortName ? widget.productListBean.shortName
@ -207,10 +207,11 @@ class _ShopGoods extends State<ShopGoods> {
), ),
Spacer(), Spacer(),
], ],
):SizedBox(height:23.h), ) : SizedBox(height:23),
Row( Row(
children: [ children: [
Expanded(child:Text( Expanded(
child:Text(
"¥${AppUtils.calculateDouble(double.tryParse(widget.isShopCart ? widget.shoppingCartSkuItemListBean.skuPrice : widget.productListBean.price) ?? 0)}", "¥${AppUtils.calculateDouble(double.tryParse(widget.isShopCart ? widget.shoppingCartSkuItemListBean.skuPrice : widget.productListBean.price) ?? 0)}",
style: TextStyle( style: TextStyle(
color: Color(0xFFFF4500), color: Color(0xFFFF4500),
@ -218,7 +219,8 @@ class _ShopGoods extends State<ShopGoods> {
fontFamily: 'JDZhengHT', fontFamily: 'JDZhengHT',
fontWeight: MyFontWeight.medium, fontWeight: MyFontWeight.medium,
), ),
)), ),
),
// SizedBox( // SizedBox(
// width:5.w, // width:5.w,
// ), // ),
@ -587,7 +589,7 @@ class _ShopGoods extends State<ShopGoods> {
], ],
), ),
SizedBox( SizedBox(
height: 4.h, height: 4,
), ),
///VIP价格 ///VIP价格
// if (widget.productListBean.vipPrice != null) // if (widget.productListBean.vipPrice != null)

2
lib/store/store_view/store_order_list.dart

@ -114,7 +114,7 @@ class _StoreOrderListPage extends State<StoreOrderListPage> {
// refreshController.refreshFailed(); // refreshController.refreshFailed();
SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type), SmartDialog.showToast(AppUtils.dioErrorTypeToString(error.type),
alignment: Alignment.center); alignment: Alignment.center);
debugPrint(error); debugPrint("${error.toString()}");
}); });
if (baseData != null && baseData.isSuccess) { if (baseData != null && baseData.isSuccess) {
// refreshController.refreshCompleted(); // refreshController.refreshCompleted();

1
lib/union/union_list.dart

@ -129,6 +129,7 @@ class _UnionList extends State<UnionList> with AutomaticKeepAliveClientMixin {
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
{ {
//uiui仿商城模式
// if (storeList[position].storeName == "一心回乡商城") { // if (storeList[position].storeName == "一心回乡商城") {
// Navigator.of(context).pushNamed( // Navigator.of(context).pushNamed(
// '/router/shopping_mall_home', // '/router/shopping_mall_home',

65
lib/utils/flutter_utils.dart

@ -288,4 +288,69 @@ class AppUtils {
return true; return true;
} }
} }
///im列表时间显示
static String timeFormatter(DateTime time) {
final now = DateTime.now();
final diff = now.difference(time).inHours;
final diff2 = now.difference(time).inDays;
if (diff < 24) {
return DateFormat('HH:mm').format(time); // 24
} else if (diff < 48) {
return '昨天 ${DateFormat('HH:mm').format(time)}'; // +
} else if (diff < 72 && diff2<7) {
return DateFormat('${DateFormat('EEEE').format(time)}').format(time);// 72
} else if (time.year == now.year) {
return DateFormat('MM月dd日').format(time); //
} else {
return DateFormat('yyyy年MM月dd日').format(time); //
}
}
///
static String milliTimeFormatter(DateTime time) {
final now = DateTime.now();
final diff = now.difference(time).inHours;
if (diff < 24) {
return DateFormat('HH:mm').format(time); // 24
} else if (diff < 48) {
return '昨天 ${DateFormat('HH:mm').format(time)}'; // +
} else if (diff < 72) {
return DateFormat('${DateFormat('EEEE').format(time)}').format(time) + " ${DateFormat('HH:mm').format(time)}";// 72+
} else if (time.year == now.year) {
return DateFormat('MM月dd日').format(time) + " ${DateFormat('HH:mm').format(time)}"; // +
} else {
return DateFormat('yyyy年MM月dd日').format(time) + " ${DateFormat('HH:mm').format(time)}"; // +
}
}
///
static String getTimeDisplay(DateTime previousTime, DateTime currentTime) {
final diffInMinutes = (currentTime.difference(previousTime)).inMinutes;
if (diffInMinutes < 5) {
// 5
return '';
} else if (currentTime.year == DateTime.now().year) {
//
if (currentTime.day == DateTime.now().day) {
//
return DateFormat('HH:mm').format(currentTime);
} else if (currentTime.day == DateTime.now().day - 1) {
//
return '昨天 ${DateFormat('HH:mm').format(currentTime)}';
} else if ((currentTime.day - DateTime.now().day).abs() < 7) {
//
return '${DateFormat('EEEE').format(currentTime)} ${DateFormat('HH:mm').format(currentTime)}';
} else {
//
return DateFormat('MM/dd').format(currentTime);
}
} else {
//
return DateFormat('yyyy/MM/dd').format(currentTime);
}
}
} }

85
lib/utils/qiniu.dart

@ -0,0 +1,85 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:huixiang/constant.dart';
import 'package:huixiang/retrofit/data/base_data.dart';
import 'package:huixiang/retrofit/retrofit_api.dart';
import 'package:path_provider/path_provider.dart';
import 'package:qiniu_flutter_sdk/qiniu_flutter_sdk.dart';
class Qiniu {
Storage storage = Storage(config: Config());
PutController putController = PutController();
progressListener() {
//
putController.addProgressListener((double percent) {
print('任务进度变化:已发送:$percent');
});
//
putController.addSendProgressListener((double percent) {
print('已上传进度变化:已发送:$percent');
});
//
putController.addStatusListener((StorageStatus status) {
print('状态变化: 当前任务状态:$status');
});
}
Future<String> uploadFile(ApiService apiService, String filePath) async {
String token = await _getToken(apiService);
File file = File(filePath);
PutResponse putResponse = await storage.putFile(file, token, options: PutOptions(
controller: putController,
key: filePath.split('/').last,
));
debugPrint("qiniuToken-result: ${putResponse.toJson()}");
return "$chatImageHost/${putResponse.key}";
}
Future<String> _getToken(ApiService apiService) async {
BaseData<String> baseData = await apiService.getQiniuToken()
.catchError((error){
debugPrint("getQiniuToken: $error");
});
if (baseData.isSuccess) {
return baseData.data;
} else {
debugPrint("get token fail, check network");
throw Error();
}
}
final Dio dio = Dio();
Future<String> downloadFile(String urlPath, {String savePath}) async {
Directory dir = await getTemporaryDirectory();
File newFile;
if (savePath != null && savePath != '') {
newFile = File(savePath);
} else {
newFile = File("${dir.path}/hx_${urlPath.split('/').last}");
}
Response response = await dio.download(urlPath, newFile.path);
if (response.statusCode == 200) {
return newFile.path;
}
return null;
}
}

7
lib/utils/upload_async.dart

@ -34,7 +34,8 @@ class UploadAsync {
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.grey, borderRadius: BorderRadius.circular(2)), color: Colors.grey, borderRadius: BorderRadius.circular(2)),
child: dynamicType == 0 child: dynamicType == 0
? Expanded(child: Column( ? Expanded(
child: Column(
children: [ children: [
Text( Text(
dynamicText, dynamicText,
@ -49,8 +50,8 @@ class UploadAsync {
Expanded(child: Expanded(child:
Container( Container(
height: 20.h, height: 20.h,
)) ),),
])) ],),)
: ClipRRect( : ClipRRect(
borderRadius: BorderRadius.circular(2), borderRadius: BorderRadius.circular(2),
child: Image.file( child: Image.file(

1
lib/view_widget/classic_header.dart

@ -17,6 +17,7 @@ class MyHeader extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MyClassicHeader( return MyClassicHeader(
height: 80,
refreshStyle: RefreshStyle.Follow, refreshStyle: RefreshStyle.Follow,
completeText: S.of(context).shuaxinchenggong, completeText: S.of(context).shuaxinchenggong,
failedText: S.of(context).shuaxinshibai, failedText: S.of(context).shuaxinshibai,

1
lib/web/web_view/comment_list.dart

@ -259,6 +259,7 @@ class CommentListState extends State<CommentList> {
Navigator.of(context) Navigator.of(context)
.pushNamed('/router/personal_page', arguments: { .pushNamed('/router/personal_page', arguments: {
"memberId": memberList.createUser, "memberId": memberList.createUser,
"inletType":0
}); });
}); });
}, },

11
pubspec.yaml

@ -63,7 +63,7 @@ dependencies:
visibility_detector: ^0.3.3 visibility_detector: ^0.3.3
steel_crypt: ^3.0.0+1 steel_crypt: ^3.0.0+1
encrypt: ^5.0.1 # encrypt: any
event_bus: ^2.0.0 event_bus: ^2.0.0
intl: ^0.17.0 intl: ^0.17.0
@ -115,17 +115,18 @@ dependencies:
emoji_picker_flutter: ^1.4.1 emoji_picker_flutter: ^1.4.1
mqtt_client: ^9.6.8
shimmer: ^3.0.0 shimmer: ^3.0.0
# 时间选择器 # 时间选择器
flutter_datetime_picker: ^1.5.1 flutter_datetime_picker: ^1.5.1
qiniu_flutter_sdk:
path: qiniu-dart-sdk/flutter
widgetpicker: ^0.1.1 widgetpicker: ^0.1.1
floor: ^1.4.2 # floor: ^1.4.2
sqflite: ^2.2.2
syncfusion_flutter_datepicker: ^19.4.38 syncfusion_flutter_datepicker: ^19.4.38
protobuf: ^3.1.0 protobuf: ^3.1.0
dev_dependencies: dev_dependencies:

58
qiniu-dart-sdk/.travis.yml

@ -0,0 +1,58 @@
language: dart
dart:
- stable
install:
- cd $TRAVIS_BUILD_DIR/base && pub get
# - cd $$TRAVIS_BUILD_DIR/flutter && pub get
before_script:
- cd $TRAVIS_BUILD_DIR/base && rm -rf .env
jobs:
include:
#######################################
######### jobs for base #############
#######################################
# 检查 lint 并在 warnings、infos 时报错退出
- stage: base(analyze,format,test)
name: "Analyze"
os: linux
script: cd $TRAVIS_BUILD_DIR/base && dartanalyzer --fatal-warnings --fatal-infos .
# 检查格式并在异常时退出
- stage: base(analyze,format,test)
name: "Format"
os: linux
script: cd $TRAVIS_BUILD_DIR/base && dartfmt -n --set-exit-if-changed .
# 执行测试(已开启 null safe)
- stage: base(analyze,format,test)
name: "Vm Tests"
os: linux
script: cd $TRAVIS_BUILD_DIR/base && pub run test_coverage --print-test-output && bash <(curl -s https://codecov.io/bash)
#######################################
####### jobs for flutter_sdk ##########
#######################################
# - stage: flutter_sdk(analyze,format,test)
# name: "Analyze"
# os: linux
# script: cd $TRAVIS_BUILD_DIR/flutter
# - stage: flutter_sdk(analyze,format,test)
# name: "Format"
# os: linux
# script: cd $TRAVIS_BUILD_DIR/flutter
# - stage: flutter_sdk(analyze,format,test)
# name: "Vm Tests"
# os: linux
# script: cd $TRAVIS_BUILD_DIR/flutter
stages:
- base(analyze,format,test)
# - flutter_sdk(analyze,format,test)
# - flutter_sdk_example(analyze,format,test)
cache:
directories:
- $HOME/.pub-cache

76
qiniu-dart-sdk/CODE_OF_CONDUCT.md

@ -0,0 +1,76 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at yinxulai@qiniu.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

15
qiniu-dart-sdk/README.md

@ -0,0 +1,15 @@
# Dart SDK
[![codecov](https://codecov.io/gh/qiniu/dart-sdk/branch/master/graph/badge.svg?token=5VOX6NJTKF)](https://codecov.io/gh/qiniu/dart-sdk)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![qiniu_sdk_base](https://img.shields.io/pub/v/qiniu_sdk_base.svg?label=qiniu_sdk_base)](https://pub.dev/packages/qiniu_sdk_base)
[![qiniu_flutter_sdk](https://img.shields.io/pub/v/qiniu_flutter_sdk.svg?label=qiniu_flutter_sdk)](https://pub.dev/packages/qiniu_flutter_sdk)
## 目录说明
- base 封装了七牛各业务的基础实现
- flutter 该目录是 base + Flutter 的绑定实现,同时导出为单独的 package 提供给用户使用
### [Flutter SDK](https://github.com/qiniu/dart-sdk/tree/master/flutter)
七牛云业务基于 Dart 绑定 Flutter 的实现,为 Flutter 提供简易的使用方式,更多信息查看该目录下的 [README.md](https://github.com/qiniu/dart-sdk/tree/master/flutter/README.md) 文件。

12
qiniu-dart-sdk/base/CHANGELOG.md

@ -0,0 +1,12 @@
## 0.1.0
- Initial Release.
## 0.2.0
- 优化了 `StorageError` 输出的调用栈
- `CacheProvider` 的方法都改成异步的
## 0.2.1
- 修复关闭 App 缓存丢失的问题

201
qiniu-dart-sdk/base/LICENSE

@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

29
qiniu-dart-sdk/base/README.md

@ -0,0 +1,29 @@
# Qiniu Sdk Base [![qiniu_sdk_base](https://img.shields.io/pub/v/qiniu_sdk_base.svg?label=qiniu_sdk_base)](https://pub.dev/packages/qiniu_sdk_base) [![codecov](https://codecov.io/gh/qiniu/dart-sdk/branch/master/graph/badge.svg?token=5VOX6NJTKF)](https://codecov.io/gh/qiniu/dart-sdk)
七牛 dart 平台 sdk 的 base 包,为上层 sdk 提供基础设施和共享代码。
## 功能列表
* 单文件上传
* 分片上传
* 任务状态
* 任务进度
* 上传进度
* 失败重试
## 如何测试
创建 `.env` 文件,并输入如下内容
```
export QINIU_DART_SDK_ACCESS_KEY=
export QINIU_DART_SDK_SECRET_KEY=
export QINIU_DART_SDK_TOKEN_SCOPE=
```
`.env` 文件中填好敏感数据,即 ak、sk、scope
接着运行如下指令
`pub run test`

90
qiniu-dart-sdk/base/analysis_options.yaml

@ -0,0 +1,90 @@
# copy from https://github.com/dart-lang/http/blob/master/analysis_options.yaml
include: package:pedantic/analysis_options.yaml
analyzer:
# enable-experiment:
# - non-nullable
strong-mode:
implicit-casts: false
implicit-dynamic: false
linter:
rules:
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_classes_with_only_static_members
- avoid_empty_else
- avoid_function_literals_in_foreach_calls
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_relative_lib_imports
- avoid_renaming_method_parameters
- avoid_return_types_on_setters
- avoid_returning_null_for_void
- avoid_returning_this
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_types_as_parameter_names
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_types
- cascade_invocations
# comment_references
- control_flow_in_finally
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- empty_statements
- file_names
- hash_and_equals
- invariant_booleans
- iterable_contains_unrelated_type
- library_names
- library_prefixes
- list_remove_unrelated_type
- no_adjacent_strings_in_list
- no_duplicate_case_values
- non_constant_identifier_names
- null_closures
- omit_local_variable_types
- only_throw_errors
- overridden_fields
- package_names
- package_prefixed_library_names
- prefer_adjacent_string_concatenation
- prefer_conditional_assignment
- prefer_contains
- prefer_equal_for_default_values
- prefer_final_fields
- prefer_collection_literals
- prefer_generic_function_type_aliases
- prefer_initializing_formals
- prefer_is_empty
- prefer_is_not_empty
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_typing_uninitialized_variables
- recursive_getters
- slash_for_doc_comments
- test_types_in_equals
- throw_in_finally
- type_init_formals
- unawaited_futures
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_overrides
- unnecessary_parenthesis
- unnecessary_statements
- unnecessary_this
- unrelated_type_equality_checks
- use_rethrow_when_possible
- valid_regexps
- void_checks

5
qiniu-dart-sdk/base/lib/qiniu_sdk_base.dart

@ -0,0 +1,5 @@
library qiniu_sdk_base;
export 'src/auth/auth.dart';
export 'src/error/error.dart';
export 'src/storage/storage.dart';

125
qiniu-dart-sdk/base/lib/src/auth/auth.dart

@ -0,0 +1,125 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:meta/meta.dart';
import './put_policy.dart';
export 'put_policy.dart';
class TokenInfo {
final String accessKey;
final PutPolicy putPolicy;
const TokenInfo(this.accessKey, this.putPolicy);
}
///
///
/// [-](https://developer.qiniu.com/kodo/manual/1644/security)
class Auth {
/// [accessKey]
///
/// [- AccessKey/SecretKey](https://developer.qiniu.com/kodo/manual/1644/security#aksk)
/// 使[-使](https://developer.qiniu.com/kodo/kb/1334/the-access-key-secret-key-encryption-key-safe-use-instructions)
final String accessKey;
/// [secretKey]
///
/// 使 [accessKey]
final String secretKey;
const Auth({
@required this.accessKey,
@required this.secretKey,
}) : assert(accessKey != null),
assert(secretKey != null);
/// 使 Token
///
/// [PutPolicy]
String generateUploadToken({
@required PutPolicy putPolicy,
}) {
assert(putPolicy != null);
var data = jsonEncode(putPolicy);
var encodedPutPolicy = base64Url.encode(utf8.encode(data));
var baseToken = generateAccessToken(bytes: utf8.encode(encodedPutPolicy));
return '$baseToken:$encodedPutPolicy';
}
/// Token
///
/// [key]
/// [deadline] 1451491200
/// [bucketDomain] http://test.bucket.com
String generateDownloadToken({
@required String key,
@required int deadline,
@required String bucketDomain,
}) {
assert(key != null);
assert(deadline != null);
assert(bucketDomain != null);
var downloadURL = '$bucketDomain/$key?e=$deadline';
return generateAccessToken(bytes: utf8.encode(downloadURL));
}
/// Token访
///
/// 访, Token
String generateAccessToken({@required List<int> bytes}) {
assert(bytes != null);
var hmacEncoder = Hmac(sha1, utf8.encode(secretKey));
var sign = hmacEncoder.convert(bytes);
var encodedSign = base64Url.encode(sign.bytes);
return '$accessKey:$encodedSign';
}
/// token
///
/// Token [accessKey][PutPolicy]
static TokenInfo parseToken(String token) {
assert(token != null && token != '');
var segments = token.split(':');
if (segments.length < 2) {
throw ArgumentError('invalid token');
}
PutPolicy putPolicy;
var accessKey = segments.first;
/// token
/// [](https://github.com/qbox/product/blob/master/kodo/auths/UpToken.md#admin-uptoken-authorization)
if (segments.length >= 3) {
if (segments.last == '') {
throw ArgumentError('invalid token');
}
putPolicy = PutPolicy.fromJson(jsonDecode(
String.fromCharCodes(
base64Url.decode(
segments.last,
),
),
) as Map<String, dynamic>);
}
return TokenInfo(accessKey, putPolicy);
}
/// up token
///
/// Token [accessKey][PutPolicy]
static TokenInfo parseUpToken(String token) {
assert(token != null && token != '');
final tokenInfo = parseToken(token);
if (tokenInfo.putPolicy == null) {
throw ArgumentError('invalid up token');
}
return tokenInfo;
}
}

209
qiniu-dart-sdk/base/lib/src/auth/put_policy.dart

@ -0,0 +1,209 @@
import 'package:meta/meta.dart';
///
///
/// [-](https://developer.qiniu.com/kodo/manual/1206/put-policy)
class PutPolicy {
/// Bucket Key 750
///
///
/// <Bucket> Bucket
/// <Bucket>:<Key> Key
/// <Bucket>:<KeyPrefix> KeyPrefix
///
final String scope;
/// Bucket
///
/// [scope] Bucket
String getBucket() {
return scope.split(':').first;
}
/// 1 [scope] KeyPrefix
final int isPrefixalScope;
///
///
/// Unix
///
/// + 3600s
final int deadline;
///
///
/// 0 [scope]
final int insertOnly;
///
///
/// App-Client
final String endUser;
/// Web 303 URL
///
/// <[returnUrl]>?upload_ret=<QueryString>
/// <QueryString> [returnBody]
/// [returnUrl] [returnBody]
final String returnUrl;
/// [returnBody]
///
/// 使 <> <> JSON
/// <> [-](https://developer.qiniu.com/kodo/manual/1235/vars#magicvar)
/// <> [-](https://developer.qiniu.com/kodo/manual/1235/vars#xvar)
final String returnBody;
/// POST URL
final String callbackUrl;
/// Host
///
/// [callbackUrl] 使 [callbackUrl]
final String callbackHost;
///
///
/// Content-Type: application/x-www-form-urlencoded POST
/// :{"key":"$(key)","hash":"$(etag)","w":"$(imageInfo.width)","h":"$(imageInfo.height)"}
/// 使 <> <>
final String callbackBody;
/// Content-Type
///
/// application/x-www-form-urlencoded application/json
final String callbackBodyType;
///
///
/// [fileType] = 2使
/// API ;
/// 使 <> <>
/// 使[-#persistentOps](https://developer.qiniu.com/kodo/manual/1206/put-policy#persistentOps)
final String persistentOps;
/// URL
///
/// POST HTTP/1.1 200 OK URL
/// URL
/// body Content-Type application/json POST
/// body
final String persistentNotifyUrl;
///
///
///
/// 使使
final String persistentPipeline;
/// [saveKey]
///
/// true [saveKey] Key使 [saveKey]
/// false
final String forceSaveKey;
///
///
/// <><>, [forceSaveKey] false
/// key
/// [forceSaveKey] true
final String saveKey;
/// Byte
final int fsizeMin;
/// Byte
///
/// 413
final int fsizeLimit;
/// MimeType
final int detectMime;
///
final String mimeLimit;
///
///
/// 0
/// 1
/// 2
final int fileType;
const PutPolicy({
@required this.scope,
@required this.deadline,
this.isPrefixalScope,
this.insertOnly,
this.endUser,
this.returnUrl,
this.returnBody,
this.callbackUrl,
this.callbackHost,
this.callbackBody,
this.callbackBodyType,
this.persistentOps,
this.persistentNotifyUrl,
this.persistentPipeline,
this.forceSaveKey,
this.saveKey,
this.fsizeMin,
this.fsizeLimit,
this.detectMime,
this.mimeLimit,
this.fileType,
}) : assert(scope != null),
assert(deadline != null);
Map<String, dynamic> toJson() {
return <String, dynamic>{
'scope': scope,
'isPrefixalScope': isPrefixalScope,
'deadline': deadline,
'insertOnly': insertOnly,
'endUser': endUser,
'returnUrl': returnUrl,
'returnBody': returnBody,
'callbackUrl': callbackUrl,
'callbackHost': callbackHost,
'callbackBody': callbackBody,
'callbackBodyType': callbackBodyType,
'persistentOps': persistentOps,
'persistentNotifyUrl': persistentNotifyUrl,
'persistentPipeline': persistentPipeline,
'forceSaveKey': forceSaveKey,
'saveKey': saveKey,
'fsizeMin': fsizeMin,
'fsizeLimit': fsizeLimit,
'detectMime': detectMime,
'mimeLimit': mimeLimit,
'fileType': fileType,
}..removeWhere((key, dynamic value) => value == null);
}
factory PutPolicy.fromJson(Map<String, dynamic> json) {
return PutPolicy(
scope: json['scope'] as String,
deadline: json['deadline'] as int,
isPrefixalScope: json['isPrefixalScope'] as int,
insertOnly: json['insertOnly'] as int,
endUser: json['endUser'] as String,
returnUrl: json['returnUrl'] as String,
returnBody: json['returnBody'] as String,
callbackUrl: json['callbackUrl'] as String,
callbackHost: json['callbackHost'] as String,
callbackBody: json['callbackBody'] as String,
callbackBodyType: json['callbackBodyType'] as String,
persistentOps: json['persistentOps'] as String,
persistentNotifyUrl: json['persistentNotifyUrl'] as String,
persistentPipeline: json['persistentPipeline'] as String,
forceSaveKey: json['forceSaveKey'] as String,
saveKey: json['saveKey'] as String,
fsizeMin: json['fsizeMin'] as int,
fsizeLimit: json['fsizeLimit'] as int,
detectMime: json['detectMime'] as int,
mimeLimit: json['mimeLimit'] as String,
fileType: json['fileType'] as int,
);
}
}

12
qiniu-dart-sdk/base/lib/src/error/error.dart

@ -0,0 +1,12 @@
class QiniuError extends Error {
final Error rawError;
final String _message;
String get message => _message ?? rawError?.toString() ?? '';
@override
StackTrace get stackTrace => rawError?.stackTrace ?? super.stackTrace;
QiniuError({this.rawError, String message}) : _message = message;
}

39
qiniu-dart-sdk/base/lib/src/storage/config/cache.dart

@ -0,0 +1,39 @@
part of 'config.dart';
abstract class CacheProvider {
///
Future setItem(String key, String item);
/// key
Future<String> getItem(String key);
/// key
Future removeItem(String key);
///
Future clear();
}
class DefaultCacheProvider extends CacheProvider {
Map<String, String> value = {};
@override
Future clear() async {
value.clear();
}
@override
Future<String> getItem(String key) async {
return value[key];
}
@override
Future removeItem(String key) async {
value.remove(key);
}
@override
Future setItem(String key, String item) async {
value[key] = item;
}
}

28
qiniu-dart-sdk/base/lib/src/storage/config/config.dart

@ -0,0 +1,28 @@
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:qiniu_sdk_base/src/storage/error/error.dart';
part 'protocol.dart';
part 'host.dart';
part 'cache.dart';
class Config {
final HostProvider hostProvider;
final CacheProvider cacheProvider;
final HttpClientAdapter httpClientAdapter;
///
///
///
final int retryLimit;
Config({
HostProvider hostProvider,
CacheProvider cacheProvider,
HttpClientAdapter httpClientAdapter,
this.retryLimit = 3,
}) : hostProvider = hostProvider ?? DefaultHostProvider(),
cacheProvider = cacheProvider ?? DefaultCacheProvider(),
httpClientAdapter = httpClientAdapter ?? DefaultHttpClientAdapter();
}

124
qiniu-dart-sdk/base/lib/src/storage/config/host.dart

@ -0,0 +1,124 @@
part of 'config.dart';
abstract class HostProvider {
Future<String> getUpHost({
@required String accessKey,
@required String bucket,
});
bool isFrozen(String host);
void freezeHost(String host);
}
class DefaultHostProvider extends HostProvider {
final protocol = Protocol.Https.value;
final _http = Dio();
//
final _stashedUpDomains = <_Domain>[];
// accessKey:bucket key up host
String _cacheKey;
//
final List<_Domain> _frozenUpDomains = [];
@override
Future<String> getUpHost({
@required String accessKey,
@required String bucket,
}) async {
// host
_frozenUpDomains.removeWhere((domain) => !domain.isFrozen());
var _upDomains = <_Domain>[];
if ('$accessKey:$bucket' == _cacheKey && _stashedUpDomains.isNotEmpty) {
_upDomains.addAll(_stashedUpDomains);
} else {
final url =
'$protocol://api.qiniu.com/v4/query?ak=$accessKey&bucket=$bucket';
final res = await _http.get<Map>(url);
final hosts = res.data['hosts']
.map((dynamic json) => _Host.fromJson(json as Map))
.cast<_Host>()
.toList() as List<_Host>;
for (var host in hosts) {
final domainList = host.up['domains'].cast<String>() as List<String>;
final domains = domainList.map((domain) => _Domain(domain));
_upDomains.addAll(domains);
}
_cacheKey = '$accessKey:$bucket';
_stashedUpDomains.addAll(_upDomains);
}
// host
for (var index = 0; index < _upDomains.length; index++) {
final availableDomain = _upDomains.elementAt(index);
// host
final frozen = isFrozen(protocol + '://' + availableDomain.value);
if (!frozen) {
return protocol + '://' + availableDomain.value;
}
}
//
throw StorageError(
type: StorageErrorType.NO_AVAILABLE_HOST,
message: '没有可用的上传域名',
);
}
@override
bool isFrozen(String host) {
final uri = Uri.parse(host);
final frozenDomain = _frozenUpDomains.firstWhere(
(domain) => domain.isFrozen() && domain.value == uri.host,
orElse: () => null);
return frozenDomain != null;
}
@override
void freezeHost(String host) {
// http://example.org
// scheme: http
// host: example.org
final uri = Uri.parse(host);
_frozenUpDomains.add(_Domain(uri.host)..freeze());
}
}
class _Host {
String region;
int ttl;
// domains: []
Map<String, dynamic> up;
_Host({this.region, this.ttl, this.up});
factory _Host.fromJson(Map json) {
return _Host(
region: json['region'] as String,
ttl: json['ttl'] as int,
up: json['up'] as Map<String, dynamic>,
);
}
}
class _Domain {
int frozenTime = 0;
final _lockTime = 1000 * 60 * 10;
bool isFrozen() {
return frozenTime + _lockTime > DateTime.now().millisecondsSinceEpoch;
}
void freeze() {
frozenTime = DateTime.now().millisecondsSinceEpoch;
}
String value;
_Domain(this.value);
}

11
qiniu-dart-sdk/base/lib/src/storage/config/protocol.dart

@ -0,0 +1,11 @@
part of 'config.dart';
enum Protocol { Http, Https }
extension ProtocolExt on Protocol {
String get value {
if (this == Protocol.Http) return 'http';
if (this == Protocol.Https) return 'https';
return 'https';
}
}

75
qiniu-dart-sdk/base/lib/src/storage/error/error.dart

@ -0,0 +1,75 @@
import 'package:dio/dio.dart';
import 'package:qiniu_sdk_base/src/error/error.dart';
enum StorageErrorType {
///
CONNECT_TIMEOUT,
///
SEND_TIMEOUT,
///
RECEIVE_TIMEOUT,
/// 400
RESPONSE,
///
CANCEL,
///
NO_AVAILABLE_HOST,
///
IN_PROGRESS,
///
UNKNOWN,
}
class StorageError extends QiniuError {
/// [type] [StorageErrorType.RESPONSE] null
final int code;
final StorageErrorType type;
StorageError({this.type, this.code, Error rawError, String message})
: super(rawError: rawError, message: message);
factory StorageError.fromError(Error error) {
return StorageError(type: StorageErrorType.UNKNOWN, rawError: error);
}
factory StorageError.fromDioError(DioError error) {
return StorageError(
type: _mapDioErrorType(error.type),
code: error.response?.statusCode,
message: error.response?.data.toString(),
rawError: error.error is Error ? (error.error as Error) : null,
);
}
@override
String toString() {
var msg = 'StorageError [$type, $code]: $message';
msg += '\n$stackTrace';
return msg;
}
}
StorageErrorType _mapDioErrorType(DioErrorType type) {
switch (type) {
case DioErrorType.CONNECT_TIMEOUT:
return StorageErrorType.CONNECT_TIMEOUT;
case DioErrorType.SEND_TIMEOUT:
return StorageErrorType.SEND_TIMEOUT;
case DioErrorType.RECEIVE_TIMEOUT:
return StorageErrorType.RECEIVE_TIMEOUT;
case DioErrorType.RESPONSE:
return StorageErrorType.RESPONSE;
case DioErrorType.CANCEL:
return StorageErrorType.CANCEL;
case DioErrorType.DEFAULT:
default:
return StorageErrorType.UNKNOWN;
}
}

20
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/cache_mixin.dart

@ -0,0 +1,20 @@
part of 'put_parts_task.dart';
/// mixin
///
///
mixin CacheMixin<T> on RequestTask<T> {
String get _cacheKey;
Future clearCache() async {
await config.cacheProvider.removeItem(_cacheKey);
}
Future setCache(String data) async {
await config.cacheProvider.setItem(_cacheKey, data);
}
Future<String> getCache() async {
return await config.cacheProvider.getItem(_cacheKey);
}
}

51
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/complete_parts_task.dart

@ -0,0 +1,51 @@
part of 'put_parts_task.dart';
///
class CompletePartsTask extends RequestTask<PutResponse> {
final String token;
final String uploadId;
final List<Part> parts;
final String key;
TokenInfo _tokenInfo;
CompletePartsTask({
@required this.token,
@required this.uploadId,
@required this.parts,
this.key,
PutController controller,
}) : super(controller: controller);
@override
void preStart() {
_tokenInfo = Auth.parseUpToken(token);
super.preStart();
}
@override
Future<PutResponse> createTask() async {
final bucket = _tokenInfo.putPolicy.getBucket();
final host = await config.hostProvider.getUpHost(
bucket: bucket,
accessKey: _tokenInfo.accessKey,
);
final headers = <String, dynamic>{'Authorization': 'UpToken $token'};
final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~';
final paramUrl =
'$host/buckets/$bucket/objects/$encodedKey/uploads/$uploadId';
final response = await client.post<Map<String, dynamic>>(
paramUrl,
data: {
'parts': parts
..sort((a, b) => a.partNumber - b.partNumber)
..map((part) => part.toJson()).toList()
},
options: Options(headers: headers),
);
return PutResponse.fromJson(response.data);
}
}

92
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/init_parts_task.dart

@ -0,0 +1,92 @@
part of 'put_parts_task.dart';
/// initParts
class InitParts {
final int expireAt;
final String uploadId;
InitParts({
@required this.expireAt,
@required this.uploadId,
});
factory InitParts.fromJson(Map json) {
return InitParts(
uploadId: json['uploadId'] as String,
expireAt: json['expireAt'] as int,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'uploadId': uploadId,
'expireAt': expireAt,
};
}
}
/// [UploadPartsTask] uploadId
class InitPartsTask extends RequestTask<InitParts> with CacheMixin<InitParts> {
final File file;
final String token;
final String key;
@override
String _cacheKey;
TokenInfo _tokenInfo;
InitPartsTask({
@required this.file,
@required this.token,
this.key,
PutController controller,
}) : super(controller: controller);
static String getCacheKey(String path, int length, String key) {
return 'qiniu_dart_sdk_init_parts_task_${path}_key_${key}_size_$length';
}
@override
void preStart() {
_tokenInfo = Auth.parseUpToken(token);
_cacheKey = InitPartsTask.getCacheKey(file.path, file.lengthSync(), key);
super.preStart();
}
@override
Future<InitParts> createTask() async {
final headers = {'Authorization': 'UpToken $token'};
final initPartsCache = await getCache();
if (initPartsCache != null) {
return InitParts.fromJson(
json.decode(initPartsCache) as Map<String, dynamic>);
}
final bucket = _tokenInfo.putPolicy.getBucket();
final host = await config.hostProvider.getUpHost(
bucket: bucket,
accessKey: _tokenInfo.accessKey,
);
final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~';
final paramUrl = '$host/buckets/$bucket/objects/$encodedKey/uploads';
final response = await client.post<Map<String, dynamic>>(
paramUrl,
/// data dio cancel
data: <String, dynamic>{},
options: Options(headers: headers),
);
return InitParts.fromJson(response.data);
}
@override
void postReceive(data) async {
await setCache(json.encode(data.toJson()));
super.postReceive(data);
}
}

26
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/part.dart

@ -0,0 +1,26 @@
part of 'put_parts_task.dart';
///
class Part {
final String etag;
final int partNumber;
Part({
@required this.etag,
@required this.partNumber,
});
factory Part.fromJson(Map<String, dynamic> json) {
return Part(
etag: json['etag'] as String,
partNumber: json['partNumber'] as int,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'etag': etag,
'partNumber': partNumber,
};
}
}

25
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_by_part_options.dart

@ -0,0 +1,25 @@
import '../put_controller.dart';
class PutByPartOptions {
///
///
final String key;
/// MB
///
/// [partSize] [partSize]
/// 4MB 1MB 1024 MB
final int partSize;
final int maxPartsRequestNumber;
///
final PutController controller;
const PutByPartOptions({
this.key,
this.partSize = 4,
this.maxPartsRequestNumber = 5,
this.controller,
});
}

187
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/put_parts_task.dart

@ -0,0 +1,187 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:qiniu_sdk_base/qiniu_sdk_base.dart';
part 'cache_mixin.dart';
part 'complete_parts_task.dart';
part 'init_parts_task.dart';
part 'part.dart';
part 'upload_part_task.dart';
part 'upload_parts_task.dart';
///
class PutByPartTask extends RequestTask<PutResponse> {
final File file;
final String token;
final int partSize;
final int maxPartsRequestNumber;
final String key;
/// 0 [PutByPartTask]
@override
int get retryLimit => 0;
PutByPartTask({
@required this.file,
@required this.token,
@required this.partSize,
@required this.maxPartsRequestNumber,
this.key,
PutController controller,
}) : assert(file != null),
assert(token != null),
assert(partSize != null),
assert(maxPartsRequestNumber != null),
assert(() {
if (partSize < 1 || partSize > 1024) {
throw RangeError.range(partSize, 1, 1024, 'partSize',
'partSize must be greater than 1 and less than 1024');
}
return true;
}()),
super(controller: controller);
RequestTaskController _currentWorkingTaskController;
@override
void preStart() {
super.preStart();
//
final sameTaskExsist = manager.getTasks().firstWhere(
(element) => element is PutByPartTask && isEquals(element),
orElse: () => null,
);
if (sameTaskExsist != null) {
throw StorageError(
type: StorageErrorType.IN_PROGRESS,
message: '$file 已在上传队列中',
);
}
// controller
controller?.cancelToken?.whenCancel?.then((_) {
_currentWorkingTaskController?.cancel();
});
}
@override
void postReceive(PutResponse data) {
_currentWorkingTaskController = null;
super.postReceive(data);
}
@override
Future<PutResponse> createTask() async {
controller?.notifyStatusListeners(StorageStatus.Request);
final initPartsTask = _createInitParts();
final initParts = await initPartsTask.future;
//
controller?.notifyProgressListeners(0.002);
final uploadParts = _createUploadParts(initParts.uploadId);
PutResponse putResponse;
try {
final parts = await uploadParts.future;
putResponse =
await _createCompleteParts(initParts.uploadId, parts).future;
} catch (error) {
// initPartsTask uploadParts postError
if (error is StorageError) {
///
/// 1
/// 2 uploadId 400
if (error.code == 612 || error.code == 400) {
await initPartsTask.clearCache();
await uploadParts.clearCache();
}
///
if (error.code == 612) {
controller?.notifyStatusListeners(StorageStatus.Retry);
return createTask();
}
}
rethrow;
}
///
await initPartsTask.clearCache();
await uploadParts.clearCache();
return putResponse;
}
bool isEquals(PutByPartTask target) {
return target.file.path == file.path &&
target.key == key &&
target.file.lengthSync() == file.lengthSync();
}
///
InitPartsTask _createInitParts() {
final _controller = PutController();
final task = InitPartsTask(
file: file,
token: token,
key: key,
controller: _controller,
);
manager.addTask(task);
_currentWorkingTaskController = _controller;
return task;
}
UploadPartsTask _createUploadParts(String uploadId) {
final _controller = PutController();
final task = UploadPartsTask(
file: file,
token: token,
partSize: partSize,
uploadId: uploadId,
maxPartsRequestNumber: maxPartsRequestNumber,
key: key,
controller: _controller,
);
_controller.addSendProgressListener(onSendProgress);
manager.addTask(task);
_currentWorkingTaskController = _controller;
return task;
}
///
CompletePartsTask _createCompleteParts(
String uploadId,
List<Part> parts,
) {
final _controller = PutController();
final task = CompletePartsTask(
token: token,
uploadId: uploadId,
parts: parts,
key: key,
controller: _controller,
);
manager.addTask(task);
_currentWorkingTaskController = _controller;
return task;
}
}

112
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_part_task.dart

@ -0,0 +1,112 @@
part of 'put_parts_task.dart';
// part
class UploadPartTask extends RequestTask<UploadPart> {
final String token;
final String uploadId;
final RandomAccessFile raf;
final int partSize;
// data Stream Dio content-length onSendProgress
// https://github.com/flutterchina/dio/blob/21136168ab39a7536835c7a59ce0465bb05feed4/dio/lib/src/dio.dart#L1000
final int byteLength;
final int partNumber;
final String key;
TokenInfo _tokenInfo;
UploadPartTask({
@required this.token,
@required this.raf,
@required this.uploadId,
@required this.byteLength,
@required this.partNumber,
@required this.partSize,
this.key,
PutController controller,
}) : super(controller: controller);
@override
void preStart() {
_tokenInfo = Auth.parseUpToken(token);
super.preStart();
}
@override
void postReceive(data) {
controller?.notifyProgressListeners(1);
super.postReceive(data);
}
@override
Future<UploadPart> createTask() async {
final headers = <String, dynamic>{
'Authorization': 'UpToken $token',
Headers.contentLengthHeader: byteLength,
};
final bucket = _tokenInfo.putPolicy.getBucket();
final host = await config.hostProvider.getUpHost(
bucket: bucket,
accessKey: _tokenInfo.accessKey,
);
final encodedKey = key != null ? base64Url.encode(utf8.encode(key)) : '~';
final paramUrl = 'buckets/$bucket/objects/$encodedKey';
final response = await client.put<Map<String, dynamic>>(
'$host/$paramUrl/uploads/$uploadId/$partNumber',
data: Stream.fromIterable([_readFileByPartNumber(partNumber)]),
// data stream interceptor cancelToken bug
cancelToken: controller.cancelToken,
options: Options(headers: headers),
);
return UploadPart.fromJson(response.data);
}
// File 4m(穿 File )
// 21m 4 * 5
// 90% 100%
// onSendProgress onSendProgress Progress
// ( postReceive)
@override
void onSendProgress(double percent) {
controller?.notifySendProgressListeners(percent);
}
// partNumber
List<int> _readFileByPartNumber(int partNumber) {
final startOffset = (partNumber - 1) * partSize * 1024 * 1024;
raf.setPositionSync(startOffset);
return raf.readSync(byteLength);
}
}
// uploadPart
class UploadPart {
final String md5;
final String etag;
UploadPart({
@required this.md5,
@required this.etag,
});
factory UploadPart.fromJson(Map json) {
return UploadPart(
md5: json['md5'] as String,
etag: json['etag'] as String,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'etag': etag,
'md5': md5,
};
}
}

260
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_part/upload_parts_task.dart

@ -0,0 +1,260 @@
part of 'put_parts_task.dart';
// parts [CompletePartsTask] [Part]
class UploadPartsTask extends RequestTask<List<Part>> with CacheMixin {
final File file;
final String token;
final String uploadId;
final int partSize;
final int maxPartsRequestNumber;
final String key;
@override
String _cacheKey;
/// 0 [UploadPartsTask]
@override
int get retryLimit => 0;
// bytes
int _fileByteLength;
//
//
//
int _partByteLength;
//
int _totalPartCount;
// part
final Map<int, Part> _uploadedPartMap = {};
// UploadPartTask
final List<RequestTaskController> _workingUploadPartTaskControllers = [];
//
int _sentPartCount = 0;
//
int _sentPartToServerCount = 0;
//
int _idleRequestNumber;
RandomAccessFile _raf;
UploadPartsTask({
@required this.file,
@required this.token,
@required this.uploadId,
@required this.partSize,
@required this.maxPartsRequestNumber,
this.key,
PutController controller,
}) : super(controller: controller);
static String getCacheKey(
String path,
int length,
int partSize,
String key,
) {
final keyList = [
'key/$key',
'path/$path',
'file_size/$length',
'part_size/$partSize',
];
return 'qiniu_dart_sdk_upload_parts_task@[${keyList..join("/")}]';
}
@override
void preStart() {
// controller
controller?.cancelToken?.whenCancel?.then((_) {
for (final controller in _workingUploadPartTaskControllers) {
controller.cancel();
}
});
_fileByteLength = file.lengthSync();
_partByteLength = partSize * 1024 * 1024;
_idleRequestNumber = maxPartsRequestNumber;
_totalPartCount = (_fileByteLength / _partByteLength).ceil();
_cacheKey = getCacheKey(file.path, _fileByteLength, partSize, key);
// UploadPartTask file open
// close file stream close open stream
//
// bytes
_raf = file.openSync();
super.preStart();
}
@override
void postReceive(data) async {
await _raf.close();
super.postReceive(data);
}
@override
void postError(Object error) async {
await _raf.close();
//
await storeUploadedPart();
super.postError(error);
}
Future storeUploadedPart() async {
if (_uploadedPartMap.isEmpty) {
return;
}
await setCache(jsonEncode(_uploadedPartMap.values.toList()));
}
// part
Future recoverUploadedPart() async {
//
final cachedData = await getCache();
//
if (cachedData != null) {
var cachedList = <Part>[];
try {
final _cachedList = json.decode(cachedData) as List<dynamic>;
cachedList = _cachedList
.map((dynamic item) => Part.fromJson(item as Map<String, dynamic>))
.toList();
} catch (error) {
rethrow;
}
for (final part in cachedList) {
_uploadedPartMap[part.partNumber] = part;
}
}
}
@override
Future<List<Part>> createTask() async {
///
// ignore: null_aware_in_condition
if (controller != null && controller.cancelToken.isCancelled) {
throw StorageError(type: StorageErrorType.CANCEL);
}
controller.notifyStatusListeners(StorageStatus.Request);
//
await recoverUploadedPart();
//
await _uploadParts();
return _uploadedPartMap.values.toList();
}
int _uploadingPartIndex = 0;
//
Future<void> _uploadParts() async {
final tasksLength =
min(_idleRequestNumber, _totalPartCount - _uploadingPartIndex);
final taskFutures = <Future<Null>>[];
while (taskFutures.length < tasksLength &&
_uploadingPartIndex < _totalPartCount) {
// partNumber 1
final partNumber = ++_uploadingPartIndex;
final _uploadedPart = _uploadedPartMap[partNumber];
if (_uploadedPart != null) {
_sentPartCount++;
_sentPartToServerCount++;
notifySendProgress();
notifyProgress();
continue;
}
final future = _createUploadPartTaskFutureByPartNumber(partNumber);
taskFutures.add(future);
}
await Future.wait<Null>(taskFutures);
}
Future<Null> _createUploadPartTaskFutureByPartNumber(int partNumber) async {
// (part)
final _byteLength = _getPartSizeByPartNumber(partNumber);
_idleRequestNumber--;
final _controller = PutController();
_workingUploadPartTaskControllers.add(_controller);
final task = UploadPartTask(
token: token,
raf: _raf,
uploadId: uploadId,
byteLength: _byteLength,
partNumber: partNumber,
partSize: partSize,
key: key,
controller: _controller,
);
_controller
// UploadPartTask chunk
..addSendProgressListener((percent) {
_sentPartCount++;
notifySendProgress();
})
// UploadPartTask
..addProgressListener((percent) {
_sentPartToServerCount++;
notifyProgress();
});
manager.addTask(task);
final data = await task.future;
_idleRequestNumber++;
_uploadedPartMap[partNumber] =
Part(partNumber: partNumber, etag: data.etag);
_workingUploadPartTaskControllers.remove(_controller);
await storeUploadedPart();
//
if (_uploadedPartMap.length != _totalPartCount) {
//
await _uploadParts();
}
}
// partNumber byte
int _getPartSizeByPartNumber(int partNumber) {
final startOffset = (partNumber - 1) * _partByteLength;
if (partNumber == _totalPartCount) {
return _fileByteLength - startOffset;
}
return _partByteLength;
}
void notifySendProgress() {
controller?.notifySendProgressListeners(_sentPartCount / _totalPartCount);
}
void notifyProgress() {
controller?.notifyProgressListeners(_sentPartToServerCount /
_totalPartCount *
RequestTask.onSendProgressTakePercentOfTotal);
}
// UploadPartsTask
@override
void onSendProgress(double percent) {}
}

13
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_options.dart

@ -0,0 +1,13 @@
import '../put_controller.dart';
class PutBySingleOptions {
///
///
///
final String key;
///
final PutController controller;
const PutBySingleOptions({this.key, this.controller});
}

60
qiniu-dart-sdk/base/lib/src/storage/methods/put/by_single/put_by_single_task.dart

@ -0,0 +1,60 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:qiniu_sdk_base/src/storage/task/task.dart';
import '../../../../auth/auth.dart';
import '../put_response.dart';
//
class PutBySingleTask extends RequestTask<PutResponse> {
///
final File file;
///
final String token;
///
///
final String key;
TokenInfo _tokenInfo;
PutBySingleTask({
@required this.file,
@required this.token,
this.key,
RequestTaskController controller,
}) : assert(file != null),
assert(token != null),
super(controller: controller);
@override
void preStart() {
_tokenInfo = Auth.parseUpToken(token);
super.preStart();
}
@override
Future<PutResponse> createTask() async {
final formData = FormData.fromMap(<String, dynamic>{
'file': await MultipartFile.fromFile(file.path),
'token': token,
'key': key,
});
final host = await config.hostProvider.getUpHost(
accessKey: _tokenInfo.accessKey,
bucket: _tokenInfo.putPolicy.getBucket(),
);
final response = await client.post<Map<String, dynamic>>(
host,
data: formData,
cancelToken: controller?.cancelToken,
);
return PutResponse.fromJson(response.data);
}
}

5
qiniu-dart-sdk/base/lib/src/storage/methods/put/put.dart

@ -0,0 +1,5 @@
export 'by_part/put_by_part_options.dart';
export 'by_single/put_by_single_options.dart';
export 'put_controller.dart';
export 'put_options.dart';
export 'put_response.dart';

3
qiniu-dart-sdk/base/lib/src/storage/methods/put/put_controller.dart

@ -0,0 +1,3 @@
import '../../task/request_task.dart';
class PutController extends RequestTaskController {}

28
qiniu-dart-sdk/base/lib/src/storage/methods/put/put_options.dart

@ -0,0 +1,28 @@
import 'put_controller.dart';
class PutOptions {
///
///
///
final String key;
/// 使使 false
final bool forceBySingle;
/// 使 4 MB
final int partSize;
/// 5
final int maxPartsRequestNumber;
///
final PutController controller;
const PutOptions({
this.key,
this.forceBySingle = false,
this.partSize = 4,
this.maxPartsRequestNumber = 5,
this.controller,
});
}

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

Loading…
Cancel
Save