初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
@@ -0,0 +1,55 @@
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/api_service/search_service.dart';
import 'package:hgdj/tools_base/debug_log.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'chose_tag_page.dart';
class ChoseTopicLogic extends GetxController {
ChoseTopicLogic({
required this.entry,
List<TagsBean> initialSelectTags = const [],
}) : selectTags = initialSelectTags.obs;
// ========== 属性声明 ==========
final ChoseTopicEntry entry;
final RxList<TagsBean> selectTags;
final dataSource = <TagsBean>[];
RefreshController? refreshCtr;
bool isLoading = true;
// ========== 生命周期 ==========
@override
void onReady() {
super.onReady();
loadTags();
}
// ========== 公开方法 ==========
/// 标签一次拉全量,接口本身没有分页
Future<void> loadTags() async {
try {
dataSource.addAll(await SearchService.fetchPublishTags());
} catch (e) {
//失败也要收 loading,否则页面一直转圈
debugLog('fetchPublishTags $e');
}
refreshCtr?.loadNoData();
isLoading = false;
update();
}
/// 单选并返回
void pickSingleAndBack(TagsBean model) {
//点的就是已选中那项:不必重复赋值,直接关页面(原来 return 掉会点了没反应)
if (selectTags.any((e) => e.id == model.id)) {
Get.back();
return;
}
selectTags
..clear()
..add(model);
Get.back(result: model);
}
}
@@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/community/publish_page/chose_tag_logic.dart';
import 'package:hgdj/hj_page/home/search_page/widget/topic_item.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
enum ChoseTopicEntry {
home('选择标签'),
squire('选择话题');
final String title;
const ChoseTopicEntry(this.title);
}
class ChoseTopicPage extends StatelessWidget {
final ChoseTopicEntry? entry;
final List<TagsBean>? selectTags;
const ChoseTopicPage({super.key, this.entry, this.selectTags});
@override
Widget build(BuildContext context) {
return GetBuilder<ChoseTopicLogic>(
init: ChoseTopicLogic(
entry: entry ?? ChoseTopicEntry.home,
initialSelectTags: selectTags ?? const [],
),
global: false,
builder: (logic) => Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(logic.entry.title),
),
body: pullYsRefresh(
onInit: (ctr) => logic.refreshCtr = ctr,
//标签一次拉全量,没有下拉刷新;不关掉的话下拉会卡在转圈(onRefresh 为空没人收)
enablePullDown: false,
child: _buildBody(logic),
),
),
);
}
Widget _buildBody(ChoseTopicLogic logic) {
if (logic.isLoading) return LoadingCenterWidget();
if (logic.dataSource.isEmpty)
return CErrorWidget(retryOnTap: logic.loadTags);
return _buildList(logic);
}
/// 单选 list
Widget _buildList(ChoseTopicLogic logic) {
return ListView.separated(
padding: EdgeInsets.only(left: 18.w, right: 18.w, top: 14.h),
separatorBuilder: (_, __) => 12.h.sizeBoxH,
itemCount: logic.dataSource.length,
itemBuilder: (_, index) {
final model = logic.dataSource[index];
final selected =
logic.selectTags.indexWhere((e) => e.id == model.id) != -1;
return TopicItem(
model: model,
isSelect: selected,
onTap: () => logic.pickSingleAndBack(model),
);
},
);
}
}
@@ -0,0 +1,384 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:image_pickers/image_pickers.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
import 'package:hgdj/hj_utils/file_util.dart';
import 'package:hgdj/hj_utils/media_info.dart';
import 'package:hgdj/tools_base/file_upload/file_upload_tool.dart';
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:path_provider/path_provider.dart';
import 'package:video_thumbnail/video_thumbnail.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
import 'publish_page.dart';
/// 社区发帖的视频载体:单条视频的本地/远程地址 + 元信息
class PublishVideoModel {
String? localPath;
String? localCover;
String? remotePath;
String? remoteCover;
String? customCover;
double? ratio;
String? name;
String? md5;
int? playTime;
int? size;
String? sourceid;
String? resolution;
PublishVideoModel(
{this.localPath,
this.localCover,
this.remotePath,
this.remoteCover,
this.ratio,
this.name,
this.playTime,
this.size,
this.sourceid,
this.resolution,
this.md5,
this.customCover});
copy() {
return PublishVideoModel(
localCover: localCover,
localPath: localPath,
remotePath: remotePath,
remoteCover: remoteCover,
resolution: resolution,
ratio: ratio,
name: name,
playTime: playTime,
size: size,
sourceid: sourceid,
md5: md5,
customCover: customCover);
}
}
class PublishLogic extends GetxController {
// Rx<TagsBean?> topic = Rx(null);
RxList<TagsBean> topics = <TagsBean>[].obs;
Rx<String?> gold = Rx(null);
RxList<String> imgs = [''].obs;
final describeController = TextEditingController();
final titleController = TextEditingController();
final titleFouceN = FocusNode();
final contentFouceN = FocusNode();
final goldFouceN = FocusNode();
// 链接的视频
Rx<PublishVideoModel> video = Rx(PublishVideoModel());
final PublishType type;
PublishLogic(this.type);
@override
void onClose() {
describeController.dispose();
titleController.dispose();
titleFouceN.dispose();
contentFouceN.dispose();
goldFouceN.dispose();
super.onClose();
}
void unfocus() {
if (titleFouceN.context != null && titleFouceN.hasFocus) {
titleFouceN.unfocus();
}
if (contentFouceN.context != null && contentFouceN.hasFocus) {
contentFouceN.unfocus();
}
if (goldFouceN.context != null && goldFouceN.hasFocus) {
goldFouceN.unfocus();
}
}
/// 视频封面相关
choseCover() async {
final listMedia = await ImagePickers.pickerPaths(
uiConfig: UIConfig(uiThemeColor: Colors.white),
galleryMode: GalleryMode.image,
selectCount: 1,
showCamera: false,
);
if (listMedia.isEmpty) return;
video.value = video.value.copy()..customCover = listMedia.first.path ?? '';
}
deleteCover() {
video.value = video.value.copy()..customCover = null;
}
///视频相关
choseVideo() async {
var pickedFile = await ImagePicker().pickVideo(source: ImageSource.gallery);
if (pickedFile == null) return null;
if (!File(pickedFile.path).existsSync()) {
showToast("用户视频文件损坏或格式错误");
return;
}
var path = pickedFile.path;
final mediaInfo = await _checkVideoRule(path);
if (mediaInfo == null) return;
final coverPath_ = await VideoThumbnail.thumbnailFile(
video: path,
thumbnailPath: (await getTemporaryDirectory()).path,
imageFormat: ImageFormat.PNG,
quality: 100,
);
final videoM_ = PublishVideoModel(
localCover: coverPath_,
localPath: path,
ratio: mediaInfo.ratio ?? 1,
name: "${DateTime.now().toIso8601String()}.mp4",
playTime: mediaInfo.playTime,
size: mediaInfo.size,
resolution: mediaInfo.resolution);
video.value = videoM_..customCover = video.value.customCover;
}
deleteSelectVideo() {
video.value = PublishVideoModel()..customCover = video.value.customCover;
}
/// 图集相关
choseImg() async {
final listMedia = await ImagePickers.pickerPaths(
uiConfig: UIConfig(uiThemeColor: Colors.white),
galleryMode: GalleryMode.image,
selectCount: 9 - (imgs.length - 1),
showCamera: false,
);
if (listMedia.isEmpty) return;
final ret = listMedia.map((e) => e.path ?? '').toList();
imgs.insertAll(0, ret);
}
publish() async {
final res = _checkEnable();
if (!res) return;
FocusManager.instance.primaryFocus?.unfocus();
//上传图片前 先移除第一个占位
List<String> imgs_ = [];
if (type != PublishType.squireVideo) {
imgs_.addAll(imgs);
imgs_.remove('');
}
// 上传图片(封面),成功后再走视频上传 + 提交
FileUploadTool().uploadImagesWithProgress(
imgs_,
onFailure: () => showToast("图片上传失败"),
onSuccess: (seriesCover) => _publishWithCover(seriesCover),
);
}
/// 封面上传完成后:上传视频(可选)→ 提交发布
Future<void> _publishWithCover(List<String> seriesCover) async {
// 无视频:直接提交
if (video.value.localPath == null) {
_submitPost(seriesCover, null);
return;
}
// 有视频:取元信息 → 校验封面 → 上传视频 → 提交
// 取元信息期间也要有遮挡,否则这段空窗期发布按钮可重复点
LoadingAlertWidget.show(title: "正在处理视频...");
final mediaInfo = await getMediaInfo(video.value.localPath ?? "");
LoadingAlertWidget.cancel();
video.value.remoteCover = seriesCover.first;
if (video.value.remoteCover == null) {
showToast("视频封面上传失败");
return;
}
FileUploadTool().uploadVideoWithProgress(
video.value.localPath ?? '',
onFailure: () => showToast("视频上传失败"),
onSuccess: (fileR) {
video.value.remotePath = fileR.videoUri;
video.value.md5 = fileR.md5;
video.value.sourceid = fileR.id;
_submitPost(seriesCover, mediaInfo);
},
);
}
/// 提交发布([mediaInfo] 仅视频帖有)
Future<void> _submitPost(
List<String> seriesCover, MediaInfo? mediaInfo) async {
final result = await VidService.submit(
title: titleController.text,
newsType: _generateNewsType(),
tags: topics.map((element) => element.id ?? '').toList(),
seriesCover: seriesCover,
coins: int.parse(gold.value ?? '0'),
playTime: mediaInfo?.playTime,
mimeType: mediaInfo != null ? "mp4" : null,
content: describeController.text,
sourceID: video.value.sourceid,
md5: video.value.md5,
ratio: video.value.ratio,
size: video.value.size,
resolution: video.value.resolution,
sourceURL: video.value.remotePath,
cover: video.value.remoteCover,
);
if (result) {
await CommonAlert.show(
title: "发布成功",
content: '帖子发布后将在24小时内进行审核,您可在【我的】-【我的帖子】中进行查看',
showCancel: false,
);
Get.back();
} else {
showToast('发布失败');
}
}
/// 检查本地视频
Future<MediaInfo?> _checkVideoRule(String path) async {
if (path.isEmpty) {
showToast("视频地址错误");
return null;
}
MediaInfo? videoInfo = await getMediaInfo(path);
if (videoInfo == null) {
showToast('用戶視頻文件損壞或格式錯誤');
return null;
}
//时间
if ((videoInfo.playTime ?? 0) < 30 && (videoInfo.playTime ?? 0) > 0) {
showToast('請選擇30秒以上視頻');
return null;
}
//大小
int sizeM = (videoInfo.size ?? 0) ~/ (MB_SIZE);
if (sizeM > 300 || sizeM == 0) {
showToast('請選擇250M內視頻');
return null;
}
//分辨率
String resolution = videoInfo.resolution ?? '';
if (resolution.isEmpty || !resolution.contains("*")) {
showToast('用戶視頻文件損壞或格式錯誤');
return null;
}
List<String> list = resolution.split("*");
var curResolution = double.parse(list[0]) * double.parse(list[1]);
if (curResolution < 360 * 360) {
showToast('請選擇分辨率大於360*360以上視頻');
return null;
}
return videoInfo;
}
bool _checkEnable() {
if (type == PublishType.homeImgText || type == PublishType.homeImg) {
if (titleController.text.isEmpty) {
showToast('请输入标题!');
return false;
}
if (describeController.text.isEmpty && type == PublishType.homeImgText) {
showToast('请输入描述!');
return false;
}
if (imgs.length <= 1) {
showToast('请选择图片!');
return false;
}
if (topics.isEmpty) {
showToast('请选择话题哦~');
return false;
}
return true;
}
if (type == PublishType.homeVideo) {
if (titleController.text.isEmpty) {
showToast('请输入标题!');
return false;
}
if (describeController.text.isEmpty) {
showToast('请输入描述!');
return false;
}
if (video.value.localPath == null) {
showToast('请选择需要上传的视频!');
return false;
}
if (imgs.length <= 1) {
showToast('请选择图片!');
return false;
}
if (topics.isEmpty) {
showToast('请选择话题哦~');
return false;
}
return true;
}
if (type == PublishType.squireImgText) {
if (topics.isEmpty) {
showToast('至少选择一个话题哦~');
return false;
}
if (describeController.text.isEmpty) {
showToast('请输入描述!');
return false;
}
if (imgs.length == 1) {
showToast('请选择图片!');
return false;
}
return true;
}
if (topics.isEmpty) {
showToast('至少选择一个话题哦~');
return false;
}
if (describeController.text.isEmpty) {
showToast('请输入描述!');
return false;
}
if (video.value.localPath == null) {
showToast('请选择需要上传的视频!');
return false;
}
return true;
}
String _generateNewsType() {
if (type == PublishType.homeImg ||
type == PublishType.homeImgText ||
type == PublishType.squireImgText) return 'COVER';
final duration = video.value.playTime ?? 0;
if (duration <= 300) return 'SP';
return 'SP';
}
}
@@ -0,0 +1,143 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/hj_page/community/publish_page/chose_tag_page.dart';
import 'package:hgdj/hj_page/community/publish_page/publish_logic.dart';
import 'package:hgdj/hj_page/community/publish_page/publish_rule_page.dart';
import 'package:hgdj/hj_page/community/publish_page/widget/publish_widget.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/common_alert.dart';
enum PublishType {
homeImg(RuleType.home, ChoseTopicEntry.home, '发布图片'),
homeImgText(RuleType.home, ChoseTopicEntry.home, '发布图文'),
homeVideo(RuleType.home, ChoseTopicEntry.home, '发布视频'),
squireImgText(RuleType.squire, ChoseTopicEntry.squire, '发布帖子'),
squireVideo(RuleType.squire, ChoseTopicEntry.squire, '发布帖子');
final RuleType rule;
final ChoseTopicEntry tag;
final String title;
const PublishType(this.rule, this.tag, this.title);
}
extension PublishMore on PublishType {
/// 各类型的表单项,顺序即页面从上到下的排列
List<Widget> get children => switch (this) {
PublishType.homeImg => [
TopicWidget(),
InputTitleWidget(),
ChoseImageWidget(highlightText: '添加图集'),
],
PublishType.homeImgText => [
TopicWidget(),
InputTitleWidget(),
InputDescribeWidget(),
ChoseImageWidget(highlightText: '添加图集'),
],
PublishType.homeVideo => [
TopicWidget(),
InputTitleWidget(),
InputDescribeWidget(),
ChoseVideoWidget(),
ChoseImageWidget(
hint: '「默认第一张为封面」图片数量最多9张哦~',
highlightText: '「默认第一张为封面」',
highlightTextSize: 12.sp,
highlightTextColor: const Color(0xffB40400),
),
SettingGoldWidget(),
],
PublishType.squireImgText => [
TopicWidget(),
InputDescribeWidget(),
ChoseImageWidget(hint: '第一张图集默认为封面'),
],
PublishType.squireVideo => [
TopicWidget(),
InputDescribeWidget(),
ChoseVideoWidget(),
],
};
}
class PublishPage extends StatelessWidget {
final PublishType type;
const PublishPage({super.key, this.type = PublishType.homeImgText});
@override
Widget build(BuildContext context) {
return GetBuilder<PublishLogic>(
init: PublishLogic(type),
builder: (logic) => Scaffold(
appBar: AppBar(
centerTitle: true,
title: Text(logic.type.title),
actions: [
TextButton(
onPressed: () => CommonAlert.show(
title: '发布规则',
content: '· 禁止发布广告图文 (含Line、WeChat、QQ、网址、二维码等外链)\n'
'· 禁止发布诱导图文 (求网址、求软体等)\n'
'· 禁止发布儿童色情、兽*内容\n'
'· 禁止发布交易相关、政治等内容',
subContent: '请遵守社区规则,否则会被官方机器人拉黑',
showCancel: false,
confirmText: '我知道了',
),
child:
const Text('规则', style: TextStyle(color: Color(0x8CFFFFFF))),
),
18.w.sizeBoxW,
],
),
//点空白处收起键盘
body: InkWell(
enableFeedback: false,
onTap: logic.unfocus,
child: Container(
margin: EdgeInsets.symmetric(horizontal: 16.w),
width: double.infinity,
child: SafeArea(
child: Column(
children: [
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: logic.type.children,
),
),
),
//确定发布
GestureDetector(
onTap: logic.publish,
child: Container(
margin: EdgeInsets.symmetric(vertical: 12.h),
padding: EdgeInsets.symmetric(vertical: 10.h),
alignment: Alignment.center,
width: double.infinity,
decoration: BoxDecoration(
color: AppColors.actionRed,
borderRadius: BorderRadius.circular(3),
),
child: const Text(
'确定发布',
style: TextStyle(
color: Colors.white, fontSize: 16, height: 27 / 14),
),
),
),
],
),
),
),
),
),
);
}
}
@@ -0,0 +1,93 @@
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:flutter/material.dart';
const homeRule = '''欢迎优秀的您加入黑料社,我们珍视每一位UP主,始终致力于为大家带来最优质的产品与服务。您的作品在黑料社的售卖无时间、次数限制,销售的次数越多,获得的收入就会越多。无论您是在睡觉还是在工作,它们都将持续为您带来源源不断的收入。
上传规则
1.UP主及普通用户上传收费视频比例为2:1,即上传2个免费视频才可上传1个收费视频。
2.原创举牌up主上传收费视频比例为1:1,即上传1个免费视频才可上传1个收费视频。
3.视频清晰度需在360P以上,且时长不小于30秒。
4.套图大于6张才可以设置价格。
5.审核时间为48小时内,请在[创作中心]查收反馈。
6.视频中的当事人须满18岁以上,且当事人同意视频被上传分享。
审核规则
1.原创拍摄、原创剪辑作品,会更容易通过并获得官方推荐。
2.禁止直接搬运网络视频,重复率高且不容易通过,多次违规将降低账号推荐权重。
3.禁止在视频/图片中添加个人联系方式或插入广告网址将不会通过审核。
4.禁止上传幼女、人兽、真实强奸等侵害他人的视频。
5.加强用户隐私性,允许原创视频为人物面部等重要部分添加遮挡或马赛克。
6.上传的视频内容不符合上传要求将不会通过审核,如若退回视频未作修改再次发起审核将禁止上传。
定价规则
1.发布内容默认为免费,用户可根据作品内容质量调整为金币视频。
2.认证UP主发布原创举牌长视频,建议定价50-200金币
3.原创举牌作品,建议定价30-50金币
4.原创剪辑作品,建议定价10-20金币
5.非原创短片,建议定价1-10金币''';
const squireRule = '''● 添加合适的话题:通过添加#话题 给内容打上标签,让你的内容被推荐给更多有共同语言的Tapper~
● 用心起标题:用10-20个字总结:”想要表达的观点“或”想解决的问题“
● 第一张图很重要:图文的第一张图会作为封面。图片需突出重点,放大关键信息。视频同理。
● 正文简单明了:段落清晰,一句一行!用标点符号、emoji表情进行分段分区!
''';
const treeCaveRule = '''抽取纸条规则:
1.同一张纸条可以被多人抽取到
2.每个用户每天有5次免费抽取纸条机会,超出后每次抽取需要支付10积分;
3.纸条抽取次数不可累计,如当天未使用凌晨00:00分自动清零重新增加5次
投递纸条规则:
1.用户放入纸条每次需要支付5积分
2.不能发布广告,下载链接等引流信息
3.不能如111,666这类的无意义信息
''';
enum RuleType {
home(homeRule),
squire(squireRule),
treeCave(treeCaveRule);
final String content;
const RuleType(this.content);
}
class PublishRulePage extends StatelessWidget {
final RuleType type;
const PublishRulePage({super.key, this.type = RuleType.home});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('发布规则'),
),
body: SingleChildScrollView(
// controller: controller,
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: EasyRichText(
type.content,
defaultStyle: TextStyle(color: Colors.white.withValues(alpha: .6), fontSize: 14),
patternList: [
EasyRichTextPattern(targetString: '认证UP主发布原创举牌长视频', style: TextStyle(color: Color(0xffF68216), fontSize: 14)),
EasyRichTextPattern(targetString: '抽取纸条规则:', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)),
EasyRichTextPattern(targetString: '投递纸条规则:', style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600))
],
),
),
);
}
}
@@ -0,0 +1,428 @@
import 'dart:io';
import 'package:easy_rich_text/easy_rich_text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/tools_base/widget/add_media_source_button.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import '../../../home/widget/support_up_alert.dart';
import '../chose_tag_page.dart';
import '../publish_logic.dart';
import 'video_cover_widget.dart';
/// 发布页各输入区统一的卡片底色
const publishDecoration = BoxDecoration(
color: Color(0x0DFFFFFF),
borderRadius: BorderRadius.all(Radius.circular(10)),
);
enum LinkType { topic, video, link }
/// 添加视频
class ChoseVideoWidget extends StatelessWidget {
ChoseVideoWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text('添加视频',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500)),
10.sizeBoxW,
Text('最大300M以内',
style: TextStyle(color: Color(0xff666666), fontSize: 12.sp)),
],
),
14.h.sizeBoxH,
Obx(() {
final cellW = (Get.width - 52) / 3;
return Row(children: [
6.h.sizeBoxW,
if (controller.video.value.localPath == null)
AddMediaSourceButton(
width: cellW,
height: cellW,
onTap: controller.choseVideo,
isVideo: true)
else
_cover(cellW),
]);
})
],
),
);
}
/// 已选视频:封面 + 右上角删除
Widget _cover(double size) {
return Container(
constraints: const BoxConstraints(maxWidth: 230, maxHeight: 219),
child: Stack(
children: [
VideoCoverWidget(controller.video.value.localCover ?? '',
width: size, height: size, isLocal: true),
Positioned(
right: 6,
top: 6,
child: GestureDetector(
onTap: () => controller.deleteSelectVideo(),
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .8),
borderRadius: BorderRadius.circular(10)),
child:
const Icon(Icons.close, size: 12, color: Color(0xff333333)),
),
),
)
],
),
);
}
}
/// 选择标签
class TopicWidget extends StatelessWidget {
TopicWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Obx(
() => GestureDetector(
onTap: () async {
final model = await Get.to(ChoseTopicPage(
entry: controller.type.tag, selectTags: controller.topics));
if (model is TagsBean) {
controller.topics
..clear()
..add(model);
}
},
child: Container(
margin: EdgeInsets.only(top: 12.h),
padding: EdgeInsets.symmetric(horizontal: 14.w),
alignment: Alignment.centerLeft,
height: 60.h,
decoration: publishDecoration,
child: controller.topics.isEmpty ? _emptyTip() : _topicInfo(),
),
),
);
}
/// 未选标签时的占位提示
Widget _emptyTip() {
return Row(
children: [
Text("#",
style: TextStyle(
color: Color(0xff989898),
fontSize: 24.sp,
fontWeight: FontWeight.bold)),
10.sizeBoxW,
Text('选择标签',
style: TextStyle(
color: Color(0xff989898),
fontSize: 12.sp,
fontWeight: FontWeight.w500)),
const Spacer(),
Icon(Icons.arrow_forward_ios, size: 16.sp, color: Color(0xffDCDCDC)),
],
);
}
/// 已选标签:封面 + 名称(点击交给外层跳转,别再套手势,否则会截走外层的 onTap)
Widget _topicInfo() {
return Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: NetworkImageLoader(
imageUrl: controller.topics.first.coverImg ?? '',
width: 38,
height: 38),
),
5.sizeBoxW,
Text(
controller.topics.firstOrNull?.name ?? '',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500),
),
Spacer(),
Icon(Icons.arrow_forward_ios, size: 16.sp, color: Color(0xffDCDCDC)),
],
);
}
}
/// 输入标题
class InputTitleWidget extends StatelessWidget {
InputTitleWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Container(
height: 60.h,
margin: EdgeInsets.only(top: 14.h),
padding: EdgeInsets.symmetric(horizontal: 14.w),
alignment: Alignment.centerLeft,
decoration: publishDecoration,
child: TextField(
maxLines: 1,
textAlign: TextAlign.left,
textAlignVertical: TextAlignVertical.center,
style: TextStyle(color: Colors.white, fontSize: 16.sp, height: 22 / 15),
focusNode: controller.titleFouceN,
controller: controller.titleController,
decoration: InputDecoration(
border: InputBorder.none,
hintText: '请填写标题',
hintStyle: TextStyle(
color: Color(0xff525252), fontSize: 12.sp, height: 22 / 15),
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
);
}
}
/// 输入正文(右下角实时字数)
class InputDescribeWidget extends StatelessWidget {
InputDescribeWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return InkWell(
enableFeedback: false,
onTap: () => controller.contentFouceN.requestFocus(),
child: Container(
constraints: BoxConstraints(minHeight: 171.h),
margin: const EdgeInsets.only(top: 14),
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 11.h),
decoration: publishDecoration,
child: Stack(
children: [
TextField(
style: TextStyle(color: Colors.white, fontSize: 16.sp),
maxLength: 300,
maxLines: null,
focusNode: controller.contentFouceN,
controller: controller.describeController,
decoration: InputDecoration(
border: InputBorder.none,
hintStyle: TextStyle(color: Color(0xff525252), fontSize: 12.sp),
hintMaxLines: 10,
hintText: '有趣的介绍能让你的逼格提高N个档次!...',
counter: SizedBox(),
contentPadding: EdgeInsets.zero,
isDense: true,
),
),
Positioned(
bottom: 0,
right: 0,
// 字数计数:只监听不接管所有权(释放归 PublishLogic.onClose)
child: ValueListenableBuilder(
valueListenable: controller.describeController,
builder: (_, value, __) => Text(
'${value.text.length}/300',
style: TextStyle(color: Color(0xff757575), fontSize: 12),
),
),
)
],
),
),
);
}
}
/// 添加图片(最多 9 张,列表末位固定是「添加」占位)
class ChoseImageWidget extends StatelessWidget {
final String? hint;
final String? highlightText;
final Color? highlightTextColor;
final double? highlightTextSize;
ChoseImageWidget(
{super.key,
this.hint,
this.highlightText,
this.highlightTextColor,
this.highlightTextSize});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(top: 14),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(
children: [
Text('添加图片',
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w500)),
EasyRichText(
hint ?? '添加图集 图片数量最多上传9张哦~',
defaultStyle:
TextStyle(color: Color(0xff666666), fontSize: 12.sp),
patternList: [
EasyRichTextPattern(
targetString: highlightText ?? '',
style: TextStyle(
color: highlightTextColor ?? Colors.white,
fontSize: highlightTextSize ?? 16.sp,
fontWeight: FontWeight.w500),
)
],
),
],
),
14.h.sizeBoxH,
LayoutBuilder(builder: (__, constrains) {
final cellW = (constrains.maxWidth - 16) / 3;
final images = controller.imgs;
return Obx(
() => Wrap(
runSpacing: 8,
spacing: 8,
crossAxisAlignment: WrapCrossAlignment.start,
children: List.generate(images.length, (i) {
// 末位是占位项:满 10 项(9 张图)就不再给「添加」按钮
if (i < images.length - 1) return _imgCell(images[i], cellW);
if (i == 9) return const SizedBox.shrink();
return AddMediaSourceButton(
isVideo: false,
onTap: controller.choseImg,
height: cellW,
width: cellW,
title: "添加图片",
);
}),
),
);
}),
]),
);
}
/// 单张已选图片 + 右上角删除
Widget _imgCell(String path, double size) {
return ClipRRect(
borderRadius: BorderRadius.circular(8),
child: SizedBox(
width: size,
height: size,
child: Stack(
children: [
Image.file(File(path),
width: size, height: size, fit: BoxFit.cover),
Positioned(
right: 6,
top: 6,
child: GestureDetector(
onTap: () => controller.imgs.remove(path),
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: .8),
borderRadius: BorderRadius.circular(10)),
alignment: Alignment.center,
child: const Icon(Icons.close, size: 14),
),
),
)
],
),
),
);
}
}
/// 设置价格(金币)
class SettingGoldWidget extends StatelessWidget {
SettingGoldWidget({super.key});
final controller = Get.find<PublishLogic>();
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () async {
final res = await Get.bottomSheet(
SupportUPAlert(
isPublish: true,
selectCoin: int.tryParse(controller.gold.value ?? '')),
);
controller.gold.value = res?.toString();
},
child: Container(
margin: const EdgeInsets.only(top: 18),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: publishDecoration,
child: Row(
children: [
Image.asset('community_coin.webp'.communityPath,
width: 20, height: 20),
10.sizeBoxW,
const Text('设置价格',
style: TextStyle(
color: Color(0xFF989898),
fontSize: 12,
fontWeight: FontWeight.w600)),
const Spacer(),
Obx(
() => Text(
controller.gold.value == null
? '免费'
: '${controller.gold.value} 金币',
style: TextStyle(
color: controller.gold.value == null
? Color(0xFF989898)
: AppColors.actionRed,
// color: Color(0xffF68216),
fontSize: 12,
fontWeight: FontWeight.w400),
),
),
5.sizeBoxW,
const Icon(Icons.arrow_forward_ios,
color: Color(0xffDCDCDC), size: 12)
],
),
),
);
}
}
@@ -0,0 +1,57 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
/// 视频封面:本地/网络图 + 可选暗色遮罩 + 居中播放按钮
/// [ratio] 传了就按比例撑开,不传则由外部约束决定尺寸
class VideoCoverWidget extends StatelessWidget {
final String cover;
final double? ratio;
final double? width;
final double? height;
final bool isLocal;
final bool showMask;
final double borderRadius;
const VideoCoverWidget(
this.cover, {
super.key,
this.ratio,
this.width,
this.height,
this.isLocal = false,
this.showMask = false,
this.borderRadius = 8,
});
@override
Widget build(BuildContext context) {
final maskW = width ?? double.infinity;
final maskH = height ?? double.infinity;
final child = Stack(
alignment: Alignment.center,
children: [
if (isLocal)
Image.file(File(cover),
width: width, height: height, fit: BoxFit.cover)
else
NetworkImageLoader(
imageUrl: cover, width: maskW, height: maskH, borderRadius: 0),
if (showMask)
Container(
width: maskW,
height: maskH,
color: Colors.black.withValues(alpha: 0.4)),
Image.asset('circle_play.webp'.videoPath, width: 30, height: 30),
],
);
return ClipRRect(
borderRadius: BorderRadius.circular(borderRadius),
child: ratio == null
? child
: AspectRatio(aspectRatio: ratio! > 0 ? ratio! : 1, child: child),
);
}
}