opt gesture

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-01-23 11:46:16 +08:00
parent 310f497c30
commit bd158619a4
17 changed files with 407 additions and 396 deletions

View File

@@ -1,5 +1,6 @@
import 'dart:math' show max;
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/gestures.dart' show HorizontalDragGestureRecognizer;
import 'package:flutter/material.dart';
@@ -12,8 +13,10 @@ abstract class CommonSlidePage extends StatefulWidget {
}
mixin CommonSlideMixin<T extends CommonSlidePage> on State<T>, TickerProvider {
static const double offset = 30.0;
double? _downDx;
late double _maxWidth;
double get maxWidth => _maxWidth;
late bool _isRTL = false;
late final bool enableSlide;
late final AnimationController _animController;
@@ -33,7 +36,6 @@ mixin CommonSlideMixin<T extends CommonSlidePage> on State<T>, TickerProvider {
_slideDragGestureRecognizer =
SlideDragGestureRecognizer(
isDxAllowed: (double dx) {
const offset = 30;
final isLTR = dx <= offset;
final isRTL = dx >= _maxWidth - offset;
if (isLTR || isRTL) {
@@ -111,6 +113,8 @@ mixin CommonSlideMixin<T extends CommonSlidePage> on State<T>, TickerProvider {
);
}
typedef IsDxAllowed = bool Function(double dx);
class SlideDragGestureRecognizer extends HorizontalDragGestureRecognizer {
SlideDragGestureRecognizer({
super.debugOwner,
@@ -119,7 +123,24 @@ class SlideDragGestureRecognizer extends HorizontalDragGestureRecognizer {
required this.isDxAllowed,
});
final bool Function(double dx) isDxAllowed;
final IsDxAllowed isDxAllowed;
@override
bool isPointerAllowed(PointerEvent event) {
return isDxAllowed(event.localPosition.dx) && super.isPointerAllowed(event);
}
}
class TabBarDragGestureRecognizer
extends CustomHorizontalDragGestureRecognizer {
TabBarDragGestureRecognizer({
super.debugOwner,
super.supportedDevices,
super.allowedButtonsFilter,
required this.isDxAllowed,
});
final IsDxAllowed isDxAllowed;
@override
bool isPointerAllowed(PointerEvent event) {

View File

@@ -7,7 +7,6 @@ import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart';
import 'package:PiliPlus/common/widgets/image/image_save.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/common/widgets/stat/stat.dart';
import 'package:PiliPlus/http/fav.dart';
import 'package:PiliPlus/http/loading_state.dart';
@@ -205,30 +204,42 @@ class _EpisodePanelState extends State<EpisodePanel>
super.dispose();
}
late final _isMulti =
widget.type == EpisodeType.season && widget.list.length > 1;
@override
Widget buildPage(ThemeData theme) {
final isMulti = widget.type == EpisodeType.season && widget.list.length > 1;
Widget tabBar() => TabBar(
controller: _tabController,
padding: const EdgeInsets.only(right: 60),
isScrollable: true,
tabs: widget.list.map((item) => Tab(text: item.title)).toList(),
dividerHeight: 1,
dividerColor: theme.dividerColor.withValues(alpha: 0.1),
return Material(
color: showTitle ? theme.colorScheme.surface : null,
type: showTitle ? MaterialType.canvas : MaterialType.transparency,
child: Column(
children: [
_buildToolbar(theme),
if (_isMulti)
TabBar(
controller: _tabController,
padding: const EdgeInsets.only(right: 60),
isScrollable: true,
tabs: widget.list.map((item) => Tab(text: item.title)).toList(),
dividerHeight: 1,
dividerColor: theme.dividerColor.withValues(alpha: 0.1),
),
Expanded(child: enableSlide ? slideList(theme) : buildList(theme)),
],
),
);
}
if (isMulti && enableSlide) {
return CustomTabBarView(
@override
Widget buildList(ThemeData theme) {
if (_isMulti) {
return TabBarView<TabBarDragGestureRecognizer>(
controller: _tabController,
physics: const CustomTabBarViewScrollPhysics(),
bgColor: theme.colorScheme.surface,
header: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildToolbar(theme),
tabBar(),
],
horizontalDragGestureRecognizer: TabBarDragGestureRecognizer(
isDxAllowed: (double dx) => enableSlide
? dx > CommonSlideMixin.offset &&
dx < maxWidth - CommonSlideMixin.offset
: true,
),
children: List.generate(
widget.list.length,
@@ -240,37 +251,6 @@ class _EpisodePanelState extends State<EpisodePanel>
),
);
}
return Material(
color: showTitle ? theme.colorScheme.surface : null,
type: showTitle ? MaterialType.canvas : MaterialType.transparency,
child: Column(
children: [
_buildToolbar(theme),
if (isMulti) ...[
tabBar(),
Expanded(
child: tabBarView(
controller: _tabController,
children: List.generate(
widget.list.length,
(index) => _buildBody(
theme,
index,
widget.list[index].episodes,
),
),
),
),
] else
Expanded(child: enableSlide ? slideList(theme) : buildList(theme)),
],
),
);
}
@override
Widget buildList(ThemeData theme) {
return _buildBody(theme, 0, _getCurrEpisodes);
}

View File

@@ -1,5 +1,7 @@
import 'package:PiliPlus/common/widgets/appbar/appbar.dart';
import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
@@ -10,7 +12,7 @@ import 'package:PiliPlus/pages/history/controller.dart';
import 'package:PiliPlus/pages/history/widgets/item.dart';
import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart';
import 'package:PiliPlus/utils/grid.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide TabBarView;
import 'package:get/get.dart';
class HistoryPage extends StatefulWidget {
@@ -130,6 +132,8 @@ class _HistoryPageState extends State<HistoryPage>
? const NeverScrollableScrollPhysics()
: const CustomTabBarViewScrollPhysics(),
controller: _historyController.tabController,
horizontalDragGestureRecognizer:
CustomHorizontalDragGestureRecognizer(),
children: [
KeepAliveWrapper(builder: (context) => child),
..._historyController.tabs.map(

View File

@@ -1,4 +1,6 @@
import 'package:PiliPlus/common/widgets/appbar/appbar.dart';
import 'package:PiliPlus/common/widgets/flutter/page/tabs.dart';
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
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';
@@ -11,7 +13,7 @@ import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/extension/get_ext.dart';
import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart';
import 'package:PiliPlus/utils/request_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide TabBarView;
import 'package:get/get.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
@@ -137,6 +139,8 @@ class _LaterPageState extends State<LaterPage>
? const NeverScrollableScrollPhysics()
: const CustomTabBarViewScrollPhysics(),
controller: _tabController,
horizontalDragGestureRecognizer:
CustomHorizontalDragGestureRecognizer(),
children: LaterViewType.values
.map((item) => item.page)
.toList(),

View File

@@ -5,7 +5,9 @@ import 'dart:ui';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/button/icon_button.dart';
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart';
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
@@ -41,7 +43,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:canvas_danmaku/canvas_danmaku.dart';
import 'package:floating/floating.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide PageView;
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart';
@@ -721,7 +723,7 @@ class _LiveRoomPageState extends State<LiveRoomPage>
return Padding(
padding: EdgeInsets.only(bottom: 12, top: isPortrait ? 12 : 0),
child: _liveRoomController.showSuperChat
? PageView(
? PageView<CustomHorizontalDragGestureRecognizer>(
key: pageKey,
controller: _liveRoomController.pageController,
physics: const CustomTabBarViewScrollPhysics(
@@ -729,6 +731,8 @@ class _LiveRoomPageState extends State<LiveRoomPage>
),
onPageChanged: (value) =>
_liveRoomController.pageIndex.value = value,
horizontalDragGestureRecognizer:
CustomHorizontalDragGestureRecognizer(),
children: [
KeepAliveWrapper(builder: (context) => chat()),
SuperChatPanel(

View File

@@ -1,8 +1,10 @@
import 'dart:io';
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart';
import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart';
import 'package:PiliPlus/common/widgets/flutter/tabs.dart';
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart';
import 'package:PiliPlus/common/widgets/image/network_img_layer.dart';
import 'package:PiliPlus/models/common/nav_bar_config.dart';
import 'package:PiliPlus/pages/home/view.dart';
@@ -18,7 +20,7 @@ import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/storage.dart';
import 'package:PiliPlus/utils/storage_key.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide PageView;
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:tray_manager/tray_manager.dart';
@@ -393,9 +395,11 @@ class _MainAppState extends PopScopeState<MainApp>
children: _mainController.navigationBars.map((i) => i.page).toList(),
);
} else {
child = PageView(
child = PageView<CustomHorizontalDragGestureRecognizer>(
physics: const NeverScrollableScrollPhysics(),
controller: _mainController.controller,
horizontalDragGestureRecognizer:
CustomHorizontalDragGestureRecognizer(),
children: _mainController.navigationBars.map((i) => i.page).toList(),
);
}

View File

@@ -3,6 +3,8 @@ import 'dart:math' show pi, max;
import 'package:PiliPlus/common/widgets/custom_icon.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'
show touchSlopH;
import 'package:PiliPlus/common/widgets/image/custom_grid_view.dart'
show CustomGridView, ImageModel;
import 'package:PiliPlus/common/widgets/pendant_avatar.dart';
@@ -187,11 +189,9 @@ List<SettingsModel> get extraSettings => [
leading: const Icon(Icons.notifications_none),
setKey: SettingBoxKey.checkDynamic,
defaultVal: true,
onChanged: (value) {
Get.find<MainController>().checkDynamic = value;
},
onChanged: (value) => Get.find<MainController>().checkDynamic = value,
onTap: (context) {
int dynamicPeriod = Pref.dynamicPeriod;
String dynamicPeriod = Pref.dynamicPeriod.toString();
showDialog(
context: context,
builder: (context) {
@@ -199,11 +199,9 @@ List<SettingsModel> get extraSettings => [
title: const Text('检查周期'),
content: TextFormField(
autofocus: true,
initialValue: dynamicPeriod.toString(),
initialValue: dynamicPeriod,
keyboardType: TextInputType.number,
onChanged: (value) {
dynamicPeriod = int.tryParse(value) ?? 5;
},
onChanged: (value) => dynamicPeriod = value,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(suffixText: 'min'),
),
@@ -219,13 +217,14 @@ List<SettingsModel> get extraSettings => [
),
TextButton(
onPressed: () {
Get.back();
GStorage.setting.put(
SettingBoxKey.dynamicPeriod,
dynamicPeriod,
);
Get.find<MainController>().dynamicPeriod =
dynamicPeriod * 60 * 1000;
try {
final val = int.parse(dynamicPeriod);
Get.back();
GStorage.setting.put(SettingBoxKey.dynamicPeriod, val);
Get.find<MainController>().dynamicPeriod = val * 60 * 1000;
} catch (e) {
SmartDialog.showToast(e.toString());
}
},
child: const Text('确定'),
),
@@ -312,9 +311,7 @@ List<SettingsModel> get extraSettings => [
autofocus: true,
initialValue: replyLengthLimit,
keyboardType: TextInputType.number,
onChanged: (value) {
replyLengthLimit = value;
},
onChanged: (value) => replyLengthLimit = value,
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
decoration: const InputDecoration(suffixText: ''),
),
@@ -365,12 +362,8 @@ List<SettingsModel> get extraSettings => [
content: TextFormField(
autofocus: true,
initialValue: danmakuLineHeight,
keyboardType: const TextInputType.numberWithOptions(
decimal: true,
),
onChanged: (value) {
danmakuLineHeight = value;
},
keyboardType: const .numberWithOptions(decimal: true),
onChanged: (value) => danmakuLineHeight = value,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')),
],
@@ -460,6 +453,56 @@ List<SettingsModel> get extraSettings => [
setKey: SettingBoxKey.openInBrowser,
defaultVal: false,
),
NormalModel(
title: '横向滑动阈值',
getSubtitle: () => '当前:「${Pref.touchSlopH}',
onTap: (context, setState) {
String initialValue = Pref.touchSlopH.toString();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('横向滑动阈值'),
content: TextFormField(
autofocus: true,
initialValue: initialValue,
keyboardType: const .numberWithOptions(decimal: true),
onChanged: (value) => initialValue = value,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')),
],
),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
try {
final val = double.parse(initialValue);
Get.back();
touchSlopH = val;
await GStorage.setting.put(SettingBoxKey.touchSlopH, val);
setState();
} catch (e) {
SmartDialog.showToast(e.toString());
}
},
child: const Text('确定'),
),
],
);
},
);
},
leading: const Icon(Icons.pan_tool_alt_outlined),
),
NormalModel(
title: '刷新滑动距离',
leading: const Icon(Icons.refresh),
@@ -687,9 +730,7 @@ List<SettingsModel> get extraSettings => [
),
setKey: SettingBoxKey.antiGoodsDyn,
defaultVal: false,
onChanged: (value) {
DynamicsDataModel.antiGoodsDyn = value;
},
onChanged: (value) => DynamicsDataModel.antiGoodsDyn = value,
),
SwitchModel(
title: '屏蔽带货评论',
@@ -703,9 +744,7 @@ List<SettingsModel> get extraSettings => [
),
setKey: SettingBoxKey.antiGoodsReply,
defaultVal: false,
onChanged: (value) {
ReplyGrpc.antiGoodsReply = value;
},
onChanged: (value) => ReplyGrpc.antiGoodsReply = value,
),
SwitchModel(
title: '侧滑关闭二级页面',
@@ -715,9 +754,7 @@ List<SettingsModel> get extraSettings => [
),
setKey: SettingBoxKey.slideDismissReplyPage,
defaultVal: Platform.isIOS,
onChanged: (value) {
CommonSlideMixin.slideDismissReplyPage = value;
},
onChanged: (value) => CommonSlideMixin.slideDismissReplyPage = value,
),
const SwitchModel(
title: '启用双指缩小视频',
@@ -856,9 +893,7 @@ List<SettingsModel> get extraSettings => [
leading: const Icon(Icons.search_outlined),
setKey: SettingBoxKey.enableWordRe,
defaultVal: false,
onChanged: (value) {
ReplyItemGrpc.enableWordRe = value;
},
onChanged: (value) => ReplyItemGrpc.enableWordRe = value,
),
const SwitchModel(
title: '启用AI总结',

View File

@@ -21,7 +21,7 @@ class PgcIntroPanel extends CommonSlidePage {
const PgcIntroPanel({
super.key,
required this.item,
super.enableSlide = false,
super.enableSlide,
this.videoTags,
});
@@ -50,42 +50,61 @@ class _IntroDetailState extends State<PgcIntroPanel>
@override
Widget buildPage(ThemeData theme) {
return CustomTabBarView(
controller: _tabController,
physics: const CustomTabBarViewScrollPhysics(),
bgColor: theme.colorScheme.surface,
header: Row(
return Material(
color: theme.colorScheme.surface,
child: Column(
children: [
Row(
children: [
Expanded(
child: TabBar(
controller: _tabController,
dividerHeight: 0,
isScrollable: true,
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
tabs: const [
Tab(text: '详情'),
Tab(text: '点评'),
],
onTap: (index) {
if (!_tabController.indexIsChanging) {
if (index == 0) {
_controller.animToTop();
}
}
},
),
),
IconButton(
tooltip: '关闭',
icon: const Icon(Icons.close, size: 20),
onPressed: Get.back,
),
const SizedBox(width: 2),
],
),
Expanded(
child: TabBar(
controller: _tabController,
dividerHeight: 0,
isScrollable: true,
tabAlignment: TabAlignment.start,
dividerColor: Colors.transparent,
tabs: const [
Tab(text: '详情'),
Tab(text: '点评'),
],
onTap: (index) {
if (!_tabController.indexIsChanging) {
if (index == 0) {
_controller.animToTop();
}
}
},
),
child: enableSlide ? slideList(theme) : buildList(theme),
),
IconButton(
tooltip: '关闭',
icon: const Icon(Icons.close, size: 20),
onPressed: Get.back,
),
const SizedBox(width: 2),
],
),
);
}
@override
Widget buildList(ThemeData theme) {
return TabBarView<TabBarDragGestureRecognizer>(
controller: _tabController,
physics: const CustomTabBarViewScrollPhysics(),
horizontalDragGestureRecognizer: TabBarDragGestureRecognizer(
isDxAllowed: (double dx) => enableSlide
? dx > CommonSlideMixin.offset &&
dx < maxWidth - CommonSlideMixin.offset
: true,
),
children: [
KeepAliveWrapper(builder: (context) => buildList(theme)),
KeepAliveWrapper(builder: (context) => _buildInfo(theme)),
PgcReviewPage(
name: widget.item.title!,
mediaId: widget.item.mediaId,
@@ -94,8 +113,7 @@ class _IntroDetailState extends State<PgcIntroPanel>
);
}
@override
Widget buildList(ThemeData theme) {
Widget _buildInfo(ThemeData theme) {
final TextStyle smallTitle = TextStyle(
fontSize: 12,
color: theme.colorScheme.onSurface,