初始化

This commit is contained in:
谢宇宁
2026-09-15 15:44:13 +07:00
commit a23ba685e9
1065 changed files with 101122 additions and 0 deletions
+210
View File
@@ -0,0 +1,210 @@
import 'package:hgdj/hj_model/list_base_model.dart';
import 'dart:async';
import 'package:get/get.dart';
import 'package:hgdj/hj_model/video_model.dart';
import 'package:hgdj/hj_page/live/live_service.dart';
import 'package:hgdj/hj_utils/date_time_util.dart';
import 'package:hgdj/tools_base/event_bus/event_bus_util.dart';
import 'package:hgdj/tools_base/event_bus/events.dart';
import 'package:hgdj/tools_base/toast.dart';
import 'package:hgdj/tools_base/global_store/store.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:video_player/video_player.dart';
import 'package:hgdj/hj_utils/video_view_type.dart';
import '../../alert/video/share_media_dialog.dart';
import '../mine/mine_vip/mine_charge_vip_page.dart';
import 'live_model.dart';
//直播详情
class LiveDetailLogic extends GetxController {
LiveAnchor? anchor; //直播
int currentPage = 1;
bool loading = true; //是否在加载推荐列表
bool loadingUserInfo = true; //加载用户会员信息等
List<LiveAnchor> recommendList = []; //
RefreshController? controller; //由 pullYsRefresh 的 onInit 注入,组件负责释放
VideoPlayerController? playerController;
bool initedVideoController = false; //视频控制器是否加载
RxBool isLoading = true.obs; //是否在切换视频
//视频真实比例, 竖屏的比例,采用最大高度400,横屏的比例,保持原有比例
//是否是竖向视频
double? aspectRatio = 16 / 9; //0.75
bool get isVerticalVideo => (aspectRatio ?? 1) > 1;
bool isDispose = false;
//判断播放权限
bool get canWatchLive =>
DateTimeUtil.isExpireDate(globalStore.meInfo?.broadcastExpire);
LiveDetailLogic({this.anchor});
StreamSubscription? _pauseVideoSub;
@override
void onInit() {
super.onInit();
// 耳机/蓝牙断开、来电中断时暂停,防止外放泄露
_pauseVideoSub =
eventBus.on<PauseVideoEvent>((_) => playerController?.pause());
}
@override
void onReady() async {
super.onReady();
Future.wait([globalStore.updateUserInfo(), loadData(initVideo: false)])
.then(
(value) {
update(); //接口调用成功刷新一次直播状态
loadingUserInfo = false;
initPlayer();
},
);
loadrecommendAnchors();
}
@override
void onClose() async {
isDispose = true;
_pauseVideoSub?.cancel();
playerDispose();
super.onClose();
}
playerDispose() {
if (playerController != null) {
initedVideoController = false;
playerController?.removeListener(_listenerCallback);
playerController?.pause();
playerController?.dispose();
playerController = null;
}
}
initPlayer({bool isRetry = false}) async {
if (anchor?.url != null && anchor?.url?.isNotEmpty == true) {
playerController = PlayerFactory.network(anchor?.url);
try {
await playerController?.initialize();
if (isRetry)
confirmPlatformView(); // 仅重试成功(同视频 textureView 挂、platformView 放出)才落本地
playerController?.addListener(_listenerCallback);
if (isDispose) {
playerController?.pause();
playerController?.dispose();
return;
}
initedVideoController = true;
aspectRatio = playerController?.value.aspectRatio ?? 16 / 9;
update();
if (canWatchLive) playerController?.play();
} catch (e) {
playerController?.dispose();
// 首次芯片解码/渲染报错:仅内存切 platformView 重试,成功后才落本地
if (isDecoderError(e) && switchToPlatformView()) {
initPlayer(isRetry: true);
return;
}
//播放器初始化失败,强制设置为离线
anchor?.isOnline = false;
update(['player']);
}
} else {
anchor?.isOnline = false;
update();
}
}
//添加监听
_listenerCallback() {
isLoading.value = playerController?.value.isBuffering == true;
}
//获取直播详情
Future loadData({bool initVideo = true}) async {
LiveAnchor? result =
await LiveService.getLiveAnchorDetail(id: anchor?.id ?? '');
controller?.refreshCompleted();
if (result != null) {
//不需要赋值图片url,因为图片随时在变,防止图片闪动
anchor?.isOnline = result.isOnline;
anchor?.url = result.url;
anchor?.viewCount = result.viewCount;
if (initVideo) initPlayer();
} else {
anchor?.isOnline = false;
}
}
//直播推荐列表
loadrecommendAnchors({int pageNum = 1}) async {
ListBaseModel<LiveAnchor>? liveMainModels =
await LiveService.getLiveAnchorRecom(
country: anchor?.country ?? '',
pageNumber: pageNum,
pageSize: 20,
);
loading = false;
if (liveMainModels != null) {
currentPage = pageNum;
if (currentPage == 1) recommendList.clear();
if (liveMainModels.list != null &&
liveMainModels.list?.isNotEmpty == true) {
recommendList.addAll(liveMainModels.list ?? []);
}
}
controller?.refreshCompleted();
update(['recommend']);
liveMainModels?.hasNext == true
? controller?.loadComplete()
: controller?.loadNoData();
}
loadMoreData() => loadrecommendAnchors(pageNum: currentPage + 1);
//切换直播
onChangeLiveAction(LiveAnchor change) {
if (change.id == anchor?.id) {
showToast('当前在此直播间哦~');
return;
}
anchor = change;
if (playerController != null) {
final old = playerController;
old?.pause();
isLoading.value = true;
playerController?.removeListener(_listenerCallback);
}
initedVideoController = false;
if (anchor?.vidWidth != 0 && anchor?.vidHeight != 0) {
aspectRatio = (anchor?.vidWidth ?? 1) / (anchor?.vidHeight ?? 1);
} else {
aspectRatio = 16 / 9;
}
update();
loadData();
}
//跳转充值
onChargeAction() async {
Get.to(MineChargeVipPage(), preventDuplicates: false);
await globalStore.updateUserInfo();
//true表示支付成功
if (canWatchLive) update();
}
//分享
onShareAction() {
VideoModel videoModel = VideoModel()..cover = anchor?.coverImg;
Get.dialog(ShareMediaDialog(videoModel: videoModel, needDecrypt: false),
useSafeArea: false);
}
}
+336
View File
@@ -0,0 +1,336 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_page/live/live_item_widget.dart';
import 'package:hgdj/hj_page/live/live_model.dart';
import 'package:hgdj/hj_utils/screen.dart';
import 'package:hgdj/hj_utils/sliver_delegate.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/widget/net_image_widget.dart';
import 'package:video_player/video_player.dart';
import '../video/video_full_page.dart';
import 'live_detail_logic.dart';
//直播详情页
class LiveDetailPage extends StatefulWidget {
final LiveAnchor? anchor;
const LiveDetailPage({super.key, this.anchor});
@override
State<LiveDetailPage> createState() => _LiveDetailPageState();
}
class _LiveDetailPageState extends State<LiveDetailPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: GetBuilder<LiveDetailLogic>(
init: LiveDetailLogic(anchor: widget.anchor),
builder: (logic) => Stack(
children: [
_buildBody(logic),
Positioned(left: 10, top: 10, child: _buildBack()),
],
),
),
),
);
}
//页面框架
Widget _buildBody(LiveDetailLogic logic) {
return pullYsRefresh(
enablePullDown: false,
onInit: (c) => logic.controller = c,
onLoading: (c) => logic.loadMoreData(),
child: CustomScrollView(
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: MySliverDelegate(
forceRefresh: true,
maxHeight: Get.width / (logic.aspectRatio ?? 1),
minHeight: logic.isVerticalVideo
? Get.width / (logic.aspectRatio ?? 1)
: Get.height / 3 - 60,
child: ClipRRect(
//部分机型会超出比例,裁剪掉多余部分
child: Container(color: Colors.black, child: _buildPlayer()),
),
),
),
SliverToBoxAdapter(child: _buildAnchorInfo(logic)),
SliverToBoxAdapter(child: _buildAds()),
10.sliverSizeBoxH,
_buildDivider(),
SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.fromLTRB(16, 18, 16, 12),
child: const Text(
'热门直播',
style: TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.w500),
),
),
),
_buildRecommend(),
18.sliverSizeBoxH,
_buildDivider(),
],
),
);
}
//视频播放区
Widget _buildPlayer() {
return GetBuilder<LiveDetailLogic>(
id: 'player',
builder: (logic) => Stack(
alignment: Alignment.center,
children: [
if (logic.loadingUserInfo) ...[
//1.加载用户直播权益信息
AspectRatio(
aspectRatio: logic.aspectRatio ?? 1, child: _buildCover(logic)),
const LoadingCenterWidget(),
] else if (!logic.canWatchLive) ...[
//2.无权限
_buildNoPermission(logic),
] else if (logic.anchor?.isOnline != true) ...[
//3.主播离线
_buildOffline(logic),
] else ...[
//4.主播在线
Hero(
tag: 'player',
child: AspectRatio(
aspectRatio: logic.aspectRatio ?? 1,
child: logic.initedVideoController
? VideoPlayer(logic.playerController!)
: _buildCover(logic),
),
),
if (logic.initedVideoController)
Positioned(
right: 12,
bottom: 6,
child: GestureDetector(
onTap: () => Get.to(() => VideoFullPage(
playCtr: logic.playerController!, showMenu: false)),
child: Image.asset("full_icon.png".videoPath,
width: 26, height: 26),
),
),
Obx(() => Offstage(
offstage: !logic.isLoading.value,
child: const LoadingCenterWidget())),
],
],
),
);
}
//无权限
Widget _buildNoPermission(LiveDetailLogic logic) {
return Container(
color: Colors.black,
child: Stack(
fit: StackFit.expand,
children: [
_buildCover(logic),
Container(
color: Colors.black.withValues(alpha: .7),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'温馨提示',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w500),
),
12.sizeBoxH,
const Text('开通直播卡全站直播骚货任选~',
style: TextStyle(fontSize: 12, color: Colors.white)),
18.sizeBoxH,
GestureDetector(
onTap: logic.onChargeAction,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
gradient: const LinearGradient(
begin: Alignment.centerLeft,
end: Alignment.centerRight,
colors: [Color(0xffffe8be), Color(0xffe5b764)],
),
),
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
child: const Text('立即开通直播卡',
style:
TextStyle(fontSize: 14, color: Color(0xFF7b0000))),
),
),
],
),
),
],
),
);
}
//主播离线
Widget _buildOffline(LiveDetailLogic logic) {
return Container(
color: Colors.black,
child: Stack(
fit: StackFit.expand,
children: [
_buildCover(logic),
Container(
color: Colors.black.withValues(alpha: .7),
alignment: Alignment.center,
child: const Text('当前主播离线,请前往观看其她热门主播喔~',
style: TextStyle(fontSize: 14, color: Colors.white)),
),
],
),
);
}
//封面图
Widget _buildCover(LiveDetailLogic logic) {
return NetworkImageLoader(
imageUrl: logic.anchor?.coverImg ?? '',
encrypt: false,
borderRadius: 0,
width: Get.width,
);
}
//热门直播推荐列表
Widget _buildRecommend() {
return GetBuilder<LiveDetailLogic>(
id: 'recommend',
builder: (logic) {
if (logic.loading) {
return const SliverToBoxAdapter(child: LoadingWidget());
}
if (logic.recommendList.isEmpty) {
return const SliverToBoxAdapter(
child: SizedBox(height: 300, child: CErrorWidget()));
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 7,
childAspectRatio: 168 / 105,
),
delegate: SliverChildBuilderDelegate(
(context, index) {
final anchor = logic.recommendList[index];
return LiveItemWidget(
showType: 5,
anchor: anchor,
action: () => logic.onChangeLiveAction(anchor),
);
},
childCount: logic.recommendList.length,
),
),
);
},
);
}
//返回按钮
Widget _buildBack() {
return GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.only(left: 10, right: 2, top: 2, bottom: 2),
decoration:
const BoxDecoration(color: Colors.grey, shape: BoxShape.circle),
child: Icon(Icons.arrow_back_ios,
color: Colors.black.withValues(alpha: 0.8)),
),
);
}
//主播信息
Widget _buildAnchorInfo(LiveDetailLogic logic) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Column(
children: [
Row(
children: [
NetworkImageLoader(
imageUrl: logic.anchor?.coverImg ?? '',
width: 36,
height: 36,
borderRadius: 18,
encrypt: false,
),
4.sizeBoxW,
Text(logic.anchor?.name ?? '',
style: const TextStyle(fontSize: 14, color: Colors.white)),
],
),
18.sizeBoxH,
Row(
children: [
Text(
'${logic.anchor?.viewCount}人观众',
style: const TextStyle(fontSize: 12, color: Color(0xff999999)),
),
const Spacer(),
GestureDetector(
onTap: () => logic.onShareAction(),
child: Row(
children: [
Image.asset('live_share.png'.livePath, width: 24),
2.sizeBoxW,
const Text('分享',
style:
TextStyle(fontSize: 12, color: Color(0xff999999))),
],
),
),
],
),
],
),
);
}
//广告位
Widget _buildAds() {
return AdsGridViewWidget(
36,
accordingAdsType: true,
padding: const EdgeInsets.fromLTRB(16, 18, 0, 0),
);
}
//分割线
Widget _buildDivider() {
return SliverToBoxAdapter(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16),
height: .5,
color: Colors.black.withValues(alpha: .1),
),
);
}
}
+337
View File
@@ -0,0 +1,337 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/tools_base/widget/net_image_widget.dart';
import 'live_detail_page.dart';
import 'live_model.dart';
import 'live_widget.dart';
class LiveItemWidget extends StatelessWidget {
final int showType; //0-九宫格 1-六圆形 2-4宫格 3-六宫格 4-横向滑动 5-list
final int? index; //顺序
final LiveAnchor? anchor; //主播信息
final Function()? action;
const LiveItemWidget({
super.key,
this.showType = 0,
this.anchor,
this.index,
this.action,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (action != null) {
action?.call();
} else {
Get.to(() => LiveDetailPage(anchor: anchor),
preventDuplicates: false);
}
},
child: _buildContent(),
);
}
_buildContent() {
if (showType == 0) return _buildType0();
if (showType == 1) return _buildType1();
if (showType == 2) return _buildType2();
if (showType == 3) return _buildType3();
if (showType == 4) return _buildType4();
if (showType == 5) return _buildType5();
return Container();
}
_buildType0() {
return Stack(
children: [
Positioned.fill(
child: NetworkImageLoader(
imageUrl: anchor?.coverImg ?? '', encrypt: false),
),
Positioned(
left: 4,
right: 4,
bottom: 6,
child: Text(
anchor?.name ?? '',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w400),
)),
Positioned(
left: 0,
top: 0,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 6, vertical: 3),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xffFFE356),
Color(0xffE11EFB),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.only(
topLeft: Radius.circular(9),
bottomRight: Radius.circular(9),
),
),
child: Text(
'王牌主播',
style: TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.w400),
),
)),
],
);
}
_buildType1() {
return SizedBox(
height: 70,
child: Stack(
children: [
Positioned(
left: 18,
right: 0,
top: 7,
bottom: 0,
child: Image.asset(
'live_six_item_bg.webp'.livePath,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 62,
height: 62,
child: ClipOval(
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Color(0xffAE06FF),
Color(0xffE538A3),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
)),
padding: EdgeInsets.all(1),
child: Container(
clipBehavior: Clip.hardEdge,
margin: EdgeInsets.all(1),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.8),
shape: BoxShape.circle,
),
padding: EdgeInsets.all(2),
child: ClipOval(
child: NetworkImageLoader(
imageUrl: anchor?.coverImg ?? '', encrypt: false),
),
),
)),
),
Positioned(
bottom: 10,
child: AudioWaveView(
width: 2,
margin: 3,
color: Colors.white,
),
)
],
),
SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
anchor?.name ?? '',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w400),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
SizedBox(height: 2),
Container(
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999),
color: Colors.white,
boxShadow: [
BoxShadow(
color: Color(0xffFD0563).withValues(alpha: .2),
blurRadius: 15.0, //阴影模糊程度
spreadRadius: 1.0,
),
],
),
child: Text(
'霸占TA>',
style: TextStyle(
fontSize: 10,
color: Color(0xff6A00FF),
fontWeight: FontWeight.w400),
),
),
],
),
)
],
),
],
),
);
}
_buildType2() {
return Stack(
children: [
Positioned.fill(
child: NetworkImageLoader(
imageUrl: anchor?.coverImg ?? '', encrypt: false),
),
Positioned(
left: 6,
bottom: 6,
child: Text(
anchor?.name ?? '',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w400),
)),
if (index != null && index! < 3)
Positioned(
left: 0,
top: 0,
child: Image.asset(
'cp_${(index ?? 0) + 1}.webp'.livePath,
height: 24,
)),
],
);
}
_buildType3() {
return Stack(
children: [
Positioned.fill(
child: Container(
padding: EdgeInsets.all(2),
child: NetworkImageLoader(
imageUrl: anchor?.coverImg ?? '',
encrypt: false,
borderRadius: 6,
),
),
),
Positioned(
left: 6,
bottom: 6,
child: Text(
anchor?.name ?? '',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w400),
)),
],
);
}
_buildType4() {
return Container(
padding: EdgeInsets.all(4),
child: _buildType5(),
);
}
_buildType5() {
return Stack(
children: [
Positioned.fill(
child: NetworkImageLoader(
imageUrl: anchor?.coverImg ?? '',
encrypt: false,
borderRadius: 6,
),
),
Positioned(
left: 6,
bottom: 6,
child: Text(
anchor?.name ?? '',
style: TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.w400),
)),
Positioned(
right: 6,
top: 6,
height: 16,
child: Row(
children: [
Container(
height: 8,
width: 8,
decoration: BoxDecoration(
color: (anchor?.isOnline ?? false)
? Color(0xff0AEBED)
: Color(0xff656565),
borderRadius: BorderRadius.circular(4),
),
),
SizedBox(width: 4),
Text(
(anchor?.isOnline ?? false) ? '直播中' : '离线',
style: TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.w400),
)
],
)),
Positioned(
left: 6,
top: 6,
child: Container(
height: 16,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 4),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: .3),
borderRadius: BorderRadius.circular(999),
),
child: Text(
'${anchor?.viewCount ?? 0}人观众',
style: TextStyle(
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.w400),
),
)),
],
);
}
}
+44
View File
@@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_page/live/live_service.dart';
import 'live_model.dart';
//直播主逻辑
class LiveMainLogic extends GetxController with GetTickerProviderStateMixin {
TabController? tabController;
LiveMainModel? liveMainModel;
List<LiveModule> liveTabs = [];
@override
void onReady() {
super.onReady();
loadData();
}
@override
void onClose() {
tabController?.dispose();
super.onClose();
}
//获取媒体详情
loadData() async {
liveMainModel = await LiveService.getLiveModuleList();
if (liveMainModel != null) {
if (liveMainModel?.module != null &&
liveMainModel?.module?.isNotEmpty == true) {
liveTabs.clear();
liveTabs.add(LiveModule(id: '-1', title: '推荐')); //添加推荐数据
liveTabs.addAll(liveMainModel?.module ?? []);
tabController = TabController(
initialIndex: 0,
length: liveTabs.length,
vsync: this,
);
}
}
update();
}
}
+77
View File
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/app_colors.dart';
import 'package:hgdj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:hgdj/tools_base/loading/loading_center_widget.dart';
import 'package:hgdj/tools_base/widget/keep_alive_widget.dart';
import 'live_main_logic.dart';
import 'live_sub_page.dart';
//直播首页
class LiveMainPage extends StatelessWidget {
static bool broadcast = false; //开启直播模块开关
static bool jumpLiveLink = false; //直播内链跳转初始化
const LiveMainPage({super.key});
@override
Widget build(BuildContext context) {
return GetBuilder<LiveMainLogic>(
init: LiveMainLogic(),
builder: (logic) {
if (logic.liveMainModel == null) return const LoadingCenterWidget();
return Column(
children: [
_buildTabBar(logic),
Expanded(
child: TabBarView(
controller: logic.tabController,
children: List.generate(
logic.liveTabs.length,
(index) =>
LiveSubPage(tagData: logic.liveTabs[index]).keepAlive,
),
),
),
],
);
},
);
}
//国家分类
Widget _buildTabBar(LiveMainLogic logic) {
return SizedBox(
height: 44,
child: TabBar(
controller: logic.tabController,
tabs: List.generate(
logic.liveTabs.length,
(index) => Padding(
padding: const EdgeInsets.only(bottom: 7),
child: Text(logic.liveTabs[index].title ?? ''),
),
),
labelPadding: const EdgeInsets.symmetric(horizontal: 9),
tabAlignment: TabAlignment.start,
isScrollable: true,
labelColor: Colors.white.withValues(alpha: .9),
labelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500),
unselectedLabelColor: Colors.white.withValues(alpha: .45),
unselectedLabelStyle:
const TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
indicatorPadding: const EdgeInsets.only(top: -4),
indicator: CustomIndicator(
height: 2,
width: 16,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(1),
topRight: Radius.circular(1),
),
color: AppColors.actionRed,
),
),
);
}
}
+103
View File
@@ -0,0 +1,103 @@
class LiveMainModel {
List<LiveModule>? module;
List<LiveRecom>? recom;
LiveMainModel({this.module, this.recom});
LiveMainModel.fromJson(Map<String, dynamic> json) {
if (json['module'] != null) {
module = <LiveModule>[];
json['module'].forEach((v) {
module!.add(new LiveModule.fromJson(v));
});
}
if (json['recom'] != null) {
recom = <LiveRecom>[];
json['recom'].forEach((v) {
recom!.add(new LiveRecom.fromJson(v));
});
}
}
}
class LiveModule {
String? id;
String? title;
LiveModule({this.id, this.title});
LiveModule.fromJson(Map<String, dynamic> json) {
id = json['id'];
title = json['title'];
}
}
class LiveRecom {
List<LiveAnchor>? anchors;
String? recoShowType;
LiveRecom({this.anchors, this.recoShowType});
LiveRecom.fromJson(Map<String, dynamic> json) {
if (json['anchors'] != null) {
anchors = <LiveAnchor>[];
json['anchors'].forEach((v) {
anchors!.add(new LiveAnchor.fromJson(v));
});
}
recoShowType = json['recoShowType'];
}
}
class LiveAnchor {
String? id;
String? name;
String? coverImg;
String? url;
String? country;
int? vidWidth;
int? vidHeight;
bool? isOnline;
bool? isNew;
int? viewCount;
LiveAnchor(
{this.id,
this.name,
this.coverImg,
this.url,
this.country,
this.vidWidth,
this.vidHeight,
this.isOnline,
this.isNew,
this.viewCount});
LiveAnchor.fromJson(Map<String, dynamic> json) {
id = json['id'];
name = json['name'];
coverImg = json['coverImg'];
url = json['url'];
country = json['country'];
vidWidth = json['vidWidth'];
vidHeight = json['vidHeight'];
isOnline = json['isOnline'];
isNew = json['isNew'];
viewCount = json['viewCount'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['name'] = this.name;
data['coverImg'] = this.coverImg;
data['url'] = this.url;
data['country'] = this.country;
data['vidWidth'] = this.vidWidth;
data['vidHeight'] = this.vidHeight;
data['isOnline'] = this.isOnline;
data['isNew'] = this.isNew;
data['viewCount'] = this.viewCount;
return data;
}
}
+79
View File
@@ -0,0 +1,79 @@
import 'package:hgdj/hj_model/list_base_model.dart';
import 'package:hgdj/hj_page/live/live_model.dart';
import 'package:hgdj/tools_base/net/http_manager.dart';
import 'package:hgdj/tools_base/net/net_manager.dart';
class LiveService {
//获取直播模块
static Future<LiveMainModel?> getLiveModuleList() async {
int time =
netManager.getFixedCurTime().toUtc().millisecondsSinceEpoch ~/ 1000;
final param = {'time': time};
final result = await httpManager.fetchResponseByPOST(
'/live/module/list/91porn',
param: param,
jsonTransformation: (json) => LiveMainModel.fromJson(json),
);
return result.data;
}
//获取直播模块
static Future<ListBaseModel<LiveAnchor>?> getLiveAnchorList({
String? id,
int? pageNumber,
int? pageSize,
}) async {
int time =
netManager.getFixedCurTime().toUtc().millisecondsSinceEpoch ~/ 1000;
final param = {
'time': time,
'id': id,
'pageNumber': pageNumber,
'pageSize': 20,
};
final result = await httpManager.fetchResponseByPOST(
'/live/anchor/list/91porn',
param: param,
jsonTransformation: (json) => ListBaseModel<LiveAnchor>.fromJson(json),
);
return result.data;
}
//获取直播详情模块
static Future<LiveAnchor?> getLiveAnchorDetail({String? id}) async {
int time =
netManager.getFixedCurTime().toUtc().millisecondsSinceEpoch ~/ 1000;
final param = {
'time': time,
'id': id,
};
final result = await httpManager.fetchResponseByPOST(
'/live/anchor/watch/91porn',
param: param,
jsonTransformation: (json) => LiveAnchor.fromJson(json),
);
return result.data;
}
//获取直播详情推荐列表
static Future<ListBaseModel<LiveAnchor>?> getLiveAnchorRecom({
String? country,
int? pageNumber,
int? pageSize,
}) async {
int time =
netManager.getFixedCurTime().toUtc().millisecondsSinceEpoch ~/ 1000;
final param = {
'time': time,
'country': country,
'pageNumber': pageNumber,
'pageSize': pageSize,
};
final result = await httpManager.fetchResponseByPOST(
'/live/anchor/recom/91porn',
param: param,
jsonTransformation: (json) => ListBaseModel<LiveAnchor>.fromJson(json),
);
return result.data;
}
}
+80
View File
@@ -0,0 +1,80 @@
import 'package:hgdj/hj_model/list_base_model.dart';
import 'package:get/get.dart';
import 'package:hgdj/hj_page/live/live_service.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../mine/mine_vip/mine_charge_vip_page.dart';
import 'live_main_logic.dart';
import 'live_model.dart';
//直播亚模块
class LiveSubLogic extends GetxController {
LiveModule? tagData;
int currentPage = 1;
bool loading = true;
List<LiveAnchor> liveList = []; //
RefreshController? controller;
bool get isRecom => tagData?.id == '-1'; //是否是推荐模块
LiveMainLogic get mainLogic => Get.find<LiveMainLogic>();
LiveMainModel? liveMainModel;
LiveSubLogic({this.tagData});
@override
onReady() {
super.onReady();
//默认数据
liveMainModel = mainLogic.liveMainModel;
loadData();
}
//获取专题数据
loadData({int pageNum = 1, bool showLoading = false}) async {
if (isRecom) {
loadRecommendData();
update();
} else {
loadCountryAnchor(pageNum: pageNum);
}
}
//推荐直播列表
loadRecommendData() async {
final result = await LiveService.getLiveModuleList();
if (result != null) {
liveMainModel = result;
}
controller?.refreshCompleted();
update();
}
//tag直播列表
loadCountryAnchor({int pageNum = 1, bool showLoading = false}) async {
ListBaseModel<LiveAnchor>? liveMainModels =
await LiveService.getLiveAnchorList(
id: tagData?.id ?? '',
pageNumber: pageNum,
);
loading = false;
if (liveMainModels != null) {
currentPage = pageNum;
if (currentPage == 1) liveList.clear();
if (liveMainModels.list != null &&
liveMainModels.list?.isNotEmpty == true) {
liveList.addAll(liveMainModels.list ?? []);
}
}
update();
liveMainModels?.hasNext == true
? controller?.loadComplete()
: controller?.loadNoData();
controller?.refreshCompleted();
}
loadMoreData() => loadData(pageNum: currentPage + 1);
//跳转充值
onChargeAction() {
Get.to(MineChargeVipPage(), preventDuplicates: false);
}
}
+287
View File
@@ -0,0 +1,287 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgdj/assets_tool/images.dart';
import 'package:hgdj/hj_page/live/live_item_widget.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/widget/card_swiper/src/swiper.dart';
import 'live_model.dart';
import 'live_sub_logic.dart';
//直播子列表页面
class LiveSubPage extends StatefulWidget {
final LiveModule? tagData;
const LiveSubPage({super.key, this.tagData});
@override
State<LiveSubPage> createState() => _LiveSubPageState();
}
class _LiveSubPageState extends State<LiveSubPage> {
String get _tag => widget.tagData?.id ?? '';
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('live_main_bg.webp'.livePath), fit: BoxFit.fill),
),
child: GetBuilder<LiveSubLogic>(
init: LiveSubLogic(tagData: widget.tagData),
tag: _tag,
builder: (logic) => pullYsRefresh(
enablePullUp: !logic.isRecom,
onInit: (c) => logic.controller = c,
onRefresh: (c) => logic.loadData(),
onLoading: (c) => logic.loadMoreData(),
child: _buildContent(logic),
),
),
);
}
Widget _buildContent(LiveSubLogic logic) {
//列表样式
if (!logic.isRecom) {
return CustomScrollView(
slivers: [_buildAdBanner(), _buildAnchorGrid(logic)]);
}
//推荐样式
if (logic.liveMainModel?.recom == null) return const LoadingCenterWidget();
if (logic.liveMainModel?.recom?.isEmpty == true) {
return CErrorWidget(retryOnTap: () => logic.loadData());
}
return CustomScrollView(slivers: _buildRecomItems(logic));
}
//广告 banner
Widget _buildAdBanner() {
return SliverToBoxAdapter(
child: AdsGridViewWidget(
35,
accordingAdsType: true,
padding: const EdgeInsets.fromLTRB(16, 15, 16, 15),
),
);
}
//国家/标签主播列表
Widget _buildAnchorGrid(LiveSubLogic logic) {
if (logic.loading) {
return const SliverToBoxAdapter(
child: SizedBox(height: 200, child: LoadingCenterWidget()));
}
if (logic.liveList.isEmpty) {
return SliverToBoxAdapter(
child: CErrorWidget(retryOnTap: () => logic.loadData()));
}
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 16),
sliver: SliverGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 7,
childAspectRatio: 168 / 105,
),
delegate: SliverChildBuilderDelegate(
(_, index) =>
LiveItemWidget(showType: 5, anchor: logic.liveList[index]),
childCount: logic.liveList.length,
),
),
);
}
//推荐页各区块(首块额外叠充值入口)
List<Widget> _buildRecomItems(LiveSubLogic logic) {
final recomList = logic.liveMainModel?.recom ?? [];
return List.generate(recomList.length, (i) {
Widget section = _buildRecomSection(recomList[i]);
if (i == 0) section = _wrapFirstSection(logic, section);
return SliverToBoxAdapter(child: section);
});
}
//按 recoShowType 分发区块样式
Widget _buildRecomSection(LiveRecom recom) {
switch (recom.recoShowType) {
case 'nine_grid':
return _buildGridSection(recom,
bg: 'live_section_bg_1.webp',
title: 'nine_grid_bg.webp',
titleHeight: 45,
crossAxisCount: 3,
mainSpacing: 12,
crossSpacing: 5,
aspectRatio: 1,
showType: 0);
case 'six_round':
return _buildGridSection(recom,
bg: 'live_section_bg_2.webp',
title: 'six_round_bg.webp',
titleHeight: 45,
crossAxisCount: 2,
mainSpacing: 0,
crossSpacing: 6,
aspectRatio: 168 / 85,
showType: 1,
bottomSpace: 10);
case 'four_grid':
return _buildGridSection(recom,
bg: 'live_section_bg_3.webp',
title: 'four_grid_bg.webp',
titleHeight: 45,
crossAxisCount: 2,
mainSpacing: 12,
crossSpacing: 7,
aspectRatio: 1,
showType: 2,
bottomSpace: 18);
case 'six_grid':
return _buildGridSection(recom,
bg: 'live_section_bg_4.webp',
title: 'six_grid_bg.webp',
titleHeight: 36,
crossAxisCount: 3,
mainSpacing: 12,
crossSpacing: 5,
aspectRatio: 1,
showType: 3);
case 'horizontal_slide':
return _buildHorizontalSlide(recom);
case 'list':
return _buildRecomList(recom.anchors);
default:
return const SizedBox.shrink();
}
}
//首个区块叠加充值入口
Widget _wrapFirstSection(LiveSubLogic logic, Widget section) {
return Stack(
alignment: Alignment.topCenter,
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [178.sizeBoxH, section, 10.sizeBoxH]),
Positioned(
top: 20,
child: GestureDetector(
onTap: () => logic.onChargeAction(),
child: Image.asset('live_charge.webp'.livePath, height: 148),
),
),
],
);
}
//宫格类区块(九宫格/六圆形/四宫格/六宫格 共用)
Widget _buildGridSection(
LiveRecom? recom, {
required String bg,
required String title,
required double titleHeight,
required int crossAxisCount,
required double mainSpacing,
required double crossSpacing,
required double aspectRatio,
required int showType,
double bottomSpace = 0,
}) {
final anchors = recom?.anchors;
return Container(
decoration: BoxDecoration(
image:
DecorationImage(image: AssetImage(bg.livePath), fit: BoxFit.fill),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
padding: const EdgeInsets.only(left: 12, right: 12, top: 4, bottom: 34),
child: Column(
children: [
18.sizeBoxH,
Image.asset(title.livePath, height: titleHeight),
18.sizeBoxH,
GridView.builder(
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: mainSpacing,
crossAxisSpacing: crossSpacing,
childAspectRatio: aspectRatio,
),
itemCount: anchors?.length ?? 0,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
//index 仅 showType==2 用于渲染前三角标,其余样式忽略
itemBuilder: (_, index) => LiveItemWidget(
showType: showType, anchor: anchors?[index], index: index),
),
if (bottomSpace > 0) bottomSpace.sizeBoxH,
],
),
);
}
//横向滑动
Widget _buildHorizontalSlide(LiveRecom? recom) {
final anchors = recom?.anchors;
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
const Color(0xffFF8E8E).withValues(alpha: 0),
const Color(0xffFF8E8E).withValues(alpha: .2),
const Color(0xffFF8E8E).withValues(alpha: 0),
],
),
),
child: Column(
children: [
18.sizeBoxH,
Container(
height: 175,
alignment: Alignment.bottomRight,
child: Swiper(
autoplay: true,
autoplayDelay: 5000,
viewportFraction: 0.8,
scale: 0.93,
itemCount: anchors?.length ?? 0,
itemBuilder: (c, index) =>
LiveItemWidget(showType: 4, anchor: anchors?[index]),
),
),
],
),
);
}
//推荐无限下滑列表
Widget _buildRecomList(List<LiveAnchor>? anchors) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 28),
child: GridView.builder(
padding: EdgeInsets.zero,
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
mainAxisSpacing: 12,
crossAxisSpacing: 5,
childAspectRatio: 168 / 105,
),
itemCount: anchors?.length ?? 0,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (_, index) =>
LiveItemWidget(showType: 5, anchor: anchors?[index]),
),
);
}
}
+176
View File
@@ -0,0 +1,176 @@
import 'dart:math';
import 'package:flutter/material.dart';
/// 音频波形动画组件
/// 用于显示音频播放时的动态波形效果
class AudioWaveView extends StatefulWidget {
/// 波形高度
final double height;
/// 波形颜色
final Color color;
/// 动画持续时间
final Duration duration;
/// 动画曲线
final Curve curve;
/// 波形条之间的间距
final double margin;
/// 波形条的宽度
final double width;
/// 波形条的数量
final int barCount;
const AudioWaveView({
super.key,
this.height = 12,
this.margin = 3,
this.width = 1.5,
this.color = const Color(0xffF68804),
this.duration = const Duration(seconds: 5),
this.curve = Curves.easeInOut,
this.barCount = 3,
});
@override
State<AudioWaveView> createState() => _AudioWaveViewState();
}
class _AudioWaveViewState extends State<AudioWaveView>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late List<Animation<double>> _animations;
@override
void initState() {
super.initState();
_initializeAnimations();
}
/// 初始化动画
void _initializeAnimations() {
_controller = AnimationController(
vsync: this,
duration: widget.duration,
);
_animations = _createWaveAnimations();
_controller.repeat();
}
/// 创建波形动画列表
List<Animation<double>> _createWaveAnimations() {
const int sequenceCount = 15;
final List<Animation<double>> animations = [];
for (int i = 0; i < widget.barCount; i++) {
final source = _generateRandomSequence(sequenceCount);
final tweenSequence = TweenSequence<double>(
List.generate(sequenceCount, (index) {
return TweenSequenceItem<double>(
tween: Tween<double>(
begin: source[index],
end: source[index + 1],
),
weight: 100.0 / sequenceCount,
);
}),
);
animations.add(
tweenSequence.animate(
CurvedAnimation(
parent: _controller,
curve: widget.curve,
),
),
);
}
return animations;
}
/// 生成随机序列
List<double> _generateRandomSequence(int count) {
final random = Random();
final source = List.generate(count, (_) => random.nextDouble());
source.add(source.first); // 确保循环
return source;
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
mainAxisSize: MainAxisSize.min,
children: _buildWaveBars(),
);
},
);
}
/// 构建波形条列表
List<Widget> _buildWaveBars() {
final List<Widget> bars = [];
for (int i = 0; i < widget.barCount; i++) {
if (i > 0) {
bars.add(SizedBox(width: widget.margin));
}
bars.add(
WaveBar(
color: widget.color,
width: widget.width,
height: _animations[i].value * widget.height,
),
);
}
return bars;
}
}
/// 单个波形条组件
class WaveBar extends StatelessWidget {
final double width;
final double height;
final Color color;
const WaveBar({
super.key,
required this.width,
required this.height,
required this.color,
});
@override
Widget build(BuildContext context) {
return SizedBox(
width: width,
height: height,
child: DecoratedBox(
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(width / 2),
),
),
);
}
}