logo和名称替换

This commit is contained in:
谢宇宁
2026-10-14 11:23:36 +07:00
parent 1cb514cd02
commit c5c04a7967
478 changed files with 8990 additions and 4948 deletions
@@ -1,12 +1,12 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgmj/assets_tool/app_colors.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:hgmj/tools_base/debug_log.dart';
import 'package:hgmj/tools_base/refresh/pull_refresh.dart';
import 'package:wydj/assets_tool/app_colors.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:wydj/tools_base/debug_log.dart';
import 'package:wydj/tools_base/refresh/pull_refresh.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import 'package:hgmj/tools_base/widget/sheet_handle_bar.dart';
import 'package:wydj/tools_base/widget/sheet_handle_bar.dart';
import '../../../hj_model/acg/comic_chapters_model.dart';
import '../../../hj_model/cartoon_media_info.dart';
@@ -19,7 +19,8 @@ class CartoonEpisodeMenuAlert extends StatefulWidget {
final CartoonMediaInfo? cartoonModel;
final Function(ComicChapterInfo)? callback;
CartoonEpisodeMenuAlert(this.curModel, {super.key, this.callback, this.cartoonModel});
CartoonEpisodeMenuAlert(this.curModel,
{super.key, this.callback, this.cartoonModel});
@override
State<StatefulWidget> createState() {
@@ -58,7 +59,9 @@ class _CartoonEpisodeMenuAlertState extends State<CartoonEpisodeMenuAlert> {
//异步返回时 sheet 可能已关闭,避免对已卸载组件 setState / 操作已释放的 refreshCtr
if (!mounted) return;
setState(() {});
cartoonModel?.haxNextEpisode == true ? refreshCtr?.loadComplete() : refreshCtr?.loadNoData();
cartoonModel?.haxNextEpisode == true
? refreshCtr?.loadComplete()
: refreshCtr?.loadNoData();
}
@override
@@ -68,8 +71,9 @@ class _CartoonEpisodeMenuAlertState extends State<CartoonEpisodeMenuAlert> {
child: Container(
padding: EdgeInsets.fromLTRB(18.w, 21, 18.w, 32),
height: 460,
decoration:
BoxDecoration(borderRadius: BorderRadius.vertical(top: Radius.circular(18)), color: Color(0xff0F0F0F)),
decoration: BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(18)),
color: Color(0xff0F0F0F)),
child: Column(
children: [
const SheetHandleBar(color: Color(0x4DFFFFFF)),
@@ -146,7 +150,9 @@ class CartoonChapterItem extends StatelessWidget {
});
// 前 N 集免费(当前集号 <= freeEpisode)且整本无任何权限时,展示「免费」角标
bool get _isFree => fatherM.hasPermission == false && fatherM.isFreeEpisodeNumber(model.episodeNumber);
bool get _isFree =>
fatherM.hasPermission == false &&
fatherM.isFreeEpisodeNumber(model.episodeNumber);
@override
Widget build(BuildContext context) {
@@ -155,13 +161,17 @@ class CartoonChapterItem extends StatelessWidget {
decoration: BoxDecoration(
color: isSelected ? Color(0x1A6975FF) : Colors.transparent,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: isSelected ? AppColors.actionRed : Color(0x1AFFFFFF), width: 1),
border: Border.all(
color: isSelected ? AppColors.actionRed : Color(0x1AFFFFFF),
width: 1),
),
child: Stack(
alignment: Alignment.center,
children: [
//前 N 集免费且整本无权限 → 左上角「免费」角标
if (_isFree && isCanShowFree) const Positioned(top: 0, right: 0, child: FreeBadge(isCorner: true)),
if (_isFree && isCanShowFree)
const Positioned(
top: 0, right: 0, child: FreeBadge(isCorner: true)),
Container(
padding: EdgeInsets.only(top: 12),
child: Row(
@@ -1,17 +1,17 @@
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/tools_base/widget/shrink_wrap.dart';
import 'package:hgmj/hj_page/home/tag/cartoon_tag_page.dart';
import 'package:hgmj/hj_page/video/view/video_detail_bottom_menu.dart';
import 'package:hgmj/hj_utils/const.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:hgmj/routers/jump_router.dart';
import 'package:hgmj/tools_base/banner/ads_grid_view.dart';
import 'package:hgmj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:hgmj/tools_base/widget/keep_alive_widget.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/tools_base/widget/shrink_wrap.dart';
import 'package:wydj/hj_page/home/tag/cartoon_tag_page.dart';
import 'package:wydj/hj_page/video/view/video_detail_bottom_menu.dart';
import 'package:wydj/hj_utils/const.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:wydj/routers/jump_router.dart';
import 'package:wydj/tools_base/banner/ads_grid_view.dart';
import 'package:wydj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:wydj/tools_base/widget/keep_alive_widget.dart';
import 'package:video_player/video_player.dart';
import '../../../hj_model/acg/comic_chapters_model.dart';
@@ -75,7 +75,8 @@ class _VideoDetailCartoonViewState extends State<VideoDetailCartoonView>
child: Container(
padding: const EdgeInsets.only(top: 18),
margin: EdgeInsets.symmetric(horizontal: 12),
child: VideoDetailBottomMenu(model: videoModel, onShare: _onShare),
child:
VideoDetailBottomMenu(model: videoModel, onShare: _onShare),
),
),
SliverToBoxAdapter(
@@ -1,11 +1,11 @@
import 'dart:async';
import 'package:get/get.dart';
import 'package:hgmj/tools_base/event_bus/event_bus_util.dart';
import 'package:hgmj/tools_base/event_bus/events.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:wydj/tools_base/event_bus/event_bus_util.dart';
import 'package:wydj/tools_base/event_bus/events.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:video_player/video_player.dart';
import 'package:hgmj/hj_utils/video_view_type.dart';
import 'package:wydj/hj_utils/video_view_type.dart';
import '../../tools_base/debug_log.dart';
@@ -40,7 +40,8 @@ class SimpleVideoPlayerLogic extends GetxController {
videoCtr = PlayerFactory.network(videoUrl);
}
await videoCtr?.initialize();
if (isRetry) confirmPlatformView(); // 仅重试成功(同视频 textureView 挂、platformView 放出)才落本地
if (isRetry)
confirmPlatformView(); // 仅重试成功(同视频 textureView 挂、platformView 放出)才落本地
videoCtr?.play();
update();
} catch (e) {
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgmj/hj_page/video/view/video_menu_view.dart';
import 'package:hgmj/tools_base/loading/loading_center_widget.dart';
import 'package:wydj/hj_page/video/view/video_menu_view.dart';
import 'package:wydj/tools_base/loading/loading_center_widget.dart';
import 'package:video_player/video_player.dart';
import 'simple_video_player_logic.dart';
@@ -12,7 +12,8 @@ class SimpleVideoPlayerPage extends StatelessWidget {
final String title;
final String? localPath;
const SimpleVideoPlayerPage({super.key, required this.videoUrl, required this.title, this.localPath});
const SimpleVideoPlayerPage(
{super.key, required this.videoUrl, required this.title, this.localPath});
@override
Widget build(BuildContext context) {
+2 -2
View File
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgmj/hj_page/video/video_full_logic.dart';
import 'package:hgmj/hj_page/video/view/video_menu_view.dart';
import 'package:wydj/hj_page/video/video_full_logic.dart';
import 'package:wydj/hj_page/video/view/video_menu_view.dart';
import 'package:video_player/video_player.dart';
import '../../hj_model/video_model.dart';
+63 -34
View File
@@ -3,13 +3,13 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:hgmj/hj_page/video/video_full_page.dart';
import 'package:hgmj/hj_page/video/view/long_video_status.dart';
import 'package:hgmj/hj_utils/codec_support.dart';
import 'package:hgmj/hj_utils/play_diagnose.dart';
import 'package:hgmj/hj_utils/const.dart';
import 'package:hgmj/hj_utils/video_view_type.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:wydj/hj_page/video/video_full_page.dart';
import 'package:wydj/hj_page/video/view/long_video_status.dart';
import 'package:wydj/hj_utils/codec_support.dart';
import 'package:wydj/hj_utils/play_diagnose.dart';
import 'package:wydj/hj_utils/const.dart';
import 'package:wydj/hj_utils/video_view_type.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:video_player/video_player.dart';
import '../../alert/video/buy_vip_alert.dart';
@@ -73,7 +73,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
bool isFullScreen = false; // true:视频进入全屏页面
RxBool isShowBuy = false.obs; // 显示购买和vip弹窗
late final TabController tabCtr = TabController(length: 2, vsync: this); // 简介/评论
late final TabController tabCtr =
TabController(length: 2, vsync: this); // 简介/评论
CartoonMediaInfo? cartoonModel; //动漫
ComicChapterInfo? currentEpisode; //动漫集数
@@ -101,7 +102,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
double get normalRatio => 16 / 9;
/// 实际播放比例:动漫和未初始化都用 16:9 兜底,否则用控制器报告的实际比例
double get videoRatio => (!isCartoon && isInited) ? playerCtr!.value.aspectRatio : normalRatio;
double get videoRatio =>
(!isCartoon && isInited) ? playerCtr!.value.aspectRatio : normalRatio;
StreamSubscription? _pauseVideoSub;
@@ -138,13 +140,16 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
GuideManager().canShow(GuideFreeTrialSheet.scene) &&
MineMsgProvider().payTier?.status == PayTier.newUnpay &&
FreePlayManager().useFreePlay(videoModel) == true &&
GuideFreeTrialSheet.isCountLow, //必须排在 useFreePlay 之后:它会先扣一次,扣完才是该显示的剩余数
GuideFreeTrialSheet
.isCountLow, //必须排在 useFreePlay 之后:它会先扣一次,扣完才是该显示的剩余数
onShow: () async {
// 先备好卡数据再暂停,否则拉数据期间视频白停几百毫秒;场景没配卡就压根不弹,别白暂停一下
if (await loadGuideCard(GuideFreeTrialSheet.scene) == null || isDispose) return;
if (await loadGuideCard(GuideFreeTrialSheet.scene) == null || isDispose)
return;
// 只暂停本来在播的:用户自己停在那看简介/评论时,弹完别擅自续播
final wasPlaying = playerCtr?.value.isPlaying == true;
if (wasPlaying) playerCtr?.pause(); // 图标由 VideoMenuView 的 Rx 自动跟,不用 update()
if (wasPlaying)
playerCtr?.pause(); // 图标由 VideoMenuView 的 Rx 自动跟,不用 update()
final goVip = await GuideFreeTrialSheet.show();
// 跳了会员页 / 已退页 / 期间被硬拦截弹了蒙层,都不能恢复播放
if (!wasPlaying || goVip || isDispose || isShowBuy.value) return;
@@ -159,8 +164,9 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
bool get _needPayGuide => videoStatus.isNeedPay;
/// 能连续看的秒数:有试看片就是试看片长,否则是正片免费时长
int get _freeSeconds =>
videoModel?.previewURL?.isNotEmpty == true ? (playerCtr?.value.duration.inSeconds ?? 0) : (videoModel?.freeTime ?? 0);
int get _freeSeconds => videoModel?.previewURL?.isNotEmpty == true
? (playerCtr?.value.duration.inSeconds ?? 0)
: (videoModel?.freeTime ?? 0);
/// 各类「整片免看」权益:免费次数 / 免费区 / 动漫整本已购 / 动漫免费章节 / VIP 金币限免。
/// 命中任一说明用户本来就能看完整片,场景1、场景2 都不该打扰他。
@@ -169,8 +175,11 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
bool get _hasFreeWatchRight {
if (FreePlayManager().useFreePlay(videoModel)) return true;
if (videoModel?.freeArea == true) return true;
if (cartoonModel?.mediaStatus?.hasPaid == true) return true; // 动漫整本解锁, 都可以看, 不管vip权限
if (isCartoon && cartoonModel?.isFreeEpisodeNumber(currentEpisode?.episodeNumber) == true) return true;
if (cartoonModel?.mediaStatus?.hasPaid == true)
return true; // 动漫整本解锁, 都可以看, 不管vip权限
if (isCartoon &&
cartoonModel?.isFreeEpisodeNumber(currentEpisode?.episodeNumber) ==
true) return true;
return isVipCoinFree;
}
@@ -199,7 +208,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
if (playerCtr?.value.isPlaying != true) return;
_payGuideShown = true;
// 先备好卡数据再暂停,否则拉数据期间视频白停几百毫秒;场景没配卡就压根不弹,别白暂停一下
if (await loadGuideCard(GuideScene.videoPreviewEnd) == null || isDispose) return;
if (await loadGuideCard(GuideScene.videoPreviewEnd) == null ||
isDispose) return;
playerCtr?.pause(); // 图标由 VideoMenuView 的 Rx 自动跟,不用 update()
final goVip = await _showPayGuide();
// 跳了会员页 / 已退页 / 期间被硬拦截弹了蒙层,都不能恢复播放
@@ -218,7 +228,10 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
/// 是否允许直接退页。false=先弹返回挽留(VIDEO_BACK):长视频 + 场景开关允许 + 本页未弹过。
/// 供 PopScope.canPop 与返回箭头共用,类型跟当前在播内容走
bool get canPop => _backGuideShown || isCartoon || !GuideManager().canShow(GuideScene.videoBack);
bool get canPop =>
_backGuideShown ||
isCartoon ||
!GuideManager().canShow(GuideScene.videoBack);
/// VIDEO_BACK:长视频返回挽留。弹优惠倒计时,标记已弹并 update() 让 canPop 放行后续返回。
/// 用户是按了返回才弹的,所以挽留失败(点X/点蒙层/被互斥挡下)就把这次返回执行完;
@@ -232,7 +245,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
try {
await TimedPopupManager().trigger(
canShow: () => GuideManager().canShow(GuideScene.videoBack),
onShow: () async => goVip = await GuideCountdownDialog.show(scene: GuideScene.videoBack),
onShow: () async => goVip =
await GuideCountdownDialog.show(scene: GuideScene.videoBack),
);
} catch (e) {
debugLog('showBackGuide', e.toString());
@@ -276,7 +290,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
cartoonModel?.haxNextEpisode = episodeModel?.hasNext ?? true;
videoModel?.mediaInfo = cartoonModel;
currentEpisode = episodeModel?.list?.first;
MediaContent? mediaContent = await ACGService.getMediaDetail(id: currentEpisode?.id ?? "");
MediaContent? mediaContent =
await ACGService.getMediaDetail(id: currentEpisode?.id ?? "");
episodeModel?.list?.first.mediaContent = mediaContent;
videoModel = cartoonModel?.toVideoModel(currentEpisode) ?? VideoModel();
} else {
@@ -350,7 +365,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
return;
}
isFullScreen = true;
await Get.to(() => VideoFullPage(playCtr: playerCtr!, videoModel: videoModel));
await Get.to(
() => VideoFullPage(playCtr: playerCtr!, videoModel: videoModel));
isFullScreen = false;
}
@@ -358,7 +374,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
void exitFullScreen() {
if (!isFullScreen) return;
isFullScreen = false;
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: [SystemUiOverlay.bottom, SystemUiOverlay.top]);
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
Get.back();
}
@@ -395,7 +412,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
playerCtr = PlayerFactory.network(videoUrl); // 仅网络播放,不走本地文件
try {
await playerCtr?.initialize();
if (isRetry) confirmPlatformView(); // 仅重试成功(同视频 textureView 挂、platformView 放出)才落本地
if (isRetry)
confirmPlatformView(); // 仅重试成功(同视频 textureView 挂、platformView 放出)才落本地
update();
} catch (e) {
debugLog("视频初始化失败:$e play url = $videoUrl");
@@ -443,7 +461,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
playerCtr?.addListener(_onProgress);
int previewStart = videoModel?.previewStart ?? 0;
if (previewStart > 0) {
final canFree = FreePlayManager().useFreePlay(videoModel) || videoModel?.freeArea == true;
final canFree = FreePlayManager().useFreePlay(videoModel) ||
videoModel?.freeArea == true;
// 免金币视频 / 有免费观看次数(vip视频):从头播,不需要 seekTo
// 需要付费/会员才能看的视频,跳到预览起始位置开始播放
if (!canFree && videoStatus.isNeedPay) {
@@ -469,7 +488,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
if (!_isPreview()) {
//每帧都走这里,videoStatus 是重算 getter,只读一次
final status = videoStatus;
final inFreeTime = videoModel?.isInFreeTime(playerCtr!.value.position.inSeconds) == true;
final inFreeTime =
videoModel?.isInFreeTime(playerCtr!.value.position.inSeconds) == true;
final isPlaying = playerCtr?.value.isPlaying == true;
if (inFreeTime || !isPlaying) return;
// 全屏中:仍处于免费时长则不打扰;否则需付费的视频在播放时先退出全屏,给后续弹窗腾出空间
@@ -484,7 +504,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
if (!isCartoon && status.isNeedVip) {
// 刚退全屏要等 pop 动画和转屏落定再弹:exitFullScreen 的 Get.back() 带动画,
// 此刻 push 会叠在正在 pop 的全屏页上,且弹窗高度取的是实时 Get.height,横屏下算出来是错的
if (wasFullScreen) await Future.delayed(const Duration(milliseconds: 350));
if (wasFullScreen)
await Future.delayed(const Duration(milliseconds: 350));
if (!isDispose) BuyVipAlert.show(videoId: videoModel?.id);
}
}
@@ -527,7 +548,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
void _fallbackTo264() {
if (isDispose || !isPlayingH265 || _forceH264) return;
_forceH264 = true; // 本条视频后续重建都走 264
debugLog("H.265 运行时报错,回退 264 续播 pos=${playerCtr?.value.position.inSeconds}");
debugLog(
"H.265 运行时报错,回退 264 续播 pos=${playerCtr?.value.position.inSeconds}");
releasePlayer(); // 释放当前 → 下一帧按进度续播,选流因 _forceH264 走 264
}
@@ -609,8 +631,10 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
isShowBuy.value = false;
releasePlayer(rebuild: false);
update();
currentEpisode?.mediaContent ??= await ACGService.getMediaDetail(id: currentEpisode?.id ?? "");
startPlay(model: cartoonModel?.toVideoModel(currentEpisode) ?? VideoModel());
currentEpisode?.mediaContent ??=
await ACGService.getMediaDetail(id: currentEpisode?.id ?? "");
startPlay(
model: cartoonModel?.toVideoModel(currentEpisode) ?? VideoModel());
} catch (e) {
debugLog(e);
}
@@ -618,7 +642,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
/// 释放播放器。[rebuild]=false 只释放(切集前清理);[model] 非空则切新视频,为空则按上次进度续播。
/// dispose 走下一帧,避免在播放回调中销毁导致 use-after-free
void releasePlayer({VideoModel? model, bool rebuild = true, bool resetPos = false}) {
void releasePlayer(
{VideoModel? model, bool rebuild = true, bool resetPos = false}) {
final old = playerCtr;
old?.pause();
// resetPos:预览片→正片场景,两者时间轴不对应,不能沿用预览片进度,从头播
@@ -641,7 +666,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
/// 页面销毁清理:取消事件订阅 → 非预览视频上报观看记录 → 释放播放器
@override
void onClose() {
isDispose = true; // 第一时间置位,让所有异步回调(initialize/postFrame/Future.delayed)能尽早 return
isDispose =
true; // 第一时间置位,让所有异步回调(initialize/postFrame/Future.delayed)能尽早 return
TimedPopupManager().cancel(TimedPopupType.freeWatchPay);
TimedPopupManager().cancel(TimedPopupType.videoPayAhead);
@@ -651,13 +677,15 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
if (playerCtr != null) {
bool isPreview = false;
if (videoModel?.previewURL?.isNotEmpty == true) {
if (playerCtr?.dataSource.contains(videoModel?.previewURL ?? "") == true) {
if (playerCtr?.dataSource.contains(videoModel?.previewURL ?? "") ==
true) {
isPreview = true;
}
}
if (!isPreview) {
try {
_reportPlay(playerCtr?.value.position, playerCtr?.value.duration, videoModel);
_reportPlay(
playerCtr?.value.position, playerCtr?.value.duration, videoModel);
} catch (e) {
debugLog(e);
}
@@ -670,7 +698,8 @@ class VideoLogic extends GetxController with GetSingleTickerProviderStateMixin {
}
/// 上报播放记录(static:不碰实例状态,避免请求在飞时把已销毁的 logic 一起吊住)
static Future<void> _reportPlay(Duration? position, Duration? duration, VideoModel? video) async {
static Future<void> _reportPlay(
Duration? position, Duration? duration, VideoModel? video) async {
if (position == null || duration == null) return;
if (position.inMilliseconds < 1000) return; // 不足 1 秒不记
if (video?.videoType == 1) return; // 动漫不走这个接口
+22 -16
View File
@@ -6,19 +6,19 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgmj/assets_tool/app_colors.dart';
import 'package:hgmj/assets_tool/images.dart';
import 'package:hgmj/hj_page/video/video_logic.dart';
import 'package:hgmj/hj_page/video/view/video_ad_widget.dart';
import 'package:hgmj/hj_page/video/view/video_mask_buy_view.dart';
import 'package:hgmj/hj_page/video/view/video_menu_view.dart';
import 'package:hgmj/hj_page/video/view/video_status_view.dart';
import 'package:hgmj/hj_page/video/view/video_tabbar_menu_widget.dart';
import 'package:hgmj/hj_page/video/view/vip_promo_banner.dart';
import 'package:hgmj/hj_utils/codec_support.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:hgmj/tools_base/unique_tag_mixin.dart';
import 'package:hgmj/tools_base/widget/keep_alive_widget.dart';
import 'package:wydj/assets_tool/app_colors.dart';
import 'package:wydj/assets_tool/images.dart';
import 'package:wydj/hj_page/video/video_logic.dart';
import 'package:wydj/hj_page/video/view/video_ad_widget.dart';
import 'package:wydj/hj_page/video/view/video_mask_buy_view.dart';
import 'package:wydj/hj_page/video/view/video_menu_view.dart';
import 'package:wydj/hj_page/video/view/video_status_view.dart';
import 'package:wydj/hj_page/video/view/video_tabbar_menu_widget.dart';
import 'package:wydj/hj_page/video/view/vip_promo_banner.dart';
import 'package:wydj/hj_utils/codec_support.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:wydj/tools_base/unique_tag_mixin.dart';
import 'package:wydj/tools_base/widget/keep_alive_widget.dart';
import 'package:video_player/video_player.dart';
import '../../hj_model/video_model.dart';
@@ -159,12 +159,16 @@ class _VideoPageState extends State<VideoPage> with UniqueTagMixin {
alignment: Alignment.center,
child: AspectRatio(
aspectRatio: logic.videoRatio,
child: logic.playerCtr != null ? VideoPlayer(logic.playerCtr!) : SizedBox(),
child: logic.playerCtr != null
? VideoPlayer(logic.playerCtr!)
: SizedBox(),
),
),
),
if (logic.playerCtr?.value.isInitialized != true)
NetworkImageLoader(imageUrl: logic.videoModel?.cover ?? widget.videoCover ?? "", borderRadius: 0),
NetworkImageLoader(
imageUrl: logic.videoModel?.cover ?? widget.videoCover ?? "",
borderRadius: 0),
if (logic.playerCtr?.value.isInitialized == true) ...[
Positioned(
top: 12,
@@ -240,7 +244,9 @@ class _VideoPageState extends State<VideoPage> with UniqueTagMixin {
return ValueListenableBuilder<VideoPlayerValue>(
valueListenable: ctr,
builder: (_, value, __) => value.isInitialized && value.isBuffering
? const Center(child: CupertinoActivityIndicator(color: AppColors.actionRed, radius: 15))
? const Center(
child: CupertinoActivityIndicator(
color: AppColors.actionRed, radius: 15))
: const SizedBox(),
);
}
+18 -13
View File
@@ -2,15 +2,15 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgmj/assets_tool/images.dart';
import 'package:hgmj/hj_page/video/video_full_page.dart';
import 'package:hgmj/hj_page/video/view/video_menu_view.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:hgmj/tools_base/event_bus/event_bus_util.dart';
import 'package:hgmj/tools_base/event_bus/events.dart';
import 'package:hgmj/tools_base/loading/loading_center_widget.dart';
import 'package:wydj/assets_tool/images.dart';
import 'package:wydj/hj_page/video/video_full_page.dart';
import 'package:wydj/hj_page/video/view/video_menu_view.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:wydj/tools_base/event_bus/event_bus_util.dart';
import 'package:wydj/tools_base/event_bus/events.dart';
import 'package:wydj/tools_base/loading/loading_center_widget.dart';
import 'package:video_player/video_player.dart';
import 'package:hgmj/hj_utils/video_view_type.dart';
import 'package:wydj/hj_utils/video_view_type.dart';
import '../../tools_base/widget/net_image_widget.dart';
@@ -40,7 +40,8 @@ class _VideoPlayerAlertState extends State<VideoPlayerAlert> {
videoCtr = PlayerFactory.network(widget.movieUrl);
try {
await videoCtr?.initialize();
if (isRetry) confirmPlatformView(); // 仅重试成功(同视频 textureView 挂、platformView 放出)才落本地
if (isRetry)
confirmPlatformView(); // 仅重试成功(同视频 textureView 挂、platformView 放出)才落本地
if (mounted) {
videoCtr?.play();
setState(() {});
@@ -74,11 +75,13 @@ class _VideoPlayerAlertState extends State<VideoPlayerAlert> {
Positioned(
left: 0,
top: 0,
child: InkWell(enableFeedback: false,
child: InkWell(
enableFeedback: false,
onTap: Get.back,
child: Container(
padding: EdgeInsets.only(left: 16, top: 6),
child: Image.asset('back_circle.png'.commonImgPath, width: 24, height: 24),
child: Image.asset('back_circle.png'.commonImgPath,
width: 24, height: 24),
),
),
),
@@ -94,7 +97,8 @@ class _VideoPlayerAlertState extends State<VideoPlayerAlert> {
return Stack(
alignment: Alignment.center,
children: [
if (widget.imgCover?.isNotEmpty == true) NetworkImageLoader(imageUrl: widget.imgCover ?? ""),
if (widget.imgCover?.isNotEmpty == true)
NetworkImageLoader(imageUrl: widget.imgCover ?? ""),
LoadingCenterWidget(),
],
);
@@ -113,7 +117,8 @@ class _VideoPlayerAlertState extends State<VideoPlayerAlert> {
VideoMenuView(
playCtr: videoCtr!,
isFull: true,
onFullScreen: () => Get.to(() => VideoFullPage(playCtr: videoCtr!, isAutoV: false)),
onFullScreen: () =>
Get.to(() => VideoFullPage(playCtr: videoCtr!, isAutoV: false)),
),
],
);
+27 -13
View File
@@ -1,6 +1,6 @@
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/hj_utils/date_time_util.dart';
import 'package:hgmj/tools_base/global_store/store.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/hj_utils/date_time_util.dart';
import 'package:wydj/tools_base/global_store/store.dart';
import '../../../hj_utils/free_play_manager.dart';
import '../../pre_sale/pre_sale_provider.dart';
@@ -37,9 +37,12 @@ class LongVideoStatus {
//复用多处的固定状态,抽出来免得同一串文案散落
const _none = LongVideoStatus();
const _coinFree = LongVideoStatus(type: LongVideoStatusType.coinFree, desc: '已享金币视频免费特权');
const _skipVip = LongVideoStatus(type: LongVideoStatusType.skipPreview, desc: '跳过预览', isNeedVip: true);
const _skipBuy = LongVideoStatus(type: LongVideoStatusType.skipPreview, desc: '跳过预览', isNeedBuy: true);
const _coinFree =
LongVideoStatus(type: LongVideoStatusType.coinFree, desc: '已享金币视频免费特权');
const _skipVip = LongVideoStatus(
type: LongVideoStatusType.skipPreview, desc: '跳过预览', isNeedVip: true);
const _skipBuy = LongVideoStatus(
type: LongVideoStatusType.skipPreview, desc: '跳过预览', isNeedBuy: true);
/// 计算长视频的观看状态(展示文案 + 是否需付费/开会员)
///
@@ -70,35 +73,46 @@ LongVideoStatus longVideoStatus(VideoModel? model) {
late final canFree = FreePlayManager().useFreePlay(model);
if (model.freeArea == true) {
return const LongVideoStatus(type: LongVideoStatusType.freeVideo, desc: '免费视频');
return const LongVideoStatus(
type: LongVideoStatusType.freeVideo, desc: '免费视频');
}
if (!isVIP && freeCount >= 0 && isZeroCoin && canFree) {
return LongVideoStatus(type: LongVideoStatusType.freeRemain, desc: '免费视频剩余${freeCount + 1}次');
return LongVideoStatus(
type: LongVideoStatusType.freeRemain, desc: '免费视频剩余${freeCount + 1}次');
}
if (isVIP && isZeroCoin) {
// 交给 VipFreeTipView 显示,独立于操作台、3 秒后消失
return const LongVideoStatus(type: LongVideoStatusType.vipFree, desc: '已享VIP免费特权');
return const LongVideoStatus(
type: LongVideoStatusType.vipFree, desc: '已享VIP免费特权');
}
if (hasPaid) {
return const LongVideoStatus(type: LongVideoStatusType.purchased, desc: '已购买完整版');
return const LongVideoStatus(
type: LongVideoStatusType.purchased, desc: '已购买完整版');
}
if (!isVIP && isZeroCoin && canFree) {
return _none; // 免费次数内看过的视频,重复观看仍免费(不显示文案)
}
if (coinFreeLeft >= 0) {
return LongVideoStatus(type: LongVideoStatusType.coinFreeRemain, desc: '免费金币观影数剩余: $coinFreeLeft次');
return LongVideoStatus(
type: LongVideoStatusType.coinFreeRemain,
desc: '免费金币观影数剩余: $coinFreeLeft次');
}
// 往下不再判 !hasPaid:已购的上面就 return 了,走到这儿必定是未购
if (hasCoin && globalStore.isAWVIP) {
// 暗网视频要顶级会员,否则仍需单独买
if (model.isDarkTag && !globalStore.isVIPTopLevel) {
return LongVideoStatus(type: LongVideoStatusType.skipPreview, desc: '${model.coins}金币 跳过预览', isNeedBuy: true);
return LongVideoStatus(
type: LongVideoStatusType.skipPreview,
desc: '${model.coins}金币 跳过预览',
isNeedBuy: true);
}
return _coinFree;
}
// 二级会员 / 金币免费期内 / 本片金币已抵扣,都算已享金币免费特权
if ((hasCoin && globalStore.isSuperVip) ||
(isVIP && DateTimeUtil.calTime3(globalStore.meInfo?.goldVideoFreeExpire) > 0 && (coins ?? 50) <= 50) ||
(isVIP &&
DateTimeUtil.calTime3(globalStore.meInfo?.goldVideoFreeExpire) > 0 &&
(coins ?? 50) <= 50) ||
(isVIP && hasCoin && model.coins == 0)) {
return _coinFree;
}
+1 -1
View File
@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgmj/assets_tool/images.dart';
import 'package:wydj/assets_tool/images.dart';
import '../../../hj_model/splash/ads_model.dart';
import '../../../tools_base/banner/ads_banner.dart';
@@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:hgmj/assets_tool/images.dart';
import 'package:hgmj/extension/extensions.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/hj_utils/api_service/acg_service.dart';
import 'package:hgmj/hj_utils/api_service/common_service.dart';
import 'package:hgmj/hj_utils/api_service/mine_service.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:hgmj/tools_base/debug_log.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:wydj/assets_tool/images.dart';
import 'package:wydj/extension/extensions.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/hj_utils/api_service/acg_service.dart';
import 'package:wydj/hj_utils/api_service/common_service.dart';
import 'package:wydj/hj_utils/api_service/mine_service.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:wydj/tools_base/debug_log.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:like_button/like_button.dart';
/// 底部操作栏:观看量 + 点赞/收藏/分享。计数与状态就地改在传入的 [VideoModel] 上,
@@ -37,7 +37,8 @@ class _VideoDetailBottomMenuState extends State<VideoDetailBottomMenu> {
static const _labelStyle = TextStyle(color: Color(0xff989898), fontSize: 12);
/// 计数±1:原值为 null 时按操作前的状态兜底,保证取消后不会变成负数
int _nextCount(int? cur, bool wasOn) => (cur ?? (wasOn ? 1 : 0)) + (wasOn ? -1 : 1);
int _nextCount(int? cur, bool wasOn) =>
(cur ?? (wasOn ? 1 : 0)) + (wasOn ? -1 : 1);
// ========== 点赞 ==========
Future<bool> _onLike() async {
@@ -74,7 +75,8 @@ class _VideoDetailBottomMenuState extends State<VideoDetailBottomMenu> {
} else {
await MineService.postCollect(videoModel?.id, "SP", !preCollect);
}
videoModel?.collectCount = _nextCount(videoModel?.collectCount, preCollect);
videoModel?.collectCount =
_nextCount(videoModel?.collectCount, preCollect);
videoModel?.vidStatus?.hasCollected = !preCollect;
videoModel?.mediaInfo?.mediaStatus?.hasCollected = !preCollect;
videoModel?.mediaInfo?.countCollect = videoModel?.collectCount;
@@ -91,7 +93,8 @@ class _VideoDetailBottomMenuState extends State<VideoDetailBottomMenu> {
return Row(
children: [
Expanded(
child: Text("${videoModel?.playCount?.countStr ?? ""}观看量", style: _labelStyle),
child: Text("${videoModel?.playCount?.countStr ?? ""}观看量",
style: _labelStyle),
),
_likeItem(),
12.sizeBoxW,
@@ -110,7 +113,9 @@ class _VideoDetailBottomMenuState extends State<VideoDetailBottomMenu> {
isLiked: isLike,
size: 24,
likeBuilder: (isLiked) => Image.asset(
isLiked ? "like_red.png".commonImgPath : "video_like_grey.webp".videoPath,
isLiked
? "like_red.png".commonImgPath
: "video_like_grey.webp".videoPath,
),
onTap: (_) => _onLike(),
),
@@ -128,15 +133,20 @@ class _VideoDetailBottomMenuState extends State<VideoDetailBottomMenu> {
// 收藏状态切换时,图标 grey↔red 做 scale 弹出过渡
AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
transitionBuilder: (child, anim) => ScaleTransition(scale: anim, child: child),
transitionBuilder: (child, anim) =>
ScaleTransition(scale: anim, child: child),
child: Image.asset(
isCollect ? 'collect_yellow.png'.commonImgPath : 'collect_grey.png'.commonImgPath,
key: ValueKey(isCollect), // key 按状态区分,AnimatedSwitcher 才当成新 child 触发动画
isCollect
? 'collect_yellow.png'.commonImgPath
: 'collect_grey.png'.commonImgPath,
key: ValueKey(
isCollect), // key 按状态区分,AnimatedSwitcher 才当成新 child 触发动画
width: 24,
),
),
2.sizeBoxW,
Text(videoModel?.collectCount.countOr('收藏') ?? '收藏', style: _labelStyle),
Text(videoModel?.collectCount.countOr('收藏') ?? '收藏',
style: _labelStyle),
],
),
);
+29 -17
View File
@@ -1,17 +1,17 @@
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgmj/hj_model/cartoon_media_info.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/tools_base/widget/shrink_wrap.dart';
import 'package:hgmj/hj_page/home/tag/video_tag_page.dart';
import 'package:hgmj/hj_page/video/view/video_detail_bottom_menu.dart';
import 'package:hgmj/hj_utils/const.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:hgmj/tools_base/banner/ads_grid_view.dart';
import 'package:hgmj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:hgmj/tools_base/widget/keep_alive_widget.dart';
import 'package:wydj/hj_model/cartoon_media_info.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/tools_base/widget/shrink_wrap.dart';
import 'package:wydj/hj_page/home/tag/video_tag_page.dart';
import 'package:wydj/hj_page/video/view/video_detail_bottom_menu.dart';
import 'package:wydj/hj_utils/const.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:wydj/tools_base/banner/ads_grid_view.dart';
import 'package:wydj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:wydj/tools_base/widget/keep_alive_widget.dart';
import 'package:video_player/video_player.dart';
import '../../../alert/video/share_media_dialog.dart';
@@ -35,11 +35,13 @@ class VideoDetailView extends StatefulWidget {
State<VideoDetailView> createState() => _VideoDetailViewState();
}
class _VideoDetailViewState extends State<VideoDetailView> with SingleTickerProviderStateMixin {
class _VideoDetailViewState extends State<VideoDetailView>
with SingleTickerProviderStateMixin {
VideoModel? get videoModel => widget.model;
// 视频(SP)推荐按当前视频第一个 tag 拉同类;tags 为空则全局推荐
String? get _videoTagId => videoModel?.tags?.isNotEmpty == true ? videoModel?.tags?.first.id : null;
String? get _videoTagId =>
videoModel?.tags?.isNotEmpty == true ? videoModel?.tags?.first.id : null;
late final TabController tabCtr = TabController(length: 3, vsync: this);
@@ -66,7 +68,8 @@ class _VideoDetailViewState extends State<VideoDetailView> with SingleTickerProv
SliverToBoxAdapter(
child: Container(
margin: EdgeInsets.fromLTRB(12, 18, 12, 0),
child: VideoDetailBottomMenu(model: videoModel, onShare: _onShare),
child:
VideoDetailBottomMenu(model: videoModel, onShare: _onShare),
),
),
SliverToBoxAdapter(
@@ -137,7 +140,11 @@ class _VideoDetailViewState extends State<VideoDetailView> with SingleTickerProv
child: Text(
videoModel?.title?.trim() ?? "",
maxLines: 2,
style: const TextStyle(color: Colors.white, height: 1.5, fontSize: 16, fontWeight: FontWeight.w500),
style: const TextStyle(
color: Colors.white,
height: 1.5,
fontSize: 16,
fontWeight: FontWeight.w500),
),
);
}
@@ -193,14 +200,19 @@ class _VideoDetailViewState extends State<VideoDetailView> with SingleTickerProv
labelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
labelColor: Color(0xE5FFFFFF),
unselectedLabelColor: Color(0x8CFFFFFF),
unselectedLabelStyle: TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
unselectedLabelStyle:
TextStyle(fontSize: 14, fontWeight: FontWeight.w400),
tabs: menuTitles
.map((e) => Padding(
padding: const EdgeInsets.only(bottom: 5),
child: Text(e),
))
.toList(),
indicator: CustomIndicator(isGradient: true, width: 13, height: 3, borderRadius: BorderRadius.circular(1.5)),
indicator: CustomIndicator(
isGradient: true,
width: 13,
height: 3,
borderRadius: BorderRadius.circular(1.5)),
),
],
);
+32 -12
View File
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/hj_page/video/view/long_video_status.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/hj_page/video/view/long_video_status.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:video_player/video_player.dart';
import '../../../alert/video/buy_vip_alert.dart';
@@ -44,12 +44,16 @@ class _VideoMaskBuyViewState extends State<VideoMaskBuyView> {
String get vipBtnTitle {
if (!hasPresale) return "开通VIP免费看";
if (!presaleProvider.isPayFirst) return "开通预售免费看";
return presaleProvider.canPayBalance ? "支付尾款免费看" : "请在${presaleProvider.startTimeMD}支付尾款";
return presaleProvider.canPayBalance
? "支付尾款免费看"
: "请在${presaleProvider.startTimeMD}支付尾款";
}
/// 开通预售/VIP,回来后刷新按钮文案
Future<void> _onVipTap() async {
await (hasPresale ? Get.to(() => PreSalePage()) : BuyVipAlert.show(videoId: widget.model?.id));
await (hasPresale
? Get.to(() => PreSalePage())
: BuyVipAlert.show(videoId: widget.model?.id));
if (mounted) setState(() {});
}
@@ -80,7 +84,9 @@ class _VideoMaskBuyViewState extends State<VideoMaskBuyView> {
if (isBuying) return;
isBuying = true;
// 观影券抵扣:取能覆盖本片金币的券面额
final couponNum = useCoupon ? globalStore.meInfo?.couponGold(widget.model?.originCoins) : null;
final couponNum = useCoupon
? globalStore.meInfo?.couponGold(widget.model?.originCoins)
: null;
await PayManager().buy(
widget.model?.id,
ProductType.media,
@@ -116,18 +122,30 @@ class _VideoMaskBuyViewState extends State<VideoMaskBuyView> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(widget.maskTitle, style: const TextStyle(color: Color(0xffffffff), fontSize: 16, fontWeight: FontWeight.w500)),
Text(widget.maskTitle,
style: const TextStyle(
color: Color(0xffffffff),
fontSize: 16,
fontWeight: FontWeight.w500)),
18.sizeBoxH,
const Text("开通VIP 全站视频免费看", style: TextStyle(color: Color(0xffffffff), fontSize: 16, fontWeight: FontWeight.w500)),
const Text("开通VIP 全站视频免费看",
style: TextStyle(
color: Color(0xffffffff),
fontSize: 16,
fontWeight: FontWeight.w500)),
18.sizeBoxH,
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_btn(
title: status.isNeedBuy ? "${widget.model?.coins ?? 0}金币解锁" : "邀请得3日VIP",
title: status.isNeedBuy
? "${widget.model?.coins ?? 0}金币解锁"
: "邀请得3日VIP",
colors: const [Color(0xffFFE8BE), Color(0xffE6B764)],
textColor: const Color(0xff694923),
onTap: () => status.isNeedBuy ? _onCoinUnlock() : Get.to(() => MineSharePage()),
onTap: () => status.isNeedBuy
? _onCoinUnlock()
: Get.to(() => MineSharePage()),
),
18.sizeBoxW,
_btn(
@@ -147,11 +165,13 @@ class _VideoMaskBuyViewState extends State<VideoMaskBuyView> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text("使用观影券", style: TextStyle(color: Colors.white, fontSize: 12)),
const Text("使用观影券",
style: TextStyle(color: Colors.white, fontSize: 12)),
3.sizeBoxW,
Text(
"x${me?.couponCount ?? 0}",
style: const TextStyle(color: Color(0xffE5365C), fontSize: 12),
style: const TextStyle(
color: Color(0xffE5365C), fontSize: 12),
),
],
),
+59 -29
View File
@@ -4,11 +4,11 @@ import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:hgmj/assets_tool/app_colors.dart';
import 'package:hgmj/assets_tool/images.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:hgmj/tools_base/toast.dart';
import 'package:wydj/assets_tool/app_colors.dart';
import 'package:wydj/assets_tool/images.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:wydj/tools_base/toast.dart';
import 'package:video_player/video_player.dart';
import '../../../hj_utils/buy_util.dart';
@@ -75,13 +75,15 @@ class _VideoMenuViewState extends State<VideoMenuView> {
bool get isPreview {
final model = widget.videoModel;
if (FreePlayManager().useFreePlay(model)) return false;
return model?.previewURL?.isNotEmpty == true && (needVip(model) || needCoin(model));
return model?.previewURL?.isNotEmpty == true &&
(needVip(model) || needCoin(model));
}
/// 未购买/未开通 VIP 时,松手后超出试看区间则回弹到边界
bool get _shouldLimitSeek {
final model = widget.videoModel;
if (model?.mediaInfo?.isCartoonFreeEpisode == true) return false; // 动漫免费集:整条进度可拖,不锁试看区间
if (model?.mediaInfo?.isCartoonFreeEpisode == true)
return false; // 动漫免费集:整条进度可拖,不锁试看区间
if (FreePlayManager().useFreePlay(model)) return false;
if (model?.freeArea == true) return false;
if (model?.vidStatus?.hasPaid == true) return false;
@@ -89,7 +91,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
if (coinFreeCount(model) >= 0) return false;
if (model?.isCoinVideo() == true) {
if (globalStore.isVIP && model?.coins == 0) return false;
if (model?.videoType != 1 && presaleProvider.coinVideoFreeCount >= 0) return false;
if (model?.videoType != 1 && presaleProvider.coinVideoFreeCount >= 0)
return false;
}
final status = longVideoStatus(model);
return status.isNeedPay;
@@ -122,7 +125,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
}
/// 试看锁定:需付费(区间非空)且 [sec] 已超出免费试看区间 → 禁止拖动
bool _isSeekLocked(int sec) => _seekRange.$1 != null && widget.videoModel?.isInFreeTime(sec) != true;
bool _isSeekLocked(int sec) =>
_seekRange.$1 != null && widget.videoModel?.isInFreeTime(sec) != true;
/// 松手落点:先夹到 [0,总时长],再夹回试看区间边界
Duration _snapSeek(Duration target) {
@@ -234,7 +238,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
@override
Widget build(BuildContext context) {
final menuVisible = _showMenu || !_isInited || _isLongPress; //操作台/未初始化/长按倍速时可见
final menuVisible =
_showMenu || !_isInited || _isLongPress; //操作台/未初始化/长按倍速时可见
//试看区间每帧只算一次:_shouldLimitSeek 内含 useFreePlay 等带副作用的判断,不能每次拖动都重跑
_seekRange = _calcSeekRange();
return GestureDetector(
@@ -242,7 +247,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
onTap: _toggleMenu,
onHorizontalDragStart: (_) {
//试看结束/未初始化禁止拖动
if (_isSeekLocked(playCtr.value.position.inSeconds) || !playCtr.value.isInitialized) return;
if (_isSeekLocked(playCtr.value.position.inSeconds) ||
!playCtr.value.isInitialized) return;
_wasPlaying = playCtr.value.isPlaying;
if (_wasPlaying) playCtr.pause();
_seekPos = playCtr.value.position;
@@ -250,7 +256,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
setState(() {});
},
onHorizontalDragUpdate: (details) {
if (_isSeekLocked(playCtr.value.position.inSeconds) || !playCtr.value.isInitialized) return;
if (_isSeekLocked(playCtr.value.position.inSeconds) ||
!playCtr.value.isInitialized) return;
_wakeMenu();
_dragSeek(details.delta);
},
@@ -310,7 +317,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
Positioned(
top: 12,
right: 16,
child: VideoStatusView(model: widget.videoModel, onBuyEvent: widget.onBuyEvent),
child: VideoStatusView(
model: widget.videoModel, onBuyEvent: widget.onBuyEvent),
),
Positioned(bottom: 0, left: 0, right: 0, child: _bottomMenu()),
Obx(() {
@@ -323,7 +331,9 @@ class _VideoMenuViewState extends State<VideoMenuView> {
scale: animation,
child: FadeTransition(opacity: animation, child: child),
),
child: (!_isInited || buffering || playing) ? const SizedBox(key: ValueKey('playIconEmpty')) : _playIcon(),
child: (!_isInited || buffering || playing)
? const SizedBox(key: ValueKey('playIconEmpty'))
: _playIcon(),
);
}),
if (_showSeekText) _seekText(),
@@ -334,7 +344,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
top: 0,
bottom: 0,
right: _showSpeed ? 0 : -150,
child: IgnorePointer(ignoring: !_showSpeed, child: _speedPanel()),
child: IgnorePointer(
ignoring: !_showSpeed, child: _speedPanel()),
),
if (_skipDir != 0)
Positioned(
@@ -350,7 +361,11 @@ class _VideoMenuViewState extends State<VideoMenuView> {
duration: const Duration(milliseconds: 220),
child: Container(
color: Colors.black.withValues(alpha: 0.5),
padding: EdgeInsets.fromLTRB((_skipDir == 1) ? 12 : 24, 12, (_skipDir == 1) ? 24 : 12, 12),
padding: EdgeInsets.fromLTRB(
(_skipDir == 1) ? 12 : 24,
12,
(_skipDir == 1) ? 24 : 12,
12),
child: _skipHint(),
),
),
@@ -397,7 +412,9 @@ class _VideoMenuViewState extends State<VideoMenuView> {
child: FadeTransition(opacity: animation, child: child),
),
child: Image.asset(
showPause ? "pause_icon.webp".videoPath : "play.webp".videoPath,
showPause
? "pause_icon.webp".videoPath
: "play.webp".videoPath,
key: ValueKey(showPause),
width: 20.w,
),
@@ -442,7 +459,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
},
child: const Padding(
padding: EdgeInsets.fromLTRB(4, 6, 6, 6),
child: Text("倍速", style: TextStyle(color: Colors.white, fontSize: 14)),
child: Text("倍速",
style: TextStyle(color: Colors.white, fontSize: 14)),
),
),
InkWell(
@@ -452,7 +470,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
height: 26,
width: 26,
margin: const EdgeInsets.only(right: 6),
child: Image.asset("full_icon.png".videoPath, width: 26, height: 26),
child: Image.asset("full_icon.png".videoPath,
width: 26, height: 26),
),
),
],
@@ -480,7 +499,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
child: ListView.separated(
padding: EdgeInsets.zero,
itemCount: speeds.length,
separatorBuilder: (_, __) => Container(height: 1, color: Colors.white.withValues(alpha: 0.05)),
separatorBuilder: (_, __) => Container(
height: 1, color: Colors.white.withValues(alpha: 0.05)),
itemBuilder: (_, i) => _speedItem(speeds[i]),
),
),
@@ -538,7 +558,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
children: [
//顶部偏移叠加安全区高度,避免竖屏被刘海/灵动岛遮挡
//注意:必须用 context 的 MediaQuery(外层 SafeArea 已吃掉 top 时这里就该是 0),Get.mediaQuery 拿的是根节点值会多顶 47
((widget.isFull ? 24 : 12) + MediaQuery.of(context).padding.top).sizeBoxH,
((widget.isFull ? 24 : 12) + MediaQuery.of(context).padding.top)
.sizeBoxH,
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
//半透明黑底:亮色画面上也能看清时间
@@ -551,7 +572,8 @@ class _VideoMenuViewState extends State<VideoMenuView> {
children: [
Text(
DateTimeUtil.formatDuration(showDuration) ?? "",
style: const TextStyle(color: AppColors.actionRed, fontSize: 14), //主题黄
style: const TextStyle(
color: AppColors.actionRed, fontSize: 14), //主题黄
),
if (playCtr.value.isInitialized)
Text(
@@ -571,12 +593,14 @@ class _VideoMenuViewState extends State<VideoMenuView> {
final isForward = _skipDir == 1;
final text = Text(
isForward ? "快进10秒" : "快退10秒",
style: TextStyle(color: Colors.white.withValues(alpha: 0.8), fontSize: 13),
style:
TextStyle(color: Colors.white.withValues(alpha: 0.8), fontSize: 13),
);
final arrows = _SeekArrows(forward: isForward);
return Row(
mainAxisSize: MainAxisSize.min,
children: isForward ? [text, 4.sizeBoxW, arrows] : [arrows, 4.sizeBoxW, text],
children:
isForward ? [text, 4.sizeBoxW, arrows] : [arrows, 4.sizeBoxW, text],
);
}
}
@@ -590,8 +614,11 @@ class _SeekArrows extends StatefulWidget {
State<_SeekArrows> createState() => _SeekArrowsState();
}
class _SeekArrowsState extends State<_SeekArrows> with SingleTickerProviderStateMixin {
late final AnimationController _ctr = AnimationController(vsync: this, duration: const Duration(milliseconds: 900))..repeat();
class _SeekArrowsState extends State<_SeekArrows>
with SingleTickerProviderStateMixin {
late final AnimationController _ctr = AnimationController(
vsync: this, duration: const Duration(milliseconds: 900))
..repeat();
@override
void dispose() {
@@ -605,7 +632,8 @@ class _SeekArrowsState extends State<_SeekArrows> with SingleTickerProviderState
? const Icon(Icons.play_arrow_rounded, color: Colors.white, size: 18)
: const RotatedBox(
quarterTurns: 2,
child: Icon(Icons.play_arrow_rounded, color: Colors.white, size: 18),
child:
Icon(Icons.play_arrow_rounded, color: Colors.white, size: 18),
);
return AnimatedBuilder(
animation: _ctr,
@@ -615,9 +643,11 @@ class _SeekArrowsState extends State<_SeekArrows> with SingleTickerProviderState
// 波沿方向流动:快进 左→右、快退 右→左
final i = widget.forward ? index : (2 - index);
final phase = (_ctr.value + i / 3.0) % 1.0;
final opacity = 0.25 + 0.75 * (1 - (2 * phase - 1).abs()); // 三角波 0.25~1.0
final opacity =
0.25 + 0.75 * (1 - (2 * phase - 1).abs()); // 三角波 0.25~1.0
// 压窄每个箭头的占位宽度,让三角波间距更紧凑
return SizedBox(width: 10, child: Opacity(opacity: opacity, child: arrow));
return SizedBox(
width: 10, child: Opacity(opacity: opacity, child: arrow));
}),
),
);
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'long_video_status.dart';
@@ -39,7 +39,8 @@ class _VipFreeTipViewState extends State<VipFreeTipView> {
Widget build(BuildContext context) {
if (!_isShowTip) return const SizedBox();
// 仅「已享VIP免费特权」走本 view,3 秒后自动隐藏;其余状态归 VideoStatusView
if (longVideoStatus(widget.videoModel).type != LongVideoStatusType.vipFree) {
if (longVideoStatus(widget.videoModel).type !=
LongVideoStatusType.vipFree) {
return const SizedBox();
}
_startHideTimer();
@@ -70,9 +71,12 @@ class VideoStatusView extends StatelessWidget {
if (model == null) return const SizedBox.shrink();
final status = longVideoStatus(model);
final isCartoon = model?.videoType == 1;
final hideForCartoon =
isCartoon && (status.type == LongVideoStatusType.skipPreview || status.type == LongVideoStatusType.coinFree);
if (hideForCartoon || status.type == LongVideoStatusType.none || status.type == LongVideoStatusType.vipFree) {
final hideForCartoon = isCartoon &&
(status.type == LongVideoStatusType.skipPreview ||
status.type == LongVideoStatusType.coinFree);
if (hideForCartoon ||
status.type == LongVideoStatusType.none ||
status.type == LongVideoStatusType.vipFree) {
return const SizedBox();
}
return GestureDetector(
@@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:hgmj/assets_tool/app_colors.dart';
import 'package:hgmj/assets_tool/images.dart';
import 'package:hgmj/config/address.dart';
import 'package:hgmj/extension/extensions.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/tools_base/video_download/download_button.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:hgmj/tools_base/debug_log.dart';
import 'package:hgmj/tools_base/indicator/custom_tab_indicator.dart';
import 'package:wydj/assets_tool/app_colors.dart';
import 'package:wydj/assets_tool/images.dart';
import 'package:wydj/config/address.dart';
import 'package:wydj/extension/extensions.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/tools_base/video_download/download_button.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:wydj/tools_base/debug_log.dart';
import 'package:wydj/tools_base/indicator/custom_tab_indicator.dart';
import '../../../alert/video/video_line_menu_alert.dart';
import '../../../hj_model/splash/domain_source_model.dart';
@@ -40,7 +40,8 @@ class _VideoTabbarMenuWidgetState extends State<VideoTabbarMenuWidget> {
Widget build(BuildContext context) {
Domain? selectedCnd;
try {
selectedCnd = Address.cdnAddressLists.firstWhere((element) => element.url == Address.cdnAddress);
selectedCnd = Address.cdnAddressLists
.firstWhere((element) => element.url == Address.cdnAddress);
} catch (e) {
debugLog(e);
}
@@ -61,15 +62,18 @@ class _VideoTabbarMenuWidgetState extends State<VideoTabbarMenuWidget> {
labelPadding: const EdgeInsets.symmetric(horizontal: 12),
labelColor: const Color(0xE5FFFFFF),
unselectedLabelColor: const Color(0x73FFFFFF),
labelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(fontSize: 16, fontWeight: FontWeight.w400),
labelStyle: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w600),
unselectedLabelStyle: const TextStyle(
fontSize: 16, fontWeight: FontWeight.w400),
indicatorSize: TabBarIndicatorSize.label,
dividerHeight: 0, // 去掉 TabBar 默认底部分割线(外层已有 0.5.line)
indicator: CustomIndicator(
color: AppColors.actionRed,
width: 16,
height: 4,
borderRadius: const BorderRadius.vertical(top: Radius.circular(3)),
borderRadius:
const BorderRadius.vertical(top: Radius.circular(3)),
),
tabs: [
const Tab(text: "简介"),
@@ -104,12 +108,14 @@ class _VideoTabbarMenuWidgetState extends State<VideoTabbarMenuWidget> {
InkWell(
enableFeedback: false,
onTap: () async {
if (await VideoLineMenuAlert.show()) widget.onSwitchLine?.call();
if (await VideoLineMenuAlert.show())
widget.onSwitchLine?.call();
setState(() {});
},
child: Row(
children: [
Image.asset("line_switch.webp".videoPath, width: 16),
Image.asset("line_switch.webp".videoPath,
width: 16),
2.sizeBoxW,
Text(
selectedCnd?.desc ?? '',
+13 -6
View File
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hgmj/hj_model/video_model.dart';
import 'package:hgmj/hj_utils/screen.dart';
import 'package:wydj/hj_model/video_model.dart';
import 'package:wydj/hj_utils/screen.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart';
@@ -42,7 +42,8 @@ class _VipPromoBanner extends State<VipPromoBanner> {
if (limitTime.canShow) return LimitTimeBanner();
// 2. 支付分层横幅(playPage 图 + vipCard 跳转),都没有则不展示
final layeredConfig = MineMsgProvider().payTier?.config;
if ((layeredConfig?.playPage ?? '').isNotEmpty) return _buildLayeredBanner(layeredConfig!);
if ((layeredConfig?.playPage ?? '').isNotEmpty)
return _buildLayeredBanner(layeredConfig!);
return const SizedBox();
},
);
@@ -75,11 +76,16 @@ class _VipPromoBanner extends State<VipPromoBanner> {
fit: StackFit.expand,
children: [
// 分层横幅图(后端加密图)
NetworkImageLoader(imageUrl: config.playPage ?? "", fit: BoxFit.fill, borderRadius: 0),
NetworkImageLoader(
imageUrl: config.playPage ?? "",
fit: BoxFit.fill,
borderRadius: 0),
// 分层倒计时:监听 tick 每秒刷新,过期自动收起
ValueListenableBuilder<int>(
valueListenable: MineMsgProvider().tick,
builder: (_, __, ___) => config.hasDiscountCountdown ? _buildLayeredCountdown(config) : const SizedBox.shrink(),
builder: (_, __, ___) => config.hasDiscountCountdown
? _buildLayeredCountdown(config)
: const SizedBox.shrink(),
),
],
),
@@ -112,7 +118,8 @@ class _VipPromoBanner extends State<VipPromoBanner> {
borderRadius: BorderRadius.circular(3),
border: Border.all(color: const Color(0xffFFE381), width: 0.5),
),
child: Text(value, style: const TextStyle(color: Colors.white, fontSize: 12)),
child: Text(value,
style: const TextStyle(color: Colors.white, fontSize: 12)),
);
}