17 Commits

Author SHA1 Message Date
wuxinglong a1b98e987e fix(upload_image): 修改媒体上传文件 2026-05-29 18:26:16 +08:00
wuxinglong 037f14b52f fix(upload_image): 修改媒体上传文件 2026-05-29 17:58:44 +08:00
wuxinglong aa89e03bee feat: add video batch upload queue 2026-05-29 16:17:26 +08:00
wuxinglong 1c971fb0b2 fix:优化图片上传组件,解决png图片报错问题 2026-05-12 11:35:21 +08:00
wangliankun aa47bc97d5 feat(http):修改请求结果 2026-05-07 18:05:51 +08:00
wangliankun 0b45f6a6c1 Merge remote-tracking branch 'origin/main' 2026-05-07 14:19:36 +08:00
wangliankun 7f077cf513 feat(http):修改请求结果 2026-05-07 13:59:16 +08:00
wuxinglong 8ba36d9700 Merge remote-tracking branch 'origin/main' 2026-04-28 15:29:25 +08:00
wuxinglong b380daf206 fix(upload_image): 修改图片显示组件默认显示清晰度 2026-04-28 15:26:58 +08:00
wangliankun 89dbb79256 Merge remote-tracking branch 'origin/main' 2026-04-28 10:45:54 +08:00
wangliankun 79eb8a891b feat(http):修改请求结果 2026-04-28 10:45:44 +08:00
wuxinglong 0c7fbe5897 fix(upload_image): 修改图片显示组件默认显示清晰度 2026-04-24 16:53:25 +08:00
wuxinglong de127eeef5 fix(upload_image): 优化图片显示组件 2026-04-24 16:29:09 +08:00
wangliankun 01178bc033 Merge remote-tracking branch 'origin/main' 2026-04-22 11:36:08 +08:00
wuxinglong 724e31ad7d fix(upload_image): 请求组件新增登录被挤弹窗 2026-04-17 19:36:08 +08:00
wuxinglong e6ea8182e4 Merge remote-tracking branch 'origin/main' 2026-04-16 17:13:13 +08:00
wuxinglong c585d5ce0d fix(upload_image): 图片组件新增圆角 2026-04-16 17:12:56 +08:00
19 changed files with 1679 additions and 295 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+6
View File
@@ -63,6 +63,12 @@
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/volume_controller/example/.dart_tool" /> <excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/volume_controller/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/volume_controller/example/.pub" /> <excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/volume_controller/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/volume_controller/example/build" /> <excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/volume_controller/example/build" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/jni/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/jni/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/jni/build" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/jni/example/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/jni/example/.pub" />
<excludeFolder url="file://$MODULE_DIR$/example/linux/flutter/ephemeral/.plugin_symlinks/jni/example/build" />
</content> </content>
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
+8 -8
View File
@@ -271,7 +271,7 @@
<entry key="flutter"> <entry key="flutter">
<value> <value>
<list> <list>
<option value="$USER_HOME$/flutter/packages/flutter/lib" /> <option value="$PROJECT_DIR$/../../flutter/packages/flutter/lib" />
</list> </list>
</value> </value>
</entry> </entry>
@@ -425,14 +425,14 @@
<entry key="flutter_test"> <entry key="flutter_test">
<value> <value>
<list> <list>
<option value="$USER_HOME$/flutter/packages/flutter_test/lib" /> <option value="$PROJECT_DIR$/../../flutter/packages/flutter_test/lib" />
</list> </list>
</value> </value>
</entry> </entry>
<entry key="flutter_web_plugins"> <entry key="flutter_web_plugins">
<value> <value>
<list> <list>
<option value="$USER_HOME$/flutter/packages/flutter_web_plugins/lib" /> <option value="$PROJECT_DIR$/../../flutter/packages/flutter_web_plugins/lib" />
</list> </list>
</value> </value>
</entry> </entry>
@@ -1020,7 +1020,7 @@
<entry key="sky_engine"> <entry key="sky_engine">
<value> <value>
<list> <list>
<option value="$USER_HOME$/flutter/bin/cache/pkg/sky_engine/lib" /> <option value="$PROJECT_DIR$/../../flutter/bin/cache/pkg/sky_engine/lib" />
</list> </list>
</value> </value>
</entry> </entry>
@@ -1513,10 +1513,10 @@
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-1.1.0/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/xdg_directories-1.1.0/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/xml-6.6.1/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/xml-6.6.1/lib" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/yaml-3.1.3/lib" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/yaml-3.1.3/lib" />
<root url="file://$USER_HOME$/flutter/bin/cache/pkg/sky_engine/lib" /> <root url="file://$PROJECT_DIR$/../../flutter/bin/cache/pkg/sky_engine/lib" />
<root url="file://$USER_HOME$/flutter/packages/flutter/lib" /> <root url="file://$PROJECT_DIR$/../../flutter/packages/flutter/lib" />
<root url="file://$USER_HOME$/flutter/packages/flutter_test/lib" /> <root url="file://$PROJECT_DIR$/../../flutter/packages/flutter_test/lib" />
<root url="file://$USER_HOME$/flutter/packages/flutter_web_plugins/lib" /> <root url="file://$PROJECT_DIR$/../../flutter/packages/flutter_web_plugins/lib" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES /> <SOURCES />
+53 -53
View File
@@ -1,62 +1,62 @@
<component name="libraryTable"> <component name="libraryTable">
<library name="Flutter Plugins" type="FlutterPluginsLibraryType"> <library name="Flutter Plugins" type="FlutterPluginsLibraryType">
<CLASSES> <CLASSES>
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_gallery_saver_plus-4.0.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_linux-3.2.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.3.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_foundation-2.6.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_linux-0.2.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_windows-2.4.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_windows-0.6.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player_web-2.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/screen_brightness-2.1.7" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/device_info_plus-11.5.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker-1.2.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_for_web-3.1.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/permission_handler-11.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview-6.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/file_selector_linux-0.9.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_android-1.1.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_windows-3.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/sqflite-2.4.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/file_selector_windows-0.9.3+5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-2.4.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_macos-3.2.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_web-2.4.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_windows-0.2.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_foundation-2.5.6" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-6.3.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_web-1.1.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.2.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/permission_handler_html-0.1.3+5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_macos-1.1.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_macos-0.2.2+1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/file_selector_macos-0.9.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/sqflite_darwin-2.4.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_ios-1.1.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_linux-2.4.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_android-0.8.13+16" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_android-0.8.13+16" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-2.5.5" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/screen_brightness-2.1.7" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_android-6.3.29" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_web-0.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_ios-6.4.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_macos-1.0.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/sqflite_android-2.4.2+3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player-2.11.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_common-1.0.6" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_android-2.3.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player_avfoundation-2.9.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_ohos-0.0.3" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_ohos-0.0.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player_android-2.9.5" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_android-2.3.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/jni_flutter-1.0.1" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player_web-2.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/package_info_plus-9.0.1" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/file_selector_macos-0.9.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/photo_manager-3.9.0" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview-6.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_android-2.4.23" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_ios-1.1.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/jni-1.0.0" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_web-0.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress-2.4.0" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_web-2.4.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_ios-0.8.13+6" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/wakelock_plus-1.5.2" /> <root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/wakelock_plus-1.5.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/package_info_plus-9.0.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_linux-3.2.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/file_selector_windows-0.9.3+5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_common-1.0.6" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/device_info_plus-11.5.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_linux-0.2.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_ios-0.8.13+6" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/photo_manager-3.9.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_windows-2.3.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_macos-1.1.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_foundation-2.5.6" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_android-1.1.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_web-1.1.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player-2.11.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_macos-3.2.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_android-2.4.23" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider-2.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_windows-0.2.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_foundation-2.6.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_gallery_saver_plus-4.0.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/permission_handler_html-0.1.3+5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_for_web-3.1.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher-6.3.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_windows-2.4.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_windows-3.1.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress_macos-1.0.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_web-2.4.3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker_macos-0.2.2+1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences_linux-2.4.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/sqflite-2.4.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/jni-1.0.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_ios-6.4.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/shared_preferences-2.5.5" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/sqflite_android-2.4.2+3" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/image_picker-1.2.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/path_provider_linux-2.2.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/url_launcher_android-6.3.29" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player_avfoundation-2.9.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/jni_flutter-1.0.1" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/permission_handler-11.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/file_selector_linux-0.9.4" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_inappwebview_windows-0.6.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/flutter_image_compress-2.4.0" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/sqflite_darwin-2.4.2" />
<root url="file://$USER_HOME$/.pub-cache/hosted/pub.flutter-io.cn/video_player_android-2.9.5" />
</CLASSES> </CLASSES>
<JAVADOC /> <JAVADOC />
<SOURCES /> <SOURCES />
+1
View File
@@ -3,4 +3,5 @@ export 'calendarcalendar/custom_calendar_range_picker_widget.dart';
export 'calendarcalendar/custom_date_picker.dart'; export 'calendarcalendar/custom_date_picker.dart';
export 'upload_image/look_images_widget.dart'; export 'upload_image/look_images_widget.dart';
export 'upload_image/upload_images_tool.dart'; export 'upload_image/upload_images_tool.dart';
export 'upload_video/video_batch_upload.dart';
export 'utils/date_utils.dart'; export 'utils/date_utils.dart';
+15 -1
View File
@@ -48,6 +48,10 @@ class HttpUtils {
name: 'Response Text: ', name: 'Response Text: ',
); );
} }
Map<String, dynamic> map = response.data;
if (map.keys.contains('code') && map['code'] == 401) {
response.data = {'resultCode': 'UNLOGIN', 'resultMsg': '请重新登录'};
}
/// 对接口抛出异常的检查 /// 对接口抛出异常的检查
_apiVerify(response); _apiVerify(response);
@@ -103,6 +107,12 @@ class HttpUtils {
// value: response.headers["authorization"]![0]); // value: response.headers["authorization"]![0]);
// } // }
if (isShowLoading) EasyLoading.dismiss(); if (isShowLoading) EasyLoading.dismiss();
///转化结果
Map<String, dynamic> map = response.data;
if (map.keys.contains('code') && map['code'] == 401) {
response.data = {'resultCode': 'FAIL', 'resultMsg': '请重新登录'};
}
netResult = NetResult( netResult = NetResult(
headers: response.headers, headers: response.headers,
result: response.data, result: response.data,
@@ -189,6 +199,7 @@ class HttpUtils {
} }
if (resultCode.toUpperCase() == "UNLOGIN") { if (resultCode.toUpperCase() == "UNLOGIN") {
ToastUtils.showToast(msg: map['resultMsg']);
unLoginAction?.call(); unLoginAction?.call();
return; return;
} }
@@ -230,7 +241,10 @@ class HttpUtils {
ToastUtils.showToast(msg: '服务响应超时'); ToastUtils.showToast(msg: '服务响应超时');
break; break;
case DioExceptionType.unknown: case DioExceptionType.unknown:
ToastUtils.showToast(msg: error.message); ToastUtils.showToast(
msg: (error.message == '' || error.message?.isEmpty == true)
? '请重新登录'
: error.message);
break; break;
case DioExceptionType.badResponse: case DioExceptionType.badResponse:
try { try {
+19 -6
View File
@@ -37,8 +37,15 @@ class UploadOss {
required String ossHost, required String ossHost,
bool? isShowLoading, bool? isShowLoading,
}) async { }) async {
final String normalizedFileType =
fileType.toLowerCase() == 'jpeg' ? 'jpg' : fileType.toLowerCase();
final bool isVideo = normalizedFileType == 'mp4';
final String? imageContentType = isVideo
? null
: (normalizedFileType == 'jpg' ? 'jpeg' : normalizedFileType);
// oss的路径和文件名我这里目前设置的是moment/20201229/test.mp4 // oss的路径和文件名我这里目前设置的是moment/20201229/test.mp4
String pathName = "$rootDir/${getDate()}/app-${getRandom(12)}.$fileType"; String pathName =
"$rootDir/${getDate()}/app-${getRandom(12)}.$normalizedFileType";
// form对象 // form对象
FormData formdata = FormData.fromMap({ FormData formdata = FormData.fromMap({
@@ -51,8 +58,8 @@ class UploadOss {
'success_action_status': '200', 'success_action_status': '200',
'file': MultipartFile.fromFileSync( 'file': MultipartFile.fromFileSync(
path, path,
contentType: fileType == 'mp4' ? null : DioMediaType("image", "jpg"), contentType: isVideo ? null : DioMediaType("image", imageContentType!),
filename: "${getRandom(12)}.$fileType", filename: "${getRandom(12)}.$normalizedFileType",
), ),
}); });
if (isShowLoading == true) { if (isShowLoading == true) {
@@ -71,13 +78,19 @@ class UploadOss {
ossHost, ossHost,
data: formdata, data: formdata,
options: Options( options: Options(
contentType: "multipart/form-data;image/jpg", contentType: isVideo
headers: {'Content-Type': 'multipart/form-data;image/jpg'}, ? "multipart/form-data"
: "multipart/form-data;image/$imageContentType",
headers: {
'Content-Type': isVideo
? "multipart/form-data"
: "multipart/form-data;image/$imageContentType",
},
), ),
); );
print("response ===== $response"); print("response ===== $response");
EasyLoading.dismiss(); EasyLoading.dismiss();
if(fileType == 'jpg'){ if (!isVideo && path.contains('compressed_images_')) {
/// ///
File(path).deleteSync(); File(path).deleteSync();
} }
+3
View File
@@ -133,10 +133,13 @@ class _UploadImagesState extends State<UploadImages> {
}) { }) {
return GestureDetector( return GestureDetector(
onTap: () => widget.oneTap?.call(index), onTap: () => widget.oneTap?.call(index),
child: ClipRRect(
borderRadius: BorderRadiusGeometry.circular(20.r),
child: CustomerImagesNetworking( child: CustomerImagesNetworking(
imageUrl: imageUrl, imageUrl: imageUrl,
fit: widget.fit ?? BoxFit.cover, fit: widget.fit ?? BoxFit.cover,
), ),
),
); );
} }
+137 -17
View File
@@ -1,5 +1,4 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_common/upload_image/ossUtil.dart'; import 'package:flutter_common/upload_image/ossUtil.dart';
@@ -12,6 +11,28 @@ import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart'; import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class _CompressedImageFile {
const _CompressedImageFile({
required this.path,
required this.fileType,
});
final String path;
final String fileType;
}
class _ImageCompressionTarget {
const _ImageCompressionTarget({
required this.format,
required this.extension,
required this.fileType,
});
final CompressFormat format;
final String extension;
final String fileType;
}
class UploadImagesTool { class UploadImagesTool {
static uploadImagesTool({ static uploadImagesTool({
String? oSSAccessKeyId, String? oSSAccessKeyId,
@@ -139,7 +160,9 @@ class UploadImagesTool {
) )
: CupertinoActionSheet( : CupertinoActionSheet(
title: const Text('上传图片'), title: const Text('上传图片'),
message: (max == null || max == 0) ? null : Text('请选择上传方式\n相册最多${max}'), message: (max == null || max == 0)
? null
: Text('请选择上传方式\n相册最多$max张'),
actions: isAddOtherWidget != null actions: isAddOtherWidget != null
? <Widget>[ ? <Widget>[
isAddOtherWidget, isAddOtherWidget,
@@ -153,7 +176,8 @@ class UploadImagesTool {
policy: policy ?? '', policy: policy ?? '',
callback: callback ?? '', callback: callback ?? '',
signature: signature ?? '', signature: signature ?? '',
chooseImages: (list) => chooseImages?.call(list), chooseImages: (list) =>
chooseImages?.call(list),
); );
Get.back(); Get.back();
}, },
@@ -169,7 +193,8 @@ class UploadImagesTool {
policy: policy ?? '', policy: policy ?? '',
callback: callback ?? '', callback: callback ?? '',
signature: signature ?? '', signature: signature ?? '',
chooseImages: (list) => chooseImages?.call(list), chooseImages: (list) =>
chooseImages?.call(list),
); );
Get.back(); Get.back();
}, },
@@ -186,7 +211,8 @@ class UploadImagesTool {
policy: policy ?? '', policy: policy ?? '',
callback: callback ?? '', callback: callback ?? '',
signature: signature ?? '', signature: signature ?? '',
chooseImages: (list) => chooseImages?.call(list), chooseImages: (list) =>
chooseImages?.call(list),
); );
Get.back(); Get.back();
}, },
@@ -203,7 +229,8 @@ class UploadImagesTool {
callback: callback ?? '', callback: callback ?? '',
signature: signature ?? '', signature: signature ?? '',
isShowLoading: isShowLoading, isShowLoading: isShowLoading,
chooseImages: (list) => chooseImages?.call(list), chooseImages: (list) =>
chooseImages?.call(list),
); );
Get.back(); Get.back();
}, },
@@ -234,8 +261,7 @@ class UploadImagesTool {
source: ImageSource.camera, source: ImageSource.camera,
maxHeight: 1080, maxHeight: 1080,
maxWidth: 1920, maxWidth: 1920,
imageQuality: 80 imageQuality: 80);
);
if (file == null) { if (file == null) {
Get.back(); Get.back();
} else { } else {
@@ -267,7 +293,8 @@ class UploadImagesTool {
if (isVideo == true) { if (isVideo == true) {
final List<AssetEntity>? result = await AssetPicker.pickAssets( final List<AssetEntity>? result = await AssetPicker.pickAssets(
Get.context!, Get.context!,
pickerConfig: AssetPickerConfig(maxAssets: 1, requestType: RequestType.video), pickerConfig:
AssetPickerConfig(maxAssets: 1, requestType: RequestType.video),
); );
final File? video = await result?.first.file; final File? video = await result?.first.file;
String path = await saveNetworkImgGallery( String path = await saveNetworkImgGallery(
@@ -283,34 +310,34 @@ class UploadImagesTool {
chooseImages?.call([path]); chooseImages?.call([path]);
} else { } else {
/// ///
final Directory tempDir = Directory.systemTemp.createTempSync('compressed_images_'); final Directory tempDir =
Directory.systemTemp.createTempSync('compressed_images_');
final List<AssetEntity>? result = await AssetPicker.pickAssets( final List<AssetEntity>? result = await AssetPicker.pickAssets(
Get.context!, Get.context!,
pickerConfig: AssetPickerConfig(maxAssets: max ?? 50), pickerConfig: AssetPickerConfig(maxAssets: max ?? 50),
); );
/// ///
final List<XFile> selectedFiles = []; final List<_CompressedImageFile> selectedFiles = [];
if (result != null && result.isNotEmpty) { if (result != null && result.isNotEmpty) {
for (int i = 0; i < result.length; i++) { for (int i = 0; i < result.length; i++) {
final File? file = await result[i].file; final File? file = await result[i].file;
if (file != null) { if (file != null) {
/// final _CompressedImageFile? compressedFile =
final String extension = file.absolute.path.split('.').last; await _compressGalleryImage(file, tempDir);
///
final XFile? compressedFile = await FlutterImageCompress.compressAndGetFile(
file.absolute.path, '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}_compressed.$extension',
quality: 80, minWidth: 1920, minHeight: 1080);
if (compressedFile != null) { if (compressedFile != null) {
selectedFiles.add(compressedFile); selectedFiles.add(compressedFile);
} }
} }
} }
} }
/// ///
List<String> list = []; List<String> list = [];
for (var element in selectedFiles) { for (var element in selectedFiles) {
String path = await saveNetworkImgGallery( String path = await saveNetworkImgGallery(
element.path, element.path,
fileType: element.fileType,
oSSAccessKeyId: oSSAccessKeyId ?? '', oSSAccessKeyId: oSSAccessKeyId ?? '',
ossHost: ossHost ?? '', ossHost: ossHost ?? '',
ossDirectory: ossDirectory ?? '', ossDirectory: ossDirectory ?? '',
@@ -325,6 +352,99 @@ class UploadImagesTool {
} }
} }
static Future<_CompressedImageFile?> _compressGalleryImage(
File file,
Directory tempDir,
) async {
final _ImageCompressionTarget target =
_compressionTargetForPath(file.absolute.path);
final XFile? compressedFile = await _compressImageWithTarget(
file,
tempDir,
target,
);
if (compressedFile != null) {
return _CompressedImageFile(
path: compressedFile.path,
fileType: target.fileType,
);
}
if (target.format == CompressFormat.jpeg) {
return null;
}
const _ImageCompressionTarget fallbackTarget = _ImageCompressionTarget(
format: CompressFormat.jpeg,
extension: 'jpg',
fileType: 'jpg',
);
final XFile? fallbackFile = await _compressImageWithTarget(
file,
tempDir,
fallbackTarget,
);
if (fallbackFile == null) {
return null;
}
return _CompressedImageFile(
path: fallbackFile.path,
fileType: fallbackTarget.fileType,
);
}
static Future<XFile?> _compressImageWithTarget(
File file,
Directory tempDir,
_ImageCompressionTarget target,
) async {
try {
return await FlutterImageCompress.compressAndGetFile(
file.absolute.path,
'${tempDir.path}/${DateTime.now().microsecondsSinceEpoch}_compressed.${target.extension}',
quality: 80,
minWidth: 1920,
minHeight: 1080,
format: target.format,
);
} catch (_) {
return null;
}
}
static _ImageCompressionTarget _compressionTargetForPath(String path) {
final String extension =
path.split('?').first.split('.').last.toLowerCase();
switch (extension) {
case 'png':
return const _ImageCompressionTarget(
format: CompressFormat.png,
extension: 'png',
fileType: 'png',
);
case 'webp':
return const _ImageCompressionTarget(
format: CompressFormat.webp,
extension: 'webp',
fileType: 'webp',
);
case 'heic':
case 'heif':
return const _ImageCompressionTarget(
format: CompressFormat.heic,
extension: 'heic',
fileType: 'heic',
);
case 'jpeg':
case 'jpg':
default:
return const _ImageCompressionTarget(
format: CompressFormat.jpeg,
extension: 'jpg',
fileType: 'jpg',
);
}
}
// //
static Future<String> saveNetworkImg( static Future<String> saveNetworkImg(
XFile file, { XFile file, {
+4
View File
@@ -0,0 +1,4 @@
export 'video_batch_upload_config.dart';
export 'video_batch_upload_task.dart';
export 'video_batch_uploader.dart';
export 'video_batch_upload_widgets.dart';
@@ -0,0 +1,6 @@
class VideoBatchUploadConfig {
const VideoBatchUploadConfig._();
static const int maxAssets = 9;
static const int maxConcurrent = 2;
}
@@ -0,0 +1,347 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'video_batch_upload_queue.dart';
import 'video_batch_upload_task.dart';
class VideoBatchUploadPanel extends StatelessWidget {
const VideoBatchUploadPanel({
super.key,
required this.queue,
});
final VideoBatchUploadQueue queue;
@override
Widget build(BuildContext context) {
if (!queue.hasTasks) {
return const SizedBox.shrink();
}
return Positioned(
left: 0,
right: 0,
bottom: 0,
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 180),
child: queue.isExpanded
? _ExpandedUploadPanel(queue: queue)
: _CollapsedUploadBar(queue: queue),
),
);
}
}
class _CollapsedUploadBar extends StatelessWidget {
const _CollapsedUploadBar({required this.queue});
final VideoBatchUploadQueue queue;
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
child: GestureDetector(
onTap: queue.toggleExpanded,
child: Container(
height: 70.h,
margin: EdgeInsets.fromLTRB(24.w, 0, 24.w, 18.h),
padding: EdgeInsets.symmetric(horizontal: 22.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18.r),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 18.r,
offset: Offset(0, 6.h),
),
],
),
child: Row(
children: <Widget>[
Icon(
Icons.cloud_upload_outlined,
color: const Color(0xFF46A5FF),
size: 34.sp,
),
SizedBox(width: 12.w),
Expanded(
child: Text(
'正在上传${queue.assetType.label} ${queue.finishedCount}/${queue.totalCount}',
style: TextStyle(
fontSize: 24.sp,
color: const Color(0xFF1F2937),
fontWeight: FontWeight.w500,
),
),
),
Icon(
Icons.keyboard_arrow_up_rounded,
color: const Color(0xFF667085),
size: 34.sp,
),
],
),
),
),
);
}
}
class _ExpandedUploadPanel extends StatelessWidget {
const _ExpandedUploadPanel({required this.queue});
final VideoBatchUploadQueue queue;
@override
Widget build(BuildContext context) {
final double bottomPadding = MediaQuery.of(context).padding.bottom;
return Container(
constraints: BoxConstraints(maxHeight: 560.h),
padding: EdgeInsets.fromLTRB(24.w, 20.h, 24.w, 18.h + bottomPadding),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(24.r)),
boxShadow: <BoxShadow>[
BoxShadow(
color: Colors.black.withValues(alpha: 0.12),
blurRadius: 18.r,
offset: Offset(0, -6.h),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Text(
'上传${queue.assetType.label}',
style: TextStyle(
fontSize: 30.sp,
color: const Color(0xFF111827),
fontWeight: FontWeight.w600,
),
),
),
Text(
'${queue.finishedCount}/${queue.totalCount}',
style: TextStyle(
fontSize: 24.sp,
color: const Color(0xFF667085),
),
),
SizedBox(width: 10.w),
GestureDetector(
onTap: queue.toggleExpanded,
behavior: HitTestBehavior.opaque,
child: Icon(
Icons.keyboard_arrow_down_rounded,
color: const Color(0xFF667085),
size: 38.sp,
),
),
],
),
SizedBox(height: 16.h),
Flexible(
child: ListView.separated(
padding: EdgeInsets.zero,
shrinkWrap: true,
itemCount: queue.tasks.length,
separatorBuilder: (_, __) => SizedBox(height: 12.h),
itemBuilder: (_, int index) {
final VideoBatchUploadTask task = queue.tasks[index];
return _UploadTaskCell(
task: task,
onRetry: () => queue.retryTask(task),
onRemove: () => queue.removeTask(task),
);
},
),
),
],
),
);
}
}
class _UploadTaskCell extends StatelessWidget {
const _UploadTaskCell({
required this.task,
required this.onRetry,
required this.onRemove,
});
final VideoBatchUploadTask task;
final VoidCallback onRetry;
final VoidCallback onRemove;
@override
Widget build(BuildContext context) {
final Color statusColor = task.status == VideoBatchUploadStatus.failed
? const Color(0xFFE5484D)
: const Color(0xFF667085);
return Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(16.r),
),
child: Row(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(12.r),
child: SizedBox(
width: 86.w,
height: 86.w,
child: task.thumbnail == null
? Container(
color: const Color(0xFFEDEFF3),
alignment: Alignment.center,
child: Icon(
task.assetType == VideoBatchUploadAssetType.video
? Icons.videocam_outlined
: Icons.image_outlined,
size: 46.sp,
color: const Color(0xFF98A2B3),
),
)
: Image.memory(
task.thumbnail!,
fit: BoxFit.cover,
),
),
),
SizedBox(width: 14.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
task.fileName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 24.sp,
color: const Color(0xFF1F2937),
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 8.h),
Text(
_subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 20.sp,
color: const Color(0xFF8D90A0),
),
),
SizedBox(height: 10.h),
ClipRRect(
borderRadius: BorderRadius.circular(999),
child: LinearProgressIndicator(
value: task.progress.clamp(0, 1),
minHeight: 6.h,
backgroundColor: const Color(0xFFE5E7EB),
valueColor: AlwaysStoppedAnimation<Color>(
task.status == VideoBatchUploadStatus.failed
? const Color(0xFFE5484D)
: const Color(0xFF46A5FF),
),
),
),
],
),
),
SizedBox(width: 12.w),
SizedBox(
width: 96.w,
child: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
task.status == VideoBatchUploadStatus.failed &&
task.errorMessage.isNotEmpty
? task.errorMessage
: task.status.label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 20.sp,
color: statusColor,
),
),
SizedBox(height: 12.h),
_TaskActionButton(
task: task,
onRetry: onRetry,
onRemove: onRemove,
),
],
),
),
],
),
);
}
String get _subtitle {
final List<String> parts = <String>[
if (task.durationText.isNotEmpty) task.durationText,
if (task.fileSizeText.isNotEmpty) task.fileSizeText,
];
return parts.isEmpty ? task.assetType.localLabel : parts.join(' · ');
}
}
class _TaskActionButton extends StatelessWidget {
const _TaskActionButton({
required this.task,
required this.onRetry,
required this.onRemove,
});
final VideoBatchUploadTask task;
final VoidCallback onRetry;
final VoidCallback onRemove;
@override
Widget build(BuildContext context) {
if (task.canRetry) {
return GestureDetector(
onTap: onRetry,
behavior: HitTestBehavior.opaque,
child: Text(
'重试',
style: TextStyle(
fontSize: 22.sp,
color: const Color(0xFF46A5FF),
fontWeight: FontWeight.w500,
),
),
);
}
if (task.status == VideoBatchUploadStatus.completed) {
return Icon(
Icons.check_circle_rounded,
size: 30.sp,
color: const Color(0xFF12B76A),
);
}
return GestureDetector(
onTap: onRemove,
behavior: HitTestBehavior.opaque,
child: Icon(
task.status == VideoBatchUploadStatus.uploading
? Icons.close_rounded
: Icons.delete_outline_rounded,
size: 30.sp,
color: const Color(0xFF98A2B3),
),
);
}
}
@@ -0,0 +1,405 @@
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_common/utils/toast_utils.dart';
import 'video_batch_upload_config.dart';
import 'video_batch_upload_service.dart';
import 'video_batch_upload_task.dart';
class VideoBatchUploadQueue {
VideoBatchUploadQueue({
required this.contextProvider,
required this.uploadTask,
this.saveTask,
this.batchSaveTask,
required this.onChanged,
this.assetType = VideoBatchUploadAssetType.video,
this.onCompleted,
this.maxAssets = VideoBatchUploadConfig.maxAssets,
this.maxConcurrent = VideoBatchUploadConfig.maxConcurrent,
});
final BuildContext? Function() contextProvider;
final VideoBatchUploadUploadTask uploadTask;
final VideoBatchUploadSaveTask? saveTask;
final VideoBatchUploadBatchSaveTask? batchSaveTask;
final VoidCallback onChanged;
final VideoBatchUploadAssetType assetType;
final FutureOr<void> Function()? onCompleted;
final int maxAssets;
final int maxConcurrent;
final List<VideoBatchUploadTask> tasks = <VideoBatchUploadTask>[];
int _batchTotalCount = 0;
int _completedRemovedCount = 0;
bool isExpanded = false;
bool _isRunning = false;
bool _completionNotified = false;
bool _isDisposed = false;
bool get hasTasks => tasks.isNotEmpty;
bool get hasActiveUploads => tasks.any((VideoBatchUploadTask task) {
return task.isActive;
});
int get totalCount => _batchTotalCount > 0 ? _batchTotalCount : tasks.length;
int get completedCount => tasks
.where((VideoBatchUploadTask task) =>
task.status == VideoBatchUploadStatus.completed)
.length;
int get failedCount => tasks
.where((VideoBatchUploadTask task) =>
task.status == VideoBatchUploadStatus.failed)
.length;
int get finishedCount =>
_completedRemovedCount + completedCount + failedCount;
bool get _usesBatchSave => batchSaveTask != null;
Future<void> pickAndStart() async {
if (hasActiveUploads) {
isExpanded = true;
_notifyChanged();
ToastUtils.showToast(msg: '${assetType.label}正在上传中');
return;
}
final BuildContext? context = contextProvider();
if (context == null || !context.mounted) {
return;
}
final List<VideoBatchUploadTask> pickedTasks =
await VideoBatchUploadPicker.pickMedia(
context,
maxAssets: maxAssets,
assetType: assetType,
);
if (pickedTasks.isEmpty) {
return;
}
tasks
..clear()
..addAll(pickedTasks);
_batchTotalCount = pickedTasks.length;
_completedRemovedCount = 0;
isExpanded = true;
_completionNotified = false;
_notifyChanged();
unawaited(_runQueue());
}
void toggleExpanded() {
if (!hasTasks) {
return;
}
isExpanded = !isExpanded;
_notifyChanged();
}
void retryTask(VideoBatchUploadTask task) {
if (!tasks.contains(task) || !task.canRetry) {
return;
}
task
..status = VideoBatchUploadStatus.waiting
..progress = 0
..uploadUrl = ''
..errorMessage = '';
_completionNotified = false;
isExpanded = true;
_notifyChanged();
unawaited(_runQueue());
}
void removeTask(VideoBatchUploadTask task) {
if (!tasks.contains(task)) {
return;
}
task.cancelToken?.cancel('removed');
tasks.remove(task);
if (tasks.isEmpty) {
isExpanded = false;
_resetBatchProgress();
}
_notifyChanged();
}
void clear() {
for (final VideoBatchUploadTask task in tasks) {
task.cancelToken?.cancel('cleared');
}
tasks.clear();
isExpanded = false;
_isRunning = false;
_resetBatchProgress();
_notifyChanged();
}
void cancelAll() {
for (final VideoBatchUploadTask task in tasks) {
task.cancelToken?.cancel('cancelled');
}
for (final VideoBatchUploadTask task in tasks) {
if (task.isActive) {
task
..status = VideoBatchUploadStatus.failed
..errorMessage = '已取消';
}
}
_isRunning = false;
_notifyChanged();
}
void requestExit({required VoidCallback onExit}) {
if (!hasActiveUploads) {
onExit();
return;
}
ToastUtils.showAlterDialog(
titleText: '${assetType.label}还在上传',
contentText: '离开会中断本次上传,确定要离开吗?',
confirmText: '离开',
confirmCallback: () {
cancelAll();
onExit();
},
);
}
void dispose() {
_isDisposed = true;
for (final VideoBatchUploadTask task in tasks) {
task.cancelToken?.cancel('disposed');
}
}
void _resetBatchProgress() {
_batchTotalCount = 0;
_completedRemovedCount = 0;
_completionNotified = false;
}
Future<void> _runQueue() async {
if (_isRunning || _isDisposed) {
return;
}
_isRunning = true;
final int workerCount = maxConcurrent < 1 ? 1 : maxConcurrent;
final List<Future<void>> workers = List<Future<void>>.generate(
workerCount,
(_) => _uploadWorker(),
);
await Future.wait(workers);
_isRunning = false;
await _saveUploadedBatchIfNeeded();
await _notifyCompletedIfNeeded();
}
Future<void> _uploadWorker() async {
while (true) {
if (_isDisposed) {
return;
}
final VideoBatchUploadTask? task = _nextWaitingTask();
if (task == null) {
return;
}
await _processTask(task);
}
}
VideoBatchUploadTask? _nextWaitingTask() {
for (final VideoBatchUploadTask task in tasks) {
if (task.status == VideoBatchUploadStatus.waiting) {
return task;
}
}
return null;
}
Future<void> _processTask(VideoBatchUploadTask task) async {
task
..status = VideoBatchUploadStatus.uploading
..progress = 0
..errorMessage = ''
..cancelToken = CancelToken();
_notifyChanged();
try {
final String uploadUrl = await uploadTask(
task,
(double progress) {
if (!tasks.contains(task)) {
return;
}
task.progress = progress.clamp(0, 1);
_notifyChanged();
},
);
if (!tasks.contains(task)) {
return;
}
if (uploadUrl.trim().isEmpty) {
throw Exception('empty upload url');
}
task
..uploadUrl = uploadUrl
..progress = 1
..status = VideoBatchUploadStatus.uploaded;
_notifyChanged();
if (_usesBatchSave) {
return;
}
task.status = VideoBatchUploadStatus.saving;
_notifyChanged();
final bool saved = await saveTask!(task);
if (!tasks.contains(task)) {
return;
}
if (saved) {
task.status = VideoBatchUploadStatus.completed;
_completedRemovedCount += 1;
tasks.remove(task);
if (tasks.isEmpty) {
isExpanded = false;
}
} else {
task
..status = VideoBatchUploadStatus.failed
..errorMessage = '入库失败';
}
} on DioException catch (error) {
if (!tasks.contains(task)) {
return;
}
task
..status = VideoBatchUploadStatus.failed
..errorMessage =
CancelToken.isCancel(error) ? '已取消' : _networkErrorText(error);
} catch (_) {
if (!tasks.contains(task)) {
return;
}
task
..status = VideoBatchUploadStatus.failed
..errorMessage = '上传失败';
} finally {
task.cancelToken = null;
_notifyChanged();
}
}
Future<void> _saveUploadedBatchIfNeeded() async {
if (!_usesBatchSave || _isDisposed) {
return;
}
final List<VideoBatchUploadTask> successfulUploadTasks = tasks
.where((VideoBatchUploadTask task) =>
task.status == VideoBatchUploadStatus.uploaded)
.toList();
if (successfulUploadTasks.isEmpty) {
return;
}
for (final VideoBatchUploadTask task in successfulUploadTasks) {
task.status = VideoBatchUploadStatus.saving;
}
_notifyChanged();
try {
final bool saved = await batchSaveTask!(List<VideoBatchUploadTask>.of(
successfulUploadTasks,
growable: false,
));
if (_isDisposed) {
return;
}
if (saved) {
_completedRemovedCount += successfulUploadTasks.length;
tasks.removeWhere(successfulUploadTasks.contains);
if (tasks.isEmpty) {
isExpanded = false;
}
return;
}
for (final VideoBatchUploadTask task in successfulUploadTasks) {
if (tasks.contains(task)) {
task
..status = VideoBatchUploadStatus.failed
..errorMessage = '入库失败';
}
}
} catch (_) {
if (_isDisposed) {
return;
}
for (final VideoBatchUploadTask task in successfulUploadTasks) {
if (tasks.contains(task)) {
task
..status = VideoBatchUploadStatus.failed
..errorMessage = '入库失败';
}
}
} finally {
_notifyChanged();
}
}
String _networkErrorText(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
case DioExceptionType.connectionError:
return '网络异常';
case DioExceptionType.cancel:
return '已取消';
case DioExceptionType.badResponse:
return '上传失败';
case DioExceptionType.badCertificate:
case DioExceptionType.unknown:
return '上传失败';
}
}
Future<void> _notifyCompletedIfNeeded() async {
if (_isDisposed ||
_completionNotified ||
totalCount == 0 ||
hasActiveUploads) {
return;
}
_completionNotified = true;
await onCompleted?.call();
if (failedCount > 0) {
ToastUtils.showToast(
msg:
'已上传 $_completedRemovedCount ${assetType.unit},失败 $failedCount ${assetType.unit}');
return;
}
final int completed = _completedRemovedCount;
ToastUtils.showToast(
msg: '已上传 $completed ${assetType.unit}${assetType.label}');
tasks.clear();
isExpanded = false;
_resetBatchProgress();
_notifyChanged();
}
void _notifyChanged() {
if (_isDisposed) {
return;
}
onChanged();
}
}
@@ -0,0 +1,100 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
import 'video_batch_upload_config.dart';
import 'video_batch_upload_task.dart';
class VideoBatchUploadPicker {
const VideoBatchUploadPicker._();
static Future<List<VideoBatchUploadTask>> pickVideos(
BuildContext context, {
int maxAssets = VideoBatchUploadConfig.maxAssets,
}) async {
return pickMedia(
context,
maxAssets: maxAssets,
assetType: VideoBatchUploadAssetType.video,
);
}
static Future<List<VideoBatchUploadTask>> pickImages(
BuildContext context, {
int maxAssets = VideoBatchUploadConfig.maxAssets,
}) async {
return pickMedia(
context,
maxAssets: maxAssets,
assetType: VideoBatchUploadAssetType.image,
);
}
static Future<List<VideoBatchUploadTask>> pickMedia(
BuildContext context, {
int maxAssets = VideoBatchUploadConfig.maxAssets,
VideoBatchUploadAssetType assetType = VideoBatchUploadAssetType.video,
}) async {
final List<AssetEntity>? assets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
maxAssets: maxAssets,
requestType: _requestType(assetType),
),
);
if (assets == null || assets.isEmpty) {
return <VideoBatchUploadTask>[];
}
final List<VideoBatchUploadTask> tasks = <VideoBatchUploadTask>[];
for (final AssetEntity asset in assets) {
final File? file = await asset.file;
if (file == null) {
continue;
}
final Uint8List? thumbnail = await asset.thumbnailDataWithSize(
const ThumbnailSize.square(220),
quality: 72,
);
final int fileSize = await file.length();
tasks.add(
VideoBatchUploadTask(
id: '${asset.id}_${DateTime.now().microsecondsSinceEpoch}',
assetType: assetType,
file: file,
fileName: _normalizeFileName(asset.title, file.path, assetType),
fileSize: fileSize,
duration:
assetType == VideoBatchUploadAssetType.video ? asset.duration : 0,
thumbnail: thumbnail,
),
);
}
return tasks;
}
static RequestType _requestType(VideoBatchUploadAssetType assetType) {
switch (assetType) {
case VideoBatchUploadAssetType.image:
return RequestType.image;
case VideoBatchUploadAssetType.video:
return RequestType.video;
}
}
static String _normalizeFileName(
String? assetTitle,
String path,
VideoBatchUploadAssetType assetType,
) {
final String title = (assetTitle ?? '').trim();
if (title.isNotEmpty) {
return title;
}
final List<String> segments = path.split(Platform.pathSeparator);
final String fileName = segments.isEmpty ? '' : segments.last.trim();
return fileName.isEmpty ? assetType.defaultFileName : fileName;
}
}
@@ -0,0 +1,176 @@
import 'dart:io';
import 'dart:typed_data';
import 'package:dio/dio.dart';
typedef VideoBatchUploadProgressCallback = void Function(double progress);
typedef VideoBatchUploadUploadTask = Future<String> Function(
VideoBatchUploadTask task,
VideoBatchUploadProgressCallback onProgress,
);
typedef VideoBatchUploadSaveTask = Future<bool> Function(
VideoBatchUploadTask task,
);
typedef VideoBatchUploadBatchSaveTask = Future<bool> Function(
List<VideoBatchUploadTask> tasks,
);
enum VideoBatchUploadAssetType {
image,
video,
}
extension VideoBatchUploadAssetTypeText on VideoBatchUploadAssetType {
String get label {
switch (this) {
case VideoBatchUploadAssetType.image:
return '图片';
case VideoBatchUploadAssetType.video:
return '视频';
}
}
String get unit {
switch (this) {
case VideoBatchUploadAssetType.image:
return '';
case VideoBatchUploadAssetType.video:
return '';
}
}
String get localLabel {
switch (this) {
case VideoBatchUploadAssetType.image:
return '本地图片';
case VideoBatchUploadAssetType.video:
return '本地视频';
}
}
String get defaultFileName {
switch (this) {
case VideoBatchUploadAssetType.image:
return 'image.jpg';
case VideoBatchUploadAssetType.video:
return 'video.mp4';
}
}
}
enum VideoBatchUploadStatus {
waiting,
uploading,
uploaded,
saving,
completed,
failed,
}
extension VideoBatchUploadStatusText on VideoBatchUploadStatus {
String get label {
switch (this) {
case VideoBatchUploadStatus.waiting:
return '等待中';
case VideoBatchUploadStatus.uploading:
return '上传中';
case VideoBatchUploadStatus.uploaded:
return '上传成功';
case VideoBatchUploadStatus.saving:
return '入库中';
case VideoBatchUploadStatus.completed:
return '已完成';
case VideoBatchUploadStatus.failed:
return '失败';
}
}
}
class VideoBatchUploadTask {
VideoBatchUploadTask({
required this.id,
this.assetType = VideoBatchUploadAssetType.video,
required this.file,
required this.fileName,
required this.fileSize,
required this.duration,
this.thumbnail,
});
final String id;
final VideoBatchUploadAssetType assetType;
final File file;
final String fileName;
final int fileSize;
final int duration;
final Uint8List? thumbnail;
VideoBatchUploadStatus status = VideoBatchUploadStatus.waiting;
double progress = 0;
String uploadUrl = '';
String errorMessage = '';
CancelToken? cancelToken;
String get ossUrl => uploadUrl;
set ossUrl(String value) {
uploadUrl = value;
}
bool get isActive =>
status == VideoBatchUploadStatus.waiting ||
status == VideoBatchUploadStatus.uploading ||
status == VideoBatchUploadStatus.uploaded ||
status == VideoBatchUploadStatus.saving;
bool get canRetry => status == VideoBatchUploadStatus.failed;
bool get canRemove =>
status == VideoBatchUploadStatus.waiting ||
status == VideoBatchUploadStatus.completed ||
status == VideoBatchUploadStatus.failed;
String get fileSizeText {
if (fileSize <= 0) {
return '';
}
final double mb = fileSize / 1024 / 1024;
if (mb >= 1) {
return '${mb.toStringAsFixed(1)}MB';
}
final double kb = fileSize / 1024;
return '${kb.toStringAsFixed(0)}KB';
}
String get durationText {
if (duration <= 0) {
return '';
}
final int minutes = duration ~/ 60;
final int seconds = duration % 60;
return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
}
Map<String, dynamic> toVideoPayload({
String urlKey = 'fileURL',
String fileNameKey = 'fileName',
String durationKey = 'duration',
}) {
return <String, dynamic>{
urlKey: uploadUrl,
fileNameKey: fileName,
if (duration > 0) durationKey: duration,
};
}
Map<String, dynamic> toLibraryVideoPayload() {
return toVideoPayload();
}
String toLibraryImageURL() {
return uploadUrl;
}
}
@@ -0,0 +1,81 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'video_batch_upload_task.dart';
class VideoBatchUploadBackButton extends StatelessWidget {
const VideoBatchUploadBackButton({
super.key,
required this.onTap,
this.iconSize = 19,
});
final VoidCallback onTap;
final double iconSize;
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: onTap,
icon: Icon(
Icons.arrow_back_ios_new,
color: Colors.black,
size: iconSize,
),
);
}
}
class VideoBatchUploadIconButton extends StatelessWidget {
const VideoBatchUploadIconButton({
super.key,
required this.onTap,
this.assetType = VideoBatchUploadAssetType.video,
this.iconSize,
this.rightPadding,
});
final VoidCallback onTap;
final VideoBatchUploadAssetType assetType;
final double? iconSize;
final double? rightPadding;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Padding(
padding: EdgeInsets.only(right: rightPadding ?? 30.w),
child: Icon(
assetType == VideoBatchUploadAssetType.video
? Icons.video_call_outlined
: Icons.add_photo_alternate_outlined,
size: iconSize ?? 50.sp,
color: const Color(0xFF1D1D21),
),
),
);
}
}
class VideoBatchUploadEmptyPlaceholder extends StatelessWidget {
const VideoBatchUploadEmptyPlaceholder({
super.key,
this.title = '这个视频库里还没有视频',
});
final String title;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
title,
style: TextStyle(
fontSize: 28.sp,
color: const Color(0xFF8D90A0),
),
),
);
}
}
+100
View File
@@ -0,0 +1,100 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
import 'video_batch_upload_config.dart';
import 'video_batch_upload_panel.dart';
import 'video_batch_upload_queue.dart';
import 'video_batch_upload_task.dart';
typedef VideoBatchUploadCallback = FutureOr<void> Function();
class VideoBatchUploader {
VideoBatchUploader({
required BuildContext? Function() contextProvider,
required VideoBatchUploadUploadTask uploadTask,
VideoBatchUploadSaveTask? saveTask,
VideoBatchUploadBatchSaveTask? batchSaveTask,
VideoBatchUploadAssetType assetType = VideoBatchUploadAssetType.video,
VoidCallback? onChanged,
VideoBatchUploadCallback? onCompleted,
int? maxAssets,
int? maxConcurrent,
}) : assert(saveTask != null || batchSaveTask != null),
_contextProvider = contextProvider,
_onChanged = onChanged {
queue = VideoBatchUploadQueue(
contextProvider: contextProvider,
uploadTask: uploadTask,
saveTask: saveTask,
batchSaveTask: batchSaveTask,
onChanged: _handleChanged,
assetType: assetType,
onCompleted: onCompleted,
maxAssets: maxAssets ?? VideoBatchUploadConfig.maxAssets,
maxConcurrent: maxConcurrent ?? VideoBatchUploadConfig.maxConcurrent,
);
}
final BuildContext? Function() _contextProvider;
final VoidCallback? _onChanged;
late final VideoBatchUploadQueue queue;
OverlayEntry? _overlayEntry;
bool get hasActiveUploads => queue.hasActiveUploads;
Future<void> start() async {
await queue.pickAndStart();
}
void requestExit({required VoidCallback onExit}) {
queue.requestExit(onExit: onExit);
}
void clear() {
queue.clear();
_syncOverlay();
}
void dispose() {
queue.dispose();
_removeOverlay();
}
void _handleChanged() {
_syncOverlay();
_overlayEntry?.markNeedsBuild();
_onChanged?.call();
}
void _syncOverlay() {
if (queue.hasTasks) {
_insertOverlayIfNeeded();
return;
}
_removeOverlay();
}
void _insertOverlayIfNeeded() {
if (_overlayEntry != null) {
return;
}
final BuildContext? context = _contextProvider();
if (context == null || !context.mounted) {
return;
}
final OverlayState? overlay = Overlay.maybeOf(context);
if (overlay == null) {
return;
}
_overlayEntry = OverlayEntry(
builder: (_) => VideoBatchUploadPanel(queue: queue),
);
overlay.insert(_overlayEntry!);
}
void _removeOverlay() {
_overlayEntry?.remove();
_overlayEntry = null;
}
}
+21 -13
View File
@@ -96,22 +96,30 @@ class CustomerImagesNetworking extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String? sizeParams = ''; String sizeParams = '';
int tempHeight = 300; const defaultResizeSize = 2048;
int tempWidth = 300;
if (height != null) {
sizeParams = '?x-oss-process=image/resize,h_$tempHeight,w_$tempHeight';
}
if (width != null) {
tempWidth = width!.toInt() <= 300 ? 300 : width!.toInt();
sizeParams = '?x-oss-process=image/resize,h_$tempWidth,w_$tempWidth';
}
if (height != null && width != null) { if (height != null && width != null) {
tempHeight = height!.toInt() <= 300 ? 300 : height!.toInt(); final h = height!.toInt() <= defaultResizeSize
sizeParams = '?x-oss-process=image/resize,h_$tempHeight,w_$tempWidth'; ? defaultResizeSize
: height!.toInt();
final w = width!.toInt() <= defaultResizeSize
? defaultResizeSize
: width!.toInt();
sizeParams = 'x-oss-process=image/resize,h_$h,w_$w';
} else if (width != null) {
final w = width!.toInt() <= defaultResizeSize
? defaultResizeSize
: width!.toInt();
sizeParams = 'x-oss-process=image/resize,h_$w,w_$w';
} else if (height != null) {
final h = height!.toInt() <= defaultResizeSize
? defaultResizeSize
: height!.toInt();
sizeParams = 'x-oss-process=image/resize,h_$h,w_$h';
} }
final separator = imageUrl.contains('?') ? '&' : '?';
return CachedNetworkImage( return CachedNetworkImage(
imageUrl: imageUrl + sizeParams, imageUrl: imageUrl.isEmpty ? imageUrl : '$imageUrl$separator$sizeParams',
width: width, width: width,
height: height, height: height,
fit: fit, fit: fit,