Compare commits
17 Commits
main_2.0
...
a1b98e987e
| Author | SHA1 | Date | |
|---|---|---|---|
| a1b98e987e | |||
| 037f14b52f | |||
| aa89e03bee | |||
| 1c971fb0b2 | |||
| aa47bc97d5 | |||
| 0b45f6a6c1 | |||
| 7f077cf513 | |||
| 8ba36d9700 | |||
| b380daf206 | |||
| 89dbb79256 | |||
| 79eb8a891b | |||
| 0c7fbe5897 | |||
| de127eeef5 | |||
| 01178bc033 | |||
| 724e31ad7d | |||
| e6ea8182e4 | |||
| c585d5ce0d |
+190
-190
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
Generated
+6
@@ -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/.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/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>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
|
||||
Generated
+8
-8
@@ -271,7 +271,7 @@
|
||||
<entry key="flutter">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/flutter/packages/flutter/lib" />
|
||||
<option value="$PROJECT_DIR$/../../flutter/packages/flutter/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
@@ -425,14 +425,14 @@
|
||||
<entry key="flutter_test">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/flutter/packages/flutter_test/lib" />
|
||||
<option value="$PROJECT_DIR$/../../flutter/packages/flutter_test/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
<entry key="flutter_web_plugins">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/flutter/packages/flutter_web_plugins/lib" />
|
||||
<option value="$PROJECT_DIR$/../../flutter/packages/flutter_web_plugins/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</entry>
|
||||
@@ -1020,7 +1020,7 @@
|
||||
<entry key="sky_engine">
|
||||
<value>
|
||||
<list>
|
||||
<option value="$USER_HOME$/flutter/bin/cache/pkg/sky_engine/lib" />
|
||||
<option value="$PROJECT_DIR$/../../flutter/bin/cache/pkg/sky_engine/lib" />
|
||||
</list>
|
||||
</value>
|
||||
</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/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$/flutter/bin/cache/pkg/sky_engine/lib" />
|
||||
<root url="file://$USER_HOME$/flutter/packages/flutter/lib" />
|
||||
<root url="file://$USER_HOME$/flutter/packages/flutter_test/lib" />
|
||||
<root url="file://$USER_HOME$/flutter/packages/flutter_web_plugins/lib" />
|
||||
<root url="file://$PROJECT_DIR$/../../flutter/bin/cache/pkg/sky_engine/lib" />
|
||||
<root url="file://$PROJECT_DIR$/../../flutter/packages/flutter/lib" />
|
||||
<root url="file://$PROJECT_DIR$/../../flutter/packages/flutter_test/lib" />
|
||||
<root url="file://$PROJECT_DIR$/../../flutter/packages/flutter_web_plugins/lib" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
|
||||
Generated
+53
-53
@@ -1,62 +1,62 @@
|
||||
<component name="libraryTable">
|
||||
<library name="Flutter Plugins" type="FlutterPluginsLibraryType">
|
||||
<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/shared_preferences-2.5.5" />
|
||||
<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/screen_brightness-2.1.7" />
|
||||
<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/jni_flutter-1.0.1" />
|
||||
<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/photo_manager-3.9.0" />
|
||||
<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/jni-1.0.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/image_picker_ios-0.8.13+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_web-2.4.0" />
|
||||
<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/flutter_inappwebview-6.1.5" />
|
||||
<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/flutter_image_compress_web-0.1.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/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>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
|
||||
@@ -3,4 +3,5 @@ export 'calendarcalendar/custom_calendar_range_picker_widget.dart';
|
||||
export 'calendarcalendar/custom_date_picker.dart';
|
||||
export 'upload_image/look_images_widget.dart';
|
||||
export 'upload_image/upload_images_tool.dart';
|
||||
export 'upload_video/video_batch_upload.dart';
|
||||
export 'utils/date_utils.dart';
|
||||
|
||||
@@ -48,6 +48,10 @@ class HttpUtils {
|
||||
name: 'Response Text: ',
|
||||
);
|
||||
}
|
||||
Map<String, dynamic> map = response.data;
|
||||
if (map.keys.contains('code') && map['code'] == 401) {
|
||||
response.data = {'resultCode': 'UNLOGIN', 'resultMsg': '请重新登录'};
|
||||
}
|
||||
|
||||
/// 对接口抛出异常的检查
|
||||
_apiVerify(response);
|
||||
@@ -103,6 +107,12 @@ class HttpUtils {
|
||||
// value: response.headers["authorization"]![0]);
|
||||
// }
|
||||
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(
|
||||
headers: response.headers,
|
||||
result: response.data,
|
||||
@@ -189,6 +199,7 @@ class HttpUtils {
|
||||
}
|
||||
|
||||
if (resultCode.toUpperCase() == "UNLOGIN") {
|
||||
ToastUtils.showToast(msg: map['resultMsg']);
|
||||
unLoginAction?.call();
|
||||
return;
|
||||
}
|
||||
@@ -230,7 +241,10 @@ class HttpUtils {
|
||||
ToastUtils.showToast(msg: '服务响应超时');
|
||||
break;
|
||||
case DioExceptionType.unknown:
|
||||
ToastUtils.showToast(msg: error.message);
|
||||
ToastUtils.showToast(
|
||||
msg: (error.message == '' || error.message?.isEmpty == true)
|
||||
? '请重新登录'
|
||||
: error.message);
|
||||
break;
|
||||
case DioExceptionType.badResponse:
|
||||
try {
|
||||
|
||||
@@ -37,8 +37,15 @@ class UploadOss {
|
||||
required String ossHost,
|
||||
bool? isShowLoading,
|
||||
}) 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
|
||||
String pathName = "$rootDir/${getDate()}/app-${getRandom(12)}.$fileType";
|
||||
String pathName =
|
||||
"$rootDir/${getDate()}/app-${getRandom(12)}.$normalizedFileType";
|
||||
|
||||
// 请求参数的form对象
|
||||
FormData formdata = FormData.fromMap({
|
||||
@@ -51,8 +58,8 @@ class UploadOss {
|
||||
'success_action_status': '200',
|
||||
'file': MultipartFile.fromFileSync(
|
||||
path,
|
||||
contentType: fileType == 'mp4' ? null : DioMediaType("image", "jpg"),
|
||||
filename: "${getRandom(12)}.$fileType",
|
||||
contentType: isVideo ? null : DioMediaType("image", imageContentType!),
|
||||
filename: "${getRandom(12)}.$normalizedFileType",
|
||||
),
|
||||
});
|
||||
if (isShowLoading == true) {
|
||||
@@ -71,13 +78,19 @@ class UploadOss {
|
||||
ossHost,
|
||||
data: formdata,
|
||||
options: Options(
|
||||
contentType: "multipart/form-data;image/jpg",
|
||||
headers: {'Content-Type': 'multipart/form-data;image/jpg'},
|
||||
contentType: isVideo
|
||||
? "multipart/form-data"
|
||||
: "multipart/form-data;image/$imageContentType",
|
||||
headers: {
|
||||
'Content-Type': isVideo
|
||||
? "multipart/form-data"
|
||||
: "multipart/form-data;image/$imageContentType",
|
||||
},
|
||||
),
|
||||
);
|
||||
print("response ===== $response");
|
||||
EasyLoading.dismiss();
|
||||
if(fileType == 'jpg'){
|
||||
if (!isVideo && path.contains('compressed_images_')) {
|
||||
/// 删除临时文件
|
||||
File(path).deleteSync();
|
||||
}
|
||||
|
||||
@@ -133,10 +133,13 @@ class _UploadImagesState extends State<UploadImages> {
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.oneTap?.call(index),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadiusGeometry.circular(20.r),
|
||||
child: CustomerImagesNetworking(
|
||||
imageUrl: imageUrl,
|
||||
fit: widget.fit ?? BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.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: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 {
|
||||
static uploadImagesTool({
|
||||
String? oSSAccessKeyId,
|
||||
@@ -139,7 +160,9 @@ class UploadImagesTool {
|
||||
)
|
||||
: CupertinoActionSheet(
|
||||
title: const Text('上传图片'),
|
||||
message: (max == null || max == 0) ? null : Text('请选择上传方式\n相册最多${max}张'),
|
||||
message: (max == null || max == 0)
|
||||
? null
|
||||
: Text('请选择上传方式\n相册最多$max张'),
|
||||
actions: isAddOtherWidget != null
|
||||
? <Widget>[
|
||||
isAddOtherWidget,
|
||||
@@ -153,7 +176,8 @@ class UploadImagesTool {
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) => chooseImages?.call(list),
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
@@ -169,7 +193,8 @@ class UploadImagesTool {
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) => chooseImages?.call(list),
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
@@ -186,7 +211,8 @@ class UploadImagesTool {
|
||||
policy: policy ?? '',
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
chooseImages: (list) => chooseImages?.call(list),
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
@@ -203,7 +229,8 @@ class UploadImagesTool {
|
||||
callback: callback ?? '',
|
||||
signature: signature ?? '',
|
||||
isShowLoading: isShowLoading,
|
||||
chooseImages: (list) => chooseImages?.call(list),
|
||||
chooseImages: (list) =>
|
||||
chooseImages?.call(list),
|
||||
);
|
||||
Get.back();
|
||||
},
|
||||
@@ -234,8 +261,7 @@ class UploadImagesTool {
|
||||
source: ImageSource.camera,
|
||||
maxHeight: 1080,
|
||||
maxWidth: 1920,
|
||||
imageQuality: 80
|
||||
);
|
||||
imageQuality: 80);
|
||||
if (file == null) {
|
||||
Get.back();
|
||||
} else {
|
||||
@@ -267,7 +293,8 @@ class UploadImagesTool {
|
||||
if (isVideo == true) {
|
||||
final List<AssetEntity>? result = await AssetPicker.pickAssets(
|
||||
Get.context!,
|
||||
pickerConfig: AssetPickerConfig(maxAssets: 1, requestType: RequestType.video),
|
||||
pickerConfig:
|
||||
AssetPickerConfig(maxAssets: 1, requestType: RequestType.video),
|
||||
);
|
||||
final File? video = await result?.first.file;
|
||||
String path = await saveNetworkImgGallery(
|
||||
@@ -283,34 +310,34 @@ class UploadImagesTool {
|
||||
chooseImages?.call([path]);
|
||||
} else {
|
||||
/// 创建临时目录
|
||||
final Directory tempDir = Directory.systemTemp.createTempSync('compressed_images_');
|
||||
final Directory tempDir =
|
||||
Directory.systemTemp.createTempSync('compressed_images_');
|
||||
final List<AssetEntity>? result = await AssetPicker.pickAssets(
|
||||
Get.context!,
|
||||
pickerConfig: AssetPickerConfig(maxAssets: max ?? 50),
|
||||
);
|
||||
|
||||
/// 临时存储选中的图片
|
||||
final List<XFile> selectedFiles = [];
|
||||
final List<_CompressedImageFile> selectedFiles = [];
|
||||
if (result != null && result.isNotEmpty) {
|
||||
for (int i = 0; i < result.length; i++) {
|
||||
final File? file = await result[i].file;
|
||||
if (file != null) {
|
||||
/// 获取文件扩展名
|
||||
final String extension = file.absolute.path.split('.').last;
|
||||
/// 压缩并保存到临时文件
|
||||
final XFile? compressedFile = await FlutterImageCompress.compressAndGetFile(
|
||||
file.absolute.path, '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}_compressed.$extension',
|
||||
quality: 80, minWidth: 1920, minHeight: 1080);
|
||||
final _CompressedImageFile? compressedFile =
|
||||
await _compressGalleryImage(file, tempDir);
|
||||
if (compressedFile != null) {
|
||||
selectedFiles.add(compressedFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传选中的图片
|
||||
List<String> list = [];
|
||||
for (var element in selectedFiles) {
|
||||
String path = await saveNetworkImgGallery(
|
||||
element.path,
|
||||
fileType: element.fileType,
|
||||
oSSAccessKeyId: oSSAccessKeyId ?? '',
|
||||
ossHost: ossHost ?? '',
|
||||
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(
|
||||
XFile 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -96,22 +96,30 @@ class CustomerImagesNetworking extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String? sizeParams = '';
|
||||
int tempHeight = 300;
|
||||
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';
|
||||
}
|
||||
String sizeParams = '';
|
||||
const defaultResizeSize = 2048;
|
||||
if (height != null && width != null) {
|
||||
tempHeight = height!.toInt() <= 300 ? 300 : height!.toInt();
|
||||
sizeParams = '?x-oss-process=image/resize,h_$tempHeight,w_$tempWidth';
|
||||
final h = height!.toInt() <= defaultResizeSize
|
||||
? 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(
|
||||
imageUrl: imageUrl + sizeParams,
|
||||
imageUrl: imageUrl.isEmpty ? imageUrl : '$imageUrl$separator$sizeParams',
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
|
||||
Reference in New Issue
Block a user