初始化
This commit is contained in:
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user