flutter/lib/pages/upload_video_page/upload_video_page.dart

470 lines
15 KiB
Dart
Raw Normal View History

2025-07-21 15:46:30 +08:00
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
2025-09-06 14:57:47 +08:00
import 'package:loopin/IM/controller/im_user_info_controller.dart';
2025-09-04 22:19:56 +08:00
import 'package:loopin/IM/im_service.dart';
2025-07-21 15:46:30 +08:00
import 'package:loopin/api/common_api.dart';
import 'package:loopin/api/video_api.dart';
2025-09-04 22:19:56 +08:00
import 'package:loopin/components/image_viewer.dart';
import 'package:loopin/components/preview_video.dart';
2025-07-21 15:46:30 +08:00
import 'package:loopin/service/http.dart';
import 'package:loopin/utils/index.dart';
2025-09-13 17:01:01 +08:00
import 'package:loopin/utils/permissions.dart';
2025-09-04 22:19:56 +08:00
import 'package:loopin/utils/snapshot.dart';
2025-09-13 17:01:01 +08:00
import 'package:shirne_dialog/shirne_dialog.dart';
2025-07-21 15:46:30 +08:00
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class UploadVideoPage extends StatefulWidget {
2025-09-06 14:57:47 +08:00
final bool visible;
const UploadVideoPage({super.key, required this.visible});
2025-07-21 15:46:30 +08:00
@override
State<UploadVideoPage> createState() => _UploadVideoPageState();
}
class _UploadVideoPageState extends State<UploadVideoPage> {
final selectedVideo = Rxn<AssetEntity>();
final selectedCover = Rxn<AssetEntity>();
2025-09-04 22:19:56 +08:00
//视频
2025-07-21 15:46:30 +08:00
final uploading = false.obs;
final uploadProgress = 0.0.obs;
final status = ''.obs;
2025-09-04 22:19:56 +08:00
//图片
final uploading2 = false.obs;
final uploadProgress2 = 0.0.obs;
final status2 = ''.obs;
//地址
final uploadedVideoUrl = ''.obs; // 网络视频地址
final uploadedImgUrl = ''.obs; // 网络图片地址
final videoPath = ''.obs; // 本地视频地址
final imgPath = ''.obs; //本地图片地址
final snapshot = ''.obs;
2025-09-06 14:57:47 +08:00
// 文件id
final fileId = ''.obs;
2025-09-04 22:19:56 +08:00
2025-09-06 14:57:47 +08:00
late final FocusNode descFocusNode;
2025-09-04 22:19:56 +08:00
2025-09-06 14:57:47 +08:00
late TextEditingController descriptionController;
@override
void initState() {
super.initState();
descFocusNode = FocusNode();
descriptionController = TextEditingController();
}
2025-07-21 15:46:30 +08:00
Future<void> pickVideo() async {
2025-09-06 14:57:47 +08:00
descFocusNode.unfocus();
2025-09-13 17:01:01 +08:00
final hasPer = await Permissions.requestVideoPermission();
2025-09-04 22:19:56 +08:00
2025-09-13 17:01:01 +08:00
if (!hasPer) {
Permissions.showPermissionDialog();
2025-07-21 15:46:30 +08:00
return;
}
if (!mounted) return;
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
textDelegate: const AssetPickerTextDelegate(),
pathNameBuilder: (AssetPathEntity album) {
return Utils.translateAlbumName(album);
},
maxAssets: 1,
requestType: RequestType.video,
filterOptions: FilterOptionGroup(
videoOption: const FilterOption(
durationConstraint: DurationConstraint(
max: Duration(seconds: 30),
),
),
),
),
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
2025-09-13 17:01:01 +08:00
final asset = pickedAssets.first;
final file = await asset.file; // 获取实际文件
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 200) {
MyDialog.toast('文件大小不能超过200MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
logger.i("文件合法,大小:$sizeInMB MB");
selectedVideo.value = pickedAssets.first;
status.value = '已选择视频';
uploadVideo();
}
}
2025-07-21 15:46:30 +08:00
}
}
2025-09-04 22:19:56 +08:00
// 选择封面图
2025-07-21 15:46:30 +08:00
Future<void> pickCoverImage() async {
2025-09-06 14:57:47 +08:00
descFocusNode.unfocus();
2025-09-04 22:19:56 +08:00
2025-09-13 17:01:01 +08:00
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog();
2025-07-21 15:46:30 +08:00
return;
}
if (!mounted) return;
final pickedAssets = await AssetPicker.pickAssets(
context,
pickerConfig: AssetPickerConfig(
textDelegate: const AssetPickerTextDelegate(),
pathNameBuilder: (AssetPathEntity album) {
return Utils.translateAlbumName(album);
},
maxAssets: 1,
requestType: RequestType.image,
),
);
if (pickedAssets != null && pickedAssets.isNotEmpty) {
2025-09-13 17:01:01 +08:00
final asset = pickedAssets.first;
final file = await asset.file; // 获取实际文件
if (file != null) {
final fileSizeInBytes = await file.length();
final sizeInMB = fileSizeInBytes / (1024 * 1024);
if (sizeInMB > 20) {
MyDialog.toast('文件大小不能超过20MB', icon: const Icon(Icons.check_circle), style: ToastStyle(backgroundColor: Colors.red.withAlpha(200)));
} else {
logger.i("文件合法,大小:$sizeInMB MB");
selectedCover.value = pickedAssets.first;
uploadImg();
}
}
2025-07-21 15:46:30 +08:00
}
}
2025-09-04 22:19:56 +08:00
// 上传图片
Future<void> uploadImg() async {
2025-07-21 15:46:30 +08:00
final coverFile = await selectedCover.value?.file;
2025-09-04 22:19:56 +08:00
if (coverFile == null) {
status.value = '未选择封面图';
2025-07-21 15:46:30 +08:00
return;
}
2025-09-04 22:19:56 +08:00
imgPath.value = coverFile.path;
uploading2.value = true;
uploadProgress2.value = 0;
status2.value = '上传中...';
try {
final res = await Http.upload(
CommonApi.uploadFile,
filePath: coverFile.path,
fileKey: 'file',
onSendProgress: (sent, total) {
if (total > 0) {
uploadProgress2.value = sent / total;
if (sent == total) uploading2.value = false;
}
},
);
uploadedImgUrl.value = res['data']['url'];
status2.value = '上传成功';
} catch (e) {
if (e is SocketException) {
status2.value = '网络错误,请检查连接';
} else {
status2.value = '上传失败: ${e.toString()}';
}
descFocusNode.unfocus();
uploading2.value = false;
} finally {
descFocusNode.unfocus();
uploading2.value = false;
}
}
// 上传视频
Future<void> uploadVideo() async {
final file = await selectedVideo.value?.file;
if (file == null) {
status.value = '未选择视频';
2025-07-21 15:46:30 +08:00
return;
}
uploading.value = true;
uploadProgress.value = 0;
status.value = '上传中...';
2025-09-04 22:19:56 +08:00
videoPath.value = file.path;
logger.w(videoPath.value);
2025-09-06 14:57:47 +08:00
// vwidth.value = file.width;
2025-09-04 22:19:56 +08:00
snapshot.value = (await generateVideoThumbnail(file.path))!;
logger.w(snapshot.value);
2025-07-21 15:46:30 +08:00
try {
2025-09-04 22:19:56 +08:00
final res = await Http.upload(
2025-07-21 15:46:30 +08:00
CommonApi.uploadFile,
filePath: file.path,
fileKey: 'file',
onSendProgress: (sent, total) {
if (total > 0) {
2025-09-06 14:57:47 +08:00
final pro = sent / total;
uploadProgress.value = pro.clamp(0, 0.999);
2025-07-21 15:46:30 +08:00
}
},
);
2025-09-06 14:57:47 +08:00
logger.w('上传结果$res');
2025-09-04 22:19:56 +08:00
uploadedVideoUrl.value = res['data']['url'];
2025-09-06 14:57:47 +08:00
fileId.value = res['data']['ossId'];
2025-07-21 15:46:30 +08:00
status.value = '上传成功';
2025-09-04 22:19:56 +08:00
descFocusNode.unfocus();
2025-09-06 14:57:47 +08:00
uploading.value = false;
} catch (e) {
2025-07-21 15:46:30 +08:00
if (e is SocketException) {
status.value = '网络错误,请检查连接';
} else {
status.value = '上传失败: ${e.toString()}';
}
2025-09-06 14:57:47 +08:00
logger.e(e);
descFocusNode.unfocus();
2025-07-21 15:46:30 +08:00
} finally {
2025-09-04 22:19:56 +08:00
descFocusNode.unfocus();
2025-07-21 15:46:30 +08:00
uploading.value = false;
}
}
2025-09-04 22:19:56 +08:00
// 发布
2025-07-21 15:46:30 +08:00
Future<void> submitForm() async {
2025-09-04 22:19:56 +08:00
logger.w(descriptionController.text);
2025-09-06 14:57:47 +08:00
descFocusNode.unfocus();
if (fileId.value.isEmpty) {
2025-07-21 15:46:30 +08:00
Get.snackbar('请先上传视频', '未检测到上传的视频');
return;
}
if (descriptionController.text.trim().isEmpty) {
Get.snackbar('请填写视频描述', '描述是必填项');
return;
}
try {
2025-09-04 22:19:56 +08:00
final data = {
'url': uploadedVideoUrl.value,
'title': descriptionController.text.trim(),
2025-09-06 14:57:47 +08:00
'fileId': fileId.value,
'vlogerId': Get.find<ImUserInfoController>().userID.value,
'width': selectedVideo.value!.width,
'height': selectedVideo.value!.height,
2025-09-04 22:19:56 +08:00
};
if (uploadedImgUrl.value.isNotEmpty) {
data['cover'] = uploadedImgUrl.value;
}
logger.w('发布内容:$data');
await Http.post(VideoApi.publish, data: data);
Get.snackbar('发布成功', '视频正在审核中');
2025-07-21 15:46:30 +08:00
clearForm();
} catch (e) {
Get.snackbar('发布失败', '$e');
2025-09-04 22:19:56 +08:00
} finally {}
2025-07-21 15:46:30 +08:00
}
void clearForm() {
selectedVideo.value = null;
selectedCover.value = null;
2025-09-04 22:19:56 +08:00
uploadedImgUrl.value = '';
uploadedVideoUrl.value = '';
2025-07-21 15:46:30 +08:00
descriptionController.clear();
2025-09-04 22:19:56 +08:00
descFocusNode.unfocus();
2025-07-21 15:46:30 +08:00
status.value = '';
2025-09-04 22:19:56 +08:00
status2.value = '';
2025-07-21 15:46:30 +08:00
uploadProgress.value = 0.0;
2025-09-04 22:19:56 +08:00
uploadProgress2.value = 0.0;
videoPath.value = '';
2025-09-06 14:57:47 +08:00
// 本地回显
snapshot.value = '';
imgPath.value = '';
2025-09-04 22:19:56 +08:00
}
// 预览视频
void openVd() {
2025-09-06 14:57:47 +08:00
descFocusNode.unfocus();
2025-09-04 22:19:56 +08:00
showGeneralDialog(
context: context,
// barrierDismissible: true,
barrierColor: Colors.black.withAlpha((1.0 * 255).round()),
pageBuilder: (_, __, ___) {
return SafeArea(
child: PreviewVideo(
videoUrl: videoPath.value,
),
);
},
transitionBuilder: (_, anim, __, child) {
return FadeTransition(opacity: anim, child: child);
},
transitionDuration: const Duration(milliseconds: 200),
);
}
void openImg() {
// 图片预览
Get.to(() => ImageViewer(
images: [imgPath.value],
index: 0,
));
2025-07-21 15:46:30 +08:00
}
@override
void dispose() {
2025-09-04 22:19:56 +08:00
descriptionController.dispose();
2025-07-21 15:46:30 +08:00
descFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
2025-09-06 14:57:47 +08:00
return FocusScope(
canRequestFocus: widget.visible,
// 只有 UploadVideoPage 可见时才允许 TextField 聚焦
child: GestureDetector(
onTap: () => descFocusNode.unfocus(),
2025-07-21 15:46:30 +08:00
child: Scaffold(
appBar: AppBar(title: const Text('上传视频')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
focusNode: descFocusNode,
2025-09-04 22:19:56 +08:00
autofocus: false,
controller: descriptionController,
2025-07-21 15:46:30 +08:00
decoration: const InputDecoration(
labelText: '视频描述 *',
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 20),
2025-09-06 14:57:47 +08:00
// 视频上传区
2025-07-21 15:46:30 +08:00
Obx(() {
return Row(
2025-09-04 22:19:56 +08:00
crossAxisAlignment: CrossAxisAlignment.center,
2025-07-21 15:46:30 +08:00
children: [
2025-09-04 22:19:56 +08:00
snapshot.value.isNotEmpty
? GestureDetector(
onTap: () => openVd(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(snapshot.value),
width: 80,
fit: BoxFit.cover,
),
),
)
: const SizedBox(
height: 80,
child: Center(
child: Text(
'未选择视频',
style: TextStyle(fontSize: 16),
)),
),
const SizedBox(width: 24),
2025-07-21 15:46:30 +08:00
Expanded(
2025-09-04 22:19:56 +08:00
child: uploading.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 10),
LinearProgressIndicator(value: uploadProgress.value),
const SizedBox(height: 5),
Text('${(uploadProgress.value * 100).toStringAsFixed(1)}%'),
],
)
: ElevatedButton.icon(
onPressed: pickVideo,
icon: const Icon(Icons.upload),
label: Text(uploadedVideoUrl.value.isNotEmpty ? '重新选择' : '选择视频'),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
),
),
2025-07-21 15:46:30 +08:00
),
],
);
}),
const SizedBox(height: 20),
2025-09-06 14:57:47 +08:00
// 图片上传区
2025-07-21 15:46:30 +08:00
Obx(() {
return Row(
2025-09-04 22:19:56 +08:00
crossAxisAlignment: CrossAxisAlignment.center,
2025-07-21 15:46:30 +08:00
children: [
2025-09-04 22:19:56 +08:00
imgPath.value.isNotEmpty
? GestureDetector(
onTap: () => openImg(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(imgPath.value),
width: 80,
fit: BoxFit.cover,
),
),
2025-07-21 15:46:30 +08:00
)
2025-09-04 22:19:56 +08:00
: const SizedBox(
height: 80,
child: Center(
child: Text(
'未选择封面',
style: TextStyle(fontSize: 16),
)),
),
const SizedBox(width: 24),
Expanded(
child: uploading2.value
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 10),
LinearProgressIndicator(value: uploadProgress2.value),
const SizedBox(height: 5),
Text('${(uploadProgress2.value * 100).toStringAsFixed(1)}%'),
],
)
: ElevatedButton.icon(
onPressed: pickCoverImage,
icon: const Icon(Icons.upload),
label: Text(uploadedImgUrl.value.isNotEmpty ? '重新选择' : '选择封面(可选)'),
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(50),
),
),
2025-07-21 15:46:30 +08:00
),
],
);
}),
const SizedBox(height: 30),
2025-09-04 22:19:56 +08:00
// 发布按钮
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: submitForm,
icon: const Icon(Icons.upload),
label: const Text('发布'),
),
),
2025-07-21 15:46:30 +08:00
],
),
),
2025-09-06 14:57:47 +08:00
),
),
);
2025-07-21 15:46:30 +08:00
}
}