flutter/lib/pages/upload_video_page/upload_video_page.dart

276 lines
8.6 KiB
Dart
Raw Normal View History

2025-07-21 15:46:30 +08:00
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loopin/api/common_api.dart';
import 'package:loopin/api/video_api.dart';
import 'package:loopin/service/http.dart';
import 'package:loopin/utils/index.dart';
import 'package:wechat_assets_picker/wechat_assets_picker.dart';
class UploadVideoPage extends StatefulWidget {
const UploadVideoPage({super.key});
@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 descController = TextEditingController();
final FocusNode descFocusNode = FocusNode();
final uploadedVideoUrl = ''.obs;
final TextEditingController descriptionController = TextEditingController();
Future<void> pickVideo() async {
final result = await PhotoManager.requestPermissionExtend();
if (!result.isAuth) {
if (!mounted) return;
Get.snackbar('权限被拒绝', '请前往设置授权访问视频');
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) {
selectedVideo.value = pickedAssets.first;
status.value = '已选择视频';
}
}
Future<void> pickCoverImage() async {
final result = await PhotoManager.requestPermissionExtend();
if (!result.isAuth) {
if (!mounted) return;
Get.snackbar('权限被拒绝', '请前往设置授权访问照片');
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) {
selectedCover.value = pickedAssets.first;
}
}
Future<void> uploadVideo() async {
final file = await selectedVideo.value?.file;
final coverFile = await selectedCover.value?.file;
final desc = descController.text.trim();
if (file == null) {
status.value = '未选择视频';
return;
}
if (desc.isEmpty) {
Get.snackbar('请填写描述', '视频描述不能为空');
return;
}
uploading.value = true;
uploadProgress.value = 0;
status.value = '上传中...';
try {
final data = await Http.upload(
CommonApi.uploadFile,
filePath: file.path,
fileKey: 'file',
onSendProgress: (sent, total) {
if (total > 0) {
uploadProgress.value = sent / total;
}
},
);
uploadedVideoUrl.value = data;
status.value = '上传成功';
// 清空表单
descController.clear();
selectedVideo.value = null;
selectedCover.value = null;
uploadedVideoUrl.value = '';
} catch (e) {
if (e is SocketException) {
status.value = '网络错误,请检查连接';
} else {
status.value = '上传失败: ${e.toString()}';
}
} finally {
uploading.value = false;
}
}
Future<void> submitForm() async {
if (uploadedVideoUrl.value.isEmpty) {
Get.snackbar('请先上传视频', '未检测到上传的视频');
return;
}
if (descriptionController.text.trim().isEmpty) {
Get.snackbar('请填写视频描述', '描述是必填项');
return;
}
try {
await Http.post(VideoApi.publish, data: {
'video_url': uploadedVideoUrl.value,
'desc': descriptionController.text.trim(),
'cover': selectedCover.value,
});
Get.snackbar('发布成功', '视频已成功发布');
clearForm();
} catch (e) {
Get.snackbar('发布失败', '$e');
}
}
void clearForm() {
selectedVideo.value = null;
selectedCover.value = null;
descriptionController.clear();
status.value = '';
uploadProgress.value = 0.0;
uploadedVideoUrl.value = '';
}
@override
void dispose() {
descController.dispose();
descFocusNode.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: Scaffold(
appBar: AppBar(title: const Text('上传视频')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
TextField(
focusNode: descFocusNode,
controller: descController,
decoration: const InputDecoration(
labelText: '视频描述 *',
border: OutlineInputBorder(),
),
maxLines: 2,
),
const SizedBox(height: 20),
Obx(() {
final video = selectedVideo.value;
return Row(
children: [
Expanded(
child: Text(
video != null ? '已选择视频:${video.title}' : '未选择视频',
style: const TextStyle(fontSize: 16),
),
),
ElevatedButton(
onPressed: pickVideo,
child: const Text('选择视频'),
),
],
);
}),
const SizedBox(height: 20),
Obx(() {
final cover = selectedCover.value;
return Row(
children: [
cover != null
? FutureBuilder<Uint8List?>(
future: cover.thumbnailDataWithSize(const ThumbnailSize(80, 80)),
builder: (_, snapshot) {
if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(snapshot.data!, width: 80, height: 80),
);
} else {
return const SizedBox(width: 80, height: 80);
}
},
)
: const Text('未选择封面图'),
const SizedBox(width: 12),
ElevatedButton(
onPressed: pickCoverImage,
child: const Text('选择封面(可选)'),
),
],
);
}),
const SizedBox(height: 30),
Obx(() => uploading.value
? Column(
children: [
const CircularProgressIndicator(),
const SizedBox(height: 10),
LinearProgressIndicator(value: uploadProgress.value),
const SizedBox(height: 5),
Text('${(uploadProgress.value * 100).toStringAsFixed(1)}%'),
],
)
: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: uploadVideo,
icon: const Icon(Icons.upload),
label: const Text('上传'),
),
)),
const SizedBox(height: 20),
Obx(() => Text(status.value)),
],
),
),
));
}
}