mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-20 03:06:59 +08:00
@@ -33,7 +33,7 @@ import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart' show HapticFeedback;
|
||||
import 'package:get/get_core/src/get_main.dart';
|
||||
import 'package:get/get_navigation/get_navigation.dart';
|
||||
import 'package:get/get_navigation/src/extension_navigation.dart';
|
||||
|
||||
class ImageModel {
|
||||
ImageModel({
|
||||
@@ -74,14 +74,14 @@ class ImageGridView extends StatelessWidget {
|
||||
final bool fullScreen;
|
||||
|
||||
static bool horizontalPreview = Pref.horizontalPreview;
|
||||
static const _routes = ['/videoV', '/dynamicDetail'];
|
||||
static final _regex = RegExp(r'/videoV|/dynamicDetail$|/articlePage');
|
||||
|
||||
void _onTap(BuildContext context, int index) {
|
||||
final imgList = picArr.map(
|
||||
(item) {
|
||||
bool isLive = item.isLivePhoto;
|
||||
return SourceModel(
|
||||
sourceType: isLive ? SourceType.livePhoto : SourceType.networkImage,
|
||||
sourceType: isLive ? .livePhoto : .networkImage,
|
||||
url: item.url,
|
||||
liveUrl: isLive ? item.liveUrl : null,
|
||||
width: isLive ? item.width.toInt() : null,
|
||||
@@ -92,7 +92,7 @@ class ImageGridView extends StatelessWidget {
|
||||
).toList();
|
||||
if (horizontalPreview &&
|
||||
!fullScreen &&
|
||||
_routes.contains(Get.currentRoute) &&
|
||||
Get.currentRoute.startsWith(_regex) &&
|
||||
!context.mediaQuerySize.isPortrait) {
|
||||
final scaffoldState = Scaffold.maybeOf(context);
|
||||
if (scaffoldState != null) {
|
||||
|
||||
@@ -32,8 +32,6 @@ class ArticleController extends CommonDynController {
|
||||
|
||||
late final RxInt topIndex = 0.obs;
|
||||
|
||||
late final showDynActionBar = Pref.showDynActionBar;
|
||||
|
||||
@override
|
||||
dynamic get sourceId => commentType == 12 ? 'cv$commentId' : id;
|
||||
|
||||
|
||||
@@ -68,13 +68,12 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
appBar: _buildAppBar(),
|
||||
body: Padding(
|
||||
padding: EdgeInsets.only(left: padding.left, right: padding.right),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_buildPage(theme),
|
||||
_buildBottom(theme),
|
||||
],
|
||||
),
|
||||
child: _buildPage(theme),
|
||||
),
|
||||
floatingActionButtonLocation: floatingActionButtonLocation,
|
||||
floatingActionButton: SlideTransition(
|
||||
position: fabAnimation,
|
||||
child: _buildBottom(theme),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -494,177 +493,160 @@ class _ArticlePageState extends CommonDynPageState<ArticlePage> {
|
||||
],
|
||||
);
|
||||
|
||||
Widget _buildBottom(ThemeData theme) => Positioned(
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: SlideTransition(
|
||||
position: fabAnim,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (!controller.showDynActionBar) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: padding.bottom + kFloatingActionButtonMargin,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
Widget _buildBottom(ThemeData theme) {
|
||||
if (!controller.showDynActionBar) {
|
||||
return fabButton;
|
||||
}
|
||||
|
||||
late final primary = theme.colorScheme.primary;
|
||||
late final outline = theme.colorScheme.outline;
|
||||
late final btnStyle = TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
foregroundColor: outline,
|
||||
late final primary = theme.colorScheme.primary;
|
||||
late final outline = theme.colorScheme.outline;
|
||||
late final btnStyle = TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
foregroundColor: outline,
|
||||
);
|
||||
|
||||
Widget textIconButton({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required DynamicStat? stat,
|
||||
required VoidCallback onPressed,
|
||||
IconData? activatedIcon,
|
||||
}) {
|
||||
final status = stat?.status == true;
|
||||
final color = status ? primary : outline;
|
||||
return TextButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
status ? activatedIcon : icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
style: btnStyle,
|
||||
label: Text(
|
||||
stat?.count != null ? NumUtils.numFormat(stat!.count) : text,
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: .only(left: padding.left, right: padding.right),
|
||||
child: Obx(() {
|
||||
final stats = controller.stats.value;
|
||||
|
||||
Widget btn = Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom:
|
||||
kFloatingActionButtonMargin +
|
||||
(stats != null ? 0 : padding.bottom),
|
||||
),
|
||||
child: replyButton,
|
||||
);
|
||||
|
||||
if (stats == null) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: btn,
|
||||
);
|
||||
}
|
||||
|
||||
Widget textIconButton({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required DynamicStat? stat,
|
||||
required VoidCallback onPressed,
|
||||
IconData? activatedIcon,
|
||||
}) {
|
||||
final status = stat?.status == true;
|
||||
final color = status ? primary : outline;
|
||||
return TextButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
status ? activatedIcon : icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
style: btnStyle,
|
||||
label: Text(
|
||||
stat?.count != null ? NumUtils.numFormat(stat!.count) : text,
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Obx(() {
|
||||
final stats = controller.stats.value;
|
||||
|
||||
Widget btn = Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom:
|
||||
kFloatingActionButtonMargin +
|
||||
(stats != null ? 0 : padding.bottom),
|
||||
),
|
||||
child: replyButton,
|
||||
);
|
||||
|
||||
if (stats == null) {
|
||||
return Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: btn,
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
btn,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
),
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
btn,
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (btnContext) {
|
||||
final forward = stats.forward;
|
||||
return textIconButton(
|
||||
text: '转发',
|
||||
icon: FontAwesomeIcons.shareFromSquare,
|
||||
stat: forward,
|
||||
onPressed: () {
|
||||
if (controller.opusData == null &&
|
||||
controller.articleData?.dynIdStr == null) {
|
||||
SmartDialog.showToast(
|
||||
'err: ${controller.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final summary = controller.summary;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => RepostPanel(
|
||||
item: controller.opusData,
|
||||
dynIdStr: controller.articleData?.dynIdStr,
|
||||
pic: summary.cover,
|
||||
title: summary.title,
|
||||
uname: summary.author?.name,
|
||||
onSuccess: () {
|
||||
if (forward != null) {
|
||||
int count = forward.count ?? 0;
|
||||
forward.count = count + 1;
|
||||
if (btnContext.mounted) {
|
||||
(btnContext as Element?)
|
||||
?.markNeedsBuild();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: padding.bottom),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (btnContext) {
|
||||
final forward = stats.forward;
|
||||
return textIconButton(
|
||||
text: '转发',
|
||||
icon: FontAwesomeIcons.shareFromSquare,
|
||||
stat: forward,
|
||||
onPressed: () {
|
||||
if (controller.opusData == null &&
|
||||
controller.articleData?.dynIdStr == null) {
|
||||
SmartDialog.showToast(
|
||||
'err: ${controller.id}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
final summary = controller.summary;
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => RepostPanel(
|
||||
item: controller.opusData,
|
||||
dynIdStr: controller.articleData?.dynIdStr,
|
||||
pic: summary.cover,
|
||||
title: summary.title,
|
||||
uname: summary.author?.name,
|
||||
onSuccess: () {
|
||||
if (forward != null) {
|
||||
int count = forward.count ?? 0;
|
||||
forward.count = count + 1;
|
||||
if (btnContext.mounted) {
|
||||
(btnContext as Element?)
|
||||
?.markNeedsBuild();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
text: '分享',
|
||||
icon: CustomIcons.share_node,
|
||||
stat: null,
|
||||
onPressed: () => Utils.shareText(controller.url),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: FontAwesomeIcons.star,
|
||||
activatedIcon: FontAwesomeIcons.solidStar,
|
||||
text: '收藏',
|
||||
stat: stats.favorite,
|
||||
onPressed: controller.onFav,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: FontAwesomeIcons.thumbsUp,
|
||||
activatedIcon: FontAwesomeIcons.solidThumbsUp,
|
||||
text: '点赞',
|
||||
stat: stats.like,
|
||||
onPressed: controller.onLike,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
text: '分享',
|
||||
icon: CustomIcons.share_node,
|
||||
stat: null,
|
||||
onPressed: () => Utils.shareText(controller.url),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: FontAwesomeIcons.star,
|
||||
activatedIcon: FontAwesomeIcons.solidStar,
|
||||
text: '收藏',
|
||||
stat: stats.favorite,
|
||||
onPressed: controller.onFav,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: FontAwesomeIcons.thumbsUp,
|
||||
activatedIcon: FontAwesomeIcons.solidThumbsUp,
|
||||
text: '点赞',
|
||||
stat: stats.like,
|
||||
onPressed: controller.onLike,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ abstract class CommonDynController extends ReplyController<MainListReply> {
|
||||
late final horizontalPreview = Pref.horizontalPreview;
|
||||
late final List<double> ratio = Pref.dynamicDetailRatio;
|
||||
|
||||
late final showDynActionBar = Pref.showDynActionBar;
|
||||
|
||||
@override
|
||||
Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList(
|
||||
type: replyType,
|
||||
|
||||
@@ -9,6 +9,7 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
|
||||
import 'package:PiliPlus/pages/common/fab_mixin.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/view.dart';
|
||||
import 'package:PiliPlus/utils/extension/num_ext.dart';
|
||||
@@ -22,7 +23,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
with SingleTickerProviderStateMixin {
|
||||
with SingleTickerProviderStateMixin, FabMixin {
|
||||
CommonDynController get controller;
|
||||
|
||||
late final ScrollController scrollController;
|
||||
@@ -36,26 +37,9 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
late double maxWidth;
|
||||
late double maxHeight;
|
||||
|
||||
bool _showFab = true;
|
||||
|
||||
final fabOffset = const Offset(0, 1);
|
||||
|
||||
late final AnimationController _fabAnimationCtr;
|
||||
late final Animation<Offset> fabAnim;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
)..forward();
|
||||
fabAnim = _fabAnimationCtr.drive(
|
||||
Tween<Offset>(
|
||||
begin: fabOffset,
|
||||
end: Offset.zero,
|
||||
).chain(CurveTween(curve: Curves.easeInOut)),
|
||||
);
|
||||
scrollController = ScrollController()..addListener(listener);
|
||||
}
|
||||
|
||||
@@ -69,20 +53,6 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
}
|
||||
}
|
||||
|
||||
void showFab() {
|
||||
if (!_showFab) {
|
||||
_showFab = true;
|
||||
_fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void hideFab() {
|
||||
if (_showFab) {
|
||||
_showFab = false;
|
||||
_fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
@@ -98,7 +68,6 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
scrollController
|
||||
..removeListener(listener)
|
||||
..dispose();
|
||||
_fabAnimationCtr.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -304,6 +273,16 @@ abstract class CommonDynPageState<T extends StatefulWidget> extends State<T>
|
||||
),
|
||||
);
|
||||
|
||||
FloatingActionButtonLocation get floatingActionButtonLocation =>
|
||||
controller.showDynActionBar
|
||||
? const ActionBarLocation()
|
||||
: const NoBottomPaddingFabLocation();
|
||||
|
||||
Widget get fabButton => Padding(
|
||||
padding: .only(bottom: padding.bottom + kFloatingActionButtonMargin),
|
||||
child: replyButton,
|
||||
);
|
||||
|
||||
Widget get replyButton => FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
|
||||
78
lib/pages/common/fab_mixin.dart
Normal file
78
lib/pages/common/fab_mixin.dart
Normal file
@@ -0,0 +1,78 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
mixin FabMixin<T extends StatefulWidget> on State<T>, TickerProvider {
|
||||
bool _isFabVisible = true;
|
||||
late final AnimationController _fabAnimationCtr;
|
||||
late final Animation<Offset> fabAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
);
|
||||
fabAnimation = _fabAnimationCtr.drive(
|
||||
Tween<Offset>(
|
||||
begin: Offset.zero,
|
||||
end: const Offset(0.0, 1.0),
|
||||
).chain(CurveTween(curve: Curves.easeInOut)),
|
||||
);
|
||||
}
|
||||
|
||||
void showFab() {
|
||||
if (!_isFabVisible) {
|
||||
_isFabVisible = true;
|
||||
_fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
void hideFab() {
|
||||
if (_isFabVisible) {
|
||||
_isFabVisible = false;
|
||||
_fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_fabAnimationCtr.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
mixin _NoRightMarginMixin on StandardFabLocation {
|
||||
@override
|
||||
double getOffsetX(scaffoldGeometry, _) {
|
||||
return scaffoldGeometry.scaffoldSize.width -
|
||||
scaffoldGeometry.minInsets.right -
|
||||
scaffoldGeometry.floatingActionButtonSize.width;
|
||||
}
|
||||
}
|
||||
|
||||
mixin _NoBottomPaddingMixin on StandardFabLocation {
|
||||
@override
|
||||
double getOffsetY(scaffoldGeometry, _) {
|
||||
return scaffoldGeometry.contentBottom -
|
||||
scaffoldGeometry.floatingActionButtonSize.height;
|
||||
}
|
||||
}
|
||||
|
||||
class NoRightMarginFabLocation extends StandardFabLocation
|
||||
with FabFloatOffsetY, _NoRightMarginMixin {
|
||||
const NoRightMarginFabLocation();
|
||||
}
|
||||
|
||||
class NoBottomPaddingFabLocation extends StandardFabLocation
|
||||
with FabEndOffsetX, _NoBottomPaddingMixin {
|
||||
const NoBottomPaddingFabLocation();
|
||||
}
|
||||
|
||||
class ActionBarLocation extends StandardFabLocation with _NoBottomPaddingMixin {
|
||||
const ActionBarLocation();
|
||||
|
||||
@override
|
||||
double getOffsetX(scaffoldGeometry, _) {
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import 'package:PiliPlus/http/reply.dart';
|
||||
import 'package:PiliPlus/models/dynamics/result.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -15,8 +14,6 @@ class DynamicDetailController extends CommonDynController {
|
||||
late int replyType;
|
||||
late DynamicItemModel dynItem;
|
||||
|
||||
late final showDynActionBar = Pref.showDynActionBar;
|
||||
|
||||
@override
|
||||
dynamic get sourceId => replyType == 1 ? IdUtils.av2bv(oid) : oid;
|
||||
|
||||
|
||||
@@ -70,6 +70,11 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
)
|
||||
: _buildBody(theme),
|
||||
),
|
||||
floatingActionButtonLocation: floatingActionButtonLocation,
|
||||
floatingActionButton: SlideTransition(
|
||||
position: fabAnimation,
|
||||
child: _buildBottom(theme),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,166 +324,139 @@ class _DynamicDetailPageState extends CommonDynPageState<DynamicDetailPage> {
|
||||
],
|
||||
);
|
||||
}
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
child,
|
||||
_buildBottom(theme),
|
||||
],
|
||||
);
|
||||
return child;
|
||||
}
|
||||
|
||||
Widget _buildBottom(ThemeData theme) {
|
||||
return Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SlideTransition(
|
||||
position: fabAnim,
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
if (!controller.showDynActionBar) {
|
||||
return Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: padding.bottom + kFloatingActionButtonMargin,
|
||||
if (!controller.showDynActionBar) {
|
||||
return fabButton;
|
||||
}
|
||||
|
||||
final primary = theme.colorScheme.primary;
|
||||
final outline = theme.colorScheme.outline;
|
||||
final btnStyle = TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
foregroundColor: outline,
|
||||
);
|
||||
|
||||
Widget textIconButton({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required DynamicStat? stat,
|
||||
required ValueChanged<Color> onPressed,
|
||||
IconData? activatedIcon,
|
||||
}) {
|
||||
final status = stat?.status == true;
|
||||
final color = status ? primary : outline;
|
||||
final iconWidget = Icon(
|
||||
status ? activatedIcon : icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
);
|
||||
return TextButton.icon(
|
||||
onPressed: () => onPressed(iconWidget.color!),
|
||||
icon: iconWidget,
|
||||
style: btnStyle,
|
||||
label: Text(
|
||||
stat?.count != null ? NumUtils.numFormat(stat!.count) : text,
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final moduleStat = controller.dynItem.modules.moduleStat;
|
||||
return Padding(
|
||||
padding: .only(left: padding.left, right: padding.right),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: kFloatingActionButtonMargin,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final moduleStat = controller.dynItem.modules.moduleStat;
|
||||
final primary = theme.colorScheme.primary;
|
||||
final outline = theme.colorScheme.outline;
|
||||
final btnStyle = TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
foregroundColor: outline,
|
||||
);
|
||||
|
||||
Widget textIconButton({
|
||||
required IconData icon,
|
||||
required String text,
|
||||
required DynamicStat? stat,
|
||||
required ValueChanged<Color> onPressed,
|
||||
IconData? activatedIcon,
|
||||
}) {
|
||||
final status = stat?.status == true;
|
||||
final color = status ? primary : outline;
|
||||
final iconWidget = Icon(
|
||||
status ? activatedIcon : icon,
|
||||
size: 16,
|
||||
color: color,
|
||||
);
|
||||
return TextButton.icon(
|
||||
onPressed: () => onPressed(iconWidget.color!),
|
||||
icon: iconWidget,
|
||||
style: btnStyle,
|
||||
label: Text(
|
||||
stat?.count != null ? NumUtils.numFormat(stat!.count) : text,
|
||||
style: TextStyle(color: color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: padding.bottom),
|
||||
child: Row(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: kFloatingActionButtonMargin,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (btnContext) {
|
||||
final forward = moduleStat?.forward;
|
||||
return textIconButton(
|
||||
icon: FontAwesomeIcons.shareFromSquare,
|
||||
text: '转发',
|
||||
stat: forward,
|
||||
onPressed: (_) => showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => RepostPanel(
|
||||
item: controller.dynItem,
|
||||
onSuccess: () {
|
||||
if (forward != null) {
|
||||
int count = forward.count ?? 0;
|
||||
forward.count = count + 1;
|
||||
if (btnContext.mounted) {
|
||||
(btnContext as Element)
|
||||
.markNeedsBuild();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: CustomIcons.share_node,
|
||||
text: '分享',
|
||||
stat: null,
|
||||
onPressed: (_) => Utils.shareText(
|
||||
'${HttpString.dynamicShareBaseUrl}/${controller.dynItem.idStr}',
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (btnContext) {
|
||||
final forward = moduleStat?.forward;
|
||||
return textIconButton(
|
||||
icon: FontAwesomeIcons.shareFromSquare,
|
||||
text: '转发',
|
||||
stat: forward,
|
||||
onPressed: (_) => showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useSafeArea: true,
|
||||
builder: (context) => RepostPanel(
|
||||
item: controller.dynItem,
|
||||
onSuccess: () {
|
||||
if (forward != null) {
|
||||
int count = forward.count ?? 0;
|
||||
forward.count = count + 1;
|
||||
if (btnContext.mounted) {
|
||||
(btnContext as Element).markNeedsBuild();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return textIconButton(
|
||||
icon: FontAwesomeIcons.thumbsUp,
|
||||
activatedIcon: FontAwesomeIcons.solidThumbsUp,
|
||||
text: '点赞',
|
||||
stat: moduleStat?.like,
|
||||
onPressed: (iconColor) =>
|
||||
RequestUtils.onLikeDynamic(
|
||||
controller.dynItem,
|
||||
iconColor == primary,
|
||||
() {
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: CustomIcons.share_node,
|
||||
text: '分享',
|
||||
stat: null,
|
||||
onPressed: (_) => Utils.shareText(
|
||||
'${HttpString.dynamicShareBaseUrl}/${controller.dynItem.idStr}',
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) {
|
||||
return textIconButton(
|
||||
icon: FontAwesomeIcons.thumbsUp,
|
||||
activatedIcon: FontAwesomeIcons.solidThumbsUp,
|
||||
text: '点赞',
|
||||
stat: moduleStat?.like,
|
||||
onPressed: (iconColor) => RequestUtils.onLikeDynamic(
|
||||
controller.dynItem,
|
||||
iconColor == primary,
|
||||
() {
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/models/common/fav_order_type.dart';
|
||||
import 'package:PiliPlus/models_new/fav/fav_detail/media.dart';
|
||||
import 'package:PiliPlus/models_new/fav/fav_folder/list.dart';
|
||||
import 'package:PiliPlus/pages/common/fab_mixin.dart'
|
||||
show NoRightMarginFabLocation;
|
||||
import 'package:PiliPlus/pages/dynamics_repost/view.dart';
|
||||
import 'package:PiliPlus/pages/fav_detail/controller.dart';
|
||||
import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart';
|
||||
@@ -58,7 +60,7 @@ class _FavDetailPageState extends State<FavDetailPage> with GridMixin {
|
||||
},
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButtonLocation: const CustomFabLocation(),
|
||||
floatingActionButtonLocation: const NoRightMarginFabLocation(),
|
||||
floatingActionButton: Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
@@ -507,18 +509,3 @@ class _FavDetailPageState extends State<FavDetailPage> with GridMixin {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class CustomFabLocation extends StandardFabLocation with FabFloatOffsetY {
|
||||
const CustomFabLocation();
|
||||
|
||||
@override
|
||||
double getOffsetX(
|
||||
ScaffoldPrelayoutGeometry scaffoldGeometry,
|
||||
double adjustment,
|
||||
) {
|
||||
return scaffoldGeometry.scaffoldSize.width -
|
||||
scaffoldGeometry.minInsets.right -
|
||||
scaffoldGeometry.floatingActionButtonSize.width +
|
||||
adjustment;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart';
|
||||
import 'package:PiliPlus/common/widgets/view_safe_area.dart';
|
||||
import 'package:PiliPlus/models/common/later_view_type.dart';
|
||||
import 'package:PiliPlus/models_new/later/list.dart';
|
||||
import 'package:PiliPlus/pages/fav_detail/view.dart';
|
||||
import 'package:PiliPlus/pages/common/fab_mixin.dart'
|
||||
show NoRightMarginFabLocation;
|
||||
import 'package:PiliPlus/pages/later/base_controller.dart';
|
||||
import 'package:PiliPlus/pages/later/controller.dart';
|
||||
import 'package:PiliPlus/utils/accounts.dart';
|
||||
@@ -74,7 +75,7 @@ class _LaterPageState extends State<LaterPage>
|
||||
child: Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
appBar: _buildAppbar(enableMultiSelect),
|
||||
floatingActionButtonLocation: const CustomFabLocation(),
|
||||
floatingActionButtonLocation: const NoRightMarginFabLocation(),
|
||||
floatingActionButton: Padding(
|
||||
padding: const .only(right: kFloatingActionButtonMargin),
|
||||
child: Obx(
|
||||
|
||||
@@ -38,7 +38,6 @@ import 'package:PiliPlus/utils/video_utils.dart';
|
||||
import 'package:canvas_danmaku/canvas_danmaku.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
@@ -373,9 +372,9 @@ class LiveRoomController extends GetxController {
|
||||
|
||||
void listener() {
|
||||
final userScrollDirection = scrollController.position.userScrollDirection;
|
||||
if (userScrollDirection == ScrollDirection.forward) {
|
||||
if (userScrollDirection == .forward) {
|
||||
disableAutoScroll.value = true;
|
||||
} else if (userScrollDirection == ScrollDirection.reverse) {
|
||||
} else if (userScrollDirection == .reverse) {
|
||||
final pos = scrollController.position;
|
||||
if (pos.maxScrollExtent - pos.pixels <= 100) {
|
||||
disableAutoScroll.value = false;
|
||||
|
||||
@@ -3,35 +3,18 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
import 'package:PiliPlus/grpc/reply.dart';
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/pages/common/reply_controller.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MainReplyController extends ReplyController<MainListReply>
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
class MainReplyController extends ReplyController<MainListReply> {
|
||||
late final int oid;
|
||||
late final int replyType;
|
||||
|
||||
@override
|
||||
int get sourceId => oid;
|
||||
|
||||
bool _showFab = true;
|
||||
|
||||
late final AnimationController _fabAnimationCtr;
|
||||
late final Animation<Offset> fabAnim;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
)..forward();
|
||||
fabAnim = _fabAnimationCtr.drive(
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0.0, 2.0),
|
||||
end: Offset.zero,
|
||||
).chain(CurveTween(curve: Curves.easeInOut)),
|
||||
);
|
||||
final args = Get.arguments;
|
||||
oid = args['oid'];
|
||||
replyType = args['replyType'];
|
||||
@@ -39,20 +22,6 @@ class MainReplyController extends ReplyController<MainListReply>
|
||||
queryData();
|
||||
}
|
||||
|
||||
void showFab() {
|
||||
if (!_showFab) {
|
||||
_showFab = true;
|
||||
_fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void hideFab() {
|
||||
if (_showFab) {
|
||||
_showFab = false;
|
||||
_fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<LoadingState<MainListReply>> customGetData() => ReplyGrpc.mainList(
|
||||
type: replyType,
|
||||
@@ -64,10 +33,4 @@ class MainReplyController extends ReplyController<MainListReply>
|
||||
|
||||
@override
|
||||
List<ReplyInfo>? getDataList(MainListReply response) => response.replies;
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_fabAnimationCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/view_safe_area.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/pages/common/fab_mixin.dart';
|
||||
import 'package:PiliPlus/pages/main_reply/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/view.dart';
|
||||
@@ -38,7 +39,8 @@ class MainReplyPage extends StatefulWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _MainReplyPageState extends State<MainReplyPage> {
|
||||
class _MainReplyPageState extends State<MainReplyPage>
|
||||
with SingleTickerProviderStateMixin, FabMixin {
|
||||
final _controller = Get.put(
|
||||
MainReplyController(),
|
||||
tag: Utils.generateRandomString(8),
|
||||
@@ -62,9 +64,9 @@ class _MainReplyPageState extends State<MainReplyPage> {
|
||||
onNotification: (notification) {
|
||||
final direction = notification.direction;
|
||||
if (direction == .forward) {
|
||||
_controller.showFab();
|
||||
showFab();
|
||||
} else if (direction == .reverse) {
|
||||
_controller.hideFab();
|
||||
hideFab();
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -87,22 +89,26 @@ class _MainReplyPageState extends State<MainReplyPage> {
|
||||
),
|
||||
).constraintWidth(),
|
||||
),
|
||||
floatingActionButtonLocation: const NoBottomPaddingFabLocation(),
|
||||
floatingActionButton: SlideTransition(
|
||||
position: _controller.fabAnim,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
try {
|
||||
feedBack();
|
||||
_controller.onReply(
|
||||
null,
|
||||
oid: _controller.oid,
|
||||
replyType: _controller.replyType,
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
tooltip: '评论',
|
||||
child: const Icon(Icons.reply),
|
||||
position: fabAnimation,
|
||||
child: Padding(
|
||||
padding: .only(bottom: padding.bottom + kFloatingActionButtonMargin),
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
try {
|
||||
feedBack();
|
||||
_controller.onReply(
|
||||
null,
|
||||
oid: _controller.oid,
|
||||
replyType: _controller.replyType,
|
||||
);
|
||||
} catch (_) {}
|
||||
},
|
||||
tooltip: '评论',
|
||||
child: const Icon(Icons.reply),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,6 +8,8 @@ import 'package:PiliPlus/models/common/image_type.dart';
|
||||
import 'package:PiliPlus/models_new/match/match_info/contest.dart';
|
||||
import 'package:PiliPlus/models_new/match/match_info/team.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart';
|
||||
import 'package:PiliPlus/pages/common/fab_mixin.dart'
|
||||
show NoBottomPaddingFabLocation;
|
||||
import 'package:PiliPlus/pages/match_info/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/view.dart';
|
||||
import 'package:PiliPlus/utils/date_utils.dart';
|
||||
@@ -36,9 +38,6 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
|
||||
@override
|
||||
dynamic get arguments => null;
|
||||
|
||||
@override
|
||||
Offset get fabOffset => const Offset(0, 2);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
@@ -59,9 +58,10 @@ class _MatchInfoPageState extends CommonDynPageState<MatchInfoPage> {
|
||||
),
|
||||
),
|
||||
).constraintWidth(),
|
||||
floatingActionButtonLocation: const NoBottomPaddingFabLocation(),
|
||||
floatingActionButton: SlideTransition(
|
||||
position: fabAnim,
|
||||
child: replyButton,
|
||||
position: fabAnimation,
|
||||
child: fabButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/http/music.dart';
|
||||
import 'package:PiliPlus/models_new/music/bgm_detail.dart';
|
||||
import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class MusicDetailController extends CommonDynController {
|
||||
@@ -18,8 +17,6 @@ class MusicDetailController extends CommonDynController {
|
||||
|
||||
late final String musicId;
|
||||
|
||||
bool get showDynActionBar => Pref.showDynActionBar;
|
||||
|
||||
String get shareUrl =>
|
||||
'https://music.bilibili.com/h5/music-detail?music_id=$musicId';
|
||||
|
||||
|
||||
@@ -204,7 +204,24 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
});
|
||||
|
||||
Widget _buildBottom(ThemeData theme, MusicDetail item) {
|
||||
if (!controller.showDynActionBar) {
|
||||
return Positioned(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: 0,
|
||||
child: SlideTransition(
|
||||
position: fabAnimation,
|
||||
child: fabButton,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final primary = theme.colorScheme.primary;
|
||||
final outline = theme.colorScheme.outline;
|
||||
final style = TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
foregroundColor: outline,
|
||||
);
|
||||
|
||||
Widget textIconButton({
|
||||
required IconData icon,
|
||||
@@ -214,7 +231,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
required VoidCallback onPressed,
|
||||
IconData? activatedIcon,
|
||||
}) {
|
||||
final color = status ? theme.colorScheme.primary : outline;
|
||||
final color = status ? primary : outline;
|
||||
return TextButton.icon(
|
||||
onPressed: onPressed,
|
||||
icon: Icon(
|
||||
@@ -222,11 +239,7 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
size: 16,
|
||||
color: color,
|
||||
),
|
||||
style: TextButton.styleFrom(
|
||||
tapTargetSize: .padded,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
foregroundColor: outline,
|
||||
),
|
||||
style: style,
|
||||
label: Text(
|
||||
count != null ? NumUtils.numFormat(count) : text,
|
||||
style: TextStyle(color: color),
|
||||
@@ -239,116 +252,104 @@ class _MusicDetailPageState extends CommonDynPageState<MusicDetailPage> {
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SlideTransition(
|
||||
position: fabAnim,
|
||||
child: controller.showDynActionBar
|
||||
? Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: kFloatingActionButtonMargin,
|
||||
position: fabAnimation,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: kFloatingActionButtonMargin,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: theme.colorScheme.outline.withValues(
|
||||
alpha: 0.08,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
// TODO
|
||||
// Expanded(
|
||||
// child: textIconButton(
|
||||
// icon: FontAwesomeIcons.shareFromSquare,
|
||||
// text: '转发',
|
||||
// count: item.musicShares,
|
||||
// onPressed: () {
|
||||
// final data = controller.infoState.value.dataOrNull;
|
||||
// if (data != null) {
|
||||
// showModalBottomSheet(
|
||||
// context: context,
|
||||
// isScrollControlled: true,
|
||||
// useSafeArea: true,
|
||||
// builder: (context) => RepostPanel(
|
||||
// rid: controller.oid,
|
||||
// dynType: null,
|
||||
// pic: data.mvCover,
|
||||
// title: data.musicTitle,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: CustomIcons.share_node,
|
||||
text: '分享',
|
||||
onPressed: () => Utils.shareText(controller.shareUrl),
|
||||
),
|
||||
padding: EdgeInsets.only(bottom: padding.bottom),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
// TODO
|
||||
// Expanded(
|
||||
// child: textIconButton(
|
||||
// icon: FontAwesomeIcons.shareFromSquare,
|
||||
// text: '转发',
|
||||
// count: item.musicShares,
|
||||
// onPressed: () {
|
||||
// final data = controller.infoState.value.dataOrNull;
|
||||
// if (data != null) {
|
||||
// showModalBottomSheet(
|
||||
// context: context,
|
||||
// isScrollControlled: true,
|
||||
// useSafeArea: true,
|
||||
// builder: (context) => RepostPanel(
|
||||
// rid: controller.oid,
|
||||
// dynType: null,
|
||||
// pic: data.mvCover,
|
||||
// title: data.musicTitle,
|
||||
// ),
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
// ),
|
||||
// ),
|
||||
Expanded(
|
||||
child: textIconButton(
|
||||
icon: CustomIcons.share_node,
|
||||
text: '分享',
|
||||
onPressed: () =>
|
||||
Utils.shareText(controller.shareUrl),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) => textIconButton(
|
||||
icon: FontAwesomeIcons.thumbsUp,
|
||||
activatedIcon: FontAwesomeIcons.solidThumbsUp,
|
||||
text: '点赞',
|
||||
count: item.wishCount,
|
||||
status: item.wishListen ?? false,
|
||||
onPressed: () async {
|
||||
if (!Accounts.main.isLogin) {
|
||||
SmartDialog.showToast('请先登录');
|
||||
return;
|
||||
}
|
||||
final hasLike = item.wishListen ?? false;
|
||||
final res = await MusicHttp.wishUpdate(
|
||||
controller.musicId,
|
||||
hasLike,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
if (hasLike) {
|
||||
item.wishCount--;
|
||||
} else {
|
||||
item.wishCount++;
|
||||
}
|
||||
item.wishListen = !hasLike;
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: Builder(
|
||||
builder: (context) => textIconButton(
|
||||
icon: FontAwesomeIcons.thumbsUp,
|
||||
activatedIcon: FontAwesomeIcons.solidThumbsUp,
|
||||
text: '点赞',
|
||||
count: item.wishCount,
|
||||
status: item.wishListen ?? false,
|
||||
onPressed: () async {
|
||||
if (!Accounts.main.isLogin) {
|
||||
SmartDialog.showToast('请先登录');
|
||||
return;
|
||||
}
|
||||
final hasLike = item.wishListen ?? false;
|
||||
final res = await MusicHttp.wishUpdate(
|
||||
controller.musicId,
|
||||
hasLike,
|
||||
);
|
||||
if (res.isSuccess) {
|
||||
if (hasLike) {
|
||||
item.wishCount--;
|
||||
} else {
|
||||
item.wishCount++;
|
||||
}
|
||||
item.wishListen = !hasLike;
|
||||
if (context.mounted) {
|
||||
(context as Element).markNeedsBuild();
|
||||
}
|
||||
} else {
|
||||
res.toast();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: padding.bottom + kFloatingActionButtonMargin,
|
||||
),
|
||||
child: replyButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,11 +6,9 @@ import 'package:PiliPlus/models/common/video/video_type.dart';
|
||||
import 'package:PiliPlus/pages/common/reply_controller.dart';
|
||||
import 'package:PiliPlus/pages/video/controller.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class VideoReplyController extends ReplyController<MainListReply>
|
||||
with GetSingleTickerProviderStateMixin {
|
||||
class VideoReplyController extends ReplyController<MainListReply> {
|
||||
VideoReplyController({
|
||||
required this.aid,
|
||||
required this.videoType,
|
||||
@@ -26,39 +24,6 @@ class VideoReplyController extends ReplyController<MainListReply>
|
||||
@override
|
||||
dynamic get sourceId => IdUtils.av2bv(aid);
|
||||
|
||||
bool _isFabVisible = true;
|
||||
late final AnimationController _fabAnimationCtr;
|
||||
late final Animation<Offset> animation;
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
_fabAnimationCtr = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 100),
|
||||
)..forward();
|
||||
animation = _fabAnimationCtr.drive(
|
||||
Tween<Offset>(
|
||||
begin: const Offset(0.0, 2.0),
|
||||
end: Offset.zero,
|
||||
).chain(CurveTween(curve: Curves.easeInOut)),
|
||||
);
|
||||
}
|
||||
|
||||
void showFab() {
|
||||
if (!_isFabVisible) {
|
||||
_isFabVisible = true;
|
||||
_fabAnimationCtr.forward();
|
||||
}
|
||||
}
|
||||
|
||||
void hideFab() {
|
||||
if (_isFabVisible) {
|
||||
_isFabVisible = false;
|
||||
_fabAnimationCtr.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<ReplyInfo>? getDataList(MainListReply response) {
|
||||
return response.replies;
|
||||
@@ -72,10 +37,4 @@ class VideoReplyController extends ReplyController<MainListReply>
|
||||
cursorNext: cursorNext,
|
||||
offset: paginationReply?.nextOffset,
|
||||
);
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
_fabAnimationCtr.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart';
|
||||
import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'
|
||||
show ReplyInfo;
|
||||
import 'package:PiliPlus/http/loading_state.dart';
|
||||
import 'package:PiliPlus/pages/common/fab_mixin.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/controller.dart';
|
||||
import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart';
|
||||
import 'package:PiliPlus/pages/video/reply_reply/view.dart';
|
||||
@@ -13,7 +14,6 @@ import 'package:PiliPlus/utils/feed_back.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class VideoReplyPanel extends StatefulWidget {
|
||||
@@ -33,7 +33,10 @@ class VideoReplyPanel extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
with
|
||||
AutomaticKeepAliveClientMixin,
|
||||
SingleTickerProviderStateMixin,
|
||||
FabMixin {
|
||||
late VideoReplyController _videoReplyController;
|
||||
|
||||
String get heroTag => widget.heroTag;
|
||||
@@ -64,11 +67,12 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
final theme = Theme.of(context);
|
||||
final child = NotificationListener<UserScrollNotification>(
|
||||
onNotification: (notification) {
|
||||
final direction = notification.direction;
|
||||
if (direction == ScrollDirection.forward) {
|
||||
_videoReplyController.showFab();
|
||||
} else if (direction == ScrollDirection.reverse) {
|
||||
_videoReplyController.hideFab();
|
||||
switch (notification.direction) {
|
||||
case .forward:
|
||||
showFab();
|
||||
case .reverse:
|
||||
hideFab();
|
||||
case _:
|
||||
}
|
||||
return false;
|
||||
},
|
||||
@@ -129,22 +133,28 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: kFloatingActionButtonMargin + bottom,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
child: SlideTransition(
|
||||
position: _videoReplyController.animation,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
feedBack();
|
||||
_videoReplyController.onReply(
|
||||
null,
|
||||
oid: _videoReplyController.aid,
|
||||
replyType: _videoReplyController.videoType.replyType,
|
||||
);
|
||||
},
|
||||
tooltip: '发表评论',
|
||||
child: const Icon(Icons.reply),
|
||||
position: fabAnimation,
|
||||
child: Padding(
|
||||
padding: .only(
|
||||
right: kFloatingActionButtonMargin,
|
||||
bottom: kFloatingActionButtonMargin + bottom,
|
||||
),
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
onPressed: () {
|
||||
feedBack();
|
||||
_videoReplyController.onReply(
|
||||
null,
|
||||
oid: _videoReplyController.aid,
|
||||
replyType: _videoReplyController.videoType.replyType,
|
||||
);
|
||||
},
|
||||
tooltip: '发表评论',
|
||||
child: const Icon(Icons.reply),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user