初始化
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/config/config.dart';
|
||||
import '../../main_page/provider/bottom_bar_provider.dart';
|
||||
|
||||
class CommunityMainModuleLogic extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
final int defaultIndex;
|
||||
final modules = [];
|
||||
late final TabController tabController;
|
||||
|
||||
CommunityMainModuleLogic(this.defaultIndex);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
modules.addAll(Config.plateModule?.community ?? []);
|
||||
tabController = TabController(
|
||||
length: modules.length, vsync: this, initialIndex: defaultIndex);
|
||||
tabController.addListener(() {
|
||||
if (!tabController.indexIsChanging) {
|
||||
CommunityBottomProvider().subIndex = tabController.index;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void skipToTabIndex(int index) {
|
||||
tabController.index = index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
|
||||
import '../../../tools_base/indicator/custom_tab_indicator.dart';
|
||||
import '../../../tools_base/loading/loading_center_widget.dart';
|
||||
import '../../home/widget/publist_entry_alert.dart';
|
||||
import 'community_module_logic.dart';
|
||||
import 'community_tab_page.dart';
|
||||
|
||||
//社区帖子亚模块
|
||||
class CommunityModulePage extends StatelessWidget {
|
||||
final int defaultIndex;
|
||||
const CommunityModulePage({super.key, this.defaultIndex = 0});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<CommunityMainModuleLogic>(
|
||||
init: CommunityMainModuleLogic(defaultIndex),
|
||||
builder: (logic) {
|
||||
if (logic.modules.isEmpty) return CErrorWidget();
|
||||
return Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
TabBar(
|
||||
labelPadding: EdgeInsets.zero,
|
||||
padding: EdgeInsets.zero,
|
||||
tabAlignment: TabAlignment.start,
|
||||
indicator: CustomIndicator(offsetY: 6),
|
||||
indicatorWeight: 1,
|
||||
unselectedLabelStyle:
|
||||
TextStyle(color: Color(0x8cFFFFFF), fontSize: 16),
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
isScrollable: true,
|
||||
tabs: logic.modules
|
||||
.map(
|
||||
(e) => Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 16.w, vertical: 13.h),
|
||||
child: Text(e.moduleName ?? ''),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
controller: logic.tabController,
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: logic.tabController,
|
||||
children: logic.modules
|
||||
.asMap()
|
||||
.map(
|
||||
(key, value) => MapEntry(
|
||||
key,
|
||||
CommunityTabPage(
|
||||
model: value,
|
||||
index: key,
|
||||
cancelCallBack: () {
|
||||
logic.tabController.index =
|
||||
logic.tabController.previousIndex;
|
||||
},
|
||||
).keepAlive,
|
||||
),
|
||||
)
|
||||
.values
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: 20,
|
||||
bottom: 50,
|
||||
child: PublishButton(entry: PublishEntry.community),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/event_bus/event_bus_util.dart';
|
||||
import 'package:hgdj/tools_base/event_bus/events.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../../hj_model/home/plate_model.dart';
|
||||
import '../../home/widget/collect_status_wrapper.dart';
|
||||
import '../../main_page/provider/bottom_bar_provider.dart';
|
||||
|
||||
class CommunityTabLogic extends GetxController {
|
||||
final ModuleData? model;
|
||||
final int index;
|
||||
|
||||
CommunityTabLogic(this.model, {this.index = 0});
|
||||
|
||||
RefreshController? refreshController;
|
||||
late ScrollController sctrl =
|
||||
CommunityBottomProvider().scrollCtr(MediaStyle.Community, index);
|
||||
|
||||
int page = 1;
|
||||
bool isLoading = true;
|
||||
|
||||
/// 索引
|
||||
int sort = 0;
|
||||
//标题与后端 moduleSort 绑在一起:原来标题在 CommunityMiddleHeader、值在这里,
|
||||
//两个数组分家,任一边加减一项就会整体错位
|
||||
static const sortTabs = [
|
||||
SortTab('推荐', 7),
|
||||
SortTab('最新', 1),
|
||||
SortTab('最热', 2),
|
||||
SortTab('热评', 9),
|
||||
];
|
||||
int? tagIndex;
|
||||
String? tagId;
|
||||
List<VideoModel> dataSource = [];
|
||||
|
||||
/// 顶部的专题
|
||||
final List<TagsBean> specials = [];
|
||||
|
||||
StreamSubscription? _collectStatusSub;
|
||||
|
||||
void _onCollectStatusChanged(CollectStatusModel event) {
|
||||
if (event.isLiked == null && event.likeCountDelta == null) return;
|
||||
var changed = false;
|
||||
for (final item in dataSource) {
|
||||
if (item.id == event.id) {
|
||||
applyVideoModelCollectStatus(item, event);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_collectStatusSub =
|
||||
eventBus.on<CollectStatusModel>(_onCollectStatusChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_collectStatusSub?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
tagId = model?.defaultTagId;
|
||||
fetchPagedata();
|
||||
}
|
||||
|
||||
void changeSpecialsIndex(int index) {
|
||||
if (tagIndex == index) {
|
||||
tagId = null;
|
||||
tagIndex = null;
|
||||
} else {
|
||||
tagIndex = index;
|
||||
tagId = specials[index].id;
|
||||
}
|
||||
fetchPagedata(needLoading: true);
|
||||
}
|
||||
|
||||
void scrollerToTop() {
|
||||
sctrl.jumpTo(0);
|
||||
}
|
||||
|
||||
Future<void> fetchPagedata(
|
||||
{bool isRefresh = true, bool needLoading = false}) async {
|
||||
if (isRefresh) {
|
||||
page = 1;
|
||||
}
|
||||
if (needLoading) {
|
||||
isLoading = true;
|
||||
update();
|
||||
}
|
||||
try {
|
||||
final res = await VidService.getModuleDetail(
|
||||
model?.id ?? '',
|
||||
pageNumber: page,
|
||||
moduleSort: sortTabs[sort].sort,
|
||||
tagId: tagId,
|
||||
);
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
refreshController?.refreshCompleted();
|
||||
}
|
||||
(res?.hasNext ?? false)
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
dataSource.addAll(res?.allVideoInfo ?? []);
|
||||
if ((res?.allSection?.isNotEmpty ?? false) && specials.isEmpty) {
|
||||
specials.addAll(res!.allSection?.firstOrNull?.allTags ?? []);
|
||||
if (model?.defaultTagId?.isNotEmpty == true) {
|
||||
tagIndex = specials
|
||||
.indexWhere((element) => element.id == model?.defaultTagId);
|
||||
}
|
||||
}
|
||||
page += 1;
|
||||
} catch (e) {
|
||||
// 接口异常兜底:结束刷新/加载态,避免 loading、下拉/上拉指示器卡死
|
||||
debugLog(e);
|
||||
isRefresh
|
||||
? refreshController?.refreshCompleted()
|
||||
: refreshController?.loadComplete();
|
||||
} finally {
|
||||
isLoading = false;
|
||||
update();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
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_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/banner/ads_grid_view_widget.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../../../hj_model/home/plate_model.dart';
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../../hj_utils/sliver_delegate.dart';
|
||||
import '../../../tools_base/widget/net_image_widget.dart';
|
||||
import '../widget/community_middle_header.dart';
|
||||
import '../widget/community_post_widget.dart';
|
||||
import 'community_tab_logic.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
|
||||
class CommunityTabPage extends StatelessWidget {
|
||||
final ModuleData model;
|
||||
final int index;
|
||||
final Function()? cancelCallBack;
|
||||
|
||||
const CommunityTabPage({
|
||||
super.key,
|
||||
required this.model,
|
||||
this.index = 0,
|
||||
this.cancelCallBack,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<CommunityTabLogic>(
|
||||
tag: 'community_tab_${model.id ?? index}',
|
||||
init: CommunityTabLogic(model, index: index),
|
||||
builder: (logic) {
|
||||
return pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshController = ctr,
|
||||
onRefresh: (_) => logic.fetchPagedata(),
|
||||
onLoading: (_) => logic.fetchPagedata(isRefresh: false),
|
||||
child: CustomScrollView(
|
||||
controller: logic.sctrl,
|
||||
slivers: [
|
||||
if (model.pureVersion != true)
|
||||
SliverToBoxAdapter(
|
||||
child: AdsGridViewWidget(
|
||||
7,
|
||||
accordingAdsType: true,
|
||||
padding: EdgeInsets.fromLTRB(16.w, 0, 16.w, 10.h),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: _buildSpecialView(logic),
|
||||
),
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
delegate: MySliverDelegate(
|
||||
maxHeight: 34,
|
||||
minHeight: 34,
|
||||
child: CommunityMiddleHeader(
|
||||
sortOnTap: (sort) {
|
||||
logic.sort = sort;
|
||||
logic.fetchPagedata(needLoading: true);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildTableView(logic),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpecialView(CommunityTabLogic logic) {
|
||||
if (logic.specials.isEmpty) return SizedBox.shrink();
|
||||
return GridView.builder(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.only(left: 16.w, right: 16.w, bottom: 18.h),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 7,
|
||||
crossAxisSpacing: 7,
|
||||
childAspectRatio: 120 / 57,
|
||||
),
|
||||
itemCount: logic.specials.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => logic.changeSpecialsIndex(index),
|
||||
child: _buildSpecialTagCell(logic, index),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSpecialTagCell(CommunityTabLogic logic, int index) {
|
||||
TagsBean model = logic.specials[index];
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Container(
|
||||
decoration: logic.tagIndex == index
|
||||
? BoxDecoration(
|
||||
color: Color(0xff303030),
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
border: Border.all(
|
||||
color: AppColors.actionRed,
|
||||
width: 1,
|
||||
))
|
||||
: BoxDecoration(
|
||||
color: Color(0xff303030),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: 0),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model.coverImg ?? '',
|
||||
borderRadius: 6,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: Colors.black.withValues(alpha: .6)),
|
||||
),
|
||||
if (model.hotMark?.isNotEmpty == true)
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 3, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color.fromRGBO(252, 118, 118, 1),
|
||||
Color.fromRGBO(212, 39, 39, 1),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomRight: Radius.circular(6),
|
||||
topLeft: Radius.circular(6),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
"${model.hotMark}",
|
||||
style: TextStyle(color: Colors.white, fontSize: 11),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
"${model.tagName}",
|
||||
style: TextStyle(
|
||||
color: logic.tagIndex == index
|
||||
? AppColors.actionRed
|
||||
: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
"${model.videoCount?.countStr}个帖子",
|
||||
style: TextStyle(
|
||||
color: logic.tagIndex == index
|
||||
? AppColors.actionRed
|
||||
: Colors.white,
|
||||
fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTableView(CommunityTabLogic logic) {
|
||||
if (logic.isLoading) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: LoadingCenterWidget(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (logic.dataSource.isEmpty) {
|
||||
return SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 300,
|
||||
child: CErrorWidget(
|
||||
retryOnTap: () => logic.fetchPagedata(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SliverList.builder(
|
||||
itemCount: logic.dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
VideoModel videoModel = logic.dataSource[index];
|
||||
return CommunityPostWidget(
|
||||
videoModel: videoModel,
|
||||
videoModels: logic.dataSource,
|
||||
showLine: true,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
|
||||
import '../../main_page/main_logic.dart';
|
||||
import '../../main_page/provider/bottom_bar_provider.dart';
|
||||
|
||||
class CommunityMainLogic extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
final int index;
|
||||
|
||||
CommunityMainLogic({this.index = 0});
|
||||
|
||||
List<MainTabModel> tabs = [
|
||||
MainTabModel(
|
||||
title: '社区',
|
||||
imageNor: 'community_main_sel.png'.communityPath,
|
||||
imageSel: 'community_main_sel.png'.communityPath,
|
||||
),
|
||||
MainTabModel(
|
||||
title: '色图',
|
||||
imageNor: 'community_pic_sel.png'.communityPath,
|
||||
imageSel: 'community_pic_sel.png'.communityPath,
|
||||
),
|
||||
MainTabModel(
|
||||
title: '小说',
|
||||
imageNor: 'community_noval_sel.png'.communityPath,
|
||||
imageSel: 'community_noval_sel.png'.communityPath,
|
||||
),
|
||||
];
|
||||
|
||||
// tab 顺序对应的媒体类型:社区/色图/小说
|
||||
static const List<MediaStyle> _tabTypes = [
|
||||
MediaStyle.Community,
|
||||
MediaStyle.Pic,
|
||||
MediaStyle.Novel
|
||||
];
|
||||
|
||||
late TabController tabCtr;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
tabCtr =
|
||||
TabController(length: tabs.length, vsync: this, initialIndex: index);
|
||||
tabCtr.addListener(() {
|
||||
if (tabCtr.indexIsChanging) return;
|
||||
CommunityBottomProvider().communityType = _tabTypes[tabCtr.index];
|
||||
});
|
||||
super.onInit();
|
||||
}
|
||||
|
||||
// 跳转相关模块的亚模块
|
||||
void jumpSubModule(int index, {int tabIndex = 0}) {
|
||||
tabCtr.index = index;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
|
||||
import '../../../config/config.dart';
|
||||
import '../../cartoon/cartoon_sub_module_page.dart';
|
||||
import '../../home/search_page/search_main_page.dart';
|
||||
import '../../mine/welfare/sign_daily_page.dart';
|
||||
import '../community/community_module_page.dart';
|
||||
import 'community_main_logic.dart';
|
||||
import 'community_section_tab.dart';
|
||||
|
||||
class CommunityMainPage extends StatefulWidget {
|
||||
final int index;
|
||||
final int subTabIndex;
|
||||
|
||||
CommunityMainPage({super.key, this.index = 0, this.subTabIndex = 0});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return CommunityMainPageState();
|
||||
}
|
||||
}
|
||||
|
||||
class CommunityMainPageState extends State<CommunityMainPage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
//提前加载图片资源
|
||||
precacheImage(AssetImage('community_main_sel.png'.communityPath), context);
|
||||
precacheImage(AssetImage('community_pic_sel.png'.communityPath), context);
|
||||
precacheImage(
|
||||
AssetImage('community_chatgroup_sel.png'.communityPath), context);
|
||||
precacheImage(AssetImage('community_noval_sel.png'.communityPath), context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: GetBuilder(
|
||||
init: CommunityMainLogic(index: widget.index),
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
screen.paddingTop.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Expanded(child: _buildTabbar(_)),
|
||||
if (Config.signIcon != null &&
|
||||
Config.signIcon!.isNotEmpty) ...[
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(SignDailyPage()),
|
||||
child: Image.asset(
|
||||
"community_sign.webp".communityPath,
|
||||
width: 20,
|
||||
),
|
||||
),
|
||||
],
|
||||
12.sizeBoxW,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(SearchMainPage()),
|
||||
child: Image.asset(
|
||||
'search_icon.png'.commonImgPath,
|
||||
width: 24,
|
||||
color: Color(0xff989898),
|
||||
),
|
||||
),
|
||||
16.sizeBoxW,
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: _.tabCtr,
|
||||
children: [
|
||||
//社区
|
||||
CommunityModulePage(
|
||||
defaultIndex:
|
||||
widget.index == 0 ? widget.subTabIndex : 0,
|
||||
).keepAlive,
|
||||
//图集
|
||||
CartoonSubModulePage(
|
||||
type: MediaStyle.Pic,
|
||||
tabIndex: widget.index == 1 ? widget.subTabIndex : 0,
|
||||
).keepAlive,
|
||||
// CartoonSubModulePage(
|
||||
// type: MediaStyle.Game,
|
||||
// tabIndex: widget.index == 2 ? widget.subTabIndex : 0,
|
||||
// ).keepAlive,
|
||||
//小说
|
||||
CartoonSubModulePage(
|
||||
type: MediaStyle.Novel,
|
||||
tabIndex: widget.index == 3 ? widget.subTabIndex : 0,
|
||||
).keepAlive,
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
_buildTabbar(CommunityMainLogic _) {
|
||||
return CommunitySectionTabMenu(
|
||||
_.tabs,
|
||||
curIndex: widget.index,
|
||||
tabCtr: _.tabCtr,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
// Get.delete<CommunityMainPageLogic>();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
//发现模块tabbar
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../hj_utils/widget_util.dart';
|
||||
import '../../main_page/main_logic.dart';
|
||||
|
||||
class CommunitySectionTabMenu extends StatefulWidget {
|
||||
final int curIndex;
|
||||
final List<MainTabModel> tabs;
|
||||
final TabController? tabCtr;
|
||||
final double? fontSize;
|
||||
|
||||
const CommunitySectionTabMenu(
|
||||
this.tabs, {
|
||||
super.key,
|
||||
this.curIndex = 0,
|
||||
this.tabCtr,
|
||||
this.fontSize = 16,
|
||||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _CommunitySectionTabMenuState();
|
||||
}
|
||||
}
|
||||
|
||||
class _CommunitySectionTabMenuState extends State<CommunitySectionTabMenu> {
|
||||
late int curIndex = widget.curIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.tabCtr?.addListener(_listener);
|
||||
}
|
||||
|
||||
void _listener() {
|
||||
setState(() => curIndex = widget.tabCtr?.index ?? 0);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.tabCtr?.removeListener(_listener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: TabBar(
|
||||
tabAlignment: TabAlignment.start,
|
||||
padding: const EdgeInsets.only(left: 6),
|
||||
controller: widget.tabCtr,
|
||||
indicatorWeight: 0,
|
||||
isScrollable: true,
|
||||
labelPadding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
labelStyle: TextStyle(fontSize: widget.fontSize, fontWeight: FontWeight.w600),
|
||||
unselectedLabelStyle: TextStyle(fontSize: widget.fontSize, fontWeight: FontWeight.w400),
|
||||
indicator: const BoxDecoration(),
|
||||
tabs: List.generate(widget.tabs.length, _tabItem),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tabItem(int index) {
|
||||
final sel = curIndex == index;
|
||||
// 固定 49 宽槽位,选中(图标)/未选中(文字)就地交叉淡入淡出,宽度恒定不跳动
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 250),
|
||||
child: SizedBox(
|
||||
key: ValueKey(sel),
|
||||
width: 49,
|
||||
child: sel
|
||||
? Center(child: Image.asset(widget.tabs[index].imageNor, height: 31))
|
||||
: Align(
|
||||
alignment: Alignment(0, -0.5),
|
||||
child: Text(
|
||||
widget.tabs[index].title,
|
||||
style: textStyle(14, Colors.white.withValues(alpha: 0.55), FontWeight.w400),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/tag_service.dart';
|
||||
|
||||
class CommunityTagLogic extends GetxController
|
||||
with GetTickerProviderStateMixin {
|
||||
final scrollCtr = ScrollController();
|
||||
double? offset;
|
||||
bool isPinned = false;
|
||||
TagsBean model;
|
||||
StreamController<bool> showAppCtr = StreamController.broadcast();
|
||||
late final TabController mainTabCtr =
|
||||
TabController(length: sorts.length, vsync: this);
|
||||
|
||||
RxBool isLoadingFollow = false.obs;
|
||||
final sorts = [2, 1, 5]; //排序类型 1:最新上架 2:最多收藏 3:本月最热 4:最多观看
|
||||
|
||||
CommunityTagLogic(this.model);
|
||||
|
||||
@override
|
||||
onReady() async {
|
||||
super.onReady();
|
||||
final newsType = model.newsType;
|
||||
final res = await TagService.fetchInfo(model.id ?? '');
|
||||
if (res != null) {
|
||||
model = res;
|
||||
model.newsType = newsType;
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
showAppCtr.close();
|
||||
|
||||
mainTabCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../../tools_base/widget/follow_button.dart';
|
||||
import '../../home/widget/collect_status_wrapper.dart';
|
||||
import '../widget/community_sort_header.dart';
|
||||
import 'community_tag_logic.dart';
|
||||
import 'subviews/tag_detail_sub_controller.dart';
|
||||
import 'subviews/tag_detail_subview.dart';
|
||||
|
||||
class UserCenterSortTypeEvent {
|
||||
int sortIndex;
|
||||
UserCenterSortTypeEvent(this.sortIndex);
|
||||
}
|
||||
|
||||
class CommunityTagDetailPage extends StatefulWidget {
|
||||
final TagsBean model;
|
||||
|
||||
const CommunityTagDetailPage({super.key, required this.model});
|
||||
|
||||
@override
|
||||
State<CommunityTagDetailPage> createState() => _CommunityTagDetailPageState();
|
||||
}
|
||||
|
||||
class _CommunityTagDetailPageState extends State<CommunityTagDetailPage>
|
||||
with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<CommunityTagLogic>(
|
||||
tag: uniqueTag,
|
||||
init: CommunityTagLogic(widget.model),
|
||||
builder: (controller) => Scaffold(
|
||||
extendBodyBehindAppBar: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
ExtendedNestedScrollView(
|
||||
controller: controller.scrollCtr,
|
||||
pinnedHeaderSliverHeightBuilder: () =>
|
||||
AppBar().preferredSize.height + screen.paddingTop,
|
||||
headerSliverBuilder: (_, istop) {
|
||||
return [
|
||||
SliverAppBar(
|
||||
pinned: true,
|
||||
floating: false,
|
||||
expandedHeight: 64,
|
||||
titleSpacing: 18.w,
|
||||
automaticallyImplyLeading: false,
|
||||
title: _buildAppBarTitle(controller),
|
||||
flexibleSpace: FlexibleSpaceBar(
|
||||
background: SizedBox(),
|
||||
),
|
||||
)
|
||||
];
|
||||
},
|
||||
body: Column(
|
||||
children: [
|
||||
12.sizeBoxH,
|
||||
CommunitySortHeader(
|
||||
controller: controller.mainTabCtr,
|
||||
sortOnTap: (index) {},
|
||||
),
|
||||
12.sizeBoxH,
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.mainTabCtr,
|
||||
children: controller.sorts
|
||||
.map(
|
||||
(e) => TagDetailSubView<TagCOVERController>(
|
||||
tag: controller.model, postSort: e)
|
||||
.keepAlive,
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppBarTitle(CommunityTagLogic controller) {
|
||||
return ChangeNotifierProvider.value(
|
||||
value: controller.scrollCtr,
|
||||
child: Consumer<ScrollController>(
|
||||
builder: (_, ctr, __) {
|
||||
final isTop = (180 - ctr.offset - kToolbarHeight) == 0;
|
||||
return Row(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Image.asset(
|
||||
isTop
|
||||
? 'common_back.png'.commonImgPath
|
||||
: 'back_circle.png'.commonImgPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
if (isTop) ...[
|
||||
4.sizeBoxW,
|
||||
NetworkImageLoader(
|
||||
imageUrl: controller.model.coverImg ?? '',
|
||||
width: 30,
|
||||
height: 30,
|
||||
borderRadius: 15,
|
||||
),
|
||||
6.sizeBoxW,
|
||||
Text(
|
||||
controller.model.name ?? '',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xffffffff),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
CollectStatusWrapper(
|
||||
tagModel: controller.model,
|
||||
builder: () => FollowButton(
|
||||
mediaId: controller.model.id,
|
||||
isFollow: controller.model.hasCollected ?? false,
|
||||
followType: FollowEnum.tag,
|
||||
successsAction: (success) =>
|
||||
controller.model.hasCollected = success,
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
controller.model.name ?? '',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
(24 + 18.w).sizeBoxH,
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/tag_service.dart';
|
||||
import 'package:hgdj/tools_base/event_bus/event_bus_util.dart';
|
||||
import 'package:hgdj/tools_base/event_bus/events.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../../home/widget/collect_status_wrapper.dart';
|
||||
|
||||
abstract class TagDetailSubController extends GetxController {
|
||||
List<VideoModel> dataSource = [];
|
||||
bool isGridStyle = true;
|
||||
double childRatio = 1;
|
||||
bool isLoading = true;
|
||||
String? loadType;
|
||||
String? newsType;
|
||||
String? sort;
|
||||
int? postSort;
|
||||
int page = 1;
|
||||
TagsBean tag;
|
||||
|
||||
TagDetailSubController(this.tag, {this.postSort});
|
||||
|
||||
RefreshController? controller;
|
||||
|
||||
StreamSubscription? _collectStatusSub;
|
||||
|
||||
void _onCollectStatusChanged(CollectStatusModel event) {
|
||||
if (event.isLiked == null && event.likeCountDelta == null) return;
|
||||
var changed = false;
|
||||
for (final item in dataSource) {
|
||||
if (item.id == event.id) {
|
||||
applyVideoModelCollectStatus(item, event);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) update();
|
||||
}
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_collectStatusSub =
|
||||
eventBus.on<CollectStatusModel>(_onCollectStatusChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_collectStatusSub?.cancel();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
// isLoading = false;
|
||||
fetchPageData(needLoading: true);
|
||||
}
|
||||
|
||||
fetchPageData({bool isRefresh = true, bool needLoading = false}) async {
|
||||
if (isRefresh) {
|
||||
page = 1;
|
||||
}
|
||||
|
||||
isLoading = needLoading;
|
||||
update();
|
||||
final res = await TagService.fetchList(
|
||||
tag.id ?? '',
|
||||
newsType: newsType,
|
||||
sortType: postSort,
|
||||
page: page,
|
||||
size: 10,
|
||||
);
|
||||
if (isRefresh) {
|
||||
dataSource.clear();
|
||||
controller?.refreshCompleted();
|
||||
}
|
||||
|
||||
dataSource.addAll(res?.videos ?? []);
|
||||
page += 1;
|
||||
isLoading = false;
|
||||
update();
|
||||
res?.hasNext ?? false
|
||||
? controller?.loadComplete()
|
||||
: controller?.loadNoData();
|
||||
}
|
||||
}
|
||||
|
||||
class TagVideoController extends TagDetailSubController {
|
||||
TagVideoController(super.tag);
|
||||
|
||||
@override
|
||||
String get loadType => 'MOVIE';
|
||||
String get newsType => tag.newsType ?? "MOVIE";
|
||||
|
||||
@override
|
||||
double get childRatio => 168 / 119;
|
||||
}
|
||||
|
||||
class TagSPController extends TagDetailSubController {
|
||||
TagSPController(super.tag);
|
||||
@override
|
||||
String get loadType => 'SP';
|
||||
String get newsType => tag.newsType ?? "SP";
|
||||
|
||||
@override
|
||||
double get childRatio => 168 / 248;
|
||||
}
|
||||
|
||||
class TagCOVERController extends TagDetailSubController {
|
||||
TagCOVERController(super.tag, {super.postSort});
|
||||
|
||||
@override
|
||||
String? get loadType => null;
|
||||
String get newsType => tag.newsType ?? "";
|
||||
|
||||
@override
|
||||
bool get isGridStyle => false;
|
||||
}
|
||||
|
||||
T instanceController<T>(TagsBean tag, {int? postSort}) {
|
||||
// Type 对象比较,避免 release 混淆下类名字符串匹配失败导致 throw→灰屏
|
||||
if (T == TagCOVERController)
|
||||
return TagCOVERController(tag, postSort: postSort) as T;
|
||||
if (T == TagSPController) return TagSPController(tag) as T;
|
||||
if (T == TagVideoController) return TagVideoController(tag) as T;
|
||||
throw '$T 没有对应类型';
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../../../home/home_cell_style/video_simple_cell.dart';
|
||||
import '../../widget/community_post_widget.dart';
|
||||
import 'tag_detail_sub_controller.dart';
|
||||
|
||||
class TagDetailSubView<T extends TagDetailSubController>
|
||||
extends StatelessWidget {
|
||||
final TagsBean tag;
|
||||
|
||||
final int? postSort;
|
||||
|
||||
const TagDetailSubView({super.key, required this.tag, this.postSort});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<T>(
|
||||
init: instanceController<T>(tag, postSort: postSort),
|
||||
global: false,
|
||||
builder: (controller) {
|
||||
if (controller.isLoading) return LoadingCenterWidget();
|
||||
return pullYsRefresh(
|
||||
onLoading: (refreshController) =>
|
||||
controller.fetchPageData(isRefresh: false),
|
||||
onRefresh: (refreshController) => controller.fetchPageData(),
|
||||
child: () {
|
||||
if (controller.dataSource.isEmpty) return CErrorWidget();
|
||||
if (controller.isGridStyle)
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: controller.childRatio,
|
||||
crossAxisSpacing: 7,
|
||||
mainAxisSpacing: 12,
|
||||
),
|
||||
itemCount: controller.dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final model = controller.dataSource[index];
|
||||
return GestureDetector(
|
||||
onTap: () => pushToVideoPage(videoModel: model),
|
||||
child: VideoSimpleCell(videoModel: model),
|
||||
);
|
||||
},
|
||||
);
|
||||
return ListView.separated(
|
||||
padding: EdgeInsets.only(top: 0),
|
||||
separatorBuilder: (_, __) => 14.sizeBoxH,
|
||||
itemCount: controller.dataSource.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final model = controller.dataSource[index];
|
||||
return CommunityPostWidget(
|
||||
videoModel: model,
|
||||
videoModels: controller.dataSource,
|
||||
showTags: false,
|
||||
);
|
||||
},
|
||||
);
|
||||
}(),
|
||||
onInit: (ctr) => controller.controller = ctr,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/hj_utils/pay/pay_manager.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/widget/sheet_handle_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../model/im_group_list_model.dart';
|
||||
|
||||
class GroupBuyBottomSheet extends StatefulWidget {
|
||||
final IMGroupItemModel groupModel;
|
||||
|
||||
const GroupBuyBottomSheet({super.key, required this.groupModel});
|
||||
|
||||
@override
|
||||
State<GroupBuyBottomSheet> createState() => _GroupBuyBottomSheetState();
|
||||
}
|
||||
|
||||
class _GroupBuyBottomSheetState extends State<GroupBuyBottomSheet> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
globalStore.updateUserInfo();
|
||||
globalStore.refreshWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<GlobalStore>(
|
||||
builder: (_, store, __) {
|
||||
final enough =
|
||||
(store.wallet?.amount ?? 0) >= (widget.groupModel.price ?? 0);
|
||||
return _buildNormal(enough, store);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNormal(bool enough, GlobalStore store) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryColor,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SheetHandleBar(),
|
||||
18.sizeBoxH,
|
||||
Text(
|
||||
'购买',
|
||||
style: TextStyle(
|
||||
color: Color(0xE5ffffff),
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
36.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'解锁群聊',
|
||||
style: TextStyle(
|
||||
color: Color(0xE5ffffff),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: () => pushToWalletPage(tabPosition: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'${widget.groupModel.price ?? 0} 金币',
|
||||
style:
|
||||
TextStyle(color: AppColors.actionRed, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
0.5.line,
|
||||
18.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'我的金币',
|
||||
style: TextStyle(
|
||||
color: Color(0xE5ffffff),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'${GlobalStore().wallet?.amount?.toDouble().fixed(2) ?? 0}',
|
||||
style: TextStyle(
|
||||
color: Color(0xE5ffffff),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
12.sizeBoxW,
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => pushToWalletPage(tabPosition: 1),
|
||||
child: Text(
|
||||
'充值',
|
||||
style: TextStyle(
|
||||
color: Color(0xffA4634D),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
decoration: TextDecoration.underline,
|
||||
decorationColor: Color(0xffA4634D),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => _doPayAction(enough),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
enough
|
||||
? '${widget.groupModel.price ?? 0} 金币/立即支付'
|
||||
: '余额不足,前往充值',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _doPayAction(bool enough) async {
|
||||
if (!enough) {
|
||||
await pushToWalletPage(tabPosition: 1);
|
||||
return;
|
||||
}
|
||||
await PayManager().buy(
|
||||
widget.groupModel.id,
|
||||
ProductType.group,
|
||||
source: 'group_buy',
|
||||
onSuccess: (data) {
|
||||
globalStore.refreshWallet();
|
||||
Get.back(result: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../../../hj_utils/api_service/group_service.dart';
|
||||
import '../../../../tools_base/file_upload/file_upload_tool.dart';
|
||||
import '../../../../tools_base/file_upload/upload_result_model.dart';
|
||||
import '../../../comment/comment_input_view.dart';
|
||||
import '../model/im_group_list_model.dart';
|
||||
import '../model/im_message_resp.dart';
|
||||
|
||||
class GroupChatDetailLogic extends GetxController {
|
||||
final IMGroupItemModel model;
|
||||
|
||||
GroupChatDetailLogic(this.model);
|
||||
|
||||
int pageNum = 1;
|
||||
List<IMMessageModel>? msgList;
|
||||
RefreshController? refreshController;
|
||||
TextEditingController textEditController = TextEditingController();
|
||||
Rx<String?> selectedImagePath = Rx<String?>(null);
|
||||
RxBool isSendingMsg = false.obs;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
refreshData();
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
void refreshData() {
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void loadMoreData() {
|
||||
_loadData(page: pageNum + 1);
|
||||
}
|
||||
|
||||
void _loadData({int page = 1, int size = 20}) async {
|
||||
try {
|
||||
final retResp = await GroupService.getMessages(model.groupId, page, size);
|
||||
pageNum = page;
|
||||
msgList ??= [];
|
||||
if (pageNum == 1) {
|
||||
msgList?.clear();
|
||||
}
|
||||
msgList?.addAll(retResp.list ?? []);
|
||||
retResp.hasNext == true
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
refreshController?.loadComplete();
|
||||
}
|
||||
refreshController?.refreshCompleted();
|
||||
update();
|
||||
}
|
||||
|
||||
Future readySendMsg({bool? isSend}) async {
|
||||
if (isSend == true &&
|
||||
(textEditController.text.isNotEmpty ||
|
||||
selectedImagePath.value?.isNotEmpty == true)) {
|
||||
// 点击发送按钮有内容,直接发送内容, 否则弹出输入框
|
||||
sendMsgEvent();
|
||||
} else {
|
||||
final result = await Get.bottomSheet(
|
||||
CommentInputView(
|
||||
hint: "请输入消息",
|
||||
textCtr: textEditController,
|
||||
imgPath: selectedImagePath.value,
|
||||
onImgChange: (value) => selectedImagePath.value = value,
|
||||
),
|
||||
backgroundColor: Colors.transparent,
|
||||
barrierColor: Colors.transparent,
|
||||
enableDrag: false,
|
||||
settings: RouteSettings(name: CommentInputView.routeName),
|
||||
useRootNavigator: true,
|
||||
);
|
||||
if (result == true) {
|
||||
update();
|
||||
}
|
||||
sendMsgEvent();
|
||||
}
|
||||
}
|
||||
|
||||
Future sendMsgEvent() async {
|
||||
if (textEditController.text.isEmpty &&
|
||||
selectedImagePath.value?.isNotEmpty != true) {
|
||||
showToast("请输入消息内容");
|
||||
return;
|
||||
}
|
||||
if (isSendingMsg.value) return;
|
||||
isSendingMsg.value = true;
|
||||
String? sendImagePath;
|
||||
if (selectedImagePath.value?.isNotEmpty == true) {
|
||||
try {
|
||||
ImageUploadResultModel? imgResult =
|
||||
await FileUploadTool().uploadImage(selectedImagePath.value!);
|
||||
sendImagePath = imgResult?.coverImg ?? "";
|
||||
if (sendImagePath.isEmpty) {
|
||||
isSendingMsg.value = false;
|
||||
showToast("图片上传失败!");
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
isSendingMsg.value = false;
|
||||
showToast("图片上传失败!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
var ret = await GroupService.sendMessage(
|
||||
model.groupId, textEditController.text, sendImagePath);
|
||||
if (ret.isSuccess) {
|
||||
textEditController.text = "";
|
||||
selectedImagePath.value = null;
|
||||
showToast("消息已发送");
|
||||
if (ret.data is Map && (ret.data['message'] is Map)) {
|
||||
IMMessageModel sendModel =
|
||||
IMMessageModel.fromJson(ret.data['message']);
|
||||
sendModel.portrait = globalStore.meInfo?.portrait;
|
||||
sendModel.name = globalStore.meInfo?.name;
|
||||
msgList?.insert(0, sendModel);
|
||||
update();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
showToast("消息发送失败!");
|
||||
debugLog(e);
|
||||
}
|
||||
isSendingMsg.value = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/comment/widget/comment_foot_view.dart';
|
||||
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import '../model/im_group_list_model.dart';
|
||||
import 'soul_group_detail_logic.dart';
|
||||
import 'widget/chat_item_cell.dart';
|
||||
|
||||
class GroupChatDetailPage extends StatelessWidget {
|
||||
final IMGroupItemModel model;
|
||||
|
||||
const GroupChatDetailPage(this.model, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<GroupChatDetailLogic>(
|
||||
global: false,
|
||||
init: GroupChatDetailLogic(model),
|
||||
builder: (logic) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(model.name ?? ""),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshController = ctr,
|
||||
onRefresh: (ctr) => logic.refreshData(),
|
||||
onLoading: (ctr) => logic.loadMoreData(),
|
||||
child: ListView.builder(
|
||||
itemCount: logic.msgList?.length ?? 0,
|
||||
reverse: true,
|
||||
padding: EdgeInsets.only(bottom: 12, top: 12),
|
||||
itemBuilder: (context, index) {
|
||||
return ChatItemCell(model: logic.msgList![index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => CommentFootView(
|
||||
imgPath: logic.selectedImagePath.value,
|
||||
isSending: logic.isSendingMsg.value,
|
||||
onImgChange: (value) => logic.selectedImagePath.value = value,
|
||||
onComment: (isSend) => logic.readySendMsg(isSend: isSend),
|
||||
textCtr: logic.textEditController,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../../../../tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
|
||||
import '../../model/im_message_resp.dart';
|
||||
|
||||
class ChatItemCell extends StatefulWidget {
|
||||
final IMMessageModel? model;
|
||||
|
||||
ChatItemCell({super.key, this.model});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ChatItemCellState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatItemCellState extends State<ChatItemCell> {
|
||||
bool get isImg => widget.model?.image?.isNotEmpty == true;
|
||||
|
||||
bool get isMe => globalStore.isMe(widget.model?.uid);
|
||||
|
||||
double imageWidth = 173;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
void _showImageScan(String imageUrl) {
|
||||
if (imageUrl.isNotEmpty) {
|
||||
ImageBrowserPage.open([widget.model?.image ?? '']);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isMe) {
|
||||
return _buildRightStyle();
|
||||
} else {
|
||||
return _buildLeftStyle();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildLeftStyle() {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(0, 12, 0, 6),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTime(),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(16, 0, 50, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
pushToPersonCenter(widget.model?.uid);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: widget.model?.portrait ?? "",
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.model?.name ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (widget.model?.content?.isNotEmpty == true)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 10),
|
||||
margin: EdgeInsets.only(top: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.05),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
topRight: Radius.circular(12),
|
||||
topLeft: Radius.circular(2)),
|
||||
),
|
||||
child: Text(
|
||||
widget.model?.content ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isImg)
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
_showImageScan(widget.model?.image ?? "");
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 5),
|
||||
alignment: Alignment.centerLeft,
|
||||
constraints: BoxConstraints(maxWidth: 173),
|
||||
child: _buildImageWidget(isLeft: true),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRightStyle() {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(0, 12, 0, 6),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildTime(),
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(50, 0, 16, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
widget.model?.name ?? "",
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
if (widget.model?.content?.isNotEmpty == true)
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: 20, vertical: 10),
|
||||
margin: EdgeInsets.only(top: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff1DC194),
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
topLeft: Radius.circular(12),
|
||||
topRight: Radius.circular(4)),
|
||||
),
|
||||
child: Text(
|
||||
widget.model?.content ?? "",
|
||||
style: TextStyle(
|
||||
color: Color(0xff141414),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isImg)
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () {
|
||||
_showImageScan(widget.model?.image ?? "");
|
||||
},
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(top: 5),
|
||||
alignment: Alignment.centerRight,
|
||||
constraints: BoxConstraints(maxWidth: 173),
|
||||
child: _buildImageWidget(isLeft: false),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
GestureDetector(
|
||||
onTap: () {},
|
||||
child: SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: widget.model?.portrait,
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageWidget({bool isLeft = false}) {
|
||||
return Stack(
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: widget.model?.image ?? "",
|
||||
width: imageWidth,
|
||||
imgBorderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(12),
|
||||
bottomRight: Radius.circular(12),
|
||||
topRight: Radius.circular(isLeft ? 12 : 0),
|
||||
topLeft: Radius.circular(isLeft ? 0 : 12),
|
||||
),
|
||||
),
|
||||
if (isLeft)
|
||||
Positioned(
|
||||
bottom: 12,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
width: 72,
|
||||
height: 26,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: AppColors.actionRed,
|
||||
),
|
||||
child: Text(
|
||||
'查看原图',
|
||||
style: TextStyle(fontSize: 12, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTime() {
|
||||
// if (widget.model?.isShowTime == true) {
|
||||
// return Container(
|
||||
// margin: EdgeInsets.fromLTRB(0, 12, 0, 12),
|
||||
// padding: EdgeInsets.symmetric(horizontal: 20, vertical: 2),
|
||||
// // decoration: BoxDecoration(
|
||||
// // borderRadius: BorderRadius.circular(30),
|
||||
// // color: Color(0xfff7f7f7).withValues(alpha: 0.42),
|
||||
// // ),
|
||||
// child: Text(
|
||||
// widget.model?.createAtDesc ?? "",
|
||||
// style: TextStyle(
|
||||
// color: Color(0xff666666),
|
||||
// fontSize: 12,
|
||||
// ),
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
return SizedBox();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:pull_to_refresh/pull_to_refresh.dart';
|
||||
|
||||
import '../../../hj_utils/api_service/group_service.dart';
|
||||
import 'model/im_group_list_model.dart';
|
||||
|
||||
class GroupChatLogic extends GetxController {
|
||||
int pageNum = 1;
|
||||
List<IMGroupItemModel>? groupList;
|
||||
RefreshController? refreshController;
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
refreshData();
|
||||
super.onReady();
|
||||
}
|
||||
|
||||
void refreshData() {
|
||||
_loadData();
|
||||
}
|
||||
|
||||
void loadMoreData() {
|
||||
_loadData(page: pageNum + 1);
|
||||
}
|
||||
|
||||
void _loadData({int page = 1}) async {
|
||||
try {
|
||||
final retResp = await GroupService.getList(page);
|
||||
pageNum = page;
|
||||
groupList ??= [];
|
||||
if (pageNum == 1) {
|
||||
groupList?.clear();
|
||||
}
|
||||
groupList?.addAll(retResp?.list ?? []);
|
||||
retResp?.hasNext == true
|
||||
? refreshController?.loadComplete()
|
||||
: refreshController?.loadNoData();
|
||||
} catch (e) {
|
||||
debugLog(e);
|
||||
refreshController?.loadComplete();
|
||||
}
|
||||
refreshController?.refreshCompleted();
|
||||
update();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_page/main_page/provider/bottom_bar_provider.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
import 'package:hgdj/tools_base/banner/ads_grid_view_widget.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
|
||||
import 'group_chat_logic.dart';
|
||||
import 'widget/chat_group_cell.dart';
|
||||
|
||||
class GroupChatPage extends StatelessWidget {
|
||||
const GroupChatPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<GroupChatLogic>(
|
||||
global: false,
|
||||
init: GroupChatLogic(),
|
||||
builder: (logic) {
|
||||
return pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshController = ctr,
|
||||
onRefresh: (ctr) => logic.refreshData(),
|
||||
onLoading: (ctr) => logic.loadMoreData(),
|
||||
child: CustomScrollView(
|
||||
controller:
|
||||
CommunityBottomProvider().scrollCtr(MediaStyle.GroupChat, 0),
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: AdsGridViewWidget(
|
||||
6,
|
||||
accordingAdsType: true,
|
||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 18),
|
||||
),
|
||||
),
|
||||
if (logic.groupList == null)
|
||||
SliverToBoxAdapter(child: LoadingCenterWidget(height: 400))
|
||||
else if (logic.groupList!.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 400,
|
||||
child: CErrorWidget(
|
||||
retryOnTap: () => logic.refreshData(),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SliverList.builder(
|
||||
itemBuilder: (ctx, index) =>
|
||||
ChatGroupCell(logic.groupList![index]),
|
||||
itemCount: logic.groupList!.length,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
class IMGroupListModel {
|
||||
bool? hasNext;
|
||||
List<IMGroupItemModel>? list;
|
||||
int? total;
|
||||
|
||||
IMGroupListModel({this.hasNext, this.list, this.total});
|
||||
|
||||
IMGroupListModel.fromJson(Map<String, dynamic> json) {
|
||||
hasNext = json['hasNext'];
|
||||
if (json['list'] != null) {
|
||||
list = <IMGroupItemModel>[];
|
||||
json['list'].forEach((v) {
|
||||
list!.add(new IMGroupItemModel.fromJson(v));
|
||||
});
|
||||
}
|
||||
total = json['total'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['hasNext'] = this.hasNext;
|
||||
if (this.list != null) {
|
||||
data['list'] = this.list!.map((v) => v.toJson()).toList();
|
||||
}
|
||||
data['total'] = this.total;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
class IMGroupItemModel {
|
||||
String? cover;
|
||||
String? createdAt;
|
||||
|
||||
///成员数量(假的)
|
||||
int? fakeMemberNum;
|
||||
int? groupId;
|
||||
String? id;
|
||||
|
||||
///成员数量(真实)
|
||||
int? memberNum;
|
||||
String? name;
|
||||
|
||||
///加入群聊价格 0-免费
|
||||
int? price;
|
||||
String? summary;
|
||||
String? updatedAt;
|
||||
|
||||
IMGroupItemModel(
|
||||
{this.cover,
|
||||
this.createdAt,
|
||||
this.fakeMemberNum,
|
||||
this.groupId,
|
||||
this.id,
|
||||
this.memberNum,
|
||||
this.name,
|
||||
this.price,
|
||||
this.summary,
|
||||
this.updatedAt});
|
||||
|
||||
IMGroupItemModel.fromJson(Map<String, dynamic> json) {
|
||||
cover = json['cover'];
|
||||
createdAt = json['createdAt'];
|
||||
fakeMemberNum = json['fakeMemberNum'];
|
||||
groupId = json['groupId'];
|
||||
id = json['id'];
|
||||
memberNum = json['memberNum'];
|
||||
name = json['name'];
|
||||
price = json['price'];
|
||||
summary = json['summary'];
|
||||
updatedAt = json['updatedAt'];
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
final Map<String, dynamic> data = new Map<String, dynamic>();
|
||||
data['cover'] = this.cover;
|
||||
data['createdAt'] = this.createdAt;
|
||||
data['fakeMemberNum'] = this.fakeMemberNum;
|
||||
data['groupId'] = this.groupId;
|
||||
data['id'] = this.id;
|
||||
data['memberNum'] = this.memberNum;
|
||||
data['name'] = this.name;
|
||||
data['price'] = this.price;
|
||||
data['summary'] = this.summary;
|
||||
data['updatedAt'] = this.updatedAt;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
class IMMessageResp {
|
||||
bool? hasNext;
|
||||
List<IMMessageModel>? list;
|
||||
int? total;
|
||||
|
||||
IMMessageResp({this.hasNext, this.list, this.total});
|
||||
|
||||
IMMessageResp.fromJson(Map<String, dynamic> json) {
|
||||
hasNext = json['hasNext'];
|
||||
if (json['list'] is List) {
|
||||
list = <IMMessageModel>[];
|
||||
json['list'].forEach((v) {
|
||||
list!.add(IMMessageModel.fromJson(v));
|
||||
});
|
||||
}
|
||||
total = json['total'];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class IMMessageModel {
|
||||
|
||||
String? id;
|
||||
int? groupId;
|
||||
int? uid;
|
||||
String? name;
|
||||
String? gender;
|
||||
String? portrait;
|
||||
String? content;
|
||||
String? image;
|
||||
String? createdAt;
|
||||
|
||||
|
||||
IMMessageModel.fromJson(Map<String, dynamic> json) {
|
||||
id = json['id'];
|
||||
groupId = json['groupId'];
|
||||
uid = json['uid'];
|
||||
name = json['name'];
|
||||
// gender = json['gender'];
|
||||
portrait = json['portrait'];
|
||||
content = json['content'];
|
||||
image = json['image'];
|
||||
createdAt = json['createdAt'];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/debug_log.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_alert_widget.dart';
|
||||
|
||||
import '../../../../alert/mine/vip_level_dialog.dart';
|
||||
import '../../../../assets_tool/app_colors.dart';
|
||||
import '../../../../hj_utils/api_service/group_service.dart';
|
||||
import '../../../../tools_base/widget/header_widget.dart';
|
||||
import '../alert/group_buy_bottom_sheet.dart';
|
||||
import '../detail/soul_group_detail_page.dart';
|
||||
import '../model/im_group_list_model.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
|
||||
class ChatGroupCell extends StatefulWidget {
|
||||
final IMGroupItemModel model;
|
||||
|
||||
ChatGroupCell(this.model, {super.key});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
return _ChatGroupCellState();
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatGroupCellState extends State<ChatGroupCell> {
|
||||
IMGroupItemModel get model => widget.model;
|
||||
|
||||
void _addGroupEvent() async {
|
||||
if (!globalStore.isVIPTopLevel) {
|
||||
showVipLevelDialog('本内容需要开通至尊VIP会员\n\n开通会员 即可畅享高级群聊特权');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
LoadingAlertWidget.show();
|
||||
final retResp = await GroupService.getHasJoin(model.groupId);
|
||||
LoadingAlertWidget.cancel();
|
||||
if (retResp.isSuccess) {
|
||||
int status = retResp.data['status']; // 0-已被禁言 1-可发言 2-未加入
|
||||
if (status == 0) {
|
||||
showToast("您已被禁言");
|
||||
} else if (status == 1) {
|
||||
Get.to(GroupChatDetailPage(model));
|
||||
} else if (status == 2) {
|
||||
var ret = await Get.bottomSheet(
|
||||
GroupBuyBottomSheet(groupModel: widget.model));
|
||||
if (ret == true) {
|
||||
Get.to(GroupChatDetailPage(model));
|
||||
}
|
||||
} else {
|
||||
showToast("群未知状态");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
LoadingAlertWidget.cancel();
|
||||
showToast("网络数据异常");
|
||||
debugLog(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(left: 16, right: 16, bottom: 12),
|
||||
padding: EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: .05),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _addGroupEvent,
|
||||
child: Row(
|
||||
children: [
|
||||
HeaderWidget(
|
||||
headPath: model.cover ?? '',
|
||||
level: 1,
|
||||
headWidth: 64,
|
||||
headHeight: 64,
|
||||
isCircle: false,
|
||||
radius: 32,
|
||||
),
|
||||
12.sizeBoxW,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
model.name ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
model.summary ?? '',
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
6.sizeBoxH,
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'ID: ${model.groupId ?? 0} ${model.fakeMemberNum.countStr}人在玩',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
Spacer(),
|
||||
GestureDetector(
|
||||
onTap: _addGroupEvent,
|
||||
child: Container(
|
||||
width: 52,
|
||||
height: 24,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: AppColors.actionRed),
|
||||
child: Text(
|
||||
'加入群聊',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/video/video_player_alert.dart';
|
||||
import 'package:hgdj/hj_utils/pay/pay_manager.dart';
|
||||
import 'package:hgdj/hj_utils/buy_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/hj_utils/widget_util.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
|
||||
|
||||
import '../../../../alert/video/vip_acg_dialog.dart';
|
||||
import '../../../../assets_tool/app_colors.dart';
|
||||
import '../../../mine/mine_share/mine_share_page.dart';
|
||||
|
||||
/// 帖子详情视频封面:未解锁时盖一层金币 / VIP 蒙层
|
||||
class PostDetailVideoContent extends StatefulWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailVideoContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
State<PostDetailVideoContent> createState() => _PostDetailVideoContentState();
|
||||
}
|
||||
|
||||
class _PostDetailVideoContentState extends State<PostDetailVideoContent> {
|
||||
VideoModel get model => widget.model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPlayable = model.isVideo() || model.isSeedPost();
|
||||
if (!isPlayable || model.sourceURL?.isNotEmpty != true)
|
||||
return const SizedBox.shrink();
|
||||
|
||||
final isLocked = needPostCoin(model);
|
||||
//非免费区的金币帖走金币解锁,其余走 VIP 解锁
|
||||
final isCoin = !(model.freeArea ?? false) && model.originCoins != 0;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
if (needPostCoin(model)) return;
|
||||
_openPlayer();
|
||||
},
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 220),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 391 / 220,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: model.cover ?? '',
|
||||
width: double.infinity,
|
||||
height: double.infinity),
|
||||
Image.asset('circle_play.webp'.videoPath,
|
||||
width: 30, height: 30),
|
||||
//未解锁蒙层
|
||||
if (isLocked)
|
||||
Positioned.fill(
|
||||
child: ClipRect(
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4),
|
||||
child: Container(
|
||||
color: Colors.black.withValues(alpha: .6),
|
||||
child: isCoin ? _coinMask() : _vipMask(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (model.isSeedPost()) 10.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//金币解锁蒙层
|
||||
Widget _coinMask() {
|
||||
final coins = model.originCoins ?? 0;
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
EasyRichText(
|
||||
'你只需花费 $coins 金币',
|
||||
defaultStyle: textStyle(16, Colors.white, FontWeight.w400),
|
||||
patternList: [
|
||||
EasyRichTextPattern(
|
||||
targetString: '$coins',
|
||||
style: textStyle(16, AppColors.actionRed, FontWeight.w400)),
|
||||
],
|
||||
),
|
||||
16.sizeBoxH,
|
||||
const Text('即可解锁继续查看',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400)),
|
||||
16.sizeBoxH,
|
||||
_btn(
|
||||
'立即解锁',
|
||||
width: 84,
|
||||
colors: const [Color(0xffFF6E6E), Color(0xffFF4D4D)],
|
||||
onTap: () async {
|
||||
//购买期间可能已被其他入口解锁,点的时候再判一次
|
||||
if (!needPostCoin(model)) {
|
||||
_openPlayer();
|
||||
return;
|
||||
}
|
||||
if (!await _pay()) return;
|
||||
model.vidStatus?.hasPaid = true;
|
||||
if (mounted) setState(() {});
|
||||
_openPlayer();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//VIP 解锁蒙层
|
||||
Widget _vipMask() {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Text('本视频需开通VIP免费观看',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400)),
|
||||
16.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_btn(
|
||||
'邀请得3日VIP',
|
||||
width: 110,
|
||||
colors: const [Color(0xffFFE8BE), Color(0xffE6B764)],
|
||||
textColor: const Color(0xff694923),
|
||||
onTap: () => Get.to(() => MineSharePage()),
|
||||
),
|
||||
18.sizeBoxW,
|
||||
_btn(
|
||||
'开通VIP免费看',
|
||||
width: 115,
|
||||
colors: const [Color(0xffFF6E6E), Color(0xffFF4D4D)],
|
||||
onTap: () => Get.dialog(VipACGDialog()),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//蒙层上的胶囊按钮
|
||||
Widget _btn(
|
||||
String text, {
|
||||
required double width,
|
||||
required List<Color> colors,
|
||||
required VoidCallback onTap,
|
||||
Color textColor = Colors.white,
|
||||
}) {
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: width,
|
||||
height: 30,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(colors: colors),
|
||||
borderRadius: BorderRadius.circular(15)),
|
||||
child: Text(text, style: textStyle(12, textColor, FontWeight.w400)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> _pay() async {
|
||||
bool isOk = false;
|
||||
await PayManager().buy(
|
||||
model.id,
|
||||
ProductType.media,
|
||||
source: 'post_cover',
|
||||
onSuccess: (data) {
|
||||
globalStore.updateUserInfo();
|
||||
isOk = true;
|
||||
},
|
||||
);
|
||||
return isOk;
|
||||
}
|
||||
|
||||
void _openPlayer() => Get.to(() => VideoPlayerAlert(model.realVideoUrl));
|
||||
}
|
||||
|
||||
/// 帖子详情多图封面:竖向拼接,只有首尾切圆角
|
||||
class PostDetailCoverContent extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailCoverContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//有视频源时封面已在视频区展示,这里剔除避免重复
|
||||
if (model.sourceURL?.isNotEmpty == true)
|
||||
model.seriesCover?.remove(model.cover);
|
||||
final covers = model.seriesCover ?? [];
|
||||
return Column(
|
||||
children: [
|
||||
if (covers.isNotEmpty)
|
||||
Wrap(
|
||||
runSpacing: 2,
|
||||
children: [
|
||||
for (final (index, url) in covers.indexed)
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => ImageBrowserPage.open(covers, index: index),
|
||||
child: ClipRRect(
|
||||
borderRadius: _radius(index, covers.length),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: url,
|
||||
width: double.infinity,
|
||||
fit: BoxFit.fill,
|
||||
borderRadius: 0),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
BorderRadius _radius(int index, int total) {
|
||||
const radius = Radius.circular(12);
|
||||
if (total == 1) return const BorderRadius.all(radius);
|
||||
if (index == 0)
|
||||
return const BorderRadius.only(topLeft: radius, topRight: radius);
|
||||
if (index == total - 1)
|
||||
return const BorderRadius.only(bottomLeft: radius, bottomRight: radius);
|
||||
return BorderRadius.zero;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/common_service.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
|
||||
import '../../../../alert/video/share_media_dialog.dart';
|
||||
import '../../../mine/collec_history_buy/col_his_buy_sub_page.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
|
||||
/// 帖子详情操作栏:浏览量 / 点赞 / 评论 / 分享
|
||||
class PostDetailOperationView extends StatefulWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailOperationView({super.key, required this.model});
|
||||
|
||||
@override
|
||||
State<PostDetailOperationView> createState() =>
|
||||
_PostDetailOperationViewState();
|
||||
}
|
||||
|
||||
class _PostDetailOperationViewState extends State<PostDetailOperationView> {
|
||||
VideoModel get model => widget.model;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 24,
|
||||
margin: EdgeInsets.only(bottom: 18.h),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 浏览量
|
||||
_iconLabel(
|
||||
icon: "eye_grey.webp".commonImgPath,
|
||||
gap: 2,
|
||||
text: model.pageViewCount?.countStr ?? "",
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
40.sizeBoxW,
|
||||
// 点赞
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: _toggleLike,
|
||||
child: _iconLabel(
|
||||
icon: model.vidStatus?.hasLiked == true
|
||||
? "like_red.png".commonImgPath
|
||||
: "like_empty_gray.png".communityPath,
|
||||
gap: 5.w,
|
||||
text: model.likeCount?.countOr("喜欢") ?? '',
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
),
|
||||
40.sizeBoxW,
|
||||
// 评论量
|
||||
_iconLabel(
|
||||
icon: "count_msg_gray.png".communityPath,
|
||||
gap: 2,
|
||||
text: model.commentCount?.countStr ?? "",
|
||||
fontSize: 12.sp,
|
||||
color: const Color(0xff999999),
|
||||
),
|
||||
40.sizeBoxW,
|
||||
// 分享
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => Get.dialog(ShareMediaDialog(videoModel: model)),
|
||||
child: _iconLabel(
|
||||
icon: 'share_grey.png'.commonImgPath,
|
||||
iconSize: 16,
|
||||
gap: 4.w,
|
||||
text: '分享',
|
||||
fontSize: 10.sp,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 图标 + 文案的横向组合
|
||||
Widget _iconLabel({
|
||||
required String icon,
|
||||
required String text,
|
||||
required double fontSize,
|
||||
double iconSize = 20,
|
||||
double gap = 2,
|
||||
Color color = const Color(0xff989898),
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(icon, width: iconSize, height: iconSize),
|
||||
gap.sizeBoxW,
|
||||
Text(text, style: TextStyle(color: color, fontSize: fontSize)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 点赞 / 取消点赞
|
||||
void _toggleLike() async {
|
||||
final wasLiked = model.vidStatus?.hasLiked ?? false;
|
||||
final count = model.likeCount ?? 0;
|
||||
final res = wasLiked
|
||||
? await CommonService.cancelLike(
|
||||
model.id ?? '', LoadDataType.post.apiType)
|
||||
: await CommonService.sendLike(
|
||||
model.id ?? '', LoadDataType.post.apiType);
|
||||
if (!res || !mounted) return;
|
||||
model.vidStatus?.hasLiked = !wasLiked;
|
||||
model.likeCount = max(0, count + (wasLiked ? -1 : 1));
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_utils/pay/pay_manager.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
/// 帖子金币解锁弹窗:解锁成功回传 true,点空白关闭交给 barrier
|
||||
class PostDetailUnlockDialog extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailUnlockDialog({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => PayManager().buy(
|
||||
model.id,
|
||||
ProductType.media,
|
||||
source: 'post_unlock',
|
||||
onSuccess: (data) {
|
||||
globalStore.updateUserInfo();
|
||||
Get.back(result: true);
|
||||
},
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color.fromRGBO(255, 238, 220, 1),
|
||||
Color.fromRGBO(249, 219, 191, 1)
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset('post_detail_coin.png'.communityPath,
|
||||
width: 30, height: 30),
|
||||
Text(
|
||||
'${model.videoCoin()}金币解锁继续',
|
||||
style: const TextStyle(
|
||||
color: Color(0xff370000),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
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_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/widget/image_browser_page.dart';
|
||||
|
||||
import '../../../../tools_base/richTextParsing/html_parser.dart';
|
||||
import '../../../../tools_base/widget/shrink_wrap.dart';
|
||||
import '../../community_tag_page/community_tag_page.dart';
|
||||
|
||||
/// 黄游帖的游戏介绍
|
||||
class GameDetailWordContent extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const GameDetailWordContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (model.newsType != 'ADULT_GAME') return const SizedBox.shrink();
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('游戏介绍', style: TextStyle(color: Colors.white, fontSize: 16)),
|
||||
18.sizeBoxH,
|
||||
Text(model.content ?? '',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 帖子正文:标题 + 标签 + 富文本 / 纯文本内容
|
||||
class PostDetailWordContent extends StatelessWidget {
|
||||
final VideoModel model;
|
||||
|
||||
const PostDetailWordContent({super.key, required this.model});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (model.newsType == 'ADULT_GAME') return const SizedBox.shrink();
|
||||
final contentStyle =
|
||||
TextStyle(color: const Color(0xFFDCDCDC), fontSize: 14.sp);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
14.sizeBoxH,
|
||||
//标题
|
||||
if (model.title?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 1.h),
|
||||
child: Text(model.title ?? '', style: contentStyle)),
|
||||
//标签
|
||||
_tags(),
|
||||
12.h.sizeBoxH,
|
||||
Divider(height: .5, color: Colors.white.withValues(alpha: .05)),
|
||||
18.h.sizeBoxH,
|
||||
//正文:富文本优先,没有富文本才走纯文本
|
||||
if (model.richText?.isNotEmpty == true)
|
||||
_richText()
|
||||
else if (model.content?.isNotEmpty == true)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 18.h),
|
||||
child: Text(model.content ?? '', style: contentStyle)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _richText() {
|
||||
return DefaultTextStyle(
|
||||
style: const TextStyle(color: Colors.white),
|
||||
child: Wrap(
|
||||
alignment: WrapAlignment.start,
|
||||
children: HtmlParser(
|
||||
width: double.infinity,
|
||||
onLinkTap: pushToPageByLink,
|
||||
imgLinkTap: (url) => ImageBrowserPage.open([url]),
|
||||
).parse(model.richText ?? ''),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _tags() {
|
||||
final tags = model.tags ?? [];
|
||||
if (tags.isEmpty) return const SizedBox();
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: ShrinkWrap(
|
||||
spacing: 0,
|
||||
runSpacing: 6,
|
||||
maxLines: 1,
|
||||
children: tags
|
||||
.map((e) => InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => Get.to(() => CommunityTagDetailPage(model: e)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.fromLTRB(8, 3, 8, 3),
|
||||
margin: const EdgeInsets.only(right: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(3)),
|
||||
child: Text('#${e.name}',
|
||||
style: const TextStyle(
|
||||
color: Color(0x8CFFFFFF), fontSize: 12)),
|
||||
),
|
||||
))
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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_utils/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/global_store/store.dart';
|
||||
|
||||
import '../../../../hj_model/video_model.dart';
|
||||
import 'package:hgdj/tools_base/toast.dart';
|
||||
import 'post_detail_unlock_dialog.dart';
|
||||
|
||||
/// 种子帖 / 黄游帖的资源信息与下载入口
|
||||
class PostResourceInfoWidget extends StatefulWidget {
|
||||
final VideoModel videoModel;
|
||||
|
||||
const PostResourceInfoWidget({super.key, required this.videoModel});
|
||||
|
||||
@override
|
||||
State<PostResourceInfoWidget> createState() => _PostResourceInfoWidgetState();
|
||||
}
|
||||
|
||||
class _PostResourceInfoWidgetState extends State<PostResourceInfoWidget> {
|
||||
VideoModel get model => widget.videoModel;
|
||||
|
||||
/// 是否还需要花金币解锁
|
||||
bool get _needLock {
|
||||
if (model.freeArea == true) return false;
|
||||
final me = globalStore.meInfo;
|
||||
//金币视频限免期内:全场免费 或 50 金币以内免费
|
||||
final isFreeTime = DateTimeUtil.calTime3(me?.goldVideoFreeExpire) > 0;
|
||||
if (isFreeTime && (me?.allGoldVideoFree == true || (model.coins ?? 0) < 50))
|
||||
return false;
|
||||
return model.isCoinVideo() &&
|
||||
!globalStore.isMe(model.publisher?.uid) &&
|
||||
!(model.vidStatus?.hasPaid ?? false);
|
||||
}
|
||||
|
||||
String get _buyDesc =>
|
||||
_needLock ? '${model.realCoins ?? 0}金币解锁 即可下载种子' : '开通VIP 即可下载种子';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isSeed = model.newsType == 'SEED_LINK';
|
||||
if (!isSeed && model.newsType != 'ADULT_GAME') return const SizedBox();
|
||||
|
||||
const descStyle =
|
||||
TextStyle(color: Color(0xff999999), fontSize: 14, height: 1.5);
|
||||
//未解锁、或非 VIP 的非免费区内容,都只能看解锁入口
|
||||
final isLocked =
|
||||
_needLock || (!globalStore.isVIP && model.freeArea != true);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//种子帖的影片信息与下载说明
|
||||
if (isSeed) ...[
|
||||
if ((model.seedSize ?? 0) > 0)
|
||||
Text('[影片大小]:${model.seedSizeDesc}', style: descStyle),
|
||||
if ((model.seedPlayTime ?? 0) > 0)
|
||||
Text('[影片时长]:${model.seedPlayTimeDesc}', style: descStyle),
|
||||
const Text(
|
||||
'[下载说明]:解锁后请复制种子的下载链接到手机自带浏览器(谷歌/UC/夸克/火狐 等)打开!或使用BT软件(迅雷/Vuze/uTorrent/BitTorrent 等)进行下载.',
|
||||
style: descStyle,
|
||||
),
|
||||
const Text('[温馨提示]:下载后请勿传播分享,仅供个人欣赏哟~~', style: descStyle),
|
||||
18.sizeBoxH,
|
||||
],
|
||||
//解锁入口 / 复制链接
|
||||
Center(
|
||||
child: isLocked
|
||||
? InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _showUnlock,
|
||||
child: Text(
|
||||
_buyDesc,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
color: AppColors.primaryHighColor,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
)
|
||||
: InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _copyLink,
|
||||
child: Image.asset('link_copy_icon.webp'.communityPath,
|
||||
width: 180, height: 32),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 需要金币的弹解锁弹窗,否则去充值页开 VIP
|
||||
void _showUnlock() async {
|
||||
if (!_needLock) {
|
||||
pushToWalletPage(tabPosition: 0);
|
||||
return;
|
||||
}
|
||||
final result = await Get.dialog(
|
||||
PostDetailUnlockDialog(model: model),
|
||||
barrierColor: const Color.fromRGBO(14, 20, 30, 0.6),
|
||||
);
|
||||
if (result == null || !mounted) return;
|
||||
model.vidStatus?.hasPaid = true;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _copyLink() {
|
||||
Clipboard.setData(ClipboardData(text: model.seedRealLink));
|
||||
showToast('链接地址复制成功');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/comment/comment_mixin.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/vid_service.dart';
|
||||
import 'package:hgdj/hj_utils/const.dart';
|
||||
|
||||
import '../../../hj_utils/history_util.dart';
|
||||
import 'post_detail_page.dart';
|
||||
|
||||
class PostDetailLogic extends GetxController with CommentMixin {
|
||||
// ===== 状态 =====
|
||||
bool isLoading = false;
|
||||
|
||||
// ===== 数据 =====
|
||||
final PostDetailPageArgument args;
|
||||
VideoModel? model;
|
||||
|
||||
// ===== Controller =====
|
||||
final scrollCtr = ScrollController();
|
||||
|
||||
PostDetailLogic(this.args);
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
id = args.id;
|
||||
objType = 'video';
|
||||
}
|
||||
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
initData();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
// scrollCtr 在本类创建,需自行释放;commentController 由 CommentMixin.onClose 处理
|
||||
scrollCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
//详情与评论并行拉,两个都回来才收 loading
|
||||
void initData() {
|
||||
isLoading = true;
|
||||
update();
|
||||
Future.wait([fetchDetail(), fetchComments(args.id ?? '')]).then((_) {
|
||||
isLoading = false;
|
||||
update();
|
||||
});
|
||||
}
|
||||
|
||||
Future fetchDetail() async {
|
||||
final res = await VidService.getDetail(args.id ?? '');
|
||||
if (res != null) {
|
||||
model = res;
|
||||
HistoryUtil.insert(model!, MediaStyle.Community);
|
||||
}
|
||||
update();
|
||||
}
|
||||
|
||||
/// 上下篇切换:原地换 id 重拉,不 push 新页
|
||||
/// 配套数据是 args.videoModels(各入口都在传),但翻页入口还没接,别当死代码删
|
||||
void nextOrPrePost(String? newId) {
|
||||
args.id = newId;
|
||||
id = newId;
|
||||
page = 1;
|
||||
comments.clear();
|
||||
initData();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/comment/cell/comment_item.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/banner/ads_grid_view_widget.dart';
|
||||
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgdj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgdj/tools_base/unique_tag_mixin.dart';
|
||||
|
||||
import '../../comment/widget/comment_foot_view.dart';
|
||||
import '../../home/widget/quick_search_view.dart';
|
||||
import '../widget/post_user_info_view.dart';
|
||||
import 'detail_widget/post_detail_cover_content.dart';
|
||||
import 'detail_widget/post_detail_operation_view.dart';
|
||||
import 'detail_widget/post_detail_word_content.dart';
|
||||
import 'detail_widget/post_resource_info_widget.dart';
|
||||
import 'post_detail_logic.dart';
|
||||
|
||||
/// 过滤掉广告位,只留真实帖子
|
||||
List<VideoModel>? _filters(List<VideoModel>? videoModel) {
|
||||
return (videoModel ?? [])
|
||||
.where((e) => !e.isRandomAd() && !e.isAdsArr())
|
||||
.toList();
|
||||
}
|
||||
|
||||
class PostDetailPageArgument {
|
||||
final List<VideoModel>? videoModels;
|
||||
String? id;
|
||||
|
||||
PostDetailPageArgument({List<VideoModel>? models, this.id})
|
||||
: videoModels = _filters(models);
|
||||
}
|
||||
|
||||
class PostDetailPage extends StatefulWidget {
|
||||
final PostDetailPageArgument argument;
|
||||
|
||||
const PostDetailPage({super.key, required this.argument});
|
||||
|
||||
@override
|
||||
State<PostDetailPage> createState() => _PostDetailPageState();
|
||||
}
|
||||
|
||||
// 详情页能点头像进用户主页,主页作品列表又能开帖子详情(A→B→A),两份会并存,
|
||||
// 所以 logic 必须按实例隔离;不能省掉 tag 让它们共用一个
|
||||
class _PostDetailPageState extends State<PostDetailPage> with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<PostDetailLogic>(
|
||||
init: PostDetailLogic(widget.argument),
|
||||
tag: uniqueTag,
|
||||
builder: (logic) {
|
||||
final model = logic.model;
|
||||
//置顶评论下标:getter 内部要遍历评论,别丢进 itemBuilder 里每条算一遍
|
||||
final topIndex = logic.topPinnedCommentIndex;
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: AppBar(
|
||||
titleSpacing: 18.w,
|
||||
automaticallyImplyLeading: false,
|
||||
title: _titleView(logic)),
|
||||
body: logic.isLoading || model == null
|
||||
? SizedBox(height: 300, child: LoadingCenterWidget())
|
||||
: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 18.w),
|
||||
child: pullYsRefresh(
|
||||
enablePullDown: false,
|
||||
onInit: (refreshCtr) => logic.refreshCtr = refreshCtr,
|
||||
onLoading: (_) =>
|
||||
logic.fetchComments(logic.args.id ?? ''),
|
||||
child: CustomScrollView(
|
||||
controller: logic.scrollCtr,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(child: 14.h.sizeBoxH),
|
||||
//发帖人信息
|
||||
SliverToBoxAdapter(
|
||||
child: PostUserInfoView(
|
||||
model: model, isDetailStyle: true)),
|
||||
//正文
|
||||
SliverToBoxAdapter(
|
||||
child: PostDetailWordContent(model: model)),
|
||||
//富文本帖自带图,不再单独铺封面
|
||||
if (model.richText?.isNotEmpty != true)
|
||||
SliverToBoxAdapter(
|
||||
child:
|
||||
PostDetailCoverContent(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: GameDetailWordContent(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: PostDetailVideoContent(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: PostResourceInfoWidget(
|
||||
videoModel: model)),
|
||||
SliverToBoxAdapter(child: 16.sizeBoxH),
|
||||
//点赞/收藏/分享
|
||||
SliverToBoxAdapter(
|
||||
child: PostDetailOperationView(model: model)),
|
||||
SliverToBoxAdapter(
|
||||
child: AdsGridViewWidget(
|
||||
7,
|
||||
accordingAdsType: true,
|
||||
),
|
||||
),
|
||||
//评论数 + 快捷搜索词
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${model.commentCount?.countStr}条评论',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: Colors.white
|
||||
.withValues(alpha: 0.9)),
|
||||
),
|
||||
if (logic.quickSearchList.isNotEmpty) ...[
|
||||
18.sizeBoxH,
|
||||
QuickSearchView(logic.quickSearchList)
|
||||
]
|
||||
],
|
||||
).paddingOnly(bottom: 14),
|
||||
),
|
||||
//评论列表
|
||||
if (logic.comments.isEmpty)
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(
|
||||
height: 220, child: CErrorWidget()))
|
||||
else
|
||||
SliverList.builder(
|
||||
itemCount: logic.comments.length,
|
||||
itemBuilder: (_, index) => CommentItem(
|
||||
logic.comments[index],
|
||||
objId: model.id ?? '',
|
||||
//置顶那条上方插一组 Banner
|
||||
topBanners: index == topIndex &&
|
||||
logic.commentTopBanners.isNotEmpty
|
||||
? logic.commentTopBanners
|
||||
: null,
|
||||
replyHandler: (comment, {reply}) =>
|
||||
logic.readyReplyComment(comment,
|
||||
replyModel: reply),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
//底部评论输入条
|
||||
Obx(
|
||||
() => CommentFootView(
|
||||
video: model,
|
||||
showCollect: true,
|
||||
onComment: (isSend) =>
|
||||
logic.readyReplyComment(null, isSend: isSend),
|
||||
isSending: logic.isSendingMsg.value,
|
||||
textCtr: logic.commentController,
|
||||
imgPath: logic.selectedImagePath.value,
|
||||
onImgChange: (value) =>
|
||||
logic.selectedImagePath.value = value,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//标题栏:未滚动 / 游戏帖显示固定标题,滚过 60 后换成发帖人信息
|
||||
Widget _titleView(PostDetailLogic logic) {
|
||||
final scrollCtr = logic.scrollCtr;
|
||||
return ListenableBuilder(
|
||||
listenable: scrollCtr,
|
||||
builder: (_, __) {
|
||||
final isGame = logic.model?.newsType == 'ADULT_GAME';
|
||||
if (!scrollCtr.hasClients || scrollCtr.offset < 60 || isGame) {
|
||||
return Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
children: [
|
||||
_backBtn(),
|
||||
Center(
|
||||
child: Text(switch (logic.model?.newsType) {
|
||||
'SEED_LINK' => '种子详情',
|
||||
'ADULT_GAME' => '游戏详情',
|
||||
_ => '帖子详情',
|
||||
}),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
children: [
|
||||
_backBtn(),
|
||||
4.sizeBoxW,
|
||||
Expanded(
|
||||
child: PostUserInfoView(
|
||||
model: logic.model!, isDetailStyle: true, avatarSize: 30)),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _backBtn() {
|
||||
return GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: const Icon(Icons.arrow_back_ios, size: 18, color: Colors.white),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
import '../community/community_tab_logic.dart';
|
||||
|
||||
/// 社区首页的排序 tab 条,标题取自 CommunityTabLogic.sortTabs
|
||||
class CommunityMiddleHeader extends StatefulWidget {
|
||||
final Function(int sort) sortOnTap;
|
||||
const CommunityMiddleHeader({super.key, required this.sortOnTap});
|
||||
|
||||
@override
|
||||
State<CommunityMiddleHeader> createState() => _CommunityMiddleHeaderState();
|
||||
}
|
||||
|
||||
class _CommunityMiddleHeaderState extends State<CommunityMiddleHeader>
|
||||
with SingleTickerProviderStateMixin {
|
||||
//别在这里另写一份标题
|
||||
final sorts = CommunityTabLogic.sortTabs.map((e) => e.name).toList();
|
||||
late final TabController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//别用 late 惰性求值:万一没走 build 就 dispose,会在 dispose 里现建一个 Ticker
|
||||
_controller = TabController(length: sorts.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ColoredBox(
|
||||
color: AppColors.primaryColor,
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 20.w),
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: widget.sortOnTap,
|
||||
tabAlignment: TabAlignment.center,
|
||||
tabs: sorts
|
||||
.map((e) => Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||
child: Text(e),
|
||||
))
|
||||
.toList(),
|
||||
indicatorWeight: 1,
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .55),
|
||||
fontSize: 16.sp,
|
||||
height: 1),
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontSize: 16.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600),
|
||||
indicator: const BoxDecoration(),
|
||||
controller: _controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../find/rank_module/rank_badge.dart';
|
||||
import '../../home/widget/collect_status_wrapper.dart';
|
||||
import '../community_tag_page/community_tag_page.dart';
|
||||
import '../post_detail_page/post_detail_page.dart';
|
||||
import 'post_user_info_view.dart';
|
||||
import 'post_word_rich_text.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/tools_base/widget/common_alert.dart';
|
||||
|
||||
typedef OnItemCheckCallback = void Function(
|
||||
VideoModel videoModel, bool isChecked);
|
||||
|
||||
/// 社区帖子卡片,社区/搜索/个人主页/我的发布/收藏历史多处复用
|
||||
class CommunityPostWidget extends StatelessWidget {
|
||||
final VideoModel videoModel;
|
||||
|
||||
/// 详情页要拿整个列表做左右滑,由列表页传进来
|
||||
final List<VideoModel>? videoModels;
|
||||
final GestureTapCallback? clickCallback;
|
||||
|
||||
/// 我的发布模式:顶部换成「发布/上传于…」+ 审核状态
|
||||
final bool isMyPublish;
|
||||
final bool showTags; //是否展示标签
|
||||
final bool showLine; //展示分割线
|
||||
final bool showCheck; //展示勾选框
|
||||
final bool isChecked; //是否选中
|
||||
final OnItemCheckCallback? onItemCheck;
|
||||
final int? rankIndex; //排行
|
||||
final EdgeInsetsGeometry? padding;
|
||||
|
||||
const CommunityPostWidget({
|
||||
super.key,
|
||||
required this.videoModel,
|
||||
this.clickCallback,
|
||||
this.isMyPublish = false,
|
||||
this.videoModels,
|
||||
this.showTags = true,
|
||||
this.padding = const EdgeInsets.fromLTRB(16, 10, 16, 10),
|
||||
this.showLine = true,
|
||||
this.showCheck = false,
|
||||
this.onItemCheck,
|
||||
this.isChecked = false,
|
||||
this.rankIndex,
|
||||
});
|
||||
|
||||
//status: 0待审核 1已成功 2未通过
|
||||
String get _statusPrefix => videoModel.status == 1 ? '发布' : '上传';
|
||||
|
||||
String get _statusDesc => switch (videoModel.status) {
|
||||
0 => '待审核',
|
||||
1 => '已成功',
|
||||
2 => '未通过原因',
|
||||
_ => '未知状态', //含 5
|
||||
};
|
||||
|
||||
Color get _statusColor => videoModel.status == 2
|
||||
? const Color(0xffFD0563)
|
||||
: const Color(0xff3476FF);
|
||||
|
||||
bool get _showStatus => videoModel.status != 1 && !showCheck;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//视频帖的封面也在图集里,去重后图集只留纯图
|
||||
if (videoModel.sourceURL?.isNotEmpty == true) {
|
||||
videoModel.seriesCover?.remove(videoModel.cover ?? "");
|
||||
}
|
||||
return InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: clickCallback ??
|
||||
() {
|
||||
//在点击时算好:PostDetailPageArgument 内部要过滤整个列表,别丢进 page builder 里重复跑
|
||||
final argument =
|
||||
PostDetailPageArgument(id: videoModel.id, models: videoModels);
|
||||
Get.to(() => PostDetailPage(argument: argument));
|
||||
},
|
||||
child: Padding(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//顶部:我的发布出「发布于…」+ 审核态,其余出用户信息行
|
||||
if (!isMyPublish)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: PostUserInfoView(model: videoModel)),
|
||||
if (rankIndex != null) rankBadge(rankIndex!),
|
||||
],
|
||||
),
|
||||
)
|
||||
else ...[
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'$_statusPrefix于${formatTimeTwo(videoModel.realCreateAt)}',
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style:
|
||||
const TextStyle(color: Color(0x8CFFFFFF), fontSize: 14),
|
||||
),
|
||||
const Spacer(),
|
||||
if (showCheck)
|
||||
GestureDetector(
|
||||
onTap: () => onItemCheck?.call(videoModel, !isChecked),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
//白底垫在勾选图标下面,选中时圆圈内部才不透出背景
|
||||
Container(
|
||||
width: 18,
|
||||
height: 18,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
isChecked ? Colors.white : Colors.transparent,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isChecked
|
||||
? CupertinoIcons.checkmark_circle_fill
|
||||
: CupertinoIcons.circle,
|
||||
color: isChecked
|
||||
? const Color(0xffF68804)
|
||||
: const Color(0xff999999),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_showStatus)
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () async {
|
||||
if (videoModel.status == 2) {
|
||||
await CommonAlert.show(
|
||||
title: "未过审原因",
|
||||
content: '${videoModel.reason}',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(_statusDesc,
|
||||
style:
|
||||
TextStyle(color: _statusColor, fontSize: 14.sp)),
|
||||
),
|
||||
],
|
||||
),
|
||||
14.sizeBoxH,
|
||||
],
|
||||
//标题(含置顶、精华标签)
|
||||
if (videoModel.title?.trim().isNotEmpty == true)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: PostWordRichText(videoModel: videoModel),
|
||||
),
|
||||
//封面:视频单图 / 单图 / 三宫格
|
||||
_coverView(),
|
||||
12.sizeBoxH,
|
||||
//底部:浏览、评论、点赞 + 右侧话题
|
||||
Row(
|
||||
children: [
|
||||
_countItem(videoModel.pageViewCount?.countStr,
|
||||
"eye_grey.webp".commonImgPath),
|
||||
12.sizeBoxW,
|
||||
_countItem(videoModel.commentCount?.countStr,
|
||||
"count_msg_gray.png".communityPath),
|
||||
12.sizeBoxW,
|
||||
CollectStatusWrapper(
|
||||
videoModel: videoModel,
|
||||
builder: () => _countItem(
|
||||
videoModel.likeCount?.countStr,
|
||||
videoModel.vidStatus?.hasLiked == true
|
||||
? "like_red.png".commonImgPath
|
||||
: "like_empty_gray.png".communityPath,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
if (showTags) _tagView(),
|
||||
],
|
||||
),
|
||||
if (showLine)
|
||||
Container(
|
||||
height: 1,
|
||||
margin: const EdgeInsets.only(top: 18),
|
||||
color: Colors.white.withValues(alpha: .1),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 封面区:无图集的视频帖出单封面 + 播放按钮,单图直接铺,多图走三宫格
|
||||
Widget _coverView() {
|
||||
final covers = videoModel.seriesCover ?? [];
|
||||
final isVideo = videoModel.sourceURL?.isNotEmpty == true;
|
||||
|
||||
if (covers.isEmpty && isVideo) {
|
||||
return SizedBox(
|
||||
height: 204,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: videoModel.cover ?? '', borderRadius: 6),
|
||||
Center(
|
||||
child: Image.asset("play_icon.png".communityPath,
|
||||
width: 32, height: 32)),
|
||||
Positioned(right: 0, top: 0, child: _coinBadge()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
if (covers.length == 1) {
|
||||
return SizedBox(
|
||||
height: 204,
|
||||
width: double.infinity,
|
||||
child: NetworkImageLoader(imageUrl: covers[0], borderRadius: 6),
|
||||
);
|
||||
}
|
||||
|
||||
//视频帖的封面被上面 remove 掉了,格子数要把它加回来
|
||||
final coverCount = covers.length + (isVideo ? 1 : 0);
|
||||
return GridView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: min(3, coverCount),
|
||||
shrinkWrap: true,
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 5,
|
||||
crossAxisSpacing: 5,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemBuilder: (context, index) {
|
||||
final isLast = index == 2 || coverCount == index + 1;
|
||||
//视频帖把视频封面排在最后一格,其余格按图集顺序取
|
||||
final imgPath = (isLast && isVideo) || index >= covers.length
|
||||
? (videoModel.cover ?? "")
|
||||
: covers[index];
|
||||
//只有首尾两格带外圆角,中间格是直角
|
||||
final radius = isLast
|
||||
? const BorderRadius.only(
|
||||
topRight: Radius.circular(6), bottomRight: Radius.circular(6))
|
||||
: index == 0
|
||||
? const BorderRadius.only(
|
||||
topLeft: Radius.circular(6), bottomLeft: Radius.circular(6))
|
||||
: BorderRadius.zero;
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(imageUrl: imgPath, imgBorderRadius: radius),
|
||||
if (isLast) Positioned(right: 0, top: 0, child: _coinBadge()),
|
||||
if (isLast && isVideo)
|
||||
Center(
|
||||
child: Image.asset("play_icon.png".communityPath, width: 30))
|
||||
else if (isLast && coverCount > 3)
|
||||
Positioned(
|
||||
bottom: 6,
|
||||
right: 6,
|
||||
child: Container(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Text(
|
||||
"+${coverCount - 3}",
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xff141414),
|
||||
fontWeight: FontWeight.w400,
|
||||
height: 1.6),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
//金币帖右上角的角标
|
||||
Widget _coinBadge() {
|
||||
if (videoModel.permission != 1) return const SizedBox.shrink();
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3),
|
||||
decoration: const BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
topRight: Radius.circular(6), bottomLeft: Radius.circular(6)),
|
||||
color: Color(0xffDB361F),
|
||||
),
|
||||
child: Image.asset("coin.webp".commonImgPath, width: 14, height: 14),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _countItem(String? title, String icon) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(icon, width: 20, height: 20),
|
||||
const SizedBox(width: 4),
|
||||
Text(title ?? "",
|
||||
style: const TextStyle(color: Color(0xff999999), fontSize: 12)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 帖子标签
|
||||
Widget _tagView() {
|
||||
if (videoModel.tags?.isNotEmpty != true) return const SizedBox();
|
||||
final tagsBean = videoModel.tags!.first;
|
||||
tagsBean.newsType = videoModel.newsType;
|
||||
return GestureDetector(
|
||||
onTap: () => Get.to(() => CommunityTagDetailPage(model: tagsBean)),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
color: const Color(0x0DFFFFFF)),
|
||||
child: Text(
|
||||
'#${tagsBean.name ?? ''}',
|
||||
style: const TextStyle(color: Color(0x8CFFFFFF), fontSize: 12),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
|
||||
class CommunitySortHeader extends StatefulWidget {
|
||||
final Function(int sort) sortOnTap;
|
||||
final TabController? controller;
|
||||
const CommunitySortHeader({
|
||||
super.key,
|
||||
required this.sortOnTap,
|
||||
this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CommunitySortHeader> createState() => _CommunitySortHeaderState();
|
||||
}
|
||||
|
||||
class _CommunitySortHeaderState extends State<CommunitySortHeader>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final sorts = ['最多收藏', '最新上架', '最新热评'];
|
||||
late TabController _controller;
|
||||
|
||||
/// 外部传进来的归外部释放,只有自己 new 的才由自己 dispose
|
||||
late final bool _isMine;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
//在 initState 定死归属,别用 late 惰性求值——那时读到的可能是 rebuild 后的新 widget
|
||||
_isMine = widget.controller == null;
|
||||
_controller =
|
||||
widget.controller ?? TabController(length: sorts.length, vsync: this);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_isMine) _controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryColor,
|
||||
),
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
labelPadding: EdgeInsets.symmetric(horizontal: 12.w),
|
||||
padding: EdgeInsets.zero,
|
||||
onTap: widget.sortOnTap,
|
||||
tabAlignment: TabAlignment.center,
|
||||
tabs: sorts
|
||||
.map((e) => Padding(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
|
||||
child: Text(e),
|
||||
))
|
||||
.toList(),
|
||||
indicatorWeight: 1,
|
||||
unselectedLabelStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .6),
|
||||
fontSize: 16.sp,
|
||||
height: 1),
|
||||
labelStyle: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16.sp,
|
||||
height: 1,
|
||||
fontWeight: FontWeight.w600),
|
||||
// 无指示器:原 ContainerTabIndicator 为全透明,选中态仅靠文字加粗变白
|
||||
indicator: const BoxDecoration(),
|
||||
controller: _controller,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/app_colors.dart';
|
||||
|
||||
/// 关注 / 收藏按钮,加载中转菊花
|
||||
class FollowLoadingButton extends StatelessWidget {
|
||||
final bool? isFollow;
|
||||
final bool? isLoading;
|
||||
final bool? isCollectStyle;
|
||||
final double radius;
|
||||
const FollowLoadingButton({
|
||||
super.key,
|
||||
this.isFollow,
|
||||
this.isLoading,
|
||||
this.isCollectStyle,
|
||||
this.radius = 6,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final followed = isFollow == true;
|
||||
//已关注是灰底灰字 10 号,未关注是红底白字 12 号
|
||||
final fg = followed ? const Color(0xff989898) : Colors.white;
|
||||
return Container(
|
||||
width: 70,
|
||||
height: 28,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
color: followed ? const Color(0x0DFFFFFF) : AppColors.actionRed,
|
||||
),
|
||||
child: isLoading == true
|
||||
? const CupertinoActivityIndicator(color: Colors.white, radius: 8)
|
||||
: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(followed ? Icons.check : Icons.add, size: 14, color: fg),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
isCollectStyle == true ? "收藏" : (followed ? "已关注" : "关注"),
|
||||
style: TextStyle(color: fg, fontSize: followed ? 10 : 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/extension/extensions.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
import '../../find/pic_collect/pics_detail_page.dart';
|
||||
|
||||
/// 图集卡片:封面 + 底部渐变条(浏览数 / 图片数) + 标题
|
||||
class PicSimpleCell extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
final int textLines;
|
||||
const PicSimpleCell({super.key, this.videoModel, this.textLines = 2});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () =>
|
||||
Get.to(PicsDetailPage(id: videoModel?.id), preventDuplicates: false),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
NetworkImageLoader(
|
||||
imageUrl: videoModel?.cover ?? '',
|
||||
borderRadius: 8,
|
||||
alignment: Alignment.topCenter,
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(6, 8, 6, 6),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.black.withValues(alpha: 0),
|
||||
Colors.black.withValues(alpha: 0.6),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset("eye_white.webp".commonImgPath, width: 16),
|
||||
1.sizeBoxW,
|
||||
Text(
|
||||
"${videoModel?.playCount?.countStr}",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Colors.white, fontSize: 10),
|
||||
),
|
||||
Spacer(),
|
||||
Container(
|
||||
padding:
|
||||
EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
child: Row(
|
||||
children: [
|
||||
Image.asset("icon_pic.png".communityPath,
|
||||
width: 16),
|
||||
2.sizeBoxW,
|
||||
Text(
|
||||
videoModel?.seriesCover?.length.toString() ??
|
||||
"",
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
"${videoModel?.title ?? ""}\n",
|
||||
maxLines: textLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/assets_tool/images.dart';
|
||||
import 'package:hgdj/hj_model/video_model.dart';
|
||||
import 'package:hgdj/hj_page/community/widget/follow_loading_button.dart';
|
||||
import 'package:hgdj/hj_page/home/widget/collect_status_wrapper.dart';
|
||||
import 'package:hgdj/hj_page/home/widget/user_name_view.dart';
|
||||
import 'package:hgdj/hj_utils/api_service/mine_service.dart';
|
||||
import 'package:hgdj/hj_utils/date_time_util.dart';
|
||||
import 'package:hgdj/hj_utils/screen.dart';
|
||||
import 'package:hgdj/routers/jump_router.dart';
|
||||
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
|
||||
|
||||
//帖子发布者信息:头像 + 昵称 + 发布时间,详情模式额外带关注按钮
|
||||
class PostUserInfoView extends StatefulWidget {
|
||||
final VideoModel model;
|
||||
final bool isDetailStyle; // 帖子详情模式
|
||||
final double? avatarSize; // 头像边长,默认 40
|
||||
|
||||
const PostUserInfoView({
|
||||
super.key,
|
||||
required this.model,
|
||||
this.avatarSize,
|
||||
this.isDetailStyle = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PostUserInfoView> createState() => _PostUserInfoViewState();
|
||||
}
|
||||
|
||||
class _PostUserInfoViewState extends State<PostUserInfoView> {
|
||||
VideoModel get model => widget.model;
|
||||
|
||||
Publisher? get publisher => model.publisher;
|
||||
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
//无发布者、黄游帖不展示
|
||||
if (publisher == null || model.newsType == 'ADULT_GAME') {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final size = widget.avatarSize ?? 40;
|
||||
return Row(
|
||||
children: [
|
||||
//头像
|
||||
InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: () => pushToPersonCenter(publisher?.uid),
|
||||
child: NetworkImageLoader(
|
||||
imageUrl: publisher?.portrait ?? "",
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size / 2,
|
||||
),
|
||||
),
|
||||
10.sizeBoxW,
|
||||
//昵称 + 发布时间
|
||||
Expanded(child: widget.isDetailStyle ? _nameDetail() : _nameNormal()),
|
||||
//关注按钮:仅详情模式,套 CollectStatusWrapper 同步全局关注态
|
||||
if (widget.isDetailStyle)
|
||||
CollectStatusWrapper(
|
||||
videoModel: model,
|
||||
builder: () => InkWell(
|
||||
enableFeedback: false,
|
||||
onTap: _toggleFollow,
|
||||
child: FollowLoadingButton(
|
||||
isFollow: publisher?.hasFollowed ?? false,
|
||||
isLoading: _isLoading),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//关注/取关:先锁定目标值,避免 await 期间 eventBus 改写 hasFollowed 导致取反基准漂移
|
||||
void _toggleFollow() async {
|
||||
if (_isLoading) return;
|
||||
final willFollow = !(publisher?.hasFollowed ?? false);
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
final res = await MineService.getFollow(publisher?.uid, willFollow);
|
||||
if (res) publisher?.hasFollowed = willFollow;
|
||||
|
||||
_isLoading = false;
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
//列表样式
|
||||
Widget _nameNormal() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
UserNameView(
|
||||
nameColor: Colors.white,
|
||||
name: publisher?.name,
|
||||
isOfficial: publisher?.officialCert,
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
formatTimeTwo(model.reviewAt),
|
||||
style: const TextStyle(color: Color(0xff999999), fontSize: 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
//详情样式:昵称后跟官方认证标
|
||||
Widget _nameDetail() {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Flexible(
|
||||
child: UserNameView(
|
||||
nameColor: Colors.white,
|
||||
name: publisher?.name,
|
||||
isOfficial: publisher?.officialCert,
|
||||
),
|
||||
),
|
||||
if (publisher?.officialCert == true) ...[
|
||||
4.sizeBoxW,
|
||||
Image.asset("official_icon.png".commonImgPath,
|
||||
width: 22, height: 22),
|
||||
],
|
||||
],
|
||||
),
|
||||
2.sizeBoxH,
|
||||
Text(
|
||||
formatTimeTwo(model.reviewAt),
|
||||
style: const TextStyle(color: Color(0x8C999999), fontSize: 12),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgdj/hj_utils/widget_util.dart';
|
||||
|
||||
import '../../../hj_model/video_model.dart';
|
||||
|
||||
/// 帖子标题:置顶 / 精华标签跟标题混排,用 WidgetSpan 才能跟着文字换行
|
||||
class PostWordRichText extends StatelessWidget {
|
||||
final VideoModel? videoModel;
|
||||
|
||||
const PostWordRichText({super.key, this.videoModel});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (videoModel?.isTopping ?? false) _badge('置顶'),
|
||||
if (videoModel?.chosen ?? false) _badge('精华'),
|
||||
TextSpan(
|
||||
text: videoModel?.title ?? '',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: .9),
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
}
|
||||
|
||||
WidgetSpan _badge(String text) => WidgetSpan(
|
||||
child: Container(
|
||||
width: 32,
|
||||
height: 18,
|
||||
alignment: Alignment.topCenter,
|
||||
margin: const EdgeInsets.only(right: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xffF52C56),
|
||||
borderRadius: BorderRadius.circular(2)),
|
||||
child:
|
||||
Text(text, style: textStyle(12, Colors.white, FontWeight.w500)),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user