logo和名称替换
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/hj_model/drama_media_info.dart';
|
||||
import 'package:hgmj/hj_model/list_base_model.dart';
|
||||
import 'package:hgmj/hj_model/media_content.dart';
|
||||
import 'package:hgmj/hj_model/video_model.dart';
|
||||
import 'package:hgmj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_episode_sheet.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_speed_sheet.dart';
|
||||
import 'package:hgmj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgmj/tools_base/debug_log.dart';
|
||||
import 'package:wydj/hj_model/drama_media_info.dart';
|
||||
import 'package:wydj/hj_model/list_base_model.dart';
|
||||
import 'package:wydj/hj_model/media_content.dart';
|
||||
import 'package:wydj/hj_model/video_model.dart';
|
||||
import 'package:wydj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_episode_sheet.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_speed_sheet.dart';
|
||||
import 'package:wydj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:wydj/tools_base/debug_log.dart';
|
||||
|
||||
/// 短剧二级页:同一部剧连播,退出本页就回到 Feed 的逐剧模式
|
||||
class DramaDetailLogic extends GetxController {
|
||||
@@ -83,7 +83,8 @@ class DramaDetailLogic extends GetxController {
|
||||
final resumeTask = dramaResume.load(); // 读本地盘,内部已兜住异常
|
||||
final resp = await Future.wait([
|
||||
DramaService.fetchDetail(drama?.id),
|
||||
DramaService.fetchEpisodeList(drama?.id ?? '', 1, drama?.episodePageSize ?? 30),
|
||||
DramaService.fetchEpisodeList(
|
||||
drama?.id ?? '', 1, drama?.episodePageSize ?? 30),
|
||||
]);
|
||||
await resumeTask;
|
||||
_detail = resp[0] as DramaMediaInfo? ?? drama;
|
||||
@@ -113,7 +114,8 @@ class DramaDetailLogic extends GetxController {
|
||||
final list = episodes;
|
||||
dramaResume.markWatched(
|
||||
mediaId: info?.id,
|
||||
contentId: list.isEmpty ? null : list[pageIndex.clamp(0, list.length - 1)].id,
|
||||
contentId:
|
||||
list.isEmpty ? null : list[pageIndex.clamp(0, list.length - 1)].id,
|
||||
drama: info,
|
||||
);
|
||||
}
|
||||
@@ -144,7 +146,8 @@ class DramaDetailLogic extends GetxController {
|
||||
}
|
||||
//置空而不是改成 bought:开了卡就是不该再挂付费标,真实状态以分集详情为准
|
||||
for (final episode in info?.episodeList ?? <MediaContent>[]) {
|
||||
if (episode.accessType == 'coin' || episode.accessType == 'card') episode.accessType = null;
|
||||
if (episode.accessType == 'coin' || episode.accessType == 'card')
|
||||
episode.accessType = null;
|
||||
}
|
||||
update([idFeed]);
|
||||
}
|
||||
@@ -157,7 +160,8 @@ class DramaDetailLogic extends GetxController {
|
||||
if (media == null || _isLoading || media.haxNextEpisode != true) return;
|
||||
_isLoading = true;
|
||||
final page = (media.episodeCurPage ?? 1) + 1;
|
||||
final res = await DramaService.fetchEpisodeList(media.id ?? '', page, media.episodePageSize);
|
||||
final res = await DramaService.fetchEpisodeList(
|
||||
media.id ?? '', page, media.episodePageSize);
|
||||
_isLoading = false;
|
||||
if (isClosed) return;
|
||||
//拉空/失败都要落 haxNextEpisode,否则 _resumeIndex 的补页循环会一直转
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.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/drama_media_info.dart';
|
||||
import 'package:hgmj/hj_page/drama/drama_detail_logic.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_bottom_bar.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_video_player.dart';
|
||||
import 'package:hgmj/hj_utils/screen.dart';
|
||||
import 'package:hgmj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgmj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:wydj/assets_tool/app_colors.dart';
|
||||
import 'package:wydj/assets_tool/images.dart';
|
||||
import 'package:wydj/hj_model/drama_media_info.dart';
|
||||
import 'package:wydj/hj_page/drama/drama_detail_logic.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_bottom_bar.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_video_player.dart';
|
||||
import 'package:wydj/hj_utils/screen.dart';
|
||||
import 'package:wydj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:wydj/tools_base/unique_tag_mixin.dart';
|
||||
|
||||
/// 短剧二级页:竖划切集 + 底部剧集条(选集/自动续播)
|
||||
class DramaDetailPage extends StatefulWidget {
|
||||
@@ -55,7 +55,8 @@ class _DramaDetailPageState extends State<DramaDetailPage> with UniqueTagMixin {
|
||||
),
|
||||
builder: (logic) {
|
||||
if (!logic.isFirstLoaded) return const LoadingCenterWidget();
|
||||
if (logic.episodeVideos.isEmpty) return CErrorWidget(retryOnTap: logic.loadDetail);
|
||||
if (logic.episodeVideos.isEmpty)
|
||||
return CErrorWidget(retryOnTap: logic.loadDetail);
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
@@ -121,7 +122,9 @@ class _DramaDetailPageState extends State<DramaDetailPage> with UniqueTagMixin {
|
||||
final seconds = logic.nextSeconds;
|
||||
return DramaBottomBar(
|
||||
highlight: seconds > 0 ? '${seconds}s后' : null,
|
||||
text: seconds > 0 ? '播放下一集' : '观看完整剧集·全${logic.info?.totalEpisode ?? logic.episodes.length}集',
|
||||
text: seconds > 0
|
||||
? '播放下一集'
|
||||
: '观看完整剧集·全${logic.info?.totalEpisode ?? logic.episodes.length}集',
|
||||
onTap: logic.openEpisodeSheet,
|
||||
);
|
||||
},
|
||||
@@ -150,13 +153,17 @@ class _DramaDetailPageState extends State<DramaDetailPage> with UniqueTagMixin {
|
||||
'stopwatch.svg'.videoPath,
|
||||
width: 18,
|
||||
height: 18,
|
||||
colorFilter: const ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
colorFilter:
|
||||
const ColorFilter.mode(Colors.white, BlendMode.srcIn),
|
||||
),
|
||||
4.sizeBoxW,
|
||||
Text(
|
||||
'${logic.speed}x',
|
||||
style:
|
||||
const TextStyle(color: Colors.white, fontSize: 12, height: 18 / 12, fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
height: 18 / 12,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -175,7 +182,9 @@ class _DramaDetailPageState extends State<DramaDetailPage> with UniqueTagMixin {
|
||||
icon: const Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Colors.white,
|
||||
shadows: [Shadow(color: Colors.black, offset: Offset(1, 1), blurRadius: 1)],
|
||||
shadows: [
|
||||
Shadow(color: Colors.black, offset: Offset(1, 1), blurRadius: 1)
|
||||
],
|
||||
),
|
||||
onPressed: Get.back,
|
||||
),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/hj_model/drama/drama_models.dart';
|
||||
import 'package:hgmj/hj_model/video_model.dart';
|
||||
import 'package:hgmj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:hgmj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgmj/tools_base/debug_log.dart';
|
||||
import 'package:wydj/hj_model/drama/drama_models.dart';
|
||||
import 'package:wydj/hj_model/video_model.dart';
|
||||
import 'package:wydj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:wydj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:wydj/tools_base/debug_log.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// AI短剧信息流:一条 = 一部剧的第 1 集,竖划换下一部剧。
|
||||
@@ -68,7 +68,8 @@ class DramaFeedLogic extends GetxController {
|
||||
if (!isRefresh && !hasNext) return;
|
||||
isLoading = true;
|
||||
try {
|
||||
final resp = await DramaService.fetchFeed(size: 20, requestId: _requestId ??= const Uuid().v4());
|
||||
final resp = await DramaService.fetchFeed(
|
||||
size: 20, requestId: _requestId ??= const Uuid().v4());
|
||||
if (resp != null) _requestId = null; // 成功才作废;失败重试沿用同一个 id,避免服务端当成两批
|
||||
//拿到什么收什么,不按剧 id 去重:服务端反复下发同一部剧时,去重会让列表不增长,
|
||||
//用户停在最后一条划不动、onPageChanged 也不再触发,就彻底卡死了。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/hj_page/drama/drama_feed_logic.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_video_player.dart';
|
||||
import 'package:hgmj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgmj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:wydj/hj_page/drama/drama_feed_logic.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_video_player.dart';
|
||||
import 'package:wydj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:wydj/tools_base/unique_tag_mixin.dart';
|
||||
|
||||
/// AI短剧信息流页:竖划一部换一部,每条播该剧第 1 集
|
||||
class DramaFeedPage extends StatefulWidget {
|
||||
@@ -21,7 +21,8 @@ class _DramaFeedPageState extends State<DramaFeedPage> with UniqueTagMixin {
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<DramaFeedLogic>(
|
||||
tag: uniqueTag, // 多 tab 并存,按实例隔离
|
||||
init: DramaFeedLogic(superTabCtr: widget.superTabCtr, tabIndex: widget.tabIndex),
|
||||
init: DramaFeedLogic(
|
||||
superTabCtr: widget.superTabCtr, tabIndex: widget.tabIndex),
|
||||
builder: (logic) {
|
||||
final Widget content;
|
||||
if (!logic.isFirstLoaded) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hgmj/hj_model/drama/drama_models.dart';
|
||||
import 'package:hgmj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgmj/tools_base/base_list_controller.dart';
|
||||
import 'package:wydj/hj_model/drama/drama_models.dart';
|
||||
import 'package:wydj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:wydj/tools_base/base_list_controller.dart';
|
||||
|
||||
import '../../hj_model/drama_media_info.dart';
|
||||
|
||||
@@ -36,7 +36,8 @@ class DramaListLogic extends ListBaseLogic<DramaMediaInfo> {
|
||||
List<DramaTopic> topics = [];
|
||||
bool topicsReady = false;
|
||||
|
||||
List<String> get sortTitles => topics.map((e) => e.name ?? '').where((e) => e.isNotEmpty).toList();
|
||||
List<String> get sortTitles =>
|
||||
topics.map((e) => e.name ?? '').where((e) => e.isNotEmpty).toList();
|
||||
|
||||
String? get _currentTopicId {
|
||||
if (topics.isEmpty) return null;
|
||||
@@ -79,7 +80,8 @@ class DramaListLogic extends ListBaseLogic<DramaMediaInfo> {
|
||||
Future<(List<DramaMediaInfo>?, bool)> _fetch(int page) async {
|
||||
final topicId = _currentTopicId;
|
||||
if (topicId == null) return (<DramaMediaInfo>[], false);
|
||||
final res = await DramaService.fetchTopicWorks(topicId, page: page, size: 20);
|
||||
final res =
|
||||
await DramaService.fetchTopicWorks(topicId, page: page, size: 20);
|
||||
return (res?.list ?? [], res?.hasNext ?? false);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/assets_tool/images.dart';
|
||||
import 'package:hgmj/hj_page/drama/drama_list_logic.dart';
|
||||
import 'package:hgmj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgmj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgmj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:wydj/assets_tool/images.dart';
|
||||
import 'package:wydj/hj_page/drama/drama_list_logic.dart';
|
||||
import 'package:wydj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:wydj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:wydj/tools_base/unique_tag_mixin.dart';
|
||||
|
||||
import '../home/home_cell_style/drama_cover_card.dart';
|
||||
|
||||
@@ -57,9 +57,13 @@ class _DramaListPageState extends State<DramaListPage> with UniqueTagMixin {
|
||||
children: [
|
||||
for (var r = 0; r < rows.length; r++) ...[
|
||||
if (r > 0) const SizedBox(height: 8),
|
||||
_buildSortRow(logic, titles, rows[r], hasToggle: _rowHasToggle(needToggle, rows, r)),
|
||||
_buildSortRow(logic, titles, rows[r],
|
||||
hasToggle: _rowHasToggle(needToggle, rows, r)),
|
||||
],
|
||||
if (needToggle && _sortExpanded && rows.isNotEmpty && rows.last.length == 4) ...[
|
||||
if (needToggle &&
|
||||
_sortExpanded &&
|
||||
rows.isNotEmpty &&
|
||||
rows.last.length == 4) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(children: [_buildExpandToggle()]),
|
||||
],
|
||||
@@ -84,7 +88,8 @@ class _DramaListPageState extends State<DramaListPage> with UniqueTagMixin {
|
||||
children: [
|
||||
for (var k = 0; k < indexes.length; k++) ...[
|
||||
if (k > 0) const SizedBox(width: 8),
|
||||
_buildSortTab(logic, indexes[k], titles[indexes[k]], expand: !hasToggle),
|
||||
_buildSortTab(logic, indexes[k], titles[indexes[k]],
|
||||
expand: !hasToggle),
|
||||
],
|
||||
if (hasToggle) ...[
|
||||
const SizedBox(width: 8),
|
||||
@@ -94,7 +99,8 @@ class _DramaListPageState extends State<DramaListPage> with UniqueTagMixin {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSortTab(DramaListLogic logic, int index, String title, {bool expand = false}) {
|
||||
Widget _buildSortTab(DramaListLogic logic, int index, String title,
|
||||
{bool expand = false}) {
|
||||
final selected = index == logic.sortIndex;
|
||||
final tab = GestureDetector(
|
||||
onTap: () => logic.changeSort(index),
|
||||
@@ -135,7 +141,8 @@ class _DramaListPageState extends State<DramaListPage> with UniqueTagMixin {
|
||||
child: AnimatedRotation(
|
||||
turns: _sortExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: Image.asset('chevron_down.webp'.commonImgPath, width: 16, height: 16),
|
||||
child: Image.asset('chevron_down.webp'.commonImgPath,
|
||||
width: 16, height: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -143,7 +150,8 @@ class _DramaListPageState extends State<DramaListPage> with UniqueTagMixin {
|
||||
|
||||
Widget _buildBody(DramaListLogic logic) {
|
||||
if (logic.isLoading) return const LoadingCenterWidget();
|
||||
if (logic.isEmptyData) return CErrorWidget(retryOnTap: () => logic.loadData());
|
||||
if (logic.isEmptyData)
|
||||
return CErrorWidget(retryOnTap: () => logic.loadData());
|
||||
final list = logic.dataList!;
|
||||
return pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshCtr = ctr,
|
||||
@@ -159,7 +167,7 @@ class _DramaListPageState extends State<DramaListPage> with UniqueTagMixin {
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
final model = list[index];
|
||||
final model = list[index];
|
||||
return DramaCoverCard(mediaInfo: model);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:hgmj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgmj/hj_utils/const.dart';
|
||||
import 'package:hgmj/tools_base/base_list_controller.dart';
|
||||
import 'package:wydj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:wydj/hj_utils/const.dart';
|
||||
import 'package:wydj/tools_base/base_list_controller.dart';
|
||||
|
||||
import '../../hj_model/drama_media_info.dart';
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/hj_model/video_model.dart';
|
||||
import 'package:hgmj/hj_page/drama/drama_tag_detail_logic.dart';
|
||||
import 'package:hgmj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:hgmj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:hgmj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:wydj/hj_model/video_model.dart';
|
||||
import 'package:wydj/hj_page/drama/drama_tag_detail_logic.dart';
|
||||
import 'package:wydj/tools_base/loading/loading_center_widget.dart';
|
||||
import 'package:wydj/tools_base/refresh/pull_refresh.dart';
|
||||
import 'package:wydj/tools_base/unique_tag_mixin.dart';
|
||||
|
||||
import '../home/home_cell_style/drama_cover_card.dart';
|
||||
|
||||
@@ -20,13 +20,15 @@ class DramaTagDetailPage extends StatefulWidget {
|
||||
/// 进短剧标签页。跳转一律走它,别在调用方自己写 Get.to——不显式传 routeName 时
|
||||
/// GetX 会用闭包的 runtimeType 拼出 `/DramaTagDetailPage Function()`,埋点反查不到
|
||||
static Future<T?>? to<T>(TagsBean tag) =>
|
||||
Get.to(() => DramaTagDetailPage(tag: tag), routeName: routeName, preventDuplicates: true);
|
||||
Get.to(() => DramaTagDetailPage(tag: tag),
|
||||
routeName: routeName, preventDuplicates: true);
|
||||
|
||||
@override
|
||||
State<DramaTagDetailPage> createState() => _DramaTagDetailPageState();
|
||||
}
|
||||
|
||||
class _DramaTagDetailPageState extends State<DramaTagDetailPage> with UniqueTagMixin {
|
||||
class _DramaTagDetailPageState extends State<DramaTagDetailPage>
|
||||
with UniqueTagMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GetBuilder<DramaTagDetailLogic>(
|
||||
@@ -71,7 +73,9 @@ class _DramaTagDetailPageState extends State<DramaTagDetailPage> with UniqueTagM
|
||||
titles[i],
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: i == logic.sortIndex ? Colors.white : const Color(0x59FFFFFF),
|
||||
color: i == logic.sortIndex
|
||||
? Colors.white
|
||||
: const Color(0x59FFFFFF),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -83,7 +87,8 @@ class _DramaTagDetailPageState extends State<DramaTagDetailPage> with UniqueTagM
|
||||
|
||||
Widget _buildBody(DramaTagDetailLogic logic) {
|
||||
if (logic.isLoading) return const LoadingCenterWidget();
|
||||
if (logic.isEmptyData) return CErrorWidget(retryOnTap: () => logic.loadData());
|
||||
if (logic.isEmptyData)
|
||||
return CErrorWidget(retryOnTap: () => logic.loadData());
|
||||
final list = logic.dataList!;
|
||||
return pullYsRefresh(
|
||||
onInit: (ctr) => logic.refreshCtr = ctr,
|
||||
@@ -99,7 +104,7 @@ class _DramaTagDetailPageState extends State<DramaTagDetailPage> with UniqueTagM
|
||||
),
|
||||
itemCount: list.length,
|
||||
itemBuilder: (context, index) {
|
||||
final model = list[index];
|
||||
final model = list[index];
|
||||
return DramaCoverCard(mediaInfo: model);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'dart:ui';
|
||||
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgmj/assets_tool/app_colors.dart';
|
||||
import 'package:hgmj/assets_tool/images.dart';
|
||||
import 'package:hgmj/hj_utils/screen.dart';
|
||||
import 'package:wydj/assets_tool/app_colors.dart';
|
||||
import 'package:wydj/assets_tool/images.dart';
|
||||
import 'package:wydj/hj_utils/screen.dart';
|
||||
|
||||
/// 短剧底部条:信息流「查看完整短剧」入口 / 二级页「观看完整剧集」选集入口 + 自动播放倒计时。
|
||||
/// 不跟左右菜单一起收:纯净模式(眼睛按钮)下也常驻,这是进整部剧的唯一入口
|
||||
@@ -55,7 +55,8 @@ class DramaBottomBar extends StatelessWidget {
|
||||
'${highlight ?? ''}$text',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
defaultStyle: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
defaultStyle:
|
||||
const TextStyle(color: Colors.white, fontSize: 13),
|
||||
patternList: [
|
||||
//highlight 是「5s后」这种自己拼的倒计时,不会有正则元字符
|
||||
if (highlight?.isNotEmpty == true)
|
||||
|
||||
@@ -2,12 +2,12 @@ import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/cupertino.dart' show CupertinoActivityIndicator;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/assets_tool/app_colors.dart';
|
||||
import 'package:hgmj/hj_model/drama_media_info.dart';
|
||||
import 'package:hgmj/hj_model/media_content.dart';
|
||||
import 'package:hgmj/hj_page/live/live_widget.dart';
|
||||
import 'package:hgmj/hj_utils/screen.dart';
|
||||
import 'package:hgmj/tools_base/widget/sheet_handle_bar.dart';
|
||||
import 'package:wydj/assets_tool/app_colors.dart';
|
||||
import 'package:wydj/hj_model/drama_media_info.dart';
|
||||
import 'package:wydj/hj_model/media_content.dart';
|
||||
import 'package:wydj/hj_page/live/live_widget.dart';
|
||||
import 'package:wydj/hj_utils/screen.dart';
|
||||
import 'package:wydj/tools_base/widget/sheet_handle_bar.dart';
|
||||
|
||||
/// 短剧选集面板:剧名 + 连载状态 + 分段(1-30/31-60…) + 集数宫格
|
||||
/// 选中某集后 Get.back(result: 该集下标)
|
||||
@@ -92,13 +92,19 @@ class _DramaEpisodeSheetState extends State<DramaEpisodeSheet> {
|
||||
widget.drama?.title ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: Colors.white.withValues(alpha: 0.9), fontSize: 16, fontWeight: FontWeight.w500),
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
// 连载中·全x集
|
||||
EasyRichText(
|
||||
'$_updateDesc·共$_total集',
|
||||
defaultStyle: TextStyle(color: Colors.white.withValues(alpha: 0.35), fontSize: 12, height: 1.5),
|
||||
defaultStyle: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.35),
|
||||
fontSize: 12,
|
||||
height: 1.5),
|
||||
patternList: [
|
||||
//updateDesc 是后端下发的、可能带正则元字符,必须开 hasSpecialCharacters 转义;
|
||||
//空串更要拦住——空 targetString 会编译成 (),在每个位置都命中
|
||||
@@ -139,7 +145,8 @@ class _DramaEpisodeSheetState extends State<DramaEpisodeSheet> {
|
||||
child: Text(
|
||||
'$from-$to',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: isSelected ? 0.9 : 0.35),
|
||||
color:
|
||||
Colors.white.withValues(alpha: isSelected ? 0.9 : 0.35),
|
||||
fontSize: 14,
|
||||
height: 1.43,
|
||||
),
|
||||
@@ -171,7 +178,8 @@ class _DramaEpisodeSheetState extends State<DramaEpisodeSheet> {
|
||||
Widget? _badgeOf(MediaContent model) {
|
||||
switch (model.accessType) {
|
||||
case 'coin':
|
||||
return _corner('金币', color: Color(0xffFFCC14),textColor: Color(0xff1F2029));
|
||||
return _corner('金币',
|
||||
color: Color(0xffFFCC14), textColor: Color(0xff1F2029));
|
||||
case 'bought':
|
||||
return _corner('已购');
|
||||
default: // free / card 及未知类型不挂角标
|
||||
@@ -195,7 +203,9 @@ class _DramaEpisodeSheetState extends State<DramaEpisodeSheet> {
|
||||
),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(text, style: TextStyle(color: textColor ?? Colors.white, fontSize: 10, height: 1.6)),
|
||||
child: Text(text,
|
||||
style: TextStyle(
|
||||
color: textColor ?? Colors.white, fontSize: 10, height: 1.6)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -205,8 +215,11 @@ class _DramaEpisodeSheetState extends State<DramaEpisodeSheet> {
|
||||
if (count <= 0) {
|
||||
return Center(
|
||||
child: _isLoading
|
||||
? const CupertinoActivityIndicator(color: AppColors.actionRed, radius: 10)
|
||||
: Text('暂无剧集', style: TextStyle(color: Colors.white.withValues(alpha: 0.35), fontSize: 12)),
|
||||
? const CupertinoActivityIndicator(
|
||||
color: AppColors.actionRed, radius: 10)
|
||||
: Text('暂无剧集',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.35), fontSize: 12)),
|
||||
);
|
||||
}
|
||||
return GridView.builder(
|
||||
@@ -238,7 +251,10 @@ class _DramaEpisodeSheetState extends State<DramaEpisodeSheet> {
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? const Color(0x0DF68804) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: isSelected ? AppColors.actionRed : Colors.white.withValues(alpha: 0.1)),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColors.actionRed
|
||||
: Colors.white.withValues(alpha: 0.1)),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
@@ -252,7 +268,9 @@ class _DramaEpisodeSheetState extends State<DramaEpisodeSheet> {
|
||||
Text(
|
||||
'${model.episodeNumber ?? index + 1}',
|
||||
style: TextStyle(
|
||||
color: isSelected ? AppColors.actionRed : Colors.white.withValues(alpha: 0.9),
|
||||
color: isSelected
|
||||
? AppColors.actionRed
|
||||
: Colors.white.withValues(alpha: 0.9),
|
||||
fontSize: isSelected ? 14 : 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/assets_tool/images.dart';
|
||||
import 'package:hgmj/hj_model/video_model.dart';
|
||||
import 'package:hgmj/hj_page/drama/drama_detail_page.dart';
|
||||
import 'package:hgmj/hj_page/drama/drama_tag_detail_page.dart';
|
||||
import 'package:hgmj/hj_page/short_video/view/media_action_menu.dart';
|
||||
import 'package:hgmj/hj_page/short_video/view/video_menu_kit.dart';
|
||||
import 'package:hgmj/hj_utils/const.dart';
|
||||
import 'package:hgmj/hj_utils/screen.dart';
|
||||
import 'package:wydj/assets_tool/images.dart';
|
||||
import 'package:wydj/hj_model/video_model.dart';
|
||||
import 'package:wydj/hj_page/drama/drama_detail_page.dart';
|
||||
import 'package:wydj/hj_page/drama/drama_tag_detail_page.dart';
|
||||
import 'package:wydj/hj_page/short_video/view/media_action_menu.dart';
|
||||
import 'package:wydj/hj_page/short_video/view/video_menu_kit.dart';
|
||||
import 'package:wydj/hj_utils/const.dart';
|
||||
import 'package:wydj/hj_utils/screen.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
import '../../../tools_base/widget/shrink_wrap.dart';
|
||||
@@ -94,7 +94,8 @@ class _DramaMenuState extends State<DramaMenu> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.playerCtr != null) CenterPlayPause(controller: widget.playerCtr!),
|
||||
if (widget.playerCtr != null)
|
||||
CenterPlayPause(controller: widget.playerCtr!),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -129,25 +130,31 @@ class _DramaMenuState extends State<DramaMenu> {
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
if (widget.showEntry) Image.asset('chevron_right.webp'.videoPath, width: 20, height: 20),
|
||||
if (widget.showEntry)
|
||||
Image.asset('chevron_right.webp'.videoPath, width: 20, height: 20),
|
||||
],
|
||||
);
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: gapValue),
|
||||
child: widget.showEntry ? InkWell(enableFeedback: false, onTap: _openDrama, child: content) : content,
|
||||
child: widget.showEntry
|
||||
? InkWell(enableFeedback: false, onTap: _openDrama, child: content)
|
||||
: content,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTagItem() {
|
||||
final episodeText = Text(
|
||||
"第${_video?.episodeNo ?? 1}集",
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(
|
||||
color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500),
|
||||
);
|
||||
if (_video?.tags?.isNotEmpty != true) {
|
||||
return Container(padding: EdgeInsets.only(top: gapValue), child: episodeText);
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: gapValue), child: episodeText);
|
||||
}
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: gapValue),
|
||||
@@ -181,7 +188,9 @@ class _DramaMenuState extends State<DramaMenu> {
|
||||
color: const Color(0x0DFFFFFF),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(e.name ?? '', style: const TextStyle(color: Color(0xffffffff), fontSize: 11)),
|
||||
child: Text(e.name ?? '',
|
||||
style: const TextStyle(
|
||||
color: Color(0xffffffff), fontSize: 11)),
|
||||
),
|
||||
))
|
||||
.toList() ??
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import 'package:easy_rich_text/easy_rich_text.dart';
|
||||
import 'package:flutter/cupertino.dart' show CupertinoActivityIndicator;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hgmj/alert/video/buy_vip_alert.dart';
|
||||
import 'package:hgmj/assets_tool/app_colors.dart';
|
||||
import 'package:hgmj/config/config.dart';
|
||||
import 'package:hgmj/hj_model/drama/drama_models.dart';
|
||||
import 'package:hgmj/hj_model/video_model.dart';
|
||||
import 'package:hgmj/hj_page/mine/mine_vip/pay_order_source.dart';
|
||||
import 'package:hgmj/hj_page/mine/mine_vip/widgets/coin_pay_bottom_sheet.dart';
|
||||
import 'package:hgmj/hj_utils/pay/pay_manager.dart';
|
||||
import 'package:hgmj/hj_utils/screen.dart';
|
||||
import 'package:hgmj/tools_base/global_store/store.dart';
|
||||
import 'package:hgmj/tools_base/net/base_resp_bean.dart';
|
||||
import 'package:hgmj/tools_base/net/net_code.dart';
|
||||
import 'package:wydj/alert/video/buy_vip_alert.dart';
|
||||
import 'package:wydj/assets_tool/app_colors.dart';
|
||||
import 'package:wydj/config/config.dart';
|
||||
import 'package:wydj/hj_model/drama/drama_models.dart';
|
||||
import 'package:wydj/hj_model/video_model.dart';
|
||||
import 'package:wydj/hj_page/mine/mine_vip/pay_order_source.dart';
|
||||
import 'package:wydj/hj_page/mine/mine_vip/widgets/coin_pay_bottom_sheet.dart';
|
||||
import 'package:wydj/hj_utils/pay/pay_manager.dart';
|
||||
import 'package:wydj/hj_utils/screen.dart';
|
||||
import 'package:wydj/tools_base/global_store/store.dart';
|
||||
import 'package:wydj/tools_base/net/base_resp_bean.dart';
|
||||
import 'package:wydj/tools_base/net/net_code.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
/// 短剧付费墙:只有「金币解锁」和「开通短剧卡」两个按钮,没有第三种选择。
|
||||
@@ -103,7 +103,8 @@ class _DramaPaywallViewState extends State<DramaPaywallView> {
|
||||
if (!mounted) return;
|
||||
}
|
||||
if (_balance < need) {
|
||||
await CoinPayBottomSheet.show(sourcePage: PaySourcePage.dramaPaywall, orderTrack: _orderTrack);
|
||||
await CoinPayBottomSheet.show(
|
||||
sourcePage: PaySourcePage.dramaPaywall, orderTrack: _orderTrack);
|
||||
await globalStore.refreshWallet();
|
||||
if (!mounted || _balance < need) return; // 没充成功,停在付费墙
|
||||
}
|
||||
@@ -149,9 +150,11 @@ class _DramaPaywallViewState extends State<DramaPaywallView> {
|
||||
}
|
||||
|
||||
/// 解锁失败:只处理余额不足,其余保留 _requestId,重试复用同一个键别重复扣费
|
||||
Future<void> _onUnlockFailed(BaseRespBean? resp, {required bool allowRetry}) async {
|
||||
Future<void> _onUnlockFailed(BaseRespBean? resp,
|
||||
{required bool allowRetry}) async {
|
||||
if (resp?.code != Code.NOT_ENOUGH_MONEY) return;
|
||||
await CoinPayBottomSheet.show(sourcePage: PaySourcePage.dramaPaywall, orderTrack: _orderTrack);
|
||||
await CoinPayBottomSheet.show(
|
||||
sourcePage: PaySourcePage.dramaPaywall, orderTrack: _orderTrack);
|
||||
_requestId = null; // 充值后是新的一次解锁,换新键;沿用旧键会拿回刚才那次「余额不足」
|
||||
await globalStore.refreshWallet();
|
||||
if (!mounted) return;
|
||||
@@ -167,7 +170,8 @@ class _DramaPaywallViewState extends State<DramaPaywallView> {
|
||||
if (_busy) return;
|
||||
_busy = true;
|
||||
try {
|
||||
await BuyVipAlert.show(vipId: Config.shortDramaCardId, orderTrack: _orderTrack);
|
||||
await BuyVipAlert.show(
|
||||
vipId: Config.shortDramaCardId, orderTrack: _orderTrack);
|
||||
if (!mounted) return;
|
||||
setState(() => _unlocking = true);
|
||||
await widget.onUnlocked(byCard: true).whenComplete(_stopUnlocking);
|
||||
@@ -190,48 +194,53 @@ class _DramaPaywallViewState extends State<DramaPaywallView> {
|
||||
color: const Color.fromRGBO(0, 7, 18, 0.8),
|
||||
//已经付过钱了,别再让人对着解锁按钮干等:这层遮罩留着盖住还没建好的播放器
|
||||
child: _unlocking
|
||||
? const CupertinoActivityIndicator(color: AppColors.actionRed, radius: 15)
|
||||
? const CupertinoActivityIndicator(
|
||||
color: AppColors.actionRed, radius: 15)
|
||||
: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
paywall?.titleUI ?? '更多精彩解锁即享',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
EasyRichText(
|
||||
'剩余金币数:$_coin',
|
||||
defaultStyle: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
patternList: [
|
||||
//纯数字,前缀里没有数字所以不会误命中
|
||||
EasyRichTextPattern(
|
||||
targetString: _coin,
|
||||
matchOption: 'first',
|
||||
style: const TextStyle(color: AppColors.actionRed),
|
||||
),
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_btn(
|
||||
title: paywall?.coinButtonUI ?? '金币解锁',
|
||||
colors: const [Color(0xffFFE8BE), Color(0xffE6B764)],
|
||||
textColor: const Color(0xff694923),
|
||||
onTap: _onCoinUnlock,
|
||||
),
|
||||
18.sizeBoxW,
|
||||
_btn(
|
||||
title: paywall?.cardButtonUI ?? '开通短剧卡免费看',
|
||||
colors: const [Color(0xffFF6E6E), Color(0xffFF4D4D)],
|
||||
textColor: Colors.white,
|
||||
onTap: _onCardTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
paywall?.titleUI ?? '更多精彩解锁即享',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
18.sizeBoxH,
|
||||
EasyRichText(
|
||||
'剩余金币数:$_coin',
|
||||
defaultStyle:
|
||||
const TextStyle(color: Colors.white, fontSize: 14),
|
||||
patternList: [
|
||||
//纯数字,前缀里没有数字所以不会误命中
|
||||
EasyRichTextPattern(
|
||||
targetString: _coin,
|
||||
matchOption: 'first',
|
||||
style: const TextStyle(color: AppColors.actionRed),
|
||||
),
|
||||
],
|
||||
),
|
||||
18.sizeBoxH,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_btn(
|
||||
title: paywall?.coinButtonUI ?? '金币解锁',
|
||||
colors: const [Color(0xffFFE8BE), Color(0xffE6B764)],
|
||||
textColor: const Color(0xff694923),
|
||||
onTap: _onCoinUnlock,
|
||||
),
|
||||
18.sizeBoxW,
|
||||
_btn(
|
||||
title: paywall?.cardButtonUI ?? '开通短剧卡免费看',
|
||||
colors: const [Color(0xffFF6E6E), Color(0xffFF4D4D)],
|
||||
textColor: Colors.white,
|
||||
onTap: _onCardTap,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:flutter/material.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_utils/screen.dart';
|
||||
import 'package:wydj/assets_tool/app_colors.dart';
|
||||
import 'package:wydj/assets_tool/images.dart';
|
||||
import 'package:wydj/hj_utils/screen.dart';
|
||||
|
||||
/// 短剧倍速弹窗:选中档主题色 + 右侧对勾,点击直接 Get.back(倍速)
|
||||
class DramaSpeedSheet extends StatelessWidget {
|
||||
@@ -16,7 +16,8 @@ class DramaSpeedSheet extends StatelessWidget {
|
||||
|
||||
const DramaSpeedSheet({super.key, this.speed = normalSpeed});
|
||||
|
||||
static Future<double?> show({double speed = normalSpeed}) => Get.bottomSheet<double>(
|
||||
static Future<double?> show({double speed = normalSpeed}) =>
|
||||
Get.bottomSheet<double>(
|
||||
DramaSpeedSheet(speed: speed),
|
||||
backgroundColor: Colors.transparent, // 圆角由内部 Container 画,别让默认底色露出直角
|
||||
);
|
||||
@@ -28,14 +29,16 @@ class DramaSpeedSheet extends StatelessWidget {
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
//底部 6 + 末档自带的 12 = 设计稿的 18
|
||||
padding: EdgeInsets.fromLTRB(16, 18, 16, 6 + Get.mediaQuery.padding.bottom),
|
||||
padding:
|
||||
EdgeInsets.fromLTRB(16, 18, 16, 6 + Get.mediaQuery.padding.bottom),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.primaryColor,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // 分割线要全宽,默认 center 会把它挤成 0
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.stretch, // 分割线要全宽,默认 center 会把它挤成 0
|
||||
children: [
|
||||
//标题行:左侧收起箭头 + 标题在剩余空间居中
|
||||
SizedBox(
|
||||
@@ -44,7 +47,8 @@ class DramaSpeedSheet extends StatelessWidget {
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Get.back(),
|
||||
child: Image.asset('chevron_down.webp'.commonImgPath, width: 24, height: 24),
|
||||
child: Image.asset('chevron_down.webp'.commonImgPath,
|
||||
width: 24, height: 24),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
@@ -89,12 +93,15 @@ class DramaSpeedSheet extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
height: 22 / 16,
|
||||
color: isSelected ? AppColors.actionRed : Colors.white.withValues(alpha: .9),
|
||||
color: isSelected
|
||||
? AppColors.actionRed
|
||||
: Colors.white.withValues(alpha: .9),
|
||||
fontWeight: isSelected ? FontWeight.w500 : FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isSelected) Image.asset('check.webp'.commonImgPath, width: 24, height: 24),
|
||||
if (isSelected)
|
||||
Image.asset('check.webp'.commonImgPath, width: 24, height: 24),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import 'package:flutter/cupertino.dart' show CupertinoActivityIndicator;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hgmj/assets_tool/app_colors.dart';
|
||||
import 'package:hgmj/hj_model/video_model.dart';
|
||||
import 'package:hgmj/hj_page/drama/drama_detail_page.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_bottom_bar.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_menu.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_paywall_view.dart';
|
||||
import 'package:hgmj/hj_page/drama/view/drama_video_player_logic.dart';
|
||||
import 'package:hgmj/hj_page/short_video/view/video_menu_kit.dart';
|
||||
import 'package:hgmj/hj_page/short_video/view/video_progress_widget.dart';
|
||||
import 'package:hgmj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:hgmj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:hgmj/tools_base/widget/like_burst_view.dart';
|
||||
import 'package:wydj/assets_tool/app_colors.dart';
|
||||
import 'package:wydj/hj_model/video_model.dart';
|
||||
import 'package:wydj/hj_page/drama/drama_detail_page.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_bottom_bar.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_menu.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_paywall_view.dart';
|
||||
import 'package:wydj/hj_page/drama/view/drama_video_player_logic.dart';
|
||||
import 'package:wydj/hj_page/short_video/view/video_menu_kit.dart';
|
||||
import 'package:wydj/hj_page/short_video/view/video_progress_widget.dart';
|
||||
import 'package:wydj/tools_base/unique_tag_mixin.dart';
|
||||
import 'package:wydj/tools_base/widget/net_image_widget.dart';
|
||||
import 'package:wydj/tools_base/widget/like_burst_view.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
@@ -51,7 +51,8 @@ class DramaVideoPlayer extends StatefulWidget {
|
||||
}
|
||||
|
||||
// PageView 里 2~3 条同时存活,用 per-实例唯一 tag 隔离各自的播放器 logic
|
||||
class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin {
|
||||
class _DramaVideoPlayerState extends State<DramaVideoPlayer>
|
||||
with UniqueTagMixin {
|
||||
//进度条压暗档位:滑动中 / 挂着付费墙都用这一档
|
||||
static const _dimOpacity = 0.4;
|
||||
|
||||
@@ -69,7 +70,8 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
startSeconds: widget.startSeconds,
|
||||
onCardUnlocked: widget.onCardUnlocked,
|
||||
//Feed 里这一集播完 = 进入连播,直接把用户带进二级页;二级页自己传 onCompleted 切下一集
|
||||
onCompleted: widget.onCompleted ?? (widget.showEntry ? _onFeedCompleted : null),
|
||||
onCompleted:
|
||||
widget.onCompleted ?? (widget.showEntry ? _onFeedCompleted : null),
|
||||
onRemaining: widget.onRemaining,
|
||||
);
|
||||
|
||||
@@ -78,7 +80,8 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
|
||||
//信息栏要让开下方这一摞,再留设计稿的 18:
|
||||
//进度条本体 28 + 进度条自己往下让的距离(二级页= 剧集条48 + 底部安全区) + Feed 才有的剧集条 48
|
||||
double get _menuBottom => 28 + (widget.progressBottom ?? 0) + (widget.showEntry ? 48 : 0) + 18;
|
||||
double get _menuBottom =>
|
||||
28 + (widget.progressBottom ?? 0) + (widget.showEntry ? 48 : 0) + 18;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant DramaVideoPlayer oldWidget) {
|
||||
@@ -156,7 +159,8 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
Positioned.fill(
|
||||
child: Visibility(
|
||||
visible: logic.showPaywall,
|
||||
child: DramaPaywallView(video: logic.video, onUnlocked: logic.onUnlocked),
|
||||
child: DramaPaywallView(
|
||||
video: logic.video, onUnlocked: logic.onUnlocked),
|
||||
),
|
||||
),
|
||||
LikeBurstView(controller: _likeBurst),
|
||||
@@ -172,7 +176,8 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
if (widget.showEntry)
|
||||
DramaBottomBar(
|
||||
leadingIcon: 'full_drama.webp',
|
||||
text: '查看完整短剧·全${logic.video?.dramaInfo?.totalEpisode ?? 0}集',
|
||||
text:
|
||||
'查看完整短剧·全${logic.video?.dramaInfo?.totalEpisode ?? 0}集',
|
||||
tailIcon: 'chevron_right.webp',
|
||||
onTap: _openDrama,
|
||||
),
|
||||
@@ -205,7 +210,8 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
() => DramaDetailPage(
|
||||
drama: widget.videoInfo?.dramaInfo,
|
||||
initialIndex: index < 0 ? 0 : index,
|
||||
initialSeconds: startNext ? 0 : (_logic.playerCtr?.value.position.inSeconds ?? 0),
|
||||
initialSeconds:
|
||||
startNext ? 0 : (_logic.playerCtr?.value.position.inSeconds ?? 0),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -223,7 +229,8 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
final (minSeek, maxSeek) = logic.previewRange;
|
||||
final Widget bar = VideoProgressWidget(
|
||||
controller: logic.playerCtr,
|
||||
padding: EdgeInsets.only(left: 12, right: 12, bottom: widget.progressBottom ?? 0),
|
||||
padding: EdgeInsets.only(
|
||||
left: 12, right: 12, bottom: widget.progressBottom ?? 0),
|
||||
//试看放的是完整视频,可拖区间夹在 [previewStart, previewStart+previewSeconds];挂墙后整条禁拖。
|
||||
//与长视频页同一套(VideoMenuView 的 minSeek/maxSeek + enableSeek)
|
||||
minSeekDuration: minSeek,
|
||||
@@ -231,7 +238,8 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
enableSeek: !logic.showPaywall,
|
||||
);
|
||||
final scrolling = widget.isScrolling;
|
||||
if (scrolling == null) return Opacity(opacity: logic.showPaywall ? _dimOpacity : 1, child: bar);
|
||||
if (scrolling == null)
|
||||
return Opacity(opacity: logic.showPaywall ? _dimOpacity : 1, child: bar);
|
||||
return ValueListenableBuilder<bool>(
|
||||
valueListenable: scrolling,
|
||||
//只做透明+屏蔽点击,不能用 Visibility——它会卸载 child,重建时 initState 会拿旧 controller 挂监听。
|
||||
@@ -251,12 +259,16 @@ class _DramaVideoPlayerState extends State<DramaVideoPlayer> with UniqueTagMixin
|
||||
/// 未就绪/缓冲中的居中转圈。挂着付费墙时不转——那不是在加载,是没权限
|
||||
Widget _bufferLoading(DramaVideoPlayerLogic logic) {
|
||||
if (logic.showPaywall) return const SizedBox();
|
||||
const indicator = Center(child: CupertinoActivityIndicator(color: AppColors.actionRed, radius: 15));
|
||||
const indicator = Center(
|
||||
child:
|
||||
CupertinoActivityIndicator(color: AppColors.actionRed, radius: 15));
|
||||
final ctr = logic.playerCtr;
|
||||
if (ctr == null) return indicator; // 控制器还没建好,同样转圈
|
||||
return ValueListenableBuilder<VideoPlayerValue>(
|
||||
valueListenable: ctr,
|
||||
builder: (_, value, __) => !value.isInitialized || value.isBuffering ? indicator : const SizedBox(),
|
||||
builder: (_, value, __) => !value.isInitialized || value.isBuffering
|
||||
? indicator
|
||||
: const SizedBox(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:hgmj/hj_model/media_content.dart';
|
||||
import 'package:hgmj/hj_model/video_model.dart';
|
||||
import 'package:hgmj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:hgmj/hj_page/short_video/view/video_player_base_logic.dart';
|
||||
import 'package:hgmj/hj_utils/api_service/common_service.dart';
|
||||
import 'package:hgmj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:hgmj/tools_base/global_store/store.dart';
|
||||
import 'package:hgmj/tools_base/toast.dart';
|
||||
import 'package:wydj/hj_model/media_content.dart';
|
||||
import 'package:wydj/hj_model/video_model.dart';
|
||||
import 'package:wydj/tools_base/cache/history/drama_resume_store.dart';
|
||||
import 'package:wydj/hj_page/short_video/view/video_player_base_logic.dart';
|
||||
import 'package:wydj/hj_utils/api_service/common_service.dart';
|
||||
import 'package:wydj/hj_utils/api_service/drama_service.dart';
|
||||
import 'package:wydj/tools_base/global_store/store.dart';
|
||||
import 'package:wydj/tools_base/toast.dart';
|
||||
import 'package:visibility_detector/visibility_detector.dart';
|
||||
|
||||
/// 单集短剧的播放逻辑,PageView 每项一个实例。
|
||||
/// 能不能播只认服务端 canPlay,拦不住就挂付费墙;续播位置纯本地记
|
||||
class DramaVideoPlayerLogic extends VideoPlayerBaseLogic with WidgetsBindingObserver {
|
||||
class DramaVideoPlayerLogic extends VideoPlayerBaseLogic
|
||||
with WidgetsBindingObserver {
|
||||
/// 开卡放行广播:PageView 预建的上下两条已各自问过 canPlay,不叫醒就还挂着买卡前那道墙
|
||||
static final ValueNotifier<int> unlockRevision = ValueNotifier(0);
|
||||
|
||||
@@ -128,7 +129,8 @@ class DramaVideoPlayerLogic extends VideoPlayerBaseLogic with WidgetsBindingObse
|
||||
showPaywall = false;
|
||||
notify();
|
||||
}
|
||||
playerCtr?.seekTo(Duration(seconds: _isPreview ? _previewStart : 0)); //试看重看要回到起播点,不是 0
|
||||
playerCtr?.seekTo(
|
||||
Duration(seconds: _isPreview ? _previewStart : 0)); //试看重看要回到起播点,不是 0
|
||||
syncPlay();
|
||||
_markWatched(); // 滑到这条才算看过:onInit 预建时 isCurrent 还是 false
|
||||
}
|
||||
@@ -165,11 +167,13 @@ class DramaVideoPlayerLogic extends VideoPlayerBaseLogic with WidgetsBindingObse
|
||||
int get _previewStart => videoInfo?.dramaEpisode?.previewStart ?? 0;
|
||||
|
||||
//试看看到第几秒为止:起播点 + 时长,previewSeconds 是时长不是绝对位置
|
||||
int get _previewEnd => _previewStart + (videoInfo?.dramaEpisode?.previewSeconds ?? 0);
|
||||
int get _previewEnd =>
|
||||
_previewStart + (videoInfo?.dramaEpisode?.previewSeconds ?? 0);
|
||||
|
||||
/// 试看可拖区间 [起, 止],不在试看态时 (null, null)。同长视频页 VideoMenuView._calcSeekRange
|
||||
(Duration?, Duration?) get previewRange =>
|
||||
_isPreview ? (Duration(seconds: _previewStart), Duration(seconds: _previewEnd)) : (null, null);
|
||||
(Duration?, Duration?) get previewRange => _isPreview
|
||||
? (Duration(seconds: _previewStart), Duration(seconds: _previewEnd))
|
||||
: (null, null);
|
||||
|
||||
/// 没解锁 + 开关开 + 有地址 + 有时长,刷剧和连播两个页面一视同仁。
|
||||
/// 秒数必须 >0:试看地址给的是完整视频、后端不裁片,配成 0 就是整集免费送
|
||||
@@ -218,8 +222,10 @@ class DramaVideoPlayerLogic extends VideoPlayerBaseLogic with WidgetsBindingObse
|
||||
episode.previewEnabled ??= prev?.previewEnabled;
|
||||
episode.previewStart ??= prev?.previewStart;
|
||||
episode.previewSeconds ??= prev?.previewSeconds;
|
||||
if (episode.previewVideoUrl?.isNotEmpty != true) episode.previewVideoUrl = prev?.previewVideoUrl;
|
||||
if (episode.previewH265Url?.isNotEmpty != true) episode.previewH265Url = prev?.previewH265Url;
|
||||
if (episode.previewVideoUrl?.isNotEmpty != true)
|
||||
episode.previewVideoUrl = prev?.previewVideoUrl;
|
||||
if (episode.previewH265Url?.isNotEmpty != true)
|
||||
episode.previewH265Url = prev?.previewH265Url;
|
||||
final usePreview = _canPreview(episode);
|
||||
target
|
||||
?..dramaEpisode = episode
|
||||
@@ -246,10 +252,13 @@ class DramaVideoPlayerLogic extends VideoPlayerBaseLogic with WidgetsBindingObse
|
||||
//从刷剧页点进来要接着那边的秒数往下放,试看片同样接着放。
|
||||
//试看不认本地续播记录(记的是正片进度,一跳就越过试看区间),没带位置就从后端给的 previewStart 起
|
||||
final last = isSerial && !usePreview ? dramaResume.of(_mediaId) : null;
|
||||
final resume = startSeconds > 0 ? startSeconds : (last?.contentId == episode.id ? last!.progressSeconds : 0);
|
||||
final resume = startSeconds > 0
|
||||
? startSeconds
|
||||
: (last?.contentId == episode.id ? last!.progressSeconds : 0);
|
||||
final seconds = usePreview && resume <= 0 ? _previewStart : resume;
|
||||
startSeconds = 0;
|
||||
if (seconds > 0 && !isClosed) await playerCtr?.seekTo(Duration(seconds: seconds));
|
||||
if (seconds > 0 && !isClosed)
|
||||
await playerCtr?.seekTo(Duration(seconds: seconds));
|
||||
if (isSerial) _resumeDone = !usePreview; // 试看那段不落进度,正片才从这一刻起作数
|
||||
if (isClosed) return;
|
||||
//位置定好才撤墙,中间不露转圈。播放器建失败也照撤——
|
||||
@@ -268,7 +277,8 @@ class DramaVideoPlayerLogic extends VideoPlayerBaseLogic with WidgetsBindingObse
|
||||
}
|
||||
|
||||
void _applySpeed() {
|
||||
if (playerCtr?.value.isInitialized == true) playerCtr?.setPlaybackSpeed(speed);
|
||||
if (playerCtr?.value.isInitialized == true)
|
||||
playerCtr?.setPlaybackSpeed(speed);
|
||||
}
|
||||
|
||||
/// 长按 2 倍抬手后回到用户选的档位,不是基类写死的 1.0
|
||||
@@ -357,7 +367,9 @@ class DramaVideoPlayerLogic extends VideoPlayerBaseLogic with WidgetsBindingObse
|
||||
final ctr = playerCtr;
|
||||
if (seconds == null && ctr?.value.isInitialized != true) return;
|
||||
final now = DateTime.now();
|
||||
if (throttle && _savedAt != null && now.difference(_savedAt!).inSeconds < 10) return;
|
||||
if (throttle &&
|
||||
_savedAt != null &&
|
||||
now.difference(_savedAt!).inSeconds < 10) return;
|
||||
_savedAt = now;
|
||||
dramaResume.record(
|
||||
mediaId: _mediaId,
|
||||
|
||||
Reference in New Issue
Block a user