diff --git a/lib/http/user.dart b/lib/http/user.dart index 8fceea41..404502b3 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -179,14 +179,16 @@ class UserHttp { } // 移除已观看 - static Future toViewDel() async { + static Future toViewDel({int? aid}) async { + final Map params = { + 'jsonp': 'jsonp', + 'csrf': await Request.getCsrf(), + }; + + params[aid != null ? 'aid' : 'viewed'] = aid ?? true; var res = await Request().post( Api.toViewDel, - queryParameters: { - 'jsonp': 'jsonp', - 'viewed': true, - 'csrf': await Request.getCsrf(), - }, + queryParameters: params, ); if (res.data['code'] == 0) { return {'status': true, 'msg': 'yeah!成功移除'}; diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index feaf60b9..89885ee9 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -112,7 +112,7 @@ class _PlDanmakuState extends State { duration: const Duration(milliseconds: 100), child: DanmakuView( createdController: (DanmakuController e) async { - _controller = e; + widget.playerController.danmakuController = _controller = e; }, option: DanmakuOption( fontSize: 15, diff --git a/lib/pages/later/controller.dart b/lib/pages/later/controller.dart index 6de9254c..3de51901 100644 --- a/lib/pages/later/controller.dart +++ b/lib/pages/later/controller.dart @@ -23,29 +23,38 @@ class LaterController extends GetxController { return res; } - Future toViewDel() async { + Future toViewDel({int? aid}) async { SmartDialog.show( useSystem: true, animationType: SmartAnimationType.centerFade_otherSlide, builder: (BuildContext context) { return AlertDialog( title: const Text('提示'), - content: const Text('即将删除所有已观看视频,此操作不可恢复。确定是否删除?'), + content: Text( + aid != null ? '即将移除该视频,确定是否移除' : '即将删除所有已观看视频,此操作不可恢复。确定是否删除?'), actions: [ TextButton( - onPressed: () => SmartDialog.dismiss(), - child: const Text('取消')), + onPressed: () => SmartDialog.dismiss(), + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), TextButton( onPressed: () async { - var res = await UserHttp.toViewDel(); + var res = await UserHttp.toViewDel(aid: aid); if (res['status']) { - laterList.clear(); - queryLaterList(); + if (aid != null) { + laterList.removeWhere((e) => e.aid == aid); + } else { + laterList.clear(); + queryLaterList(); + } } SmartDialog.dismiss(); SmartDialog.showToast(res['msg']); }, - child: const Text('确认删除'), + child: Text(aid != null ? '确认移除' : '确认删除'), ) ], ); @@ -64,8 +73,12 @@ class LaterController extends GetxController { content: const Text('确定要清空你的稍后再看列表吗?'), actions: [ TextButton( - onPressed: () => SmartDialog.dismiss(), - child: const Text('取消')), + onPressed: () => SmartDialog.dismiss(), + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), TextButton( onPressed: () async { var res = await UserHttp.toViewClear(); diff --git a/lib/pages/later/view.dart b/lib/pages/later/view.dart index 7c04f8dc..76527ca1 100644 --- a/lib/pages/later/view.dart +++ b/lib/pages/later/view.dart @@ -80,10 +80,12 @@ class _LaterPageState extends State { ? SliverList( delegate: SliverChildBuilderDelegate((context, index) { + var videoItem = _laterController.laterList[index]; return VideoCardH( - videoItem: _laterController.laterList[index], - source: 'later', - ); + videoItem: videoItem, + source: 'later', + longPress: () => _laterController.toViewDel( + aid: videoItem.aid)); }, childCount: _laterController.laterList.length), ) : _laterController.isLoading.value diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 85150349..1d296105 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; @@ -53,6 +54,7 @@ class MainController extends GetxController { final StreamController bottomBarStream = StreamController.broadcast(); Box setting = GStrorage.setting; + DateTime? _lastPressedAt; @override void onInit() { @@ -61,4 +63,16 @@ class MainController extends GetxController { Utils.checkUpdata(); } } + + Future onBackPressed(BuildContext context) { + if (_lastPressedAt == null || + DateTime.now().difference(_lastPressedAt!) > + const Duration(seconds: 2)) { + // 两次点击时间间隔超过2秒,重新记录时间戳 + _lastPressedAt = DateTime.now(); + SmartDialog.showToast("再按一次退出Pili"); + return Future.value(false); // 不退出应用 + } + return Future.value(true); // 退出应用 + } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index b771ab0f..c744098e 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -110,55 +110,58 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { MediaQuery.of(context).size.width * 9 / 16; localCache.put('sheetHeight', sheetHeight); localCache.put('statusBarHeight', statusBarHeight); - return Scaffold( - extendBody: true, - body: FadeTransition( - opacity: _fadeAnimation!, - child: SlideTransition( - position: Tween( - begin: const Offset(0, 0.5), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: _slideAnimation!, - curve: Curves.fastOutSlowIn, - reverseCurve: Curves.linear, + return WillPopScope( + onWillPop: () => _mainController.onBackPressed(context), + child: Scaffold( + extendBody: true, + body: FadeTransition( + opacity: _fadeAnimation!, + child: SlideTransition( + position: Tween( + begin: const Offset(0, 0.5), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _slideAnimation!, + curve: Curves.fastOutSlowIn, + reverseCurve: Curves.linear, + ), + ), + child: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _pageController, + onPageChanged: (index) { + selectedIndex = index; + setState(() {}); + }, + children: _mainController.pages, ), - ), - child: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: _pageController, - onPageChanged: (index) { - selectedIndex = index; - setState(() {}); - }, - children: _mainController.pages, ), ), - ), - bottomNavigationBar: StreamBuilder( - stream: _mainController.bottomBarStream.stream, - initialData: true, - builder: (context, AsyncSnapshot snapshot) { - return AnimatedSlide( - curve: Curves.easeInOutCubicEmphasized, - duration: const Duration(milliseconds: 1000), - offset: Offset(0, snapshot.data ? 0 : 1), - child: NavigationBar( - onDestinationSelected: (value) => setIndex(value), - selectedIndex: selectedIndex, - destinations: [ - ..._mainController.navigationBars.map((e) { - return NavigationDestination( - icon: e['icon'], - selectedIcon: e['selectIcon'], - label: e['label'], - ); - }).toList(), - ], - ), - ); - }, + bottomNavigationBar: StreamBuilder( + stream: _mainController.bottomBarStream.stream, + initialData: true, + builder: (context, AsyncSnapshot snapshot) { + return AnimatedSlide( + curve: Curves.easeInOutCubicEmphasized, + duration: const Duration(milliseconds: 1000), + offset: Offset(0, snapshot.data ? 0 : 1), + child: NavigationBar( + onDestinationSelected: (value) => setIndex(value), + selectedIndex: selectedIndex, + destinations: [ + ..._mainController.navigationBars.map((e) { + return NavigationDestination( + icon: e['icon'], + selectedIcon: e['selectIcon'], + label: e['label'], + ); + }).toList(), + ], + ), + ); + }, + ), ), ); } diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index 4ed55f96..bb262b57 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -55,6 +55,12 @@ class _ExtraSettingState extends State { defaultVal: true, callFn: (val) => {SmartDialog.showToast('下次启动时生效')}, ), + const SetSwitchItem( + title: '快速收藏', + subTitle: '点按收藏至默认,长按选择文件夹', + setKey: SettingBoxKey.enableQuickFav, + defaultVal: false, + ), ListTile( dense: false, title: Text('评论展示', style: titleStyle), diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart index 6d74c5b3..231c2dba 100644 --- a/lib/pages/setting/play_setting.dart +++ b/lib/pages/setting/play_setting.dart @@ -60,6 +60,18 @@ class _PlaySettingState extends State { setKey: SettingBoxKey.autoPlayEnable, defaultVal: true, ), + const SetSwitchItem( + title: '自动全屏', + subTitle: '视频开始播放时进入全屏', + setKey: SettingBoxKey.enableAutoEnter, + defaultVal: false, + ), + const SetSwitchItem( + title: '自动退出', + subTitle: '视频结束播放时退出全屏', + setKey: SettingBoxKey.enableAutoExit, + defaultVal: false, + ), const SetSwitchItem( title: '开启硬解', subTitle: '以较低功耗播放视频', @@ -90,6 +102,12 @@ class _PlaySettingState extends State { setKey: SettingBoxKey.enableAutoExit, defaultVal: false, ), + const SetSwitchItem( + title: '双击快退/快进', + subTitle: '左侧双击快退,右侧双击快进', + setKey: SettingBoxKey.enableQuickDouble, + defaultVal: true, + ), ListTile( dense: false, title: Text('默认画质', style: titleStyle), diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 9f756c7d..1a05f412 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -144,6 +144,8 @@ class VideoIntroController extends GetxController { // 获取收藏状态 Future queryHasFavVideo() async { + /// fix 延迟查询 + await Future.delayed(const Duration(milliseconds: 200)); var result = await VideoHttp.hasFavVideo(aid: IdUtils.bv2av(bvid)); if (result['status']) { hasFav.value = result["data"]['favoured']; @@ -275,7 +277,27 @@ class VideoIntroController extends GetxController { } // (取消)收藏 - Future actionFavVideo() async { + Future actionFavVideo({type = 'choose'}) async { + // 收藏至默认文件夹 + if (type == 'default') { + await queryVideoInFolder(); + int defaultFolderId = favFolderData.value.list!.first.id!; + int favStatus = favFolderData.value.list!.first.favState!; + print('favStatus: $favStatus'); + var result = await VideoHttp.favVideo( + aid: IdUtils.bv2av(bvid), + addIds: favStatus == 0 ? '$defaultFolderId' : '', + delIds: favStatus == 1 ? '$defaultFolderId' : '', + ); + if (result['status']) { + if (result['data']['prompt']) { + // 重新获取收藏状态 + await queryHasFavVideo(); + SmartDialog.showToast('✅ 操作成功'); + } + } + return; + } try { for (var i in favFolderData.value.list!.toList()) { if (i.favState == 1) { @@ -288,17 +310,19 @@ class VideoIntroController extends GetxController { // ignore: avoid_print print(e); } + SmartDialog.showLoading(msg: '请求中'); var result = await VideoHttp.favVideo( aid: IdUtils.bv2av(bvid), addIds: addMediaIdsNew.join(','), delIds: delMediaIdsNew.join(',')); + SmartDialog.dismiss(); if (result['status']) { if (result['data']['prompt']) { addMediaIdsNew = []; delMediaIdsNew = []; Get.back(); // 重新获取收藏状态 - queryHasFavVideo(); + await queryHasFavVideo(); SmartDialog.showToast('✅ 操作成功'); } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 75eef280..a0fa0b9e 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -122,6 +122,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { late final Map videoItem; Box localCache = GStrorage.localCache; + Box setting = GStrorage.setting; late double sheetHeight; late final bool loadingStatus; // 加载状态 @@ -150,19 +151,50 @@ class _VideoInfoState extends State with TickerProviderStateMixin { } // 收藏 - showFavBottomSheet() { + showFavBottomSheet({type = 'tap'}) { if (videoIntroController.userInfo == null) { SmartDialog.showToast('账号未登录'); return; } - showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - builder: (context) { - return FavPanel(ctr: videoIntroController); - }, - ); + bool enableDragQuickFav = + setting.get(SettingBoxKey.enableQuickFav, defaultValue: false); + // 快速收藏 & + // 点按 收藏至默认文件夹 + // 长按选择文件夹 + if (enableDragQuickFav) { + if (type == 'tap') { + if (!videoIntroController.hasFav.value) { + videoIntroController.actionFavVideo(type: 'default'); + } else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return FavPanel(ctr: videoIntroController); + }, + ); + } + } else { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return FavPanel(ctr: videoIntroController); + }, + ); + } + } else if (type != 'longPress') { + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (context) { + return FavPanel(ctr: videoIntroController); + }, + ); + } } // 视频介绍 @@ -510,6 +542,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { () => ActionRowItem( icon: const Icon(FontAwesomeIcons.heart), onTap: () => showFavBottomSheet(), + onLongPress: () => showFavBottomSheet(type: 'longPress'), selectStatus: videoIntroController.hasFav.value, loadingStatus: loadingStatus, text: !loadingStatus diff --git a/lib/pages/video/detail/introduction/widgets/action_row_item.dart b/lib/pages/video/detail/introduction/widgets/action_row_item.dart index 3aabe337..890a3a97 100644 --- a/lib/pages/video/detail/introduction/widgets/action_row_item.dart +++ b/lib/pages/video/detail/introduction/widgets/action_row_item.dart @@ -8,6 +8,7 @@ class ActionRowItem extends StatelessWidget { final bool? loadingStatus; final String? text; final bool selectStatus; + final Function? onLongPress; const ActionRowItem({ Key? key, @@ -17,6 +18,7 @@ class ActionRowItem extends StatelessWidget { this.loadingStatus, this.text, this.selectStatus = false, + this.onLongPress, }) : super(key: key); @override @@ -32,6 +34,12 @@ class ActionRowItem extends StatelessWidget { feedBack(), onTap!(), }, + onLongPress: () { + feedBack(); + if (onLongPress != null) { + onLongPress!(); + } + }, child: Padding( padding: const EdgeInsets.fromLTRB(15, 7, 15, 7), child: Row( diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 84a07070..6009b481 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -10,6 +10,7 @@ import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; +import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/video.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/utils/feed_back.dart'; @@ -196,6 +197,8 @@ class PlPlayerController { /// 弹幕开关 Rx isOpenDanmu = true.obs; + // 关联弹幕控制器 + DanmakuController? danmakuController; // 添加一个私有构造函数 PlPlayerController._() { @@ -312,7 +315,10 @@ class PlPlayerController { buffered.value = Duration.zero; _heartDuration = 0; _position.value = Duration.zero; - + // 初始化时清空弹幕,防止上次重叠 + if (danmakuController != null) { + danmakuController!.clear(); + } Player player = _videoPlayerController ?? Player( configuration: const PlayerConfiguration( @@ -778,8 +784,6 @@ class PlPlayerController { } toggleFullScreen(true); - print(headerControl); - print(danmuWidget); var result = await showDialog( context: Get.context!, useSafeArea: false, diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 67d25223..1b3703d4 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -69,6 +69,7 @@ class _PLVideoPlayerState extends State Box setting = GStrorage.setting; late FullScreenMode mode; late int defaultBtmProgressBehavior; + late bool enableQuickDouble; void onDoubleTapSeekBackward() { setState(() { @@ -82,6 +83,36 @@ class _PLVideoPlayerState extends State }); } + // 双击播放、暂停 + void onDoubleTapCenter() { + final _ = widget.controller; + if (_.playerStatus.status.value == PlayerStatus.playing) { + _.togglePlay(); + } else { + _.play(); + } + } + + doubleTapFuc(String type) { + if (!enableQuickDouble) { + onDoubleTapCenter(); + return; + } + switch (type) { + case 'left': + // 双击左边区域 👈 + onDoubleTapSeekBackward(); + break; + case 'center': + onDoubleTapCenter(); + break; + case 'right': + // 双击右边区域 👈 + onDoubleTapSeekForward(); + break; + } + } + @override void initState() { super.initState(); @@ -92,6 +123,8 @@ class _PLVideoPlayerState extends State widget.controller.danmuWidget = widget.danmuWidget; defaultBtmProgressBehavior = setting.get(SettingBoxKey.btmProgressBehavior, defaultValue: BtmProgresBehavior.values.first.code); + enableQuickDouble = + setting.get(SettingBoxKey.enableQuickDouble, defaultValue: true); Future.microtask(() async { try { @@ -427,26 +460,22 @@ class _PLVideoPlayerState extends State _.controls = !_.showControls.value; }, onDoubleTapDown: (details) { - // live模式下禁用 - if (_.videoType.value == 'live') { + // live模式下禁用 锁定时🔒禁用 + if (_.videoType.value == 'live' || _.controlsLock.value) { return; } final totalWidth = MediaQuery.of(context).size.width; final tapPosition = details.localPosition.dx; final sectionWidth = totalWidth / 3; + String type = 'left'; if (tapPosition < sectionWidth) { - // 双击左边区域 👈 - onDoubleTapSeekBackward(); + type = 'left'; } else if (tapPosition < sectionWidth * 2) { - if (_.playerStatus.status.value == PlayerStatus.playing) { - _.togglePlay(); - } else { - _.play(); - } + type = 'center'; } else { - // 双击右边区域 👈 - onDoubleTapSeekForward(); + type = 'right'; } + doubleTapFuc(type); }, onLongPressStart: (detail) { feedBack(); @@ -458,7 +487,8 @@ class _PLVideoPlayerState extends State /// 水平位置 快进 live模式下禁用 onHorizontalDragUpdate: (DragUpdateDetails details) { - if (_.videoType.value == 'live') { + // live模式下禁用 锁定时🔒禁用 + if (_.videoType.value == 'live' || _.controlsLock.value) { return; } final tapPosition = details.localPosition.dx; @@ -479,7 +509,7 @@ class _PLVideoPlayerState extends State _initTapPositoin = tapPosition; }, onHorizontalDragEnd: (DragEndDetails details) { - if (_.videoType.value == 'live') { + if (_.videoType.value == 'live' || _.controlsLock.value) { return; } _.onChangedSliderEnd(); @@ -491,6 +521,11 @@ class _PLVideoPlayerState extends State final tapPosition = details.localPosition.dx; final sectionWidth = totalWidth / 3; final delta = details.delta.dy; + + /// 锁定时禁用 + if (_.controlsLock.value) { + return; + } if (tapPosition < sectionWidth) { // 左边区域 👈 final brightness = _brightnessValue - delta / 100.0; @@ -626,7 +661,7 @@ class _PLVideoPlayerState extends State child: Align( alignment: Alignment.centerLeft, child: FractionalTranslation( - translation: const Offset(0.5, 0.0), + translation: const Offset(1, 0.0), child: Visibility( visible: _.showControls.value, child: ComBtn( diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 91094a39..de7ffd04 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -99,6 +99,8 @@ class SettingBoxKey { static const String enableAutoBrightness = 'enableAutoBrightness'; static const String enableAutoEnter = 'enableAutoEnter'; static const String enableAutoExit = 'enableAutoExit'; + // youtube 双击快进快退 + static const String enableQuickDouble = 'enableQuickDouble'; /// 隐私 static const String blackMidsList = 'blackMidsList'; @@ -108,6 +110,7 @@ class SettingBoxKey { static const String replySortType = 'replySortType'; static const String defaultDynamicType = 'defaultDynamicType'; static const String enableHotKey = 'enableHotKey'; + static const String enableQuickFav = 'enableQuickFav'; /// 外观 static const String themeMode = 'themeMode';