flutter/lib/pages/upload_video_page/upload_video_page.dart
2025-09-17 15:32:18 +08:00

470 lines
15 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/IM/controller/im_user_info_controller.dart';
import 'package:loopin/IM/im_service.dart';
import 'package:loopin/api/common_api.dart';
import 'package:loopin/api/video_api.dart';
import 'package:loopin/components/image_viewer.dart';
import 'package:loopin/components/preview_video.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/utils/index.dart';
import 'package:loopin/utils/permissions.dart';
import 'package:loopin/utils/snapshot.dart';
import 'package:shirne_dialog/shirne_dialog.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class UploadVideoPage extends StatefulWidget {
final bool visible;
const UploadVideoPage({super.key, required this.visible});
@override
State<UploadVideoPage> createState() => _UploadVideoPageState();
}
class _UploadVideoPageState extends State<UploadVideoPage> {
final selectedVideo = Rxn<AssetEntity>();
final selectedCover = Rxn<AssetEntity>();
//视频
final uploading = false.obs;
final uploadProgress = 0.0.obs;
final status = ''.obs;
//图片
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;
// 文件id
final fileId = ''.obs;
late final FocusNode descFocusNode;
late TextEditingController descriptionController;
@override
void initState() {
super.initState();
descFocusNode = FocusNode();
descriptionController = TextEditingController();
}
Future<void> pickVideo() async {
descFocusNode.unfocus();
final hasPer = await Permissions.requestVideoPermission();
if (!hasPer) {
Permissions.showPermissionDialog('相册');
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) {
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();
}
}
}
}
// 选择封面图
Future<void> pickCoverImage() async {
descFocusNode.unfocus();
final hasPer = await Permissions.requestPhotoPermission();
if (!hasPer) {
Permissions.showPermissionDialog('相册');
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) {
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();
}
}
}
}
// 上传图片
Future<void> uploadImg() async {
final coverFile = await selectedCover.value?.file;
if (coverFile == null) {
status.value = '未选择封面图';
return;
}
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 = '未选择视频';
return;
}
uploading.value = true;
uploadProgress.value = 0;
status.value = '上传中...';
videoPath.value = file.path;
logger.w(videoPath.value);
// vwidth.value = file.width;
snapshot.value = (await generateVideoThumbnail(file.path))!;
logger.w(snapshot.value);
try {
final res = await Http.upload(
CommonApi.uploadFile,
filePath: file.path,
fileKey: 'file',
onSendProgress: (sent, total) {
if (total > 0) {
final pro = sent / total;
uploadProgress.value = pro.clamp(0, 0.999);
}
},
);
logger.w('上传结果$res');
uploadedVideoUrl.value = res['data']['url'];
fileId.value = res['data']['ossId'];
status.value = '上传成功';
descFocusNode.unfocus();
uploading.value = false;
} catch (e) {
if (e is SocketException) {
status.value = '网络错误,请检查连接';
} else {
status.value = '上传失败: ${e.toString()}';
}
logger.e(e);
descFocusNode.unfocus();
} finally {
descFocusNode.unfocus();
uploading.value = false;
}
}
// 发布
Future<void> submitForm() async {
logger.w(descriptionController.text);
descFocusNode.unfocus();
if (fileId.value.isEmpty) {
Get.snackbar('请先上传视频', '未检测到上传的视频');
return;
}
if (descriptionController.text.trim().isEmpty) {
Get.snackbar('请填写视频描述', '描述是必填项');
return;
}
try {
final data = {
'url': uploadedVideoUrl.value,
'title': descriptionController.text.trim(),
'fileId': fileId.value,
'vlogerId': Get.find<ImUserInfoController>().userID.value,
'width': selectedVideo.value!.width,
'height': selectedVideo.value!.height,
};
if (uploadedImgUrl.value.isNotEmpty) {
data['cover'] = uploadedImgUrl.value;
}
logger.w('发布内容:$data');
await Http.post(VideoApi.publish, data: data);
Get.snackbar('发布成功', '视频正在审核中');
clearForm();
} catch (e) {
Get.snackbar('发布失败', '$e');
} finally {}
}
void clearForm() {
selectedVideo.value = null;
selectedCover.value = null;
uploadedImgUrl.value = '';
uploadedVideoUrl.value = '';
descriptionController.clear();
descFocusNode.unfocus();
status.value = '';
status2.value = '';
uploadProgress.value = 0.0;
uploadProgress2.value = 0.0;
videoPath.value = '';
// 本地回显
snapshot.value = '';
imgPath.value = '';
}
// 预览视频
void openVd() {
descFocusNode.unfocus();
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,
));
}
@override
void dispose() {
descriptionController.dispose();
descFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FocusScope(
canRequestFocus: widget.visible,
// 只有 UploadVideoPage 可见时才允许 TextField 聚焦
child: GestureDetector(
onTap: () => descFocusNode.unfocus(),
child: Scaffold(
appBar: AppBar(title: const Text('上传视频')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
focusNode: descFocusNode,
autofocus: false,
controller: descriptionController,
decoration: const InputDecoration(
labelText: '视频描述 *',
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 20),
// 视频上传区
Obx(() {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
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),
Expanded(
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),
),
),
),
],
);
}),
const SizedBox(height: 20),
// 图片上传区
Obx(() {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
imgPath.value.isNotEmpty
? GestureDetector(
onTap: () => openImg(),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.file(
File(imgPath.value),
width: 80,
fit: BoxFit.cover,
),
),
)
: 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),
),
),
),
],
);
}),
const SizedBox(height: 30),
// 发布按钮
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: submitForm,
icon: const Icon(Icons.upload),
label: const Text('发布'),
),
),
],
),
),
),
),
);
}
}