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