From 77153c153a11174c01c3e9417ca3ebab302503aa Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 12 Jul 2026 08:43:11 +0800 Subject: [PATCH] refa intro panel gesture Signed-off-by: dom --- lib/common/widgets/animated_height.dart | 6 +- lib/common/widgets/translucent_column.dart | 132 ++++ lib/models/common/avatar_badge_type.dart | 3 +- lib/pages/member/widget/user_info_card.dart | 19 +- lib/pages/video/introduction/pgc/view.dart | 16 +- lib/pages/video/introduction/ugc/view.dart | 665 ++++++++++---------- lib/utils/bili_colors.dart | 11 + lib/utils/extension/theme_ext.dart | 15 +- 8 files changed, 491 insertions(+), 376 deletions(-) create mode 100644 lib/common/widgets/translucent_column.dart create mode 100644 lib/utils/bili_colors.dart diff --git a/lib/common/widgets/animated_height.dart b/lib/common/widgets/animated_height.dart index 2688297cd..3e2c4ac85 100644 --- a/lib/common/widgets/animated_height.dart +++ b/lib/common/widgets/animated_height.dart @@ -180,15 +180,17 @@ class RenderAnimatedHeight extends RenderProxyBox { @override void performLayout() { + final BoxConstraints constraints = this.constraints; + if (_isInvisible) { _heights = const (from: 0, to: 0); - size = .zero; + child!.layout(constraints); + size = constraints.constrain(.zero); return; } _lastValue = _controller.value; - final BoxConstraints constraints = this.constraints; final childSize = (child!..layout(constraints, parentUsesSize: true)).size; final Size animatedSize; diff --git a/lib/common/widgets/translucent_column.dart b/lib/common/widgets/translucent_column.dart new file mode 100644 index 000000000..989f7d0b2 --- /dev/null +++ b/lib/common/widgets/translucent_column.dart @@ -0,0 +1,132 @@ +/* + * This file is part of PiliPlus + * + * PiliPlus is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * PiliPlus is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with PiliPlus. If not, see . + */ + +import 'package:PiliPlus/common/widgets/animated_height.dart' + show RenderAnimatedHeight; +import 'package:flutter/rendering.dart' + show RenderProxyBox, BoxHitTestResult, RenderFlex, FlexParentData; +import 'package:flutter/widgets.dart'; + +class TranslucentColumn extends Flex { + const TranslucentColumn({ + super.key, + super.mainAxisAlignment, + super.mainAxisSize, + super.crossAxisAlignment, + super.textDirection, + super.verticalDirection, + super.textBaseline, + super.spacing, + super.children, + }) : super(direction: Axis.vertical); + + @override + RenderTranslucentColumn createRenderObject(BuildContext context) { + return RenderTranslucentColumn( + direction: direction, + mainAxisAlignment: mainAxisAlignment, + mainAxisSize: mainAxisSize, + crossAxisAlignment: crossAxisAlignment, + textDirection: getEffectiveTextDirection(context), + verticalDirection: verticalDirection, + textBaseline: textBaseline, + clipBehavior: clipBehavior, + spacing: spacing, + ); + } + + @override + void updateRenderObject( + BuildContext context, + RenderTranslucentColumn renderObject, + ) { + renderObject + ..direction = direction + ..mainAxisAlignment = mainAxisAlignment + ..mainAxisSize = mainAxisSize + ..crossAxisAlignment = crossAxisAlignment + ..textDirection = getEffectiveTextDirection(context) + ..verticalDirection = verticalDirection + ..textBaseline = textBaseline + ..clipBehavior = clipBehavior + ..spacing = spacing; + } +} + +class RenderTranslucentColumn extends RenderFlex { + RenderTranslucentColumn({ + super.children, + super.direction, + super.mainAxisSize, + super.mainAxisAlignment, + super.crossAxisAlignment, + super.textDirection, + super.verticalDirection, + super.textBaseline, + super.clipBehavior, + super.spacing, + }); + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + RenderBox? child = lastChild; + final width = size.width; + while (child != null) { + final childParentData = child.parentData! as FlexParentData; + final bool isHit = result.addWithPaintOffset( + offset: childParentData.offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - childParentData.offset); + if (transformed.dx >= 0.0 && + transformed.dx < width && + transformed.dy >= 0.0 && + transformed.dy < child!.size.height) { + final hit = child.hitTest(result, position: transformed); + if (child is RenderAnimatedHeight) { + return hit; + } + if (hit) { + return true; + } + if (child is RenderNoTranslucentArea) { + return false; + } + return true; + } + return false; + }, + ); + if (isHit) { + return true; + } + child = childParentData.previousSibling; + } + return false; + } +} + +class NoTranslucentArea extends SingleChildRenderObjectWidget { + const NoTranslucentArea({super.key, required Widget super.child}); + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderNoTranslucentArea(); + } +} + +class RenderNoTranslucentArea extends RenderProxyBox {} diff --git a/lib/models/common/avatar_badge_type.dart b/lib/models/common/avatar_badge_type.dart index bb5432eb8..0d10e4014 100644 --- a/lib/models/common/avatar_badge_type.dart +++ b/lib/models/common/avatar_badge_type.dart @@ -1,9 +1,10 @@ +import 'package:PiliPlus/utils/bili_colors.dart'; import 'package:flutter/material.dart'; enum BadgeType { none(), vip('大会员'), - person('认证个人', Color(0xFFFFCC00)), + person('认证个人', BiliColors.yellow), institution('认证机构', Colors.lightBlueAccent), ; diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 9a887b204..cfc366cb2 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -24,6 +24,7 @@ import 'package:PiliPlus/pages/member_guard/view.dart'; import 'package:PiliPlus/pages/member_upower_rank/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:PiliPlus/utils/bili_colors.dart'; import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/color_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; @@ -278,13 +279,17 @@ class UserInfoCard extends StatelessWidget { shape: .circle, color: colorScheme.surface, ), - child: Icon( - Icons.offline_bolt, - color: card.officialVerify?.type == 0 - ? const Color(0xFFFFCC00) - : Colors.lightBlueAccent, - size: 18, - ), + child: card.officialVerify?.type == 0 + ? const Icon( + Icons.offline_bolt, + color: BiliColors.yellow, + size: 18, + ) + : const Icon( + Icons.offline_bolt, + color: Colors.lightBlueAccent, + size: 18, + ), ), ), const TextSpan(text: ' '), diff --git a/lib/pages/video/introduction/pgc/view.dart b/lib/pages/video/introduction/pgc/view.dart index 8d94dab63..e718ffe60 100644 --- a/lib/pages/video/introduction/pgc/view.dart +++ b/lib/pages/video/introduction/pgc/view.dart @@ -10,6 +10,7 @@ import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; +import 'package:PiliPlus/models_new/pgc/pgc_info_model/stat.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/widgets/pgc_panel.dart'; @@ -76,7 +77,7 @@ class _PgcIntroPageState extends State { ), const SizedBox(height: 6), // 点赞收藏转发 布局样式2 - if (introController.isPgc) actionGrid(item, introController), + if (introController.isPgc) actionGrid(item.stat!, introController), // 番剧分集 if (item.episodes?.isNotEmpty == true) PgcPanel( @@ -383,10 +384,7 @@ class _PgcIntroPageState extends State { ); } - Widget actionGrid( - PgcInfoModel item, - PgcIntroController introController, - ) { + Widget actionGrid(PgcStat stat, PgcIntroController introController) { return SizedBox( height: 48, child: Row( @@ -399,7 +397,7 @@ class _PgcIntroPageState extends State { selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectStatus: introController.hasLike.value, semanticsLabel: '点赞', - text: NumUtils.numFormat(item.stat!.like), + text: NumUtils.numFormat(stat.like), onStartTriple: introController.onStartTriple, onCancelTriple: introController.onCancelTriple, ), @@ -412,7 +410,7 @@ class _PgcIntroPageState extends State { onTap: introController.actionCoinVideo, selectStatus: introController.hasCoin, semanticsLabel: '投币', - text: NumUtils.numFormat(item.stat!.coin), + text: NumUtils.numFormat(stat.coin), ), ), Obx( @@ -427,7 +425,7 @@ class _PgcIntroPageState extends State { ), selectStatus: introController.hasFav.value, semanticsLabel: '收藏', - text: NumUtils.numFormat(item.stat!.favorite), + text: NumUtils.numFormat(stat.favorite), ), ), Obx( @@ -446,7 +444,7 @@ class _PgcIntroPageState extends State { onTap: () => introController.actionShareVideo(context), selectStatus: false, semanticsLabel: '转发', - text: NumUtils.numFormat(item.stat!.share), + text: NumUtils.numFormat(stat.share), ), ], ), diff --git a/lib/pages/video/introduction/ugc/view.dart b/lib/pages/video/introduction/ugc/view.dart index 8cf0a92aa..4dbf8ac53 100644 --- a/lib/pages/video/introduction/ugc/view.dart +++ b/lib/pages/video/introduction/ugc/view.dart @@ -7,15 +7,17 @@ import 'package:PiliPlus/common/widgets/expandable.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart' + show ReloadScrollPhysics; import 'package:PiliPlus/common/widgets/selectable_text.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; +import 'package:PiliPlus/common/widgets/translucent_column.dart'; import 'package:PiliPlus/http/sponsor_block.dart'; -import 'package:PiliPlus/models/common/image_type.dart'; -import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/video/video_ai_conclusion/model_result.dart'; import 'package:PiliPlus/models_new/video/video_detail/data.dart'; +import 'package:PiliPlus/models_new/video/video_detail/desc_v2.dart'; import 'package:PiliPlus/models_new/video/video_detail/staff.dart'; +import 'package:PiliPlus/models_new/video/video_detail/stat.dart'; import 'package:PiliPlus/models_new/video/video_tag/data.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; @@ -25,10 +27,10 @@ import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/page.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/season.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:PiliPlus/utils/bili_colors.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; @@ -67,6 +69,7 @@ class UgcIntroPanel extends StatefulWidget { } class _UgcIntroPanelState extends State { + late ColorScheme colorScheme; late final UgcIntroController introController; late final VideoDetailController videoDetailCtr = Get.find(tag: widget.heroTag); @@ -80,169 +83,71 @@ class _UgcIntroPanelState extends State { ); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + colorScheme = ColorScheme.of(context); + } + @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); final isPortrait = widget.isPortrait; final isHorizontal = !isPortrait && widget.isHorizontal; return SliverPadding( - padding: const EdgeInsets.only( + padding: const .only( left: Style.safeSpace, right: Style.safeSpace, top: 10, ), sliver: Obx( () { - VideoDetailData videoDetail = introController.videoDetail.value; - bool isLoading = videoDetail.bvid == null; - int? mid = videoDetail.owner?.mid; + final videoDetail = introController.videoDetail.value; + final isLoading = videoDetail.bvid == null; return SliverToBoxAdapter( child: GestureDetector( - behavior: HitTestBehavior.opaque, onTap: () { - if (isLoading) { - return; - } + if (isLoading) return; feedBack(); introController.expand.toggle(); }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: TranslucentColumn( + crossAxisAlignment: .start, children: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () {}, - child: Row( - children: [ - if (videoDetail.staff.isNullOrEmpty) ...[ - Expanded( - child: Align( - alignment: Alignment.centerLeft, - child: _buildAvatar( - theme, - () { - if (mid != null) { - feedBack(); - if (!isPortrait && - introController.horizontalMemberPage) { - widget.onShowMemberPage(mid); - } else { - Get.toNamed( - '/member?mid=$mid&from_view_aid=${videoDetailCtr.aid}', - ); - } - } - }, - ), - ), - ), - followButton(context, theme), - ] else - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - physics: ReloadScrollPhysics( - controller: introController, - ), - child: Row( - spacing: 25, - children: videoDetail.staff! - .map( - (e) => _buildStaff( - theme, - isPortrait, - mid, - e, - ), - ) - .toList(), - ), - ), - ), - if (isHorizontal) ...[ - const SizedBox(width: 10), - Expanded( - child: actionGrid( - context, - isLoading, - videoDetail, - introController, - ), - ), - ], - ], + NoTranslucentArea( + child: _buildOwnerInfo( + isLoading, + isPortrait, + isHorizontal, + videoDetail, ), ), const SizedBox(height: 8), - if (isLoading) - _buildVideoTitle(theme, videoDetail) - else if (isHorizontal && PlatformUtils.isDesktop) - SelectionArea( - child: _buildVideoTitle( - theme, - videoDetail, - isExpand: true, - ), - ) - else - Obx( - () => ExpandablePanel( - collapsed: _buildTitle(theme, videoDetail), - expanded: _buildTitle( - theme, - videoDetail, - isExpand: true, - ), - expand: introController.expand.value, - ), - ), + buildTitle(isLoading, isHorizontal, videoDetail), const SizedBox(height: 8), Stack( - clipBehavior: Clip.none, + clipBehavior: .none, children: [ - _buildInfo(theme, videoDetail), + _buildInfo(videoDetail.stat, videoDetail.pubdate), if (introController.enableAi) _aiBtn, ], ), - if (introController.showArgueMsg && - videoDetail.argueInfo?.argueMsg?.isNotEmpty == true) ...[ - const SizedBox(height: 2), - Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Padding( - padding: const .only(right: 2), - child: Icon( - size: 13, - Icons.error_outline, - color: theme.colorScheme.outline, - ), - ), - ), - TextSpan( - text: '${videoDetail.argueInfo!.argueMsg}', - ), - ], - ), - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.outline, - ), - ), - ], + if (introController.showArgueMsg) + if (videoDetail.argueInfo?.argueMsg case final argueMsg? + when argueMsg.isNotEmpty) ...[ + const SizedBox(height: 2), + _buildArgueInfo(argueMsg), + ], if (isHorizontal && PlatformUtils.isDesktop) - ..._infos(theme, videoDetail) + ..._infos(videoDetail) else Obx( () => AnimatedHeight( expand: introController.expand.value, duration: const Duration(milliseconds: 300), - child: Column( + child: TranslucentColumn( mainAxisSize: .min, crossAxisAlignment: .start, - children: _infos(theme, videoDetail), + children: _infos(videoDetail), ), ), ), @@ -271,8 +176,8 @@ class _UgcIntroPanelState extends State { actionGrid( context, isLoading, - videoDetail, introController, + videoDetail.stat, ), ], // 合集 @@ -315,8 +220,50 @@ class _UgcIntroPanelState extends State { ); } - Widget _buildTitle( - ThemeData theme, + Widget _buildArgueInfo(String argueMsg) { + return Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: .middle, + child: Padding( + padding: const .only(right: 2), + child: Icon( + size: 13, + Icons.error_outline, + color: colorScheme.outline, + ), + ), + ), + TextSpan(text: argueMsg), + ], + ), + style: TextStyle(fontSize: 12, color: colorScheme.outline), + ); + } + + Widget buildTitle( + bool isLoading, + bool isHorizontal, + VideoDetailData videoDetail, + ) { + if (isLoading) { + return _buildVideoTitle(videoDetail); + } else if (isHorizontal && PlatformUtils.isDesktop) { + return SelectionArea( + child: _buildVideoTitle(videoDetail, isExpand: true), + ); + } + return Obx( + () => ExpandablePanel( + collapsed: _expandableTitle(videoDetail), + expanded: _expandableTitle(videoDetail, isExpand: true), + expand: introController.expand.value, + ), + ); + } + + Widget _expandableTitle( VideoDetailData videoDetail, { bool isExpand = false, }) => GestureDetector( @@ -324,49 +271,41 @@ class _UgcIntroPanelState extends State { Feedback.forLongPress(context); Utils.copyText(videoDetail.title ?? ''); }, - child: _buildVideoTitle( - theme, - videoDetail, - isExpand: isExpand, - ), + child: _buildVideoTitle(videoDetail, isExpand: isExpand), ); - List _infos(ThemeData theme, VideoDetailData videoDetail) => [ + List _infos(VideoDetailData videoDetail) => [ const SizedBox(height: 8), GestureDetector( onTap: () => Utils.copyText('${videoDetail.bvid}'), child: Text( videoDetail.bvid ?? '', - style: TextStyle( - fontSize: 14, - color: theme.colorScheme.secondary, - ), + style: TextStyle(fontSize: 14, color: colorScheme.secondary), ), ), - if (videoDetail.descV2?.isNotEmpty == true) ...[ + if (videoDetail.descV2 case final descV2? when descV2.isNotEmpty) ...[ const SizedBox(height: 8), selectableRichText( style: const TextStyle(height: 1.4), - buildContent(theme, videoDetail), + buildContent(descV2), ), ], - Obx(() { - final videoTags = introController.videoTags.value; - if (videoTags == null || videoTags.isEmpty) { - return const SizedBox.shrink(); - } - return _buildTags(videoTags); - }), + NoTranslucentArea( + child: Obx(() { + final videoTags = introController.videoTags.value; + if (videoTags == null || videoTags.isEmpty) { + return const SizedBox.shrink(); + } + return _buildTags(videoTags); + }), + ), ]; WidgetSpan _labelWidget(String text, Color bgColor, Color textColor) { return WidgetSpan( - alignment: PlaceholderAlignment.middle, + alignment: .middle, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 3, - ), + padding: const .symmetric(horizontal: 4, vertical: 3), decoration: BoxDecoration( color: bgColor, borderRadius: const BorderRadius.all(Radius.circular(4)), @@ -391,11 +330,10 @@ class _UgcIntroPanelState extends State { } Widget _buildVideoTitle( - ThemeData theme, VideoDetailData videoDetail, { bool isExpand = false, }) { - late final isDark = theme.brightness == Brightness.dark; + late final isDark = colorScheme.isDark; Widget child() { final videoLabel = videoDetailCtr.videoLabel.value; return Text.rich( @@ -403,32 +341,29 @@ class _UgcIntroPanelState extends State { children: [ if (videoLabel.isNotEmpty) ...[ WidgetSpan( - alignment: PlaceholderAlignment.middle, + alignment: .middle, child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 4, - vertical: 2, - ), + padding: const .symmetric(horizontal: 4, vertical: 2), decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, + color: colorScheme.secondaryContainer, borderRadius: const BorderRadius.all(Radius.circular(4)), ), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, + clipBehavior: .none, + alignment: .center, children: [ Icon( Icons.shield_outlined, size: 16, - color: theme.colorScheme.onSecondaryContainer, + color: colorScheme.onSecondaryContainer, ), Icon( Icons.play_arrow_rounded, size: 12, - color: theme.colorScheme.onSecondaryContainer, + color: colorScheme.onSecondaryContainer, ), ], ), @@ -443,7 +378,7 @@ class _UgcIntroPanelState extends State { style: TextStyle( height: 1, fontSize: 13, - color: theme.colorScheme.onSecondaryContainer, + color: colorScheme.onSecondaryContainer, ), ), ], @@ -455,19 +390,15 @@ class _UgcIntroPanelState extends State { if (videoDetail.isUpowerExclusive == true) ...[ _labelWidget( '充电专属', - isDark - ? theme.colorScheme.error - : theme.colorScheme.errorContainer, - isDark - ? theme.colorScheme.onError - : theme.colorScheme.onErrorContainer, + isDark ? colorScheme.error : colorScheme.errorContainer, + isDark ? colorScheme.onError : colorScheme.onErrorContainer, ), const TextSpan(text: ' '), ] else if (videoDetail.rights?.isSteinGate == 1) ...[ _labelWidget( '互动视频', - theme.colorScheme.secondaryContainer, - theme.colorScheme.onSecondaryContainer, + colorScheme.secondaryContainer, + colorScheme.onSecondaryContainer, ), const TextSpan(text: ' '), ], @@ -475,7 +406,7 @@ class _UgcIntroPanelState extends State { ], ), maxLines: isExpand ? null : 2, - overflow: isExpand ? null : TextOverflow.ellipsis, + overflow: isExpand ? null : .ellipsis, style: const TextStyle(fontSize: 16), ); } @@ -486,21 +417,21 @@ class _UgcIntroPanelState extends State { return child(); } - Widget followButton(BuildContext context, ThemeData t) { + Widget followButton(BuildContext context) { return Obx( () { int attr = introController.followStatus.value.attribute ?? 0; return TextButton( onPressed: () => introController.actionRelationMod(context), style: TextButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, + tapTargetSize: .shrinkWrap, visualDensity: const VisualDensity(vertical: -2.8), foregroundColor: attr != 0 - ? t.colorScheme.outline - : t.colorScheme.onSecondaryContainer, + ? colorScheme.outline + : colorScheme.onSecondaryContainer, backgroundColor: attr != 0 - ? t.colorScheme.onInverseSurface - : t.colorScheme.secondaryContainer, + ? colorScheme.onInverseSurface + : colorScheme.secondaryContainer, ), child: Text( switch (attr) { @@ -521,13 +452,13 @@ class _UgcIntroPanelState extends State { Widget actionGrid( BuildContext context, bool isLoading, - VideoDetailData videoDetail, UgcIntroController introController, + VideoStat? stat, ) { return SizedBox( height: 48, child: Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: .start, children: [ Obx( () => ActionItem( @@ -536,9 +467,7 @@ class _UgcIntroPanelState extends State { selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectStatus: introController.hasLike.value, semanticsLabel: '点赞', - text: !isLoading - ? NumUtils.numFormat(videoDetail.stat!.like) - : null, + text: !isLoading ? NumUtils.numFormat(stat!.like) : null, onStartTriple: introController.onStartTriple, onCancelTriple: introController.onCancelTriple, ), @@ -563,9 +492,7 @@ class _UgcIntroPanelState extends State { onTap: introController.actionCoinVideo, selectStatus: introController.hasCoin, semanticsLabel: '投币', - text: !isLoading - ? NumUtils.numFormat(videoDetail.stat!.coin) - : null, + text: !isLoading ? NumUtils.numFormat(stat!.coin) : null, ), ), Obx( @@ -580,9 +507,7 @@ class _UgcIntroPanelState extends State { ), selectStatus: introController.hasFav.value, semanticsLabel: '收藏', - text: !isLoading - ? NumUtils.numFormat(videoDetail.stat!.favorite) - : null, + text: !isLoading ? NumUtils.numFormat(stat!.favorite) : null, ), ), Obx( @@ -601,9 +526,7 @@ class _UgcIntroPanelState extends State { onTap: () => introController.actionShareVideo(context), selectStatus: false, semanticsLabel: '分享', - text: !isLoading - ? NumUtils.numFormat(videoDetail.stat!.share!) - : null, + text: !isLoading ? NumUtils.numFormat(stat!.share!) : null, ), ], ), @@ -620,14 +543,10 @@ class _UgcIntroPanelState extends State { caseSensitive: false, ); - TextSpan buildContent(ThemeData theme, VideoDetailData content) { - if (content.descV2.isNullOrEmpty) { - return const TextSpan(); - } - // type + TextSpan buildContent(List descV2) { // 1 普通文本 // 2 @用户 - final List spanChildren = content.descV2!.map((currentDesc) { + final List spanChildren = descV2.map((currentDesc) { switch (currentDesc.type) { case 1: final List spanChildren = []; @@ -640,7 +559,7 @@ class _UgcIntroPanelState extends State { spanChildren.add( TextSpan( text: matchStr, - style: TextStyle(color: theme.colorScheme.primary), + style: TextStyle(color: colorScheme.primary), recognizer: NoDeadlineTapGestureRecognizer() ..onTap = () async { if (videoDetailCtr @@ -702,7 +621,7 @@ class _UgcIntroPanelState extends State { spanChildren.add( TextSpan( text: matchStr, - style: TextStyle(color: theme.colorScheme.primary), + style: TextStyle(color: colorScheme.primary), recognizer: NoDeadlineTapGestureRecognizer() ..onTap = () => PiliScheme.videoPush(aid, null), ), @@ -716,7 +635,7 @@ class _UgcIntroPanelState extends State { spanChildren.add( TextSpan( text: matchStr, - style: TextStyle(color: theme.colorScheme.primary), + style: TextStyle(color: colorScheme.primary), recognizer: NoDeadlineTapGestureRecognizer() ..onTap = () => PiliScheme.videoPush(null, matchStr), ), @@ -728,7 +647,7 @@ class _UgcIntroPanelState extends State { spanChildren.add( TextSpan( text: matchStr, - style: TextStyle(color: theme.colorScheme.primary), + style: TextStyle(color: colorScheme.primary), recognizer: NoDeadlineTapGestureRecognizer() ..onTap = () { try { @@ -754,7 +673,7 @@ class _UgcIntroPanelState extends State { ); return TextSpan(children: spanChildren); case 2: - final Color colorSchemePrimary = theme.colorScheme.primary; + final Color colorSchemePrimary = colorScheme.primary; return TextSpan( text: '@${currentDesc.rawText}', style: TextStyle(color: colorSchemePrimary), @@ -768,8 +687,67 @@ class _UgcIntroPanelState extends State { return TextSpan(children: spanChildren); } + Widget _buildOwnerInfo( + bool isLoading, + bool isPortrait, + bool isHorizontal, + VideoDetailData videoDetail, + ) { + final mid = videoDetail.owner?.mid; + return Row( + children: [ + if (videoDetail.staff case final staff? when staff.isNotEmpty) + Expanded( + child: SingleChildScrollView( + scrollDirection: .horizontal, + hitTestBehavior: .deferToChild, + physics: ReloadScrollPhysics(controller: introController), + child: Row( + spacing: 25, + children: staff + .map((e) => _buildStaff(isPortrait, mid, e)) + .toList(), + ), + ), + ) + else ...[ + Expanded( + child: Align( + alignment: .centerLeft, + child: _buildAvatar( + () { + if (mid != null) { + feedBack(); + if (!isPortrait && introController.horizontalMemberPage) { + widget.onShowMemberPage(mid); + } else { + Get.toNamed( + '/member?mid=$mid&from_view_aid=${videoDetailCtr.aid}', + ); + } + } + }, + ), + ), + ), + followButton(context), + ], + if (isHorizontal) ...[ + const SizedBox(width: 10), + Expanded( + child: actionGrid( + context, + isLoading, + introController, + videoDetail.stat, + ), + ), + ], + ], + ); + } + Widget _buildStaff( - ThemeData theme, bool isPortrait, int? ownerMid, Staff item, @@ -778,7 +756,7 @@ class _UgcIntroPanelState extends State { '/member?mid=${item.mid}&from_view_aid=${videoDetailCtr.aid}', ); return GestureDetector( - behavior: HitTestBehavior.opaque, + behavior: .opaque, onTap: () { if (item.mid == ownerMid && !isPortrait && @@ -795,96 +773,98 @@ class _UgcIntroPanelState extends State { child: Row( children: [ Stack( - clipBehavior: Clip.none, + clipBehavior: .none, children: [ NetworkImgLayer( - type: ImageType.avatar, + type: .avatar, src: item.face, width: 35, height: 35, fadeInDuration: Duration.zero, fadeOutDuration: Duration.zero, ), - if ((item.official?.type ?? -1) != -1) + if (item.official?.type case final type? when type != -1) Positioned( right: -2, bottom: -2, child: DecoratedBox( decoration: BoxDecoration( - shape: BoxShape.circle, - color: theme.colorScheme.surface, - ), - child: Icon( - Icons.offline_bolt, - color: item.official?.type == 0 - ? const Color(0xFFFFCC00) - : Colors.lightBlueAccent, - size: 14, + shape: .circle, + color: colorScheme.surface, ), + child: item.official?.type == 0 + ? const Icon( + Icons.offline_bolt, + color: BiliColors.yellow, + size: 14, + ) + : const Icon( + Icons.offline_bolt, + color: Colors.lightBlueAccent, + size: 14, + ), ), ), Positioned( top: 0, right: -6, child: Obx( - () => - introController.staffRelations['status'] == true && - introController.staffRelations['${item.mid}'] == null - ? Material( - type: MaterialType.transparency, - shape: const CircleBorder(), - child: InkWell( - customBorder: const CircleBorder(), - onTap: () => RequestUtils.actionRelationMod( - context: context, - mid: item.mid, - isFollow: false, - afterMod: (val) { + () { + if (introController.staffRelations['status'] == true && + introController.staffRelations['${item.mid}'] == null) { + return Material( + type: .transparency, + shape: const CircleBorder(), + child: InkWell( + customBorder: const CircleBorder(), + onTap: () => RequestUtils.actionRelationMod( + context: context, + mid: item.mid, + isFollow: false, + afterMod: (val) => introController.staffRelations['${item.mid}'] = - true; - }, + true, + ), + child: Ink( + padding: const .all(2), + decoration: BoxDecoration( + color: colorScheme.secondaryContainer, + shape: .circle, ), - child: Ink( - padding: const EdgeInsets.all(2), - decoration: BoxDecoration( - color: theme.colorScheme.secondaryContainer, - shape: BoxShape.circle, - ), - child: Icon( - MdiIcons.plus, - size: 16, - color: theme.colorScheme.onSecondaryContainer, - ), + child: Icon( + MdiIcons.plus, + size: 16, + color: colorScheme.onSecondaryContainer, ), ), - ) - : const SizedBox.shrink(), + ), + ); + } + return const SizedBox.shrink(); + }, ), ), ], ), const SizedBox(width: 8), Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ Text( item.name!, maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: .ellipsis, style: TextStyle( fontSize: 13, color: (item.vip?.status ?? 0) > 0 && item.vip?.type == 2 - ? theme.colorScheme.vipColor + ? colorScheme.vipColor : null, ), ), Text( item.title!, - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.outline, - ), + style: TextStyle(fontSize: 12, color: colorScheme.outline), ), ], ), @@ -894,11 +874,10 @@ class _UgcIntroPanelState extends State { } Widget _buildAvatar( - ThemeData theme, VoidCallback onPushMember, ) => GestureDetector( onTap: onPushMember, - behavior: HitTestBehavior.opaque, + behavior: .opaque, onSecondaryTap: PlatformUtils.isDesktop && introController.horizontalMemberPage ? () => Get.toNamed( @@ -910,7 +889,8 @@ class _UgcIntroPanelState extends State { final userStat = introController.userStat.value; final isVip = (userStat.card?.vip?.status ?? 0) > 0; return Row( - mainAxisSize: MainAxisSize.min, + spacing: 10, + mainAxisSize: .min, children: [ PendantAvatar( userStat.card?.face, @@ -919,28 +899,23 @@ class _UgcIntroPanelState extends State { vipStatus: userStat.card?.vip?.status, officialType: userStat.card?.official?.type, ), - const SizedBox(width: 10), Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: .start, children: [ Text( userStat.card?.name ?? "", maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: .ellipsis, style: TextStyle( fontSize: 13, color: isVip && userStat.card?.vip?.type == 2 - ? theme.colorScheme.vipColor + ? colorScheme.vipColor : null, ), ), - const SizedBox(height: 0), Text( '${NumUtils.numFormat(userStat.follower)}粉丝 ${'${NumUtils.numFormat(userStat.archiveCount)}视频'}', - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.outline, - ), + style: TextStyle(fontSize: 12, color: colorScheme.outline), ), ], ), @@ -950,51 +925,50 @@ class _UgcIntroPanelState extends State { ), ); - Widget _buildInfo(ThemeData theme, VideoDetailData videoDetail) => Row( - spacing: 10, - children: [ - StatWidget( - type: StatType.play, - value: videoDetail.stat?.view, - color: theme.colorScheme.outline, - ), - StatWidget( - type: StatType.danmaku, - value: videoDetail.stat?.danmaku, - color: theme.colorScheme.outline, - ), - Text( - DateFormatUtils.format(videoDetail.pubdate), - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.outline, + Widget _buildInfo(VideoStat? stat, int? pubdate) { + return Row( + spacing: 10, + children: [ + StatWidget( + type: .play, + value: stat?.view, + color: colorScheme.outline, ), - ), - if (MineController.anonymity.value) - Icon( - MdiIcons.incognito, - size: 15, - color: theme.colorScheme.outline, - semanticLabel: '无痕', + StatWidget( + type: .danmaku, + value: stat?.danmaku, + color: colorScheme.outline, ), - if (introController.isShowOnlineTotal) - Obx( - () => Text( - '${introController.total.value}人在看', - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.outline, - ), + Text( + DateFormatUtils.format(pubdate), + style: TextStyle( + fontSize: 12, + color: colorScheme.outline, ), ), - ], - ); + if (MineController.anonymity.value) + Icon( + MdiIcons.incognito, + size: 15, + color: colorScheme.outline, + semanticLabel: '无痕', + ), + if (introController.isShowOnlineTotal) + Obx( + () => Text( + '${introController.total.value}人在看', + style: TextStyle(fontSize: 12, color: colorScheme.outline), + ), + ), + ], + ); + } Widget get _aiBtn => Positioned( right: 8, child: Center( child: GestureDetector( - behavior: HitTestBehavior.opaque, + behavior: .opaque, onTap: () async { if (introController.aiConclusionResult == null) { await introController.aiConclusion(); @@ -1022,43 +996,38 @@ class _UgcIntroPanelState extends State { ); Widget _buildTags(List tags) { - return GestureDetector( - onTap: () {}, - behavior: HitTestBehavior.opaque, - child: Container( - width: double.infinity, - padding: const EdgeInsets.only(top: 8), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: tags - .map( - (item) => SearchText( - fontSize: 13, - text: switch (item.tagType) { - 'bgm' => item.tagName!.replaceFirst('发现', '♫ BGM:'), - 'topic' => '#${item.tagName}', - _ => item.tagName!, - }, - onTap: switch (item.tagType) { - 'bgm' => (_) => Get.toNamed( - '/musicDetail', - parameters: {'musicId': item.musicId!}, - ), - 'topic' => (_) => Get.toNamed( - '/dynTopic', - parameters: {'id': item.tagId!.toString()}, - ), - _ => (tagName) => Get.toNamed( - '/searchResult', - parameters: {'keyword': tagName}, - ), - }, - onLongPress: Utils.copyText, - ), - ) - .toList(), - ), + return Padding( + padding: const .only(top: 8), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: tags + .map( + (item) => SearchText( + fontSize: 13, + text: switch (item.tagType) { + 'bgm' => item.tagName!.replaceFirst('发现', '♫ BGM:'), + 'topic' => '#${item.tagName}', + _ => item.tagName!, + }, + onTap: switch (item.tagType) { + 'bgm' => (_) => Get.toNamed( + '/musicDetail', + parameters: {'musicId': item.musicId!}, + ), + 'topic' => (_) => Get.toNamed( + '/dynTopic', + parameters: {'id': item.tagId!.toString()}, + ), + _ => (tagName) => Get.toNamed( + '/searchResult', + parameters: {'keyword': tagName}, + ), + }, + onLongPress: Utils.copyText, + ), + ) + .toList(), ), ); } diff --git a/lib/utils/bili_colors.dart b/lib/utils/bili_colors.dart new file mode 100644 index 000000000..f4dc98ea1 --- /dev/null +++ b/lib/utils/bili_colors.dart @@ -0,0 +1,11 @@ +import 'package:flutter/widgets.dart' show Color; + +abstract final class BiliColors { + static const pinkLight = Color(0xFFFF6699); + static const pinkDark = Color(0xFFD44E7D); + + static const blueLight = Color(0xFF008AC5); + static const blueDark = Color(0xFF2C9CC8); + + static const yellow = Color(0xFFFFCC00); +} diff --git a/lib/utils/extension/theme_ext.dart b/lib/utils/extension/theme_ext.dart index ab6b1f8ed..6f4074fd4 100644 --- a/lib/utils/extension/theme_ext.dart +++ b/lib/utils/extension/theme_ext.dart @@ -1,13 +1,8 @@ +import 'package:PiliPlus/utils/bili_colors.dart'; import 'package:flex_seed_scheme/flex_seed_scheme.dart'; import 'package:flutter/material.dart' show ThemeData, Color, ColorScheme, Brightness, Colors; -const _pinkLight = Color(0xFFFF6699); -const _pinkDark = Color(0xFFD44E7D); - -const _blueLight = Color(0xFF008AC5); -const _blueDark = Color(0xFF2C9CC8); - extension ThemeDataExt on ThemeData { bool get isLight => brightness.isLight; @@ -15,12 +10,14 @@ extension ThemeDataExt on ThemeData { } extension ColorSchemeExt on ColorScheme { - Color get vipColor => brightness.isLight ? _pinkLight : _pinkDark; + Color get vipColor => + brightness.isLight ? BiliColors.pinkLight : BiliColors.pinkDark; - Color get blue => brightness.isLight ? _blueLight : _blueDark; + Color get blue => + brightness.isLight ? BiliColors.blueLight : BiliColors.blueDark; Color get btnColor => - brightness.isLight ? _pinkLight : const Color(0xFF8F0030); + brightness.isLight ? BiliColors.pinkLight : const Color(0xFF8F0030); Color get freeColor => brightness.isLight ? const Color(0xFFFF7F24) : const Color(0xFFD66011);