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);