diff --git a/.github/workflows/beta_ci.yml b/.github/workflows/beta_ci.yml index e839aca1..40f3f042 100644 --- a/.github/workflows/beta_ci.yml +++ b/.github/workflows/beta_ci.yml @@ -4,7 +4,7 @@ on: workflow_dispatch: push: branches: - - "main" + - "never" paths-ignore: - "**.md" - "**.txt" @@ -12,6 +12,7 @@ on: - ".idea/**" - "!.github/workflows/**" + jobs: update_version: name: Read and update version diff --git a/README.md b/README.md index 237bd07f..470e9a35 100644 --- a/README.md +++ b/README.md @@ -26,13 +26,13 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码 ```bash -[✓] Flutter (Channel stable, 3.16.4, on macOS 14.1.2 23B92 darwin-arm64, locale +[✓] Flutter (Channel stable, 3.16.5, on macOS 14.1.2 23B92 darwin-arm64, locale zh-Hans-CN) [✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 15.1) [✓] Chrome - develop for the web [✓] Android Studio (version 2022.3) -[✓] VS Code (version 1.85.1) +[✓] VS Code (version 1.87.2) [✓] Connected device (3 available) [✓] Network resources @@ -44,6 +44,9 @@ Xcode 13.4 不支持**auto_orientation**,请注释相关代码 ## 技术交流 Telegram: https://t.me/+lm_oOVmF0RJiODk1 + +Tg Beta版本:@PiliPala_Beta + QQ频道: https://pd.qq.com/s/365esodk3 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 8db59815..2c1a635b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -9,6 +9,7 @@ PODS: - Flutter - connectivity_plus (0.0.1): - Flutter + - FlutterMacOS - ReachabilitySwift - device_info_plus (0.0.1): - Flutter @@ -38,7 +39,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - ReachabilitySwift (5.0.0) - saver_gallery (0.0.1): @@ -71,7 +72,7 @@ DEPENDENCIES: - audio_service (from `.symlinks/plugins/audio_service/ios`) - audio_session (from `.symlinks/plugins/audio_session/ios`) - auto_orientation (from `.symlinks/plugins/auto_orientation/ios`) - - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) + - connectivity_plus (from `.symlinks/plugins/connectivity_plus/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - Flutter (from `Flutter`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) @@ -113,7 +114,7 @@ EXTERNAL SOURCES: auto_orientation: :path: ".symlinks/plugins/auto_orientation/ios" connectivity_plus: - :path: ".symlinks/plugins/connectivity_plus/ios" + :path: ".symlinks/plugins/connectivity_plus/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" Flutter: @@ -166,7 +167,7 @@ SPEC CHECKSUMS: audio_service: f509d65da41b9521a61f1c404dd58651f265a567 audio_session: 4f3e461722055d21515cf3261b64c973c062f345 auto_orientation: 102ed811a5938d52c86520ddd7ecd3a126b5d39d - connectivity_plus: 07c49e96d7fc92bc9920617b83238c4d178b446a + connectivity_plus: e2dad488011aeb593e219360e804c43cc1af5770 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 @@ -180,7 +181,7 @@ SPEC CHECKSUMS: media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 saver_gallery: 2b4e584106fde2407ab51560f3851564963e6b78 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 @@ -193,7 +194,7 @@ SPEC CHECKSUMS: volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 webview_cookie_manager: eaf920722b493bd0f7611b5484771ca53fed03f7 - webview_flutter_wkwebview: 2e2d318f21a5e036e2c3f26171342e95908bd60a + webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4 PODFILE CHECKSUM: 637cd290bed23275b5f5ffcc7eb1e73d0a5fb2be diff --git a/lib/common/pages_bottom_sheet.dart b/lib/common/pages_bottom_sheet.dart new file mode 100644 index 00000000..c64b58b6 --- /dev/null +++ b/lib/common/pages_bottom_sheet.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import '../models/common/video_episode_type.dart'; + +class EpisodeBottomSheet { + final List episodes; + final int currentCid; + final dynamic dataType; + final BuildContext context; + final Function changeFucCall; + final int? cid; + final double? sheetHeight; + bool isFullScreen = false; + + EpisodeBottomSheet({ + required this.episodes, + required this.currentCid, + required this.dataType, + required this.context, + required this.changeFucCall, + this.cid, + this.sheetHeight, + this.isFullScreen = false, + }); + + Widget buildEpisodeListItem( + dynamic episode, + int index, + bool isCurrentIndex, + ) { + Color primary = Theme.of(context).colorScheme.primary; + Color onSurface = Theme.of(context).colorScheme.onSurface; + + String title = ''; + switch (dataType) { + case VideoEpidoesType.videoEpisode: + title = episode.title; + break; + case VideoEpidoesType.videoPart: + title = episode.pagePart; + break; + case VideoEpidoesType.bangumiEpisode: + title = '第${episode.title}话 ${episode.longTitle!}'; + break; + } + return ListTile( + onTap: () { + SmartDialog.showToast('切换至「$title」'); + changeFucCall.call(episode, index); + }, + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.gif', + color: primary, + height: 12, + ) + : null, + title: Text( + title, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex ? primary : onSurface, + ), + ), + ); + } + + Widget buildTitle() { + return AppBar( + toolbarHeight: 45, + automaticallyImplyLeading: false, + centerTitle: false, + title: Text( + '合集(${episodes.length})', + style: Theme.of(context).textTheme.titleMedium, + ), + actions: !isFullScreen + ? [ + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () => Navigator.pop(context), + ), + const SizedBox(width: 14), + ] + : null, + ); + } + + Widget buildShowContent(BuildContext context) { + final ItemScrollController itemScrollController = ItemScrollController(); + int currentIndex = episodes.indexWhere((dynamic e) => e.cid == currentCid); + return StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + itemScrollController.jumpTo(index: currentIndex); + }); + return Container( + height: sheetHeight, + color: Theme.of(context).colorScheme.background, + child: Column( + children: [ + buildTitle(), + Expanded( + child: Material( + child: PageStorage( + bucket: PageStorageBucket(), + child: ScrollablePositionedList.builder( + itemScrollController: itemScrollController, + itemCount: episodes.length + 1, + itemBuilder: (BuildContext context, int index) { + bool isLastItem = index == episodes.length; + bool isCurrentIndex = currentIndex == index; + return isLastItem + ? SizedBox( + height: + MediaQuery.of(context).padding.bottom + 20, + ) + : buildEpisodeListItem( + episodes[index], + index, + isCurrentIndex, + ); + }, + ), + ), + ), + ), + ], + ), + ); + }); + } + + /// The [BuildContext] of the widget that calls the bottom sheet. + PersistentBottomSheetController show(BuildContext context) { + final PersistentBottomSheetController btmSheetCtr = showBottomSheet( + context: context, + builder: (BuildContext context) { + return buildShowContent(context); + }, + ); + return btmSheetCtr; + } +} diff --git a/lib/http/api.dart b/lib/http/api.dart index 445f6102..b6975c4b 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -490,8 +490,11 @@ class Api { /// 我的订阅 static const userSubFolder = '/x/v3/fav/folder/collected/list'; - /// 我的订阅详情 - static const userSubFolderDetail = '/x/space/fav/season/list'; + /// 我的订阅详情 type 21 + static const userSeasonList = '/x/space/fav/season/list'; + + /// 我的订阅详情 type 11 + static const userResourceList = '/x/v3/fav/resource/list'; /// 表情 static const emojiList = '/x/emote/user/panel/web'; @@ -506,4 +509,6 @@ class Api { /// 排行榜 static const String getRankApi = "/x/web-interface/ranking/v2"; + /// 取消订阅 + static const String cancelSub = '/x/v3/fav/season/unfav'; } diff --git a/lib/http/user.dart b/lib/http/user.dart index 7d3def4e..fea0a22e 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -330,12 +330,12 @@ class UserHttp { } } - static Future userSubFolderDetail({ + static Future userSeasonList({ required int seasonId, required int pn, required int ps, }) async { - var res = await Request().get(Api.userSubFolderDetail, data: { + var res = await Request().get(Api.userSeasonList, data: { 'season_id': seasonId, 'ps': ps, 'pn': pn, @@ -349,4 +349,50 @@ class UserHttp { return {'status': false, 'msg': res.data['message']}; } } + + static Future userResourceList({ + required int seasonId, + required int pn, + required int ps, + }) async { + var res = await Request().get(Api.userResourceList, data: { + 'media_id': seasonId, + 'ps': ps, + 'pn': pn, + 'keyword': '', + 'order': 'mtime', + 'type': 0, + 'tid': 0, + 'platform': 'web', + }); + if (res.data['code'] == 0) { + try { + return { + 'status': true, + 'data': SubDetailModelData.fromJson(res.data['data']) + }; + } catch (err) { + return {'status': false, 'msg': err}; + } + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + // 取消订阅 + static Future cancelSub({required int seasonId}) async { + var res = await Request().post( + Api.cancelSub, + queryParameters: { + 'platform': 'web', + 'season_id': seasonId, + 'csrf': await Request.getCsrf(), + }, + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } } diff --git a/lib/main.dart b/lib/main.dart index 44bb1dcd..3877685c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -20,6 +20,7 @@ import 'package:pilipala/services/disable_battery_opt.dart'; import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/utils/app_scheme.dart'; import 'package:pilipala/utils/data.dart'; +import 'package:pilipala/utils/global_data.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:media_kit/media_kit.dart'; // Provides [Player], [Media], [Playlist] etc. import 'package:pilipala/utils/recommend_filter.dart'; @@ -34,6 +35,7 @@ void main() async { .then((_) async { await GStrorage.init(); await setupServiceLocator(); + clearLogs(); Request(); await Request.setCookie(); RecommendFilter(); @@ -63,14 +65,8 @@ void main() async { }, ); - // 小白条、导航栏沉浸 - SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle( - systemNavigationBarColor: Colors.transparent, - systemNavigationBarDividerColor: Colors.transparent, - statusBarColor: Colors.transparent, - )); Data.init(); + GlobalData(); PiliSchame.init(); DisableBatteryOpt(); }); @@ -133,16 +129,36 @@ class MyApp extends StatelessWidget { brightness: Brightness.dark, ); } + + ThemeData themeData = ThemeData( + colorScheme: currentThemeValue == ThemeType.dark + ? darkColorScheme + : lightColorScheme, + ); + + // 小白条、导航栏沉浸 + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( + systemNavigationBarColor: GlobalData().enableMYBar + ? const Color(0x00010000) + : themeData.canvasColor, + systemNavigationBarDividerColor: GlobalData().enableMYBar + ? const Color(0x00010000) + : themeData.canvasColor, + systemNavigationBarIconBrightness: currentThemeValue == ThemeType.dark + ? Brightness.light + : Brightness.dark, + statusBarColor: Colors.transparent, + )); + // 图片缓存 // PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20; return GetMaterialApp( - title: 'PiLiPaLa', + title: 'PiliPala', theme: ThemeData( - // fontFamily: 'HarmonyOS', colorScheme: currentThemeValue == ThemeType.dark ? darkColorScheme : lightColorScheme, - useMaterial3: true, snackBarTheme: SnackBarThemeData( actionTextColor: lightColorScheme.primary, backgroundColor: lightColorScheme.secondaryContainer, @@ -159,11 +175,9 @@ class MyApp extends StatelessWidget { ), ), darkTheme: ThemeData( - // fontFamily: 'HarmonyOS', colorScheme: currentThemeValue == ThemeType.light ? lightColorScheme : darkColorScheme, - useMaterial3: true, snackBarTheme: SnackBarThemeData( actionTextColor: darkColorScheme.primary, backgroundColor: darkColorScheme.secondaryContainer, diff --git a/lib/models/common/nav_bar_config.dart b/lib/models/common/nav_bar_config.dart index 9ebe8e6f..64cebafb 100644 --- a/lib/models/common/nav_bar_config.dart +++ b/lib/models/common/nav_bar_config.dart @@ -1,5 +1,10 @@ import 'package:flutter/material.dart'; +import '../../pages/dynamics/index.dart'; +import '../../pages/home/index.dart'; +import '../../pages/media/index.dart'; +import '../../pages/rank/index.dart'; + List defaultNavigationBars = [ { 'id': 0, @@ -13,6 +18,7 @@ List defaultNavigationBars = [ ), 'label': "首页", 'count': 0, + 'page': const HomePage(), }, { 'id': 1, @@ -26,6 +32,7 @@ List defaultNavigationBars = [ ), 'label': "排行榜", 'count': 0, + 'page': const RankPage(), }, { 'id': 2, @@ -39,6 +46,7 @@ List defaultNavigationBars = [ ), 'label': "动态", 'count': 0, + 'page': const DynamicsPage(), }, { 'id': 3, @@ -52,5 +60,6 @@ List defaultNavigationBars = [ ), 'label': "媒体库", 'count': 0, + 'page': const MediaPage(), } ]; diff --git a/lib/models/common/rank_type.dart b/lib/models/common/rank_type.dart index 2ce6d3b5..07be15c2 100644 --- a/lib/models/common/rank_type.dart +++ b/lib/models/common/rank_type.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:pilipala/pages/rank/zone/index.dart'; enum RandType { @@ -74,7 +73,6 @@ List tabsConfig = [ ), 'label': '全站', 'type': RandType.all, - 'ctr': Get.put, 'page': const ZonePage(rid: 0), }, { @@ -84,7 +82,6 @@ List tabsConfig = [ ), 'label': '国创相关', 'type': RandType.creation, - 'ctr': Get.put, 'page': const ZonePage(rid: 168), }, { @@ -94,7 +91,6 @@ List tabsConfig = [ ), 'label': '动画', 'type': RandType.animation, - 'ctr': Get.put, 'page': const ZonePage(rid: 1), }, { @@ -104,7 +100,6 @@ List tabsConfig = [ ), 'label': '音乐', 'type': RandType.music, - 'ctr': Get.put, 'page': const ZonePage(rid: 3), }, { @@ -114,7 +109,6 @@ List tabsConfig = [ ), 'label': '舞蹈', 'type': RandType.dance, - 'ctr': Get.put, 'page': const ZonePage(rid: 129), }, { @@ -124,7 +118,6 @@ List tabsConfig = [ ), 'label': '游戏', 'type': RandType.game, - 'ctr': Get.put, 'page': const ZonePage(rid: 4), }, { @@ -134,7 +127,6 @@ List tabsConfig = [ ), 'label': '知识', 'type': RandType.knowledge, - 'ctr': Get.put, 'page': const ZonePage(rid: 36), }, { @@ -144,7 +136,6 @@ List tabsConfig = [ ), 'label': '科技', 'type': RandType.technology, - 'ctr': Get.put, 'page': const ZonePage(rid: 188), }, { @@ -154,7 +145,6 @@ List tabsConfig = [ ), 'label': '运动', 'type': RandType.sport, - 'ctr': Get.put, 'page': const ZonePage(rid: 234), }, { @@ -164,7 +154,6 @@ List tabsConfig = [ ), 'label': '汽车', 'type': RandType.car, - 'ctr': Get.put, 'page': const ZonePage(rid: 223), }, { @@ -174,7 +163,6 @@ List tabsConfig = [ ), 'label': '生活', 'type': RandType.life, - 'ctr': Get.put, 'page': const ZonePage(rid: 160), }, { @@ -184,7 +172,6 @@ List tabsConfig = [ ), 'label': '美食', 'type': RandType.food, - 'ctr': Get.put, 'page': const ZonePage(rid: 211), }, { @@ -194,7 +181,6 @@ List tabsConfig = [ ), 'label': '动物圈', 'type': RandType.animal, - 'ctr': Get.put, 'page': const ZonePage(rid: 217), }, { @@ -204,7 +190,6 @@ List tabsConfig = [ ), 'label': '鬼畜', 'type': RandType.madness, - 'ctr': Get.put, 'page': const ZonePage(rid: 119), }, { @@ -214,7 +199,6 @@ List tabsConfig = [ ), 'label': '时尚', 'type': RandType.fashion, - 'ctr': Get.put, 'page': const ZonePage(rid: 155), }, { @@ -224,7 +208,6 @@ List tabsConfig = [ ), 'label': '娱乐', 'type': RandType.entertainment, - 'ctr': Get.put, 'page': const ZonePage(rid: 5), }, { @@ -234,7 +217,6 @@ List tabsConfig = [ ), 'label': '影视', 'type': RandType.film, - 'ctr': Get.put, 'page': const ZonePage(rid: 181), } ]; diff --git a/lib/models/common/video_episode_type.dart b/lib/models/common/video_episode_type.dart new file mode 100644 index 00000000..4875438f --- /dev/null +++ b/lib/models/common/video_episode_type.dart @@ -0,0 +1,5 @@ +enum VideoEpidoesType { + videoEpisode, + videoPart, + bangumiEpisode, +} diff --git a/lib/models/member/info.dart b/lib/models/member/info.dart index 789131ee..83f94c54 100644 --- a/lib/models/member/info.dart +++ b/lib/models/member/info.dart @@ -47,18 +47,23 @@ class Vip { this.status, this.dueDate, this.label, + this.nicknameColor, }); int? type; int? status; int? dueDate; Map? label; + int? nicknameColor; Vip.fromJson(Map json) { type = json['type']; status = json['status']; dueDate = json['due_date']; label = json['label']; + nicknameColor = json['nickname_color'] == '' + ? null + : int.parse("0xFF${json['nickname_color'].replaceAll('#', '')}"); } } diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 0067791c..418fb99d 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -437,7 +437,8 @@ class SearchArticleItemModel { pubTime = json['pub_time']; like = json['like']; title = Em.regTitle(json['title']); - subTitle = json['title'].replaceAll(RegExp(r'<[^>]*>'), ''); + subTitle = + Em.decodeHtmlEntities(json['title'].replaceAll(RegExp(r'<[^>]*>'), '')); rankOffset = json['rank_offset']; mid = json['mid']; imageUrls = json['image_urls']; diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 12f0c053..2098302d 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -15,6 +15,10 @@ import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:share_plus/share_plus.dart'; +import '../../../common/pages_bottom_sheet.dart'; +import '../../../models/common/video_episode_type.dart'; +import '../../../utils/drawer.dart'; + class BangumiIntroController extends GetxController { // 视频bvid String bvid = Get.parameters['bvid']!; @@ -52,6 +56,7 @@ class BangumiIntroController extends GetxController { RxMap followStatus = {}.obs; int _tempThemeValue = -1; var userInfo; + PersistentBottomSheetController? bottomSheetController; @override void onInit() { @@ -291,4 +296,33 @@ class BangumiIntroController extends GetxController { int aid = episodes[nextIndex].aid!; changeSeasonOrbangu(bvid, cid, aid); } + + // 播放器底栏 选集 回调 + void showEposideHandler() { + late List episodes = bangumiDetail.value.episodes!; + VideoEpidoesType dataType = VideoEpidoesType.bangumiEpisode; + if (episodes.isEmpty) { + return; + } + VideoDetailController videoDetailCtr = + Get.find(tag: Get.arguments['heroTag']); + DrawerUtils.showRightDialog( + child: EpisodeBottomSheet( + episodes: episodes, + currentCid: videoDetailCtr.cid.value, + dataType: dataType, + context: Get.context!, + sheetHeight: Get.size.height, + isFullScreen: true, + changeFucCall: (item, index) { + changeSeasonOrbangu(item.bvid, item.cid, item.aid); + SmartDialog.dismiss(); + }, + ).buildShowContent(Get.context!), + ); + } + + hiddenEpisodeBottomSheet() { + bottomSheetController?.close(); + } } diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index 6255ffda..e47db480 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -138,6 +138,9 @@ class _BangumiInfoState extends State { cid = widget.cid!; videoDetailCtr.cid.listen((p0) { cid = p0; + if (!mounted) { + return; + } setState(() {}); }); } @@ -317,11 +320,12 @@ class _BangumiInfoState extends State { if (widget.bangumiDetail!.episodes!.isNotEmpty) ...[ BangumiPanel( pages: widget.bangumiDetail!.episodes!, - cid: cid ?? widget.bangumiDetail!.episodes!.first.cid, + cid: cid! ?? widget.bangumiDetail!.episodes!.first.cid!, sheetHeight: sheetHeight, changeFuc: (bvid, cid, aid) => bangumiIntroController.changeSeasonOrbangu(bvid, cid, aid), bangumiDetail: bangumiIntroController.bangumiDetail.value, + bangumiIntroController: bangumiIntroController, ) ], ], diff --git a/lib/pages/bangumi/view.dart b/lib/pages/bangumi/view.dart index f59f94a2..8759af65 100644 --- a/lib/pages/bangumi/view.dart +++ b/lib/pages/bangumi/view.dart @@ -2,13 +2,11 @@ import 'dart:async'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:nil/nil.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/http_error.dart'; -import 'package:pilipala/pages/home/index.dart'; -import 'package:pilipala/pages/main/index.dart'; +import 'package:pilipala/utils/main_stream.dart'; import 'controller.dart'; import 'widgets/bangumu_card_v.dart'; @@ -34,10 +32,6 @@ class _BangumiPageState extends State void initState() { super.initState(); scrollController = _bangumidController.scrollController; - StreamController mainStream = - Get.find().bottomBarStream; - StreamController searchBarStream = - Get.find().searchBarStream; _futureBuilderFuture = _bangumidController.queryBangumiListFeed(); _futureBuilderFutureFollow = _bangumidController.queryBangumiFollow(); scrollController.addListener( @@ -49,16 +43,7 @@ class _BangumiPageState extends State _bangumidController.onLoad(); }); } - - final ScrollDirection direction = - scrollController.position.userScrollDirection; - if (direction == ScrollDirection.forward) { - mainStream.add(true); - searchBarStream.add(true); - } else if (direction == ScrollDirection.reverse) { - mainStream.add(false); - searchBarStream.add(false); - } + handleScrollEvent(scrollController); }, ); } diff --git a/lib/pages/bangumi/widgets/bangumi_panel.dart b/lib/pages/bangumi/widgets/bangumi_panel.dart index 05fd814c..b01f3be7 100644 --- a/lib/pages/bangumi/widgets/bangumi_panel.dart +++ b/lib/pages/bangumi/widgets/bangumi_panel.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -6,31 +8,35 @@ import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import '../../../common/pages_bottom_sheet.dart'; +import '../../../models/common/video_episode_type.dart'; +import '../introduction/controller.dart'; class BangumiPanel extends StatefulWidget { const BangumiPanel({ super.key, required this.pages, - this.cid, + required this.cid, this.sheetHeight, this.changeFuc, this.bangumiDetail, + this.bangumiIntroController, }); final List pages; - final int? cid; + final int cid; final double? sheetHeight; final Function? changeFuc; final BangumiInfoModel? bangumiDetail; + final BangumiIntroController? bangumiIntroController; @override State createState() => _BangumiPanelState(); } class _BangumiPanelState extends State { - late int currentIndex; + late RxInt currentIndex = (-1).obs; final ScrollController listViewScrollCtr = ScrollController(); - final ScrollController listViewScrollCtr_2 = ScrollController(); Box userInfoCache = GStrorage.userInfo; dynamic userInfo; // 默认未开通 @@ -39,169 +45,72 @@ class _BangumiPanelState extends State { String heroTag = Get.arguments['heroTag']; late final VideoDetailController videoDetailCtr; final ItemScrollController itemScrollController = ItemScrollController(); + late PersistentBottomSheetController? _bottomSheetController; @override void initState() { super.initState(); - cid = widget.cid!; - currentIndex = widget.pages.indexWhere((e) => e.cid == cid); + cid = widget.cid; + videoDetailCtr = Get.find(tag: heroTag); + currentIndex.value = + widget.pages.indexWhere((EpisodeItem e) => e.cid == cid); scrollToIndex(); + videoDetailCtr.cid.listen((int p0) { + cid = p0; + currentIndex.value = + widget.pages.indexWhere((EpisodeItem e) => e.cid == cid); + scrollToIndex(); + }); + + /// 获取大会员状态 userInfo = userInfoCache.get('userInfoCache'); if (userInfo != null) { vipStatus = userInfo.vipStatus; } - videoDetailCtr = Get.find(tag: heroTag); - - videoDetailCtr.cid.listen((int p0) { - cid = p0; - setState(() {}); - currentIndex = widget.pages.indexWhere((EpisodeItem e) => e.cid == cid); - scrollToIndex(); - }); } @override void dispose() { listViewScrollCtr.dispose(); - listViewScrollCtr_2.dispose(); super.dispose(); } - Widget buildPageListItem( - EpisodeItem page, - int index, - bool isCurrentIndex, - ) { - Color primary = Theme.of(context).colorScheme.primary; - return ListTile( - onTap: () { - Get.back(); - setState(() { - changeFucCall(page, index); - }); - }, - dense: false, - leading: isCurrentIndex - ? Image.asset( - 'assets/images/live.gif', - color: primary, - height: 12, - ) - : null, - title: Text( - '第${page.title}话 ${page.longTitle!}', - style: TextStyle( - fontSize: 14, - color: isCurrentIndex - ? primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - trailing: page.badge != null - ? Text( - page.badge!, - style: TextStyle( - color: Theme.of(context).colorScheme.primary, - ), - ) - : const SizedBox(), - ); - } - - void showBangumiPanel() { - showBottomSheet( - context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - // await Future.delayed(const Duration(milliseconds: 200)); - // listViewScrollCtr_2.animateTo(currentIndex * 56, - // duration: const Duration(milliseconds: 500), - // curve: Curves.easeInOut); - itemScrollController.jumpTo(index: currentIndex); - }); - // 在这里使用 setState 更新状态 - return Container( - height: widget.sheetHeight, - color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - AppBar( - toolbarHeight: 45, - automaticallyImplyLeading: false, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '合集(${widget.pages.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - titleSpacing: 10, - ), - Expanded( - child: Material( - child: ScrollablePositionedList.builder( - itemCount: widget.pages.length + 1, - itemBuilder: (BuildContext context, int index) { - bool isLastItem = index == widget.pages.length; - bool isCurrentIndex = currentIndex == index; - return isLastItem - ? SizedBox( - height: - MediaQuery.of(context).padding.bottom + - 20, - ) - : buildPageListItem( - widget.pages[index], - index, - isCurrentIndex, - ); - }, - itemScrollController: itemScrollController, - ), - ), - ), - ], - ), - ); - }, - ); - }, - ); - } - void changeFucCall(item, i) async { if (item.badge != null && item.badge == '会员' && vipStatus != 1) { SmartDialog.showToast('需要大会员'); return; } - await widget.changeFuc!( + widget.changeFuc?.call( item.bvid, item.cid, item.aid, ); + _bottomSheetController?.close(); currentIndex = i; - setState(() {}); scrollToIndex(); } void scrollToIndex() { WidgetsBinding.instance.addPostFrameCallback((_) { // 在回调函数中获取更新后的状态 - listViewScrollCtr.animateTo(currentIndex * 150, - duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); + final double offset = min((currentIndex * 150) - 75, + listViewScrollCtr.position.maxScrollExtent); + if (currentIndex.value == 0) { + listViewScrollCtr.jumpTo(0); + } else { + listViewScrollCtr.animateTo( + offset, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } }); } @override Widget build(BuildContext context) { + Color primary = Theme.of(context).colorScheme.primary; + Color onSurface = Theme.of(context).colorScheme.onSurface; return Column( children: [ Padding( @@ -211,12 +120,14 @@ class _BangumiPanelState extends State { children: [ const Text('选集 '), Expanded( - child: Text( - ' 正在播放:${widget.pages[currentIndex].longTitle}', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.outline, + child: Obx( + () => Text( + ' 正在播放:${widget.pages[currentIndex.value].longTitle}', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline, + ), ), ), ), @@ -227,7 +138,17 @@ class _BangumiPanelState extends State { style: ButtonStyle( padding: MaterialStateProperty.all(EdgeInsets.zero), ), - onPressed: () => showBangumiPanel(), + onPressed: () { + widget.bangumiIntroController?.bottomSheetController = + _bottomSheetController = EpisodeBottomSheet( + currentCid: cid, + episodes: widget.pages, + changeFucCall: changeFucCall, + sheetHeight: widget.sheetHeight, + dataType: VideoEpidoesType.bangumiEpisode, + context: context, + ).show(context); + }, child: Text( '${widget.bangumiDetail!.newEp!['desc']}', style: const TextStyle(fontSize: 13), @@ -245,6 +166,8 @@ class _BangumiPanelState extends State { itemCount: widget.pages.length, itemExtent: 150, itemBuilder: (BuildContext context, int i) { + var page = widget.pages[i]; + bool isSelected = i == currentIndex.value; return Container( width: 150, margin: const EdgeInsets.only(right: 10), @@ -253,42 +176,37 @@ class _BangumiPanelState extends State { borderRadius: BorderRadius.circular(6), clipBehavior: Clip.hardEdge, child: InkWell( - onTap: () => changeFucCall(widget.pages[i], i), + onTap: () => changeFucCall(page, i), child: Padding( padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 10), + vertical: 8, + horizontal: 10, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - if (i == currentIndex) ...[ - Image.asset( - 'assets/images/live.png', - color: Theme.of(context).colorScheme.primary, - height: 12, - ), + if (isSelected) ...[ + Image.asset('assets/images/live.png', + color: primary, height: 12), const SizedBox(width: 6) ], Text( '第${i + 1}话', style: TextStyle( - fontSize: 13, - color: i == currentIndex - ? Theme.of(context).colorScheme.primary - : Theme.of(context) - .colorScheme - .onSurface), + fontSize: 13, + color: isSelected ? primary : onSurface, + ), ), const SizedBox(width: 2), - if (widget.pages[i].badge != null) ...[ + if (page.badge != null) ...[ const Spacer(), Text( - widget.pages[i].badge!, + page.badge!, style: TextStyle( fontSize: 12, - color: - Theme.of(context).colorScheme.primary, + color: primary, ), ), ] @@ -296,13 +214,12 @@ class _BangumiPanelState extends State { ), const SizedBox(height: 3), Text( - widget.pages[i].longTitle!, + page.longTitle!, maxLines: 1, style: TextStyle( - fontSize: 13, - color: i == currentIndex - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface), + fontSize: 13, + color: isSelected ? primary : onSurface, + ), overflow: TextOverflow.ellipsis, ) ], diff --git a/lib/pages/dlna/index.dart b/lib/pages/dlna/index.dart new file mode 100644 index 00000000..3ec5965b --- /dev/null +++ b/lib/pages/dlna/index.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:dlna_dart/dlna.dart'; +import 'package:flutter/material.dart'; + +class LiveDlnaPage extends StatefulWidget { + final String datasource; + + const LiveDlnaPage({Key? key, required this.datasource}) : super(key: key); + + @override + State createState() => _LiveDlnaPageState(); +} + +class _LiveDlnaPageState extends State { + final Map _deviceList = {}; + final DLNAManager searcher = DLNAManager(); + late final Timer stopSearchTimer; + String selectDeviceKey = ''; + bool isSearching = true; + + DLNADevice? get device => _deviceList[selectDeviceKey]; + + @override + void initState() { + stopSearchTimer = Timer(const Duration(seconds: 20), () { + setState(() => isSearching = false); + searcher.stop(); + }); + searcher.stop(); + startSearch(); + super.initState(); + } + + @override + void dispose() { + super.dispose(); + searcher.stop(); + stopSearchTimer.cancel(); + } + + void startSearch() async { + // clear old devices + isSearching = true; + selectDeviceKey = ''; + _deviceList.clear(); + setState(() {}); + // start search server + final m = await searcher.start(); + m.devices.stream.listen((deviceList) { + deviceList.forEach((key, value) { + _deviceList[key] = value; + }); + setState(() {}); + }); + // close the server, the closed server can be start by call searcher.start() + } + + void selectDevice(String key) { + if (selectDeviceKey.isNotEmpty) device?.pause(); + + selectDeviceKey = key; + device?.setUrl(widget.datasource); + device?.play(); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + Widget cur; + if (isSearching && _deviceList.isEmpty) { + cur = const Center(child: CircularProgressIndicator()); + } else if (_deviceList.isEmpty) { + cur = Center( + child: Text( + '没有找到设备', + style: Theme.of(context).textTheme.bodyLarge, + ), + ); + } else { + cur = ListView( + children: _deviceList.keys + .map((key) => ListTile( + contentPadding: const EdgeInsets.all(2), + title: Text(_deviceList[key]!.info.friendlyName), + subtitle: Text(key), + onTap: () => selectDevice(key), + )) + .toList(), + ); + } + + return AlertDialog( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('查找设备'), + IconButton( + onPressed: startSearch, + icon: const Icon(Icons.refresh_rounded), + ), + ], + ), + content: SizedBox( + height: 200, + width: 200, + child: cur, + ), + ); + } +} diff --git a/lib/pages/dynamics/detail/view.dart b/lib/pages/dynamics/detail/view.dart index 840cd33f..9da085f4 100644 --- a/lib/pages/dynamics/detail/view.dart +++ b/lib/pages/dynamics/detail/view.dart @@ -194,7 +194,7 @@ class _DynamicDetailPageState extends State centerTitle: false, titleSpacing: 0, title: StreamBuilder( - stream: titleStreamC.stream, + stream: titleStreamC.stream.distinct(), initialData: false, builder: (context, AsyncSnapshot snapshot) { return AnimatedOpacity( diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index fe594a43..82a555b1 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -3,15 +3,14 @@ import 'dart:async'; import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/common/skeleton/dynamic_card.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/no_data.dart'; import 'package:pilipala/models/dynamics/result.dart'; -import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/main_stream.dart'; import 'package:pilipala/utils/storage.dart'; import '../mine/controller.dart'; @@ -44,8 +43,6 @@ class _DynamicsPageState extends State _futureBuilderFuture = _dynamicsController.queryFollowDynamic(); _futureBuilderFutureUp = _dynamicsController.queryFollowUp(); scrollController = _dynamicsController.scrollController; - StreamController mainStream = - Get.find().bottomBarStream; scrollController.addListener( () async { if (scrollController.position.pixels >= @@ -55,14 +52,7 @@ class _DynamicsPageState extends State _dynamicsController.queryFollowDynamic(type: 'onLoad'); }); } - - final ScrollDirection direction = - scrollController.position.userScrollDirection; - if (direction == ScrollDirection.forward) { - mainStream.add(true); - } else if (direction == ScrollDirection.reverse) { - mainStream.add(false); - } + handleScrollEvent(scrollController); }, ); diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index d94f5149..74faa829 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -67,7 +67,7 @@ class _FavDetailPageState extends State { pinned: true, titleSpacing: 0, title: StreamBuilder( - stream: titleStreamC.stream, + stream: titleStreamC.stream.distinct(), initialData: false, builder: (context, AsyncSnapshot snapshot) { return AnimatedOpacity( diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index a83e118b..39c6931d 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -185,7 +185,7 @@ class HistoryItem extends StatelessWidget { ? '已看完' : '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}', right: 6.0, - bottom: 6.0, + bottom: 8.0, type: 'gray', ), // 右上角 @@ -258,6 +258,27 @@ class HistoryItem extends StatelessWidget { ), ), ), + videoItem.progress != 0 + ? Positioned( + left: 3, + right: 3, + bottom: 0, + child: ClipRRect( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular( + StyleString.imgRadius.x), + bottomRight: Radius.circular( + StyleString.imgRadius.x), + ), + child: LinearProgressIndicator( + value: videoItem.progress == -1 + ? 100 + : videoItem.progress / + videoItem.duration, + ), + ), + ) + : const SizedBox() ], ), VideoContent(videoItem: videoItem, ctr: ctr) diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index fb85be0b..ca70e1c4 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -35,7 +35,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { userLogin.value = userInfo != null; userFace.value = userInfo != null ? userInfo.face : ''; hideSearchBar = - setting.get(SettingBoxKey.hideSearchBar, defaultValue: true); + setting.get(SettingBoxKey.hideSearchBar, defaultValue: false); if (setting.get(SettingBoxKey.enableSearchWord, defaultValue: true)) { searchDefault(); } diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index b0cef90b..cc228f6b 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -171,7 +171,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { return StreamBuilder( - stream: stream, + stream: stream!.distinct(), initialData: true, builder: (BuildContext context, AsyncSnapshot snapshot) { final RxBool isUserLoggedIn = ctr!.userLogin; diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index 7a0a57ea..e2e20e73 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart'; @@ -9,9 +8,8 @@ import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; -import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/controller.dart'; -import 'package:pilipala/pages/main/index.dart'; +import 'package:pilipala/utils/main_stream.dart'; class HotPage extends StatefulWidget { const HotPage({Key? key}) : super(key: key); @@ -34,10 +32,6 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { super.initState(); _futureBuilderFuture = _hotController.queryHotFeed('init'); scrollController = _hotController.scrollController; - StreamController mainStream = - Get.find().bottomBarStream; - StreamController searchBarStream = - Get.find().searchBarStream; scrollController.addListener( () { if (scrollController.position.pixels >= @@ -47,16 +41,7 @@ class _HotPageState extends State with AutomaticKeepAliveClientMixin { _hotController.onLoad(); } } - - final ScrollDirection direction = - scrollController.position.userScrollDirection; - if (direction == ScrollDirection.forward) { - mainStream.add(true); - searchBarStream.add(true); - } else if (direction == ScrollDirection.reverse) { - mainStream.add(false); - searchBarStream.add(false); - } + handleScrollEvent(scrollController); }, ); } diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index f3f91c9e..c61d20b3 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -2,15 +2,13 @@ import 'dart:async'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart'; -import 'package:pilipala/pages/home/index.dart'; -import 'package:pilipala/pages/main/index.dart'; +import 'package:pilipala/utils/main_stream.dart'; import 'controller.dart'; import 'widgets/live_item.dart'; @@ -36,10 +34,6 @@ class _LivePageState extends State super.initState(); _futureBuilderFuture = _liveController.queryLiveList('init'); scrollController = _liveController.scrollController; - StreamController mainStream = - Get.find().bottomBarStream; - StreamController searchBarStream = - Get.find().searchBarStream; scrollController.addListener( () { if (scrollController.position.pixels >= @@ -49,16 +43,7 @@ class _LivePageState extends State _liveController.onLoad(); }); } - - final ScrollDirection direction = - scrollController.position.userScrollDirection; - if (direction == ScrollDirection.forward) { - mainStream.add(true); - searchBarStream.add(true); - } else if (direction == ScrollDirection.reverse) { - mainStream.add(false); - searchBarStream.add(false); - } + handleScrollEvent(scrollController); }, ); } diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 1e5c29c5..37981b1d 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -62,6 +62,11 @@ class _LiveRoomPageState extends State { controller: plPlayerController, liveRoomCtr: _liveRoomController, floating: floating, + onRefresh: () { + setState(() { + _futureBuilderFuture = _liveRoomController.queryLiveInfo(); + }); + }, ), ); } else { diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index 3c908d71..e5a9d6c9 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -14,10 +14,12 @@ class BottomControl extends StatefulWidget implements PreferredSizeWidget { final PlPlayerController? controller; final LiveRoomController? liveRoomCtr; final Floating? floating; + final Function? onRefresh; const BottomControl({ this.controller, this.liveRoomCtr, this.floating, + this.onRefresh, Key? key, }) : super(key: key); @@ -61,6 +63,14 @@ class _BottomControlState extends State { // ), // fuc: () => Get.back(), // ), + ComBtn( + icon: const Icon( + Icons.refresh_outlined, + size: 18, + color: Colors.white, + ), + fuc: widget.onRefresh, + ), const Spacer(), // ComBtn( // icon: const Icon( @@ -150,21 +160,3 @@ class _BottomControlState extends State { ); } } - -class MSliderTrackShape extends RoundedRectSliderTrackShape { - @override - Rect getPreferredRect({ - required RenderBox parentBox, - Offset offset = Offset.zero, - SliderThemeData? sliderTheme, - bool isEnabled = false, - bool isDiscrete = false, - }) { - const double trackHeight = 3; - final double trackLeft = offset.dx; - final double trackTop = - offset.dy + (parentBox.size.height - trackHeight) / 2 + 4; - final double trackWidth = parentBox.size.width; - return Rect.fromLTWH(trackLeft, trackTop, trackWidth, trackHeight); - } -} diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index ddbd364a..c2e5c322 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -6,23 +6,16 @@ import 'package:get/get.dart'; import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/http/common.dart'; -import 'package:pilipala/pages/dynamics/index.dart'; -import 'package:pilipala/pages/home/view.dart'; -import 'package:pilipala/pages/media/index.dart'; -import 'package:pilipala/pages/rank/index.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; import '../../models/common/dynamic_badge_mode.dart'; import '../../models/common/nav_bar_config.dart'; class MainController extends GetxController { - List pages = [ - const HomePage(), - const RankPage(), - const DynamicsPage(), - const MediaPage(), - ]; - RxList navigationBars = defaultNavigationBars.obs; + List pages = []; + RxList navigationBars = [].obs; + late List defaultNavTabs; + late List navBarSort; final StreamController bottomBarStream = StreamController.broadcast(); Box setting = GStrorage.setting; @@ -40,11 +33,8 @@ class MainController extends GetxController { if (setting.get(SettingBoxKey.autoUpdate, defaultValue: false)) { Utils.checkUpdata(); } - hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: true); - int defaultHomePage = - setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int; - selectedIndex = defaultNavigationBars - .indexWhere((item) => item['id'] == defaultHomePage); + hideTabBar = setting.get(SettingBoxKey.hideTabBar, defaultValue: false); + var userInfo = userInfoCache.get('userInfoCache'); userLogin.value = userInfo != null; dynamicBadgeType.value = DynamicBadgeMode.values[setting.get( @@ -53,6 +43,7 @@ class MainController extends GetxController { if (dynamicBadgeType.value != DynamicBadgeMode.hidden) { getUnreadDynamic(); } + setNavBarConfig(); } void onBackPressed(BuildContext context) { @@ -93,4 +84,21 @@ class MainController extends GetxController { } navigationBars.refresh(); } + + void setNavBarConfig() async { + defaultNavTabs = [...defaultNavigationBars]; + navBarSort = + setting.get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]); + defaultNavTabs.retainWhere((item) => navBarSort.contains(item['id'])); + defaultNavTabs.sort((a, b) => + navBarSort.indexOf(a['id']).compareTo(navBarSort.indexOf(b['id']))); + navigationBars.value = defaultNavTabs; + int defaultHomePage = + setting.get(SettingBoxKey.defaultHomePage, defaultValue: 0) as int; + int defaultIndex = + navigationBars.indexWhere((item) => item['id'] == defaultHomePage); + // 如果找不到匹配项,默认索引设置为0或其他合适的值 + selectedIndex = defaultIndex != -1 ? defaultIndex : 0; + pages = navigationBars.map((e) => e['page']).toList(); + } } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index c551e690..617fcf83 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -10,6 +10,7 @@ import 'package:pilipala/pages/media/index.dart'; import 'package:pilipala/pages/rank/index.dart'; import 'package:pilipala/utils/event_bus.dart'; import 'package:pilipala/utils/feed_back.dart'; +import 'package:pilipala/utils/global_data.dart'; import 'package:pilipala/utils/storage.dart'; import './controller.dart'; @@ -29,7 +30,6 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { int? _lastSelectTime; //上次点击时间 Box setting = GStrorage.setting; - late bool enableMYBar; @override void initState() { @@ -37,7 +37,6 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { _lastSelectTime = DateTime.now().millisecondsSinceEpoch; _mainController.pageController = PageController(initialPage: _mainController.selectedIndex); - enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true); } void setIndex(int value) async { @@ -127,81 +126,82 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { }, children: _mainController.pages, ), - bottomNavigationBar: StreamBuilder( - stream: _mainController.hideTabBar - ? _mainController.bottomBarStream.stream - : StreamController.broadcast().stream, - initialData: true, - builder: (context, AsyncSnapshot snapshot) { - return AnimatedSlide( - curve: Curves.easeInOutCubicEmphasized, - duration: const Duration(milliseconds: 500), - offset: Offset(0, snapshot.data ? 0 : 1), - child: Obx( - () => enableMYBar - ? NavigationBar( - onDestinationSelected: (value) => setIndex(value), - selectedIndex: _mainController.selectedIndex, - destinations: [ - ..._mainController.navigationBars.map((e) { - return NavigationDestination( - icon: Obx( - () => Badge( - label: - _mainController.dynamicBadgeType.value == + bottomNavigationBar: _mainController.navigationBars.length > 1 + ? StreamBuilder( + stream: _mainController.hideTabBar + ? _mainController.bottomBarStream.stream.distinct() + : StreamController.broadcast().stream, + initialData: true, + builder: (context, AsyncSnapshot snapshot) { + return AnimatedSlide( + curve: Curves.easeInOutCubicEmphasized, + duration: const Duration(milliseconds: 500), + offset: Offset(0, snapshot.data ? 0 : 1), + child: GlobalData().enableMYBar + ? NavigationBar( + onDestinationSelected: (value) => setIndex(value), + selectedIndex: _mainController.selectedIndex, + destinations: [ + ..._mainController.navigationBars.map((e) { + return NavigationDestination( + icon: Obx( + () => Badge( + label: _mainController + .dynamicBadgeType.value == DynamicBadgeMode.number ? Text(e['count'].toString()) : null, - padding: - const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: - _mainController.dynamicBadgeType.value != + padding: + const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: _mainController + .dynamicBadgeType.value != DynamicBadgeMode.hidden && e['count'] > 0, - child: e['icon'], - ), - ), - selectedIcon: e['selectIcon'], - label: e['label'], - ); - }).toList(), - ], - ) - : BottomNavigationBar( - currentIndex: _mainController.selectedIndex, - onTap: (value) => setIndex(value), - iconSize: 16, - selectedFontSize: 12, - unselectedFontSize: 12, - items: [ - ..._mainController.navigationBars.map((e) { - return BottomNavigationBarItem( - icon: Obx( - () => Badge( - label: - _mainController.dynamicBadgeType.value == + child: e['icon'], + ), + ), + selectedIcon: e['selectIcon'], + label: e['label'], + ); + }).toList(), + ], + ) + : BottomNavigationBar( + currentIndex: _mainController.selectedIndex, + type: BottomNavigationBarType.fixed, + onTap: (value) => setIndex(value), + iconSize: 16, + selectedFontSize: 12, + unselectedFontSize: 12, + items: [ + ..._mainController.navigationBars.map((e) { + return BottomNavigationBarItem( + icon: Obx( + () => Badge( + label: _mainController + .dynamicBadgeType.value == DynamicBadgeMode.number ? Text(e['count'].toString()) : null, - padding: - const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: - _mainController.dynamicBadgeType.value != + padding: + const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: _mainController + .dynamicBadgeType.value != DynamicBadgeMode.hidden && e['count'] > 0, - child: e['icon'], - ), - ), - activeIcon: e['selectIcon'], - label: e['label'], - ); - }).toList(), - ], - ), - ), - ); - }, - ), + child: e['icon'], + ), + ), + activeIcon: e['selectIcon'], + label: e['label'], + ); + }).toList(), + ], + ), + ); + }, + ) + : null, ), ); } diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index 460c5648..6541680a 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -1,13 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; -import 'package:media_kit/media_kit.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/models/user/fav_folder.dart'; -import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/media/index.dart'; +import 'package:pilipala/utils/main_stream.dart'; import 'package:pilipala/utils/utils.dart'; class MediaPage extends StatefulWidget { @@ -30,26 +28,11 @@ class _MediaPageState extends State super.initState(); mediaController = Get.put(MediaController()); _futureBuilderFuture = mediaController.queryFavFolder(); - ScrollController scrollController = mediaController.scrollController; - StreamController mainStream = - Get.find().bottomBarStream; - mediaController.userLogin.listen((status) { setState(() { _futureBuilderFuture = mediaController.queryFavFolder(); }); }); - scrollController.addListener( - () { - final ScrollDirection direction = - scrollController.position.userScrollDirection; - if (direction == ScrollDirection.forward) { - mainStream.add(true); - } else if (direction == ScrollDirection.reverse) { - mainStream.add(false); - } - }, - ); } @override diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 0663e94e..015750db 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -65,7 +65,7 @@ class _MemberPageState extends State children: [ AppBar( title: StreamBuilder( - stream: appbarStream.stream, + stream: appbarStream.stream.distinct(), initialData: false, builder: (BuildContext context, AsyncSnapshot snapshot) { return AnimatedOpacity( @@ -281,8 +281,8 @@ class _MemberPageState extends State future: _futureBuilderFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - Map data = snapshot.data!; - if (data['status']) { + Map? data = snapshot.data; + if (data != null && data['status']) { return Obx( () => Stack( alignment: AlignmentDirectional.center, @@ -302,7 +302,14 @@ class _MemberPageState extends State style: Theme.of(context) .textTheme .titleMedium! - .copyWith(fontWeight: FontWeight.bold), + .copyWith( + fontWeight: FontWeight.bold, + color: _memberController.memberInfo.value + .vip!.nicknameColor != + null + ? Color(_memberController.memberInfo + .value.vip!.nicknameColor!) + : null), )), const SizedBox(width: 2), if (_memberController.memberInfo.value.sex == '女') diff --git a/lib/pages/member/widgets/profile.dart b/lib/pages/member/widgets/profile.dart index 4bf7b7db..a708a35e 100644 --- a/lib/pages/member/widgets/profile.dart +++ b/lib/pages/member/widgets/profile.dart @@ -180,7 +180,9 @@ class ProfilePanel extends StatelessWidget { Obx( () => Expanded( child: TextButton( - onPressed: () => ctr.actionRelationMod(), + onPressed: () => loadingStatus + ? null + : ctr.actionRelationMod(), style: TextButton.styleFrom( foregroundColor: ctr.attribute.value == -1 ? Colors.transparent diff --git a/lib/pages/member/widgets/seasons.dart b/lib/pages/member/widgets/seasons.dart index 68c4077f..125c978f 100644 --- a/lib/pages/member/widgets/seasons.dart +++ b/lib/pages/member/widgets/seasons.dart @@ -18,45 +18,32 @@ class MemberSeasonsPanel extends StatelessWidget { itemBuilder: (context, index) { MemberSeasonsList item = data!.seasonsList![index]; return Padding( - padding: const EdgeInsets.only(bottom: 12, right: 4), + padding: const EdgeInsets.only(bottom: 12), child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(bottom: 12, left: 4), - child: Row( - children: [ - Text( - item.meta!.name!, - maxLines: 1, - style: Theme.of(context).textTheme.titleSmall!, - ), - const SizedBox(width: 10), - PBadge( - stack: 'relative', - size: 'small', - text: item.meta!.total.toString(), - ), - const Spacer(), - SizedBox( - width: 35, - height: 35, - child: IconButton( - onPressed: () => Get.toNamed( - '/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'), - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - icon: const Icon( - Icons.arrow_forward, - size: 20, - ), - ), - ) - ], + ListTile( + onTap: () => Get.toNamed( + '/memberSeasons?mid=${item.meta!.mid}&seasonId=${item.meta!.seasonId}'), + title: Text( + item.meta!.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleSmall!, + ), + dense: true, + leading: PBadge( + stack: 'relative', + size: 'small', + text: item.meta!.total.toString(), + ), + trailing: const Icon( + Icons.arrow_forward, + size: 20, ), ), + const SizedBox(height: 10), LayoutBuilder( builder: (context, boxConstraints) { return GridView.builder( diff --git a/lib/pages/member_coin/widgets/item.dart b/lib/pages/member_coin/widgets/item.dart index ea6e7ee1..de28585c 100644 --- a/lib/pages/member_coin/widgets/item.dart +++ b/lib/pages/member_coin/widgets/item.dart @@ -59,6 +59,7 @@ class MemberCoinsItem extends StatelessWidget { padding: const EdgeInsets.fromLTRB(5, 6, 0, 0), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( coinItem.title!, diff --git a/lib/pages/member_seasons/widgets/item.dart b/lib/pages/member_seasons/widgets/item.dart index 6398c5eb..4df74b70 100644 --- a/lib/pages/member_seasons/widgets/item.dart +++ b/lib/pages/member_seasons/widgets/item.dart @@ -25,7 +25,7 @@ class MemberSeasonsItem extends StatelessWidget { child: InkWell( onTap: () async { int cid = - await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid); + await SearchHttp.ab2c(aid: seasonItem.aid, bvid: seasonItem.bvid); Get.toNamed('/video?bvid=${seasonItem.bvid}&cid=$cid', arguments: {'videoItem': seasonItem, 'heroTag': heroTag}); }, @@ -51,8 +51,7 @@ class MemberSeasonsItem extends StatelessWidget { bottom: 6, right: 6, type: 'gray', - text: Utils.CustomStamp_str( - timestamp: seasonItem.pubdate, date: 'YY-MM-DD'), + text: Utils.timeFormat(seasonItem.duration), ) ], ); diff --git a/lib/pages/rank/controller.dart b/lib/pages/rank/controller.dart index da73ea02..fa906fc9 100644 --- a/lib/pages/rank/controller.dart +++ b/lib/pages/rank/controller.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; import 'package:pilipala/models/common/rank_type.dart'; +import 'package:pilipala/pages/rank/zone/index.dart'; import 'package:pilipala/utils/storage.dart'; class RankController extends GetxController with GetTickerProviderStateMixin { @@ -29,20 +30,22 @@ class RankController extends GetxController with GetTickerProviderStateMixin { void onRefresh() { int index = tabController.index; - var ctr = tabsCtrList[index]; - ctr().onRefresh(); + final ZoneController ctr = tabsCtrList[index]; + ctr.onRefresh(); } void animateToTop() { int index = tabController.index; - var ctr = tabsCtrList[index]; - ctr().animateToTop(); + final ZoneController ctr = tabsCtrList[index]; + ctr.animateToTop(); } void setTabConfig() async { tabs.value = tabsConfig; initialIndex.value = 0; - tabsCtrList = tabs.map((e) => e['ctr']).toList(); + tabsCtrList = tabs + .map((e) => Get.put(ZoneController(), tag: e['rid'].toString())) + .toList(); tabsPageList = tabs.map((e) => e['page']).toList(); tabController = TabController( @@ -50,21 +53,5 @@ class RankController extends GetxController with GetTickerProviderStateMixin { length: tabs.length, vsync: this, ); - // 监听 tabController 切换 - if (enableGradientBg) { - tabController.animation!.addListener(() { - if (tabController.indexIsChanging) { - if (initialIndex.value != tabController.index) { - initialIndex.value = tabController.index; - } - } else { - final int temp = tabController.animation!.value.round(); - if (initialIndex.value != temp) { - initialIndex.value = temp; - tabController.index = initialIndex.value; - } - } - }); - } } } diff --git a/lib/pages/rank/view.dart b/lib/pages/rank/view.dart index 7b5b4906..4efa2b4e 100644 --- a/lib/pages/rank/view.dart +++ b/lib/pages/rank/view.dart @@ -102,7 +102,7 @@ class _RankPageState extends State onTap: (value) { feedBack(); if (_rankController.initialIndex.value == value) { - _rankController.tabsCtrList[value]().animateToTop(); + _rankController.tabsCtrList[value].animateToTop(); } _rankController.initialIndex.value = value; }, diff --git a/lib/pages/rank/zone/controller.dart b/lib/pages/rank/zone/controller.dart index f9f4dc6e..71f27b93 100644 --- a/lib/pages/rank/zone/controller.dart +++ b/lib/pages/rank/zone/controller.dart @@ -42,12 +42,15 @@ class ZoneController extends GetxController { // 返回顶部并刷新 void animateToTop() async { - if (scrollController.offset >= - MediaQuery.of(Get.context!).size.height * 5) { - scrollController.jumpTo(0); - } else { - await scrollController.animateTo(0, - duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); + if (scrollController.hasClients) { + if (scrollController.offset >= + MediaQuery.of(Get.context!).size.height * 5) { + scrollController.jumpTo(0); + } else { + await scrollController.animateTo(0, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut); + } } } } diff --git a/lib/pages/rank/zone/view.dart b/lib/pages/rank/zone/view.dart index fbf8a524..04631a8c 100644 --- a/lib/pages/rank/zone/view.dart +++ b/lib/pages/rank/zone/view.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/widgets/animated_dialog.dart'; @@ -9,9 +8,8 @@ import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/skeleton/video_card_h.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/video_card_h.dart'; -import 'package:pilipala/pages/home/index.dart'; -import 'package:pilipala/pages/main/index.dart'; import 'package:pilipala/pages/rank/zone/index.dart'; +import 'package:pilipala/utils/main_stream.dart'; class ZonePage extends StatefulWidget { const ZonePage({Key? key, required this.rid}) : super(key: key); @@ -38,10 +36,6 @@ class _ZonePageState extends State _zoneController = Get.put(ZoneController(), tag: widget.rid.toString()); _futureBuilderFuture = _zoneController.queryRankFeed('init', widget.rid); scrollController = _zoneController.scrollController; - StreamController mainStream = - Get.find().bottomBarStream; - StreamController searchBarStream = - Get.find().searchBarStream; scrollController.addListener( () { if (scrollController.position.pixels >= @@ -51,16 +45,7 @@ class _ZonePageState extends State _zoneController.onLoad(); } } - - final ScrollDirection direction = - scrollController.position.userScrollDirection; - if (direction == ScrollDirection.forward) { - mainStream.add(true); - searchBarStream.add(true); - } else if (direction == ScrollDirection.reverse) { - mainStream.add(false); - searchBarStream.add(false); - } + handleScrollEvent(scrollController); }, ); } diff --git a/lib/pages/rcmd/view.dart b/lib/pages/rcmd/view.dart index d732f370..acc1e654 100644 --- a/lib/pages/rcmd/view.dart +++ b/lib/pages/rcmd/view.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:pilipala/common/constants.dart'; import 'package:pilipala/common/skeleton/video_card_v.dart'; @@ -10,8 +9,7 @@ import 'package:pilipala/common/widgets/animated_dialog.dart'; import 'package:pilipala/common/widgets/http_error.dart'; import 'package:pilipala/common/widgets/overlay_pop.dart'; import 'package:pilipala/common/widgets/video_card_v.dart'; -import 'package:pilipala/pages/home/index.dart'; -import 'package:pilipala/pages/main/index.dart'; +import 'package:pilipala/utils/main_stream.dart'; import 'controller.dart'; @@ -35,10 +33,6 @@ class _RcmdPageState extends State super.initState(); _futureBuilderFuture = _rcmdController.queryRcmdFeed('init'); ScrollController scrollController = _rcmdController.scrollController; - StreamController mainStream = - Get.find().bottomBarStream; - StreamController searchBarStream = - Get.find().searchBarStream; scrollController.addListener( () { if (scrollController.position.pixels >= @@ -49,15 +43,7 @@ class _RcmdPageState extends State _rcmdController.onLoad(); }); } - final ScrollDirection direction = - scrollController.position.userScrollDirection; - if (direction == ScrollDirection.forward) { - mainStream.add(true); - searchBarStream.add(true); - } else if (direction == ScrollDirection.reverse) { - mainStream.add(false); - searchBarStream.add(false); - } + handleScrollEvent(scrollController); }, ); } diff --git a/lib/pages/setting/pages/navigation_bar_set.dart b/lib/pages/setting/pages/navigation_bar_set.dart new file mode 100644 index 00000000..09e92bc3 --- /dev/null +++ b/lib/pages/setting/pages/navigation_bar_set.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:hive/hive.dart'; +import 'package:pilipala/models/common/tab_type.dart'; +import 'package:pilipala/utils/storage.dart'; + +import '../../../models/common/nav_bar_config.dart'; + +class NavigationBarSetPage extends StatefulWidget { + const NavigationBarSetPage({super.key}); + + @override + State createState() => _NavigationbarSetPageState(); +} + +class _NavigationbarSetPageState extends State { + Box settingStorage = GStrorage.setting; + late List defaultNavTabs; + late List navBarSort; + + @override + void initState() { + super.initState(); + defaultNavTabs = defaultNavigationBars; + navBarSort = settingStorage + .get(SettingBoxKey.navBarSort, defaultValue: [0, 1, 2, 3]); + // 对 tabData 进行排序 + defaultNavTabs.sort((a, b) { + int indexA = navBarSort.indexOf(a['id']); + int indexB = navBarSort.indexOf(b['id']); + + // 如果类型在 sortOrder 中不存在,则放在末尾 + if (indexA == -1) indexA = navBarSort.length; + if (indexB == -1) indexB = navBarSort.length; + + return indexA.compareTo(indexB); + }); + } + + void saveEdit() { + List sortedTabbar = defaultNavTabs + .where((i) => navBarSort.contains(i['id'])) + .map((i) => i['id']) + .toList(); + settingStorage.put(SettingBoxKey.navBarSort, sortedTabbar); + SmartDialog.showToast('保存成功,下次启动时生效'); + } + + void onReorder(int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final tabsItem = defaultNavTabs.removeAt(oldIndex); + defaultNavTabs.insert(newIndex, tabsItem); + }); + } + + @override + Widget build(BuildContext context) { + final listTiles = [ + for (int i = 0; i < defaultNavTabs.length; i++) ...[ + CheckboxListTile( + key: Key(defaultNavTabs[i]['label']), + value: navBarSort.contains(defaultNavTabs[i]['id']), + onChanged: (bool? newValue) { + int tabTypeId = defaultNavTabs[i]['id']; + if (!newValue!) { + navBarSort.remove(tabTypeId); + } else { + navBarSort.add(tabTypeId); + } + setState(() {}); + }, + title: Text(defaultNavTabs[i]['label']), + secondary: const Icon(Icons.drag_indicator_rounded), + enabled: defaultNavTabs[i]['id'] != 0, + ) + ] + ]; + + return Scaffold( + appBar: AppBar( + title: const Text('Navbar编辑'), + actions: [ + TextButton(onPressed: () => saveEdit(), child: const Text('保存')), + const SizedBox(width: 12) + ], + ), + body: ReorderableListView( + onReorder: onReorder, + physics: const NeverScrollableScrollPhysics(), + footer: SizedBox( + height: MediaQuery.of(context).padding.bottom + 30, + ), + children: listTiles, + ), + ); + } +} diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index 30b9a30f..364eabf0 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -284,12 +284,17 @@ class _StyleSettingState extends State { onTap: () => Get.toNamed('/tabbarSetting'), title: Text('首页tabbar', style: titleStyle), ), + ListTile( + dense: false, + onTap: () => Get.toNamed('/navbarSetting'), + title: Text('底部导航栏设置', style: titleStyle), + ), if (Platform.isAndroid) ListTile( dense: false, onTap: () => Get.toNamed('/displayModeSetting'), title: Text('屏幕帧率', style: titleStyle), - ) + ), ], ), ); diff --git a/lib/pages/setting/widgets/select_dialog.dart b/lib/pages/setting/widgets/select_dialog.dart index 72119755..50229f9e 100644 --- a/lib/pages/setting/widgets/select_dialog.dart +++ b/lib/pages/setting/widgets/select_dialog.dart @@ -44,6 +44,7 @@ class _SelectDialogState extends State> { setState(() { _tempValue = value as T; }); + Navigator.pop(context, _tempValue); }, ), ] @@ -51,19 +52,6 @@ class _SelectDialogState extends State> { ), ); }), - actions: [ - TextButton( - onPressed: () => Navigator.pop(context), - child: Text( - '取消', - style: TextStyle(color: Theme.of(context).colorScheme.outline), - ), - ), - TextButton( - onPressed: () => Navigator.pop(context, _tempValue), - child: const Text('确定'), - ) - ], ); } } diff --git a/lib/pages/subscription/controller.dart b/lib/pages/subscription/controller.dart index bf0c593c..7be8d22c 100644 --- a/lib/pages/subscription/controller.dart +++ b/lib/pages/subscription/controller.dart @@ -46,4 +46,40 @@ class SubController extends GetxController { Future onLoad() async { querySubFolder(type: 'onload'); } + + // 取消订阅 + Future cancelSub(SubFolderItemData subFolderItem) async { + showDialog( + context: Get.context!, + builder: (context) => AlertDialog( + title: const Text('提示'), + content: const Text('确定取消订阅吗?'), + actions: [ + TextButton( + onPressed: () { + Get.back(); + }, + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + TextButton( + onPressed: () async { + var res = await UserHttp.cancelSub(seasonId: subFolderItem.id!); + if (res['status']) { + subFolderData.value.list!.remove(subFolderItem); + subFolderData.update((val) {}); + SmartDialog.showToast('取消订阅成功'); + } else { + SmartDialog.showToast(res['msg']); + } + Get.back(); + }, + child: const Text('确定'), + ), + ], + ), + ); + } } diff --git a/lib/pages/subscription/view.dart b/lib/pages/subscription/view.dart index 1eee4a4f..2d7d0cb5 100644 --- a/lib/pages/subscription/view.dart +++ b/lib/pages/subscription/view.dart @@ -58,7 +58,8 @@ class _SubPageState extends State { itemBuilder: (context, index) { return SubItem( subFolderItem: - _subController.subFolderData.value.list![index]); + _subController.subFolderData.value.list![index], + cancelSub: _subController.cancelSub); }, ), ); diff --git a/lib/pages/subscription/widgets/item.dart b/lib/pages/subscription/widgets/item.dart index fd08ffa5..5b2a0134 100644 --- a/lib/pages/subscription/widgets/item.dart +++ b/lib/pages/subscription/widgets/item.dart @@ -8,7 +8,12 @@ import '../../../models/user/sub_folder.dart'; class SubItem extends StatelessWidget { final SubFolderItemData subFolderItem; - const SubItem({super.key, required this.subFolderItem}); + final Function(SubFolderItemData) cancelSub; + const SubItem({ + super.key, + required this.subFolderItem, + required this.cancelSub, + }); @override Widget build(BuildContext context) { @@ -51,7 +56,10 @@ class SubItem extends StatelessWidget { }, ), ), - VideoContent(subFolderItem: subFolderItem) + VideoContent( + subFolderItem: subFolderItem, + cancelSub: cancelSub, + ) ], ), ); @@ -64,7 +72,8 @@ class SubItem extends StatelessWidget { class VideoContent extends StatelessWidget { final SubFolderItemData subFolderItem; - const VideoContent({super.key, required this.subFolderItem}); + final Function(SubFolderItemData)? cancelSub; + const VideoContent({super.key, required this.subFolderItem, this.cancelSub}); @override Widget build(BuildContext context) { @@ -100,6 +109,24 @@ class VideoContent extends StatelessWidget { color: Theme.of(context).colorScheme.outline, ), ), + const Spacer(), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: 35, + width: 35, + child: IconButton( + onPressed: () => cancelSub?.call(subFolderItem), + style: TextButton.styleFrom( + foregroundColor: Theme.of(context).colorScheme.outline, + padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + ), + icon: const Icon(Icons.delete_outline, size: 18), + ), + ) + ], + ) ], ), ), diff --git a/lib/pages/subscription_detail/controller.dart b/lib/pages/subscription_detail/controller.dart index 6ecb894e..4245df2c 100644 --- a/lib/pages/subscription_detail/controller.dart +++ b/lib/pages/subscription_detail/controller.dart @@ -6,7 +6,6 @@ import '../../models/user/sub_folder.dart'; class SubDetailController extends GetxController { late SubFolderItemData item; - late int seasonId; late String heroTag; int currentPage = 1; @@ -26,17 +25,23 @@ class SubDetailController extends GetxController { super.onInit(); } - Future queryUserSubFolderDetail({type = 'init'}) async { + Future queryUserSeasonList({type = 'init'}) async { if (type == 'onLoad' && subList.length >= mediaCount) { loadingText.value = '没有更多了'; return; } isLoadingMore = true; - var res = await UserHttp.userSubFolderDetail( - seasonId: seasonId, - ps: 20, - pn: currentPage, - ); + var res = type == 21 + ? await UserHttp.userSeasonList( + seasonId: seasonId, + ps: 20, + pn: currentPage, + ) + : await UserHttp.userResourceList( + seasonId: seasonId, + ps: 20, + pn: currentPage, + ); if (res['status']) { subInfo.value = res['data'].info; if (currentPage == 1 && type == 'init') { @@ -55,6 +60,6 @@ class SubDetailController extends GetxController { } onLoad() { - queryUserSubFolderDetail(type: 'onLoad'); + queryUserSeasonList(type: 'onLoad'); } } diff --git a/lib/pages/subscription_detail/view.dart b/lib/pages/subscription_detail/view.dart index d56125cd..2c219e58 100644 --- a/lib/pages/subscription_detail/view.dart +++ b/lib/pages/subscription_detail/view.dart @@ -26,13 +26,11 @@ class _SubDetailPageState extends State { Get.put(SubDetailController()); late StreamController titleStreamC; // a late Future _futureBuilderFuture; - late String seasonId; @override void initState() { super.initState(); - seasonId = Get.parameters['seasonId']!; - _futureBuilderFuture = _subDetailController.queryUserSubFolderDetail(); + _futureBuilderFuture = _subDetailController.queryUserSeasonList(); titleStreamC = StreamController(); _controller.addListener( () { @@ -69,7 +67,7 @@ class _SubDetailPageState extends State { pinned: true, titleSpacing: 0, title: StreamBuilder( - stream: titleStreamC.stream, + stream: titleStreamC.stream.distinct(), initialData: false, builder: (context, AsyncSnapshot snapshot) { return AnimatedOpacity( @@ -161,15 +159,18 @@ class _SubDetailPageState extends State { ), ), const SizedBox(height: 4), - Text( - '${Utils.numFormat(_subDetailController.item.viewCount)}次播放', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize, - color: Theme.of(context).colorScheme.outline), - ), + Obx( + () => Text( + '${Utils.numFormat(_subDetailController.subInfo.value.cntInfo?['play'])}次播放', + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .labelSmall! + .fontSize, + color: + Theme.of(context).colorScheme.outline), + ), + ) ], ), ), @@ -182,14 +183,12 @@ class _SubDetailPageState extends State { SliverToBoxAdapter( child: Padding( padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), - child: Obx( - () => Text( - '共${_subDetailController.subList.length}条视频', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - letterSpacing: 1), + child: Text( + '共${_subDetailController.item.mediaCount}条视频', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + letterSpacing: 1, ), ), ), diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 5c4ac14b..4d40e535 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -22,6 +22,7 @@ import 'package:screen_brightness/screen_brightness.dart'; import '../../../models/video/subTitile/content.dart'; import '../../../http/danmaku.dart'; +import '../../../plugin/pl_player/models/bottom_control_type.dart'; import '../../../utils/id_utils.dart'; import 'widgets/header_control.dart'; @@ -91,13 +92,21 @@ class VideoDetailController extends GetxController late bool enableCDN; late int? cacheVideoQa; late String cacheDecode; - late int cacheAudioQa; + late int defaultAudioQa; PersistentBottomSheetController? replyReplyBottomSheetCtr; RxList subtitleContents = [].obs; late bool enableRelatedVideo; List subtitles = []; + RxList bottomList = [ + BottomControlType.playOrPause, + BottomControlType.time, + BottomControlType.space, + BottomControlType.fit, + BottomControlType.fullscreen, + ].obs; + RxDouble sheetHeight = 0.0.obs; @override void onInit() { @@ -146,7 +155,7 @@ class VideoDetailController extends GetxController // 预设的解码格式 cacheDecode = setting.get(SettingBoxKey.defaultDecode, defaultValue: VideoDecodeFormats.values.last.code); - cacheAudioQa = setting.get(SettingBoxKey.defaultAudioQa, + defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa, defaultValue: AudioQuality.hiRes.code); oid.value = IdUtils.bv2av(Get.parameters['bvid']!); getSubtitle(); @@ -164,6 +173,7 @@ class VideoDetailController extends GetxController firstFloor: firstFloor, replyType: ReplyType.video, source: 'videoDetail', + sheetHeight: sheetHeight.value, ); }); replyReplyBottomSheetCtr?.closed.then((value) { @@ -353,9 +363,9 @@ class VideoDetailController extends GetxController if (audiosList.isNotEmpty) { final List numbers = audiosList.map((map) => map.id!).toList(); - int closestNumber = Utils.findClosestNumber(cacheAudioQa, numbers); - if (!numbers.contains(cacheAudioQa) && - numbers.any((e) => e > cacheAudioQa)) { + int closestNumber = Utils.findClosestNumber(defaultAudioQa, numbers); + if (!numbers.contains(defaultAudioQa) && + numbers.any((e) => e > defaultAudioQa)) { closestNumber = 30280; } firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); diff --git a/lib/pages/video/detail/introduction/controller.dart b/lib/pages/video/detail/introduction/controller.dart index 8114bdaf..6469b5c5 100644 --- a/lib/pages/video/detail/introduction/controller.dart +++ b/lib/pages/video/detail/introduction/controller.dart @@ -18,6 +18,9 @@ import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:share_plus/share_plus.dart'; +import '../../../../common/pages_bottom_sheet.dart'; +import '../../../../models/common/video_episode_type.dart'; +import '../../../../utils/drawer.dart'; import '../related/index.dart'; import 'widgets/group_panel.dart'; @@ -25,15 +28,10 @@ class VideoIntroController extends GetxController { VideoIntroController({required this.bvid}); // 视频bvid String bvid; - // 请求状态 - RxBool isLoading = false.obs; - // 视频详情 请求返回 Rx videoDetail = VideoDetailData().obs; - // up主粉丝数 Map userStat = {'follower': '-'}; - // 是否点赞 RxBool hasLike = false.obs; // 是否投币 @@ -59,6 +57,8 @@ class VideoIntroController extends GetxController { bool isPaused = false; String heroTag = ''; late ModelResult modelResult; + PersistentBottomSheetController? bottomSheetController; + late bool enableRelatedVideo; @override void onInit() { @@ -75,6 +75,8 @@ class VideoIntroController extends GetxController { queryOnlineTotal(); startTimer(); // 在页面加载时启动定时器 } + enableRelatedVideo = + setting.get(SettingBoxKey.enableRelatedVideo, defaultValue: true); } // 获取视频简介&分p @@ -448,15 +450,18 @@ class VideoIntroController extends GetxController { // 重新获取视频资源 final VideoDetailController videoDetailCtr = Get.find(tag: heroTag); - final ReleatedController releatedCtr = - Get.find(tag: heroTag); + if (enableRelatedVideo) { + final ReleatedController releatedCtr = + Get.find(tag: heroTag); + releatedCtr.bvid = bvid; + releatedCtr.queryRelatedVideo(); + } + videoDetailCtr.bvid = bvid; videoDetailCtr.oid.value = aid ?? IdUtils.bv2av(bvid); videoDetailCtr.cid.value = cid; videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.queryVideoUrl(); - releatedCtr.bvid = bvid; - releatedCtr.queryRelatedVideo(); // 重新请求评论 try { /// 未渲染回复组件时可能异常 @@ -562,4 +567,52 @@ class VideoIntroController extends GetxController { } return res; } + + hiddenEpisodeBottomSheet() { + bottomSheetController?.close(); + } + + // 播放器底栏 选集 回调 + void showEposideHandler() { + late List episodes; + VideoEpidoesType dataType = VideoEpidoesType.videoEpisode; + if (videoDetail.value.ugcSeason != null) { + dataType = VideoEpidoesType.videoEpisode; + final List sections = videoDetail.value.ugcSeason!.sections!; + for (int i = 0; i < sections.length; i++) { + final List episodesList = sections[i].episodes!; + for (int j = 0; j < episodesList.length; j++) { + if (episodesList[j].cid == lastPlayCid.value) { + episodes = episodesList; + continue; + } + } + } + } + if (videoDetail.value.pages != null && + videoDetail.value.pages!.length > 1) { + dataType = VideoEpidoesType.videoPart; + episodes = videoDetail.value.pages!; + } + + DrawerUtils.showRightDialog( + child: EpisodeBottomSheet( + episodes: episodes, + currentCid: lastPlayCid.value, + dataType: dataType, + context: Get.context!, + sheetHeight: Get.size.height, + isFullScreen: true, + changeFucCall: (item, index) { + if (dataType == VideoEpidoesType.videoEpisode) { + changeSeasonOrbangu(IdUtils.av2bv(item.aid), item.cid, item.aid); + } + if (dataType == VideoEpidoesType.videoPart) { + changeSeasonOrbangu(bvid, item.cid, null); + } + SmartDialog.dismiss(); + }, + ).buildShowContent(Get.context!), + ); + } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 831491f6..a7eae6d2 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -1,3 +1,4 @@ +import 'package:expandable/expandable.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -15,12 +16,12 @@ import 'package:pilipala/pages/video/detail/widgets/ai_detail.dart'; import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/utils.dart'; -import '../widgets/expandable_section.dart'; +import '../../../../http/user.dart'; import 'widgets/action_item.dart'; import 'widgets/fav_panel.dart'; import 'widgets/intro_detail.dart'; -import 'widgets/page.dart'; -import 'widgets/season.dart'; +import 'widgets/page_panel.dart'; +import 'widgets/season_panel.dart'; class VideoIntroPanel extends StatefulWidget { final String bvid; @@ -139,6 +140,8 @@ class _VideoInfoState extends State with TickerProviderStateMixin { late bool enableAi; bool isProcessing = false; RxBool isExpand = false.obs; + late ExpandableController _expandableCtr; + void Function()? handleState(Future Function() action) { return isProcessing ? null @@ -162,6 +165,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { follower = Utils.numFormat(videoIntroController.userStat['follower']); followStatus = videoIntroController.followStatus; enableAi = setting.get(SettingBoxKey.enableAi, defaultValue: true); + _expandableCtr = ExpandableController(initialExpanded: false); } // 收藏 @@ -215,6 +219,7 @@ class _VideoInfoState extends State with TickerProviderStateMixin { showIntroDetail() { feedBack(); isExpand.value = !(isExpand.value); + _expandableCtr.toggle(); } // 用户主页 @@ -238,6 +243,12 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ); } + @override + void dispose() { + _expandableCtr.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { final ThemeData t = Theme.of(context); @@ -255,14 +266,34 @@ class _VideoInfoState extends State with TickerProviderStateMixin { GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => showIntroDetail(), - child: Text( - widget.videoDetail!.title!, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, + child: ExpandablePanel( + controller: _expandableCtr, + collapsed: Text( + widget.videoDetail!.title!, + softWrap: true, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + expanded: Text( + widget.videoDetail!.title!, + softWrap: true, + maxLines: 4, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 300), + scrollAnimationDuration: Duration(milliseconds: 300), + crossFadePoint: 0, + fadeCurve: Curves.ease, + sizeCurve: Curves.linear, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), ), Stack( @@ -327,18 +358,22 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), /// 视频简介 - Obx( - () => ExpandedSection( - expand: isExpand.value, - begin: 0, - end: 1, - child: IntroDetail(videoDetail: widget.videoDetail!), + ExpandablePanel( + controller: _expandableCtr, + collapsed: const SizedBox(height: 0), + expanded: IntroDetail(videoDetail: widget.videoDetail!), + theme: const ExpandableThemeData( + animationDuration: Duration(milliseconds: 300), + scrollAnimationDuration: Duration(milliseconds: 300), + crossFadePoint: 0, + fadeCurve: Curves.ease, + sizeCurve: Curves.linear, ), ), /// 点赞收藏转发 - actionGrid(context, videoIntroController), - // 合集 + Material(child: actionGrid(context, videoIntroController)), + // 合集 videoPart 简洁 if (widget.videoDetail!.ugcSeason != null) ...[ Obx( () => SeasonPanel( @@ -346,21 +381,33 @@ class _VideoInfoState extends State with TickerProviderStateMixin { cid: videoIntroController.lastPlayCid.value != 0 ? videoIntroController.lastPlayCid.value : widget.videoDetail!.pages!.first.cid, - sheetHeight: sheetHeight, + sheetHeight: videoDetailCtr.sheetHeight.value, changeFuc: (bvid, cid, aid) => - videoIntroController.changeSeasonOrbangu(bvid, cid, aid), + videoIntroController.changeSeasonOrbangu( + bvid, + cid, + aid, + ), + videoIntroCtr: videoIntroController, ), ) ], + // 合集 videoEpisode if (widget.videoDetail!.pages != null && widget.videoDetail!.pages!.length > 1) ...[ - Obx(() => PagesPanel( - pages: widget.videoDetail!.pages!, - cid: videoIntroController.lastPlayCid.value, - sheetHeight: sheetHeight, - changeFuc: (cid) => videoIntroController.changeSeasonOrbangu( - videoIntroController.bvid, cid, null), - )) + Obx( + () => PagesPanel( + pages: widget.videoDetail!.pages!, + cid: videoIntroController.lastPlayCid.value, + sheetHeight: videoDetailCtr.sheetHeight.value, + changeFuc: (cid) => videoIntroController.changeSeasonOrbangu( + videoIntroController.bvid, + cid, + null, + ), + videoIntroCtr: videoIntroController, + ), + ) ], GestureDetector( onTap: onPushMember, @@ -479,7 +526,11 @@ class _VideoInfoState extends State with TickerProviderStateMixin { ), ActionItem( icon: const Icon(FontAwesomeIcons.clock), - onTap: () => videoIntroController.actionShareVideo(), + onTap: () async { + final res = + await UserHttp.toViewLater(bvid: widget.videoDetail!.bvid); + SmartDialog.showToast(res['msg']); + }, selectStatus: false, text: '稍后看', ), diff --git a/lib/pages/video/detail/introduction/widgets/page.dart b/lib/pages/video/detail/introduction/widgets/page.dart deleted file mode 100644 index 8d296050..00000000 --- a/lib/pages/video/detail/introduction/widgets/page.dart +++ /dev/null @@ -1,258 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:pilipala/models/video_detail_res.dart'; -import 'package:pilipala/pages/video/detail/index.dart'; - -class PagesPanel extends StatefulWidget { - const PagesPanel({ - super.key, - required this.pages, - this.cid, - this.sheetHeight, - this.changeFuc, - }); - final List pages; - final int? cid; - final double? sheetHeight; - final Function? changeFuc; - - @override - State createState() => _PagesPanelState(); -} - -class _PagesPanelState extends State { - late List episodes; - late int cid; - late int currentIndex; - final String heroTag = Get.arguments['heroTag']; - late VideoDetailController _videoDetailController; - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - cid = widget.cid!; - episodes = widget.pages; - _videoDetailController = Get.find(tag: heroTag); - currentIndex = episodes.indexWhere((Part e) => e.cid == cid); - _videoDetailController.cid.listen((int p0) { - cid = p0; - setState(() {}); - currentIndex = episodes.indexWhere((Part e) => e.cid == cid); - }); - } - - void changeFucCall(item, i) async { - await widget.changeFuc!( - item.cid, - ); - currentIndex = i; - setState(() {}); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - Widget buildEpisodeListItem( - Part episode, - int index, - bool isCurrentIndex, - ) { - Color primary = Theme.of(context).colorScheme.primary; - return ListTile( - onTap: () { - changeFucCall(episode, index); - Get.back(); - }, - dense: false, - leading: isCurrentIndex - ? Image.asset( - 'assets/images/live.gif', - color: primary, - height: 12, - ) - : null, - title: Text( - episode.pagePart!, - style: TextStyle( - fontSize: 14, - color: isCurrentIndex - ? primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Column( - children: [ - Padding( - padding: const EdgeInsets.only(top: 10, bottom: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('视频选集 '), - Expanded( - child: Text( - ' 正在播放:${widget.pages[currentIndex].pagePart}', - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 12, - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - const SizedBox(width: 10), - SizedBox( - height: 34, - child: TextButton( - style: ButtonStyle( - padding: MaterialStateProperty.all(EdgeInsets.zero), - ), - onPressed: () { - showBottomSheet( - context: context, - builder: (BuildContext context) { - return StatefulBuilder(builder: - (BuildContext context, StateSetter setState) { - WidgetsBinding.instance - .addPostFrameCallback((_) async { - await Future.delayed( - const Duration(milliseconds: 200)); - _scrollController.jumpTo(currentIndex * 56); - }); - return Container( - height: widget.sheetHeight, - color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - Container( - height: 45, - padding: const EdgeInsets.only( - left: 14, right: 14), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - '合集(${episodes.length})', - style: Theme.of(context) - .textTheme - .titleMedium, - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - Divider( - height: 1, - color: Theme.of(context) - .dividerColor - .withOpacity(0.1), - ), - Expanded( - child: Material( - child: ListView.builder( - controller: _scrollController, - itemCount: episodes.length + 1, - itemBuilder: - (BuildContext context, int index) { - bool isLastItem = - index == episodes.length; - bool isCurrentIndex = - currentIndex == index; - return isLastItem - ? SizedBox( - height: MediaQuery.of(context) - .padding - .bottom + - 20, - ) - : buildEpisodeListItem( - episodes[index], - index, - isCurrentIndex, - ); - }, - ), - ), - ), - ], - ), - ); - }); - }, - ); - }, - child: Text( - '共${widget.pages.length}集', - style: const TextStyle(fontSize: 13), - ), - ), - ), - ], - ), - ), - Container( - height: 35, - margin: const EdgeInsets.only(bottom: 8), - child: ListView.builder( - scrollDirection: Axis.horizontal, - itemCount: widget.pages.length, - itemExtent: 150, - itemBuilder: (BuildContext context, int i) { - bool isCurrentIndex = currentIndex == i; - return Container( - width: 150, - margin: const EdgeInsets.only(right: 10), - child: Material( - color: Theme.of(context).colorScheme.onInverseSurface, - borderRadius: BorderRadius.circular(6), - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () => changeFucCall(widget.pages[i], i), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, horizontal: 8), - child: Row( - children: [ - if (isCurrentIndex) ...[ - Image.asset( - 'assets/images/live.gif', - color: Theme.of(context).colorScheme.primary, - height: 12, - ), - const SizedBox(width: 6) - ], - Expanded( - child: Text( - widget.pages[i].pagePart!, - maxLines: 1, - style: TextStyle( - fontSize: 13, - color: isCurrentIndex - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurface), - overflow: TextOverflow.ellipsis, - )) - ], - ), - ), - ), - ), - ); - }, - ), - ) - ], - ); - } -} diff --git a/lib/pages/video/detail/introduction/widgets/page_panel.dart b/lib/pages/video/detail/introduction/widgets/page_panel.dart new file mode 100644 index 00000000..83db2d52 --- /dev/null +++ b/lib/pages/video/detail/introduction/widgets/page_panel.dart @@ -0,0 +1,186 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/index.dart'; +import 'package:pilipala/pages/video/detail/introduction/index.dart'; +import '../../../../../common/pages_bottom_sheet.dart'; +import '../../../../../models/common/video_episode_type.dart'; + +class PagesPanel extends StatefulWidget { + const PagesPanel({ + super.key, + required this.pages, + required this.cid, + this.sheetHeight, + this.changeFuc, + required this.videoIntroCtr, + }); + final List pages; + final int cid; + final double? sheetHeight; + final Function? changeFuc; + final VideoIntroController videoIntroCtr; + + @override + State createState() => _PagesPanelState(); +} + +class _PagesPanelState extends State { + late List episodes; + late int cid; + late RxInt currentIndex = (-1).obs; + final String heroTag = Get.arguments['heroTag']; + late VideoDetailController _videoDetailController; + final ScrollController listViewScrollCtr = ScrollController(); + late PersistentBottomSheetController? _bottomSheetController; + + @override + void initState() { + super.initState(); + cid = widget.cid; + episodes = widget.pages; + _videoDetailController = Get.find(tag: heroTag); + currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid); + scrollToIndex(); + _videoDetailController.cid.listen((int p0) { + cid = p0; + currentIndex.value = episodes.indexWhere((Part e) => e.cid == cid); + scrollToIndex(); + }); + } + + @override + void dispose() { + listViewScrollCtr.dispose(); + super.dispose(); + } + + void changeFucCall(item, i) async { + widget.changeFuc?.call(item.cid); + currentIndex.value = i; + _bottomSheetController?.close(); + scrollToIndex(); + } + + void scrollToIndex() { + WidgetsBinding.instance.addPostFrameCallback((_) { + // 在回调函数中获取更新后的状态 + final double offset = min((currentIndex * 150) - 75, + listViewScrollCtr.position.maxScrollExtent); + if (currentIndex.value == 0) { + listViewScrollCtr.jumpTo(0); + } else { + listViewScrollCtr.animateTo( + offset, + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOut, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.only(top: 10, bottom: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('视频选集 '), + Expanded( + child: Obx(() => Text( + ' 正在播放:${widget.pages[currentIndex.value].pagePart}', + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline, + ), + )), + ), + const SizedBox(width: 10), + SizedBox( + height: 34, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + widget.videoIntroCtr.bottomSheetController = + _bottomSheetController = EpisodeBottomSheet( + currentCid: cid, + episodes: episodes, + changeFucCall: changeFucCall, + sheetHeight: widget.sheetHeight, + dataType: VideoEpidoesType.videoPart, + context: context, + ).show(context); + }, + child: Text( + '共${widget.pages.length}集', + style: const TextStyle(fontSize: 13), + ), + ), + ), + ], + ), + ), + Container( + height: 35, + margin: const EdgeInsets.only(bottom: 8), + child: ListView.builder( + scrollDirection: Axis.horizontal, + controller: listViewScrollCtr, + itemCount: widget.pages.length, + itemExtent: 150, + itemBuilder: (BuildContext context, int i) { + bool isCurrentIndex = currentIndex.value == i; + return Container( + width: 150, + margin: const EdgeInsets.only(right: 10), + child: Material( + color: Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(6), + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () => changeFucCall(widget.pages[i], i), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 8, horizontal: 8), + child: Row( + children: [ + if (isCurrentIndex) ...[ + Image.asset( + 'assets/images/live.gif', + color: Theme.of(context).colorScheme.primary, + height: 12, + ), + const SizedBox(width: 6) + ], + Expanded( + child: Text( + widget.pages[i].pagePart!, + maxLines: 1, + style: TextStyle( + fontSize: 13, + color: isCurrentIndex + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface), + overflow: TextOverflow.ellipsis, + )) + ], + ), + ), + ), + ), + ); + }, + ), + ) + ], + ); + } +} diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart deleted file mode 100644 index 0f3884ed..00000000 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ /dev/null @@ -1,226 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:pilipala/models/video_detail_res.dart'; -import 'package:pilipala/pages/video/detail/index.dart'; -import 'package:pilipala/utils/id_utils.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; - -class SeasonPanel extends StatefulWidget { - const SeasonPanel({ - super.key, - required this.ugcSeason, - this.cid, - this.sheetHeight, - this.changeFuc, - }); - final UgcSeason ugcSeason; - final int? cid; - final double? sheetHeight; - final Function? changeFuc; - - @override - State createState() => _SeasonPanelState(); -} - -class _SeasonPanelState extends State { - late List episodes; - late int cid; - late int currentIndex; - final String heroTag = Get.arguments['heroTag']; - late VideoDetailController _videoDetailController; - final ScrollController _scrollController = ScrollController(); - final ItemScrollController itemScrollController = ItemScrollController(); - - @override - void initState() { - super.initState(); - cid = widget.cid!; - _videoDetailController = Get.find(tag: heroTag); - - /// 根据 cid 找到对应集,找到对应 episodes - /// 有多个episodes时,只显示其中一个 - /// TODO 同时显示多个合集 - final List sections = widget.ugcSeason.sections!; - for (int i = 0; i < sections.length; i++) { - final List episodesList = sections[i].episodes!; - for (int j = 0; j < episodesList.length; j++) { - if (episodesList[j].cid == cid) { - episodes = episodesList; - continue; - } - } - } - - /// 取对应 season_id 的 episodes - // episodes = widget.ugcSeason.sections! - // .firstWhere((e) => e.seasonId == widget.ugcSeason.id) - // .episodes!; - currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid); - _videoDetailController.cid.listen((int p0) { - cid = p0; - setState(() {}); - currentIndex = episodes.indexWhere((EpisodeItem e) => e.cid == cid); - }); - } - - void changeFucCall(item, int i) async { - await widget.changeFuc!( - IdUtils.av2bv(item.aid), - item.cid, - item.aid, - ); - currentIndex = i; - Get.back(); - setState(() {}); - } - - @override - void dispose() { - _scrollController.dispose(); - super.dispose(); - } - - Widget buildEpisodeListItem( - EpisodeItem episode, - int index, - bool isCurrentIndex, - ) { - Color primary = Theme.of(context).colorScheme.primary; - return ListTile( - onTap: () => changeFucCall(episode, index), - dense: false, - leading: isCurrentIndex - ? Image.asset( - 'assets/images/live.gif', - color: primary, - height: 12, - ) - : null, - title: Text( - episode.title!, - style: TextStyle( - fontSize: 14, - color: isCurrentIndex - ? primary - : Theme.of(context).colorScheme.onSurface, - ), - ), - ); - } - - @override - Widget build(BuildContext context) { - return Builder(builder: (BuildContext context) { - return Container( - margin: const EdgeInsets.only( - top: 8, - left: 2, - right: 2, - bottom: 2, - ), - child: Material( - color: Theme.of(context).colorScheme.onInverseSurface, - borderRadius: BorderRadius.circular(6), - clipBehavior: Clip.hardEdge, - child: InkWell( - onTap: () => showBottomSheet( - context: context, - builder: (BuildContext context) { - return StatefulBuilder( - builder: (BuildContext context, StateSetter setState) { - WidgetsBinding.instance.addPostFrameCallback((_) async { - itemScrollController.jumpTo(index: currentIndex); - }); - return Container( - height: widget.sheetHeight, - color: Theme.of(context).colorScheme.background, - child: Column( - children: [ - Container( - height: 45, - padding: const EdgeInsets.only(left: 14, right: 14), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '合集(${episodes.length})', - style: Theme.of(context).textTheme.titleMedium, - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () => Navigator.pop(context), - ), - ], - ), - ), - Divider( - height: 1, - color: - Theme.of(context).dividerColor.withOpacity(0.1), - ), - Expanded( - child: Material( - child: ScrollablePositionedList.builder( - itemCount: episodes.length + 1, - itemBuilder: (BuildContext context, int index) { - bool isLastItem = index == episodes.length; - bool isCurrentIndex = currentIndex == index; - return isLastItem - ? SizedBox( - height: MediaQuery.of(context) - .padding - .bottom + - 20, - ) - : buildEpisodeListItem( - episodes[index], - index, - isCurrentIndex, - ); - }, - itemScrollController: itemScrollController, - ), - ), - ), - ], - ), - ); - }); - }, - ), - child: Padding( - padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), - child: Row( - children: [ - Expanded( - child: Text( - '合集:${widget.ugcSeason.title!}', - style: Theme.of(context).textTheme.labelMedium, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 15), - Image.asset( - 'assets/images/live.gif', - color: Theme.of(context).colorScheme.primary, - height: 12, - ), - const SizedBox(width: 10), - Text( - '${currentIndex + 1}/${episodes.length}', - style: Theme.of(context).textTheme.labelMedium, - ), - const SizedBox(width: 6), - const Icon( - Icons.arrow_forward_ios_outlined, - size: 13, - ) - ], - ), - ), - ), - ), - ); - }); - } -} diff --git a/lib/pages/video/detail/introduction/widgets/season_panel.dart b/lib/pages/video/detail/introduction/widgets/season_panel.dart new file mode 100644 index 00000000..2afefcb4 --- /dev/null +++ b/lib/pages/video/detail/introduction/widgets/season_panel.dart @@ -0,0 +1,164 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/pages_bottom_sheet.dart'; +import 'package:pilipala/models/video_detail_res.dart'; +import 'package:pilipala/pages/video/detail/index.dart'; +import 'package:pilipala/utils/id_utils.dart'; +import '../../../../../models/common/video_episode_type.dart'; +import '../controller.dart'; + +class SeasonPanel extends StatefulWidget { + const SeasonPanel({ + super.key, + required this.ugcSeason, + this.cid, + this.sheetHeight, + this.changeFuc, + required this.videoIntroCtr, + }); + final UgcSeason ugcSeason; + final int? cid; + final double? sheetHeight; + final Function? changeFuc; + final VideoIntroController videoIntroCtr; + + @override + State createState() => _SeasonPanelState(); +} + +class _SeasonPanelState extends State { + late List episodes; + late int cid; + late RxInt currentIndex = (-1).obs; + final String heroTag = Get.arguments['heroTag']; + late VideoDetailController _videoDetailController; + late PersistentBottomSheetController? _bottomSheetController; + + @override + void initState() { + super.initState(); + cid = widget.cid!; + _videoDetailController = Get.find(tag: heroTag); + + /// 根据 cid 找到对应集,找到对应 episodes + /// 有多个episodes时,只显示其中一个 + /// TODO 同时显示多个合集 + final List sections = widget.ugcSeason.sections!; + for (int i = 0; i < sections.length; i++) { + final List episodesList = sections[i].episodes!; + for (int j = 0; j < episodesList.length; j++) { + if (episodesList[j].cid == cid) { + episodes = episodesList; + continue; + } + } + } + + /// 取对应 season_id 的 episodes + currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid); + _videoDetailController.cid.listen((int p0) { + cid = p0; + currentIndex.value = episodes.indexWhere((EpisodeItem e) => e.cid == cid); + }); + } + + void changeFucCall(item, int i) async { + widget.changeFuc?.call( + IdUtils.av2bv(item.aid), + item.cid, + item.aid, + ); + currentIndex.value = i; + _bottomSheetController?.close(); + } + + Widget buildEpisodeListItem( + EpisodeItem episode, + int index, + bool isCurrentIndex, + ) { + Color primary = Theme.of(context).colorScheme.primary; + return ListTile( + onTap: () => changeFucCall(episode, index), + dense: false, + leading: isCurrentIndex + ? Image.asset( + 'assets/images/live.gif', + color: primary, + height: 12, + ) + : null, + title: Text( + episode.title!, + style: TextStyle( + fontSize: 14, + color: isCurrentIndex + ? primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Builder(builder: (BuildContext context) { + return Container( + margin: const EdgeInsets.only( + top: 8, + left: 2, + right: 2, + bottom: 2, + ), + child: Material( + color: Theme.of(context).colorScheme.onInverseSurface, + borderRadius: BorderRadius.circular(6), + clipBehavior: Clip.hardEdge, + child: InkWell( + onTap: () { + widget.videoIntroCtr.bottomSheetController = + _bottomSheetController = EpisodeBottomSheet( + currentCid: cid, + episodes: episodes, + changeFucCall: changeFucCall, + sheetHeight: widget.sheetHeight, + dataType: VideoEpidoesType.videoEpisode, + context: context, + ).show(context); + }, + child: Padding( + padding: const EdgeInsets.fromLTRB(8, 12, 8, 12), + child: Row( + children: [ + Expanded( + child: Text( + '合集:${widget.ugcSeason.title!}', + style: Theme.of(context).textTheme.labelMedium, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 15), + Image.asset( + 'assets/images/live.gif', + color: Theme.of(context).colorScheme.primary, + height: 12, + ), + const SizedBox(width: 10), + Obx(() => Text( + '${currentIndex.value + 1}/${episodes.length}', + style: Theme.of(context).textTheme.labelMedium, + )), + const SizedBox(width: 6), + const Icon( + Icons.arrow_forward_ios_outlined, + size: 13, + ) + ], + ), + ), + ), + ), + ); + }); + } +} diff --git a/lib/pages/video/detail/reply/view.dart b/lib/pages/video/detail/reply/view.dart index 38203f7e..2a167fe9 100644 --- a/lib/pages/video/detail/reply/view.dart +++ b/lib/pages/video/detail/reply/view.dart @@ -149,13 +149,16 @@ class _VideoReplyPanelState extends State delegate: _MySliverPersistentHeaderDelegate( child: Container( height: 40, - padding: const EdgeInsets.fromLTRB(12, 6, 6, 0), + padding: const EdgeInsets.fromLTRB(12, 0, 6, 0), + color: Theme.of(context).colorScheme.surface, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '${_videoReplyController.sortTypeLabel.value}评论', - style: const TextStyle(fontSize: 13), + Obx( + () => Text( + '${_videoReplyController.sortTypeLabel.value}评论', + style: const TextStyle(fontSize: 13), + ), ), SizedBox( height: 35, diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index e79b6159..50fe20d4 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -525,14 +525,18 @@ InlineSpan buildContent( if (jumpUrlKeysList.isNotEmpty) { patternStr += '|${jumpUrlKeysList.join('|')}'; } + RegExp bv23Regex = RegExp(r'https://b23\.tv/[a-zA-Z0-9]{7}'); final RegExp pattern = RegExp(patternStr); List matchedStrs = []; void addPlainTextSpan(str) { - spanChilds.add(TextSpan( + spanChilds.add( + TextSpan( text: str, recognizer: TapGestureRecognizer() ..onTap = () => - replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem))); + replyReply?.call(replyItem.root == 0 ? replyItem : fReplyItem), + ), + ); } // 分割文本并处理每个部分 @@ -734,8 +738,36 @@ InlineSpan buildContent( return ''; }, onNonMatch: (String nonMatchStr) { - addPlainTextSpan(nonMatchStr); - return nonMatchStr; + return nonMatchStr.splitMapJoin( + bv23Regex, + onMatch: (Match match) { + String matchStr = match[0]!; + spanChilds.add( + TextSpan( + text: ' $matchStr ', + style: isVideoPage + ? TextStyle( + color: Theme.of(context).colorScheme.primary, + ) + : null, + recognizer: TapGestureRecognizer() + ..onTap = () => Get.toNamed( + '/webview', + parameters: { + 'url': matchStr, + 'type': 'url', + 'pageTitle': matchStr + }, + ), + ), + ); + return ''; + }, + onNonMatch: (String nonMatchOtherStr) { + addPlainTextSpan(nonMatchOtherStr); + return nonMatchOtherStr; + }, + ); }, ); diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index 05351411..a94b6071 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -142,9 +142,10 @@ class _VideoReplyNewDialogState extends State @override Widget build(BuildContext context) { - double keyboardHeight = EdgeInsets.fromViewPadding( + double _keyboardHeight = EdgeInsets.fromViewPadding( View.of(context).viewInsets, View.of(context).devicePixelRatio) .bottom; + print('_keyboardHeight: $_keyboardHeight'); return Container( clipBehavior: Clip.hardEdge, decoration: BoxDecoration( @@ -235,7 +236,11 @@ class _VideoReplyNewDialogState extends State duration: const Duration(milliseconds: 300), child: SizedBox( width: double.infinity, - height: toolbarType == 'input' ? keyboardHeight : emoteHeight, + height: toolbarType == 'input' + ? (_keyboardHeight > keyboardHeight + ? _keyboardHeight + : keyboardHeight) + : emoteHeight, child: EmotePanel( onChoose: (package, emote) => onChooseEmote(package, emote), ), diff --git a/lib/pages/video/detail/reply_reply/view.dart b/lib/pages/video/detail/reply_reply/view.dart index e8754a31..3fe84c71 100644 --- a/lib/pages/video/detail/reply_reply/view.dart +++ b/lib/pages/video/detail/reply_reply/view.dart @@ -19,6 +19,7 @@ class VideoReplyReplyPanel extends StatefulWidget { this.firstFloor, this.source, this.replyType, + this.sheetHeight, super.key, }); final int? oid; @@ -27,6 +28,7 @@ class VideoReplyReplyPanel extends StatefulWidget { final ReplyItemModel? firstFloor; final String? source; final ReplyType? replyType; + final double? sheetHeight; @override State createState() => _VideoReplyReplyPanelState(); @@ -36,7 +38,6 @@ class _VideoReplyReplyPanelState extends State { late VideoReplyReplyController _videoReplyReplyController; late AnimationController replyAnimationCtl; final Box localCache = GStrorage.localCache; - late double sheetHeight; Future? _futureBuilderFuture; late ScrollController scrollController; @@ -62,7 +63,6 @@ class _VideoReplyReplyPanelState extends State { }, ); - sheetHeight = localCache.get('sheetHeight'); _futureBuilderFuture = _videoReplyReplyController.queryReplyList(); } @@ -77,33 +77,31 @@ class _VideoReplyReplyPanelState extends State { @override Widget build(BuildContext context) { return Container( - height: widget.source == 'videoDetail' ? sheetHeight : null, + height: widget.source == 'videoDetail' ? widget.sheetHeight : null, color: Theme.of(context).colorScheme.background, child: Column( children: [ if (widget.source == 'videoDetail') - Container( - height: 45, - padding: const EdgeInsets.only(left: 12, right: 2), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text('评论详情'), - IconButton( - icon: const Icon(Icons.close, size: 20), - onPressed: () { - _videoReplyReplyController.currentPage = 0; - widget.closePanel?.call; - Navigator.pop(context); - }, - ), - ], + AppBar( + toolbarHeight: 45, + automaticallyImplyLeading: false, + centerTitle: false, + title: Text( + '评论详情', + style: Theme.of(context).textTheme.titleSmall, ), + actions: [ + IconButton( + icon: const Icon(Icons.close, size: 20), + onPressed: () { + _videoReplyReplyController.currentPage = 0; + widget.closePanel?.call; + Navigator.pop(context); + }, + ), + const SizedBox(width: 14), + ], ), - Divider( - height: 1, - color: Theme.of(context).dividerColor.withOpacity(0.1), - ), Expanded( child: RefreshIndicator( onRefresh: () async { @@ -140,8 +138,8 @@ class _VideoReplyReplyPanelState extends State { future: _futureBuilderFuture, builder: (BuildContext context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { - final Map data = snapshot.data as Map; - if (data['status']) { + Map? data = snapshot.data; + if (data != null && data['status']) { // 请求成功 return Obx( () => SliverList( @@ -199,7 +197,7 @@ class _VideoReplyReplyPanelState extends State { } else { // 请求错误 return HttpError( - errMsg: data['msg'], + errMsg: data?['msg'] ?? '请求错误', fn: () => setState(() {}), ); } diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 6958a62d..0152a2cb 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -24,6 +24,7 @@ import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/services/service_locator.dart'; import 'package:pilipala/utils/storage.dart'; +import '../../../plugin/pl_player/models/bottom_control_type.dart'; import '../../../services/shutdown_timer_service.dart'; import 'widgets/app_bar.dart'; @@ -46,7 +47,7 @@ class _VideoDetailPageState extends State late BangumiIntroController bangumiIntroController; late String heroTag; - PlayerStatus playerStatus = PlayerStatus.playing; + Rx playerStatus = PlayerStatus.playing.obs; double doubleOffset = 0; final Box localCache = GStrorage.localCache; @@ -66,6 +67,7 @@ class _VideoDetailPageState extends State super.initState(); heroTag = Get.arguments['heroTag']; vdCtr = Get.put(VideoDetailController(), tag: heroTag); + vdCtr.sheetHeight.value = localCache.get('sheetHeight'); videoIntroController = Get.put( VideoIntroController(bvid: Get.parameters['bvid']!), tag: heroTag); @@ -107,10 +109,12 @@ class _VideoDetailPageState extends State // 流 appbarStreamListen() { - appbarStream = StreamController(); + appbarStream = StreamController.broadcast(); _extendNestCtr.addListener( () { final double offset = _extendNestCtr.position.pixels; + vdCtr.sheetHeight.value = + Get.size.height - videoHeight - statusBarHeight + offset; appbarStream.add(offset); }, ); @@ -118,7 +122,7 @@ class _VideoDetailPageState extends State // 播放器状态监听 void playerListener(PlayerStatus? status) async { - playerStatus = status!; + playerStatus.value = status!; if (status == PlayerStatus.completed) { // 结束播放退出全屏 if (autoExitFullcreen) { @@ -176,6 +180,25 @@ class _VideoDetailPageState extends State plPlayerController?.isFullScreen.listen((bool isFullScreen) { if (isFullScreen) { vdCtr.hiddenReplyReplyPanel(); + if (vdCtr.videoType == SearchType.video) { + videoIntroController.hiddenEpisodeBottomSheet(); + if (videoIntroController.videoDetail.value.ugcSeason != null || + (videoIntroController.videoDetail.value.pages != null && + videoIntroController.videoDetail.value.pages!.length > 1)) { + vdCtr.bottomList.insert(3, BottomControlType.episode); + } + } + if (vdCtr.videoType == SearchType.media_bangumi) { + bangumiIntroController.hiddenEpisodeBottomSheet(); + if (bangumiIntroController.bangumiDetail.value.episodes != null && + bangumiIntroController.bangumiDetail.value.episodes!.length > 1) { + vdCtr.bottomList.insert(3, BottomControlType.episode); + } + } + } else { + if (vdCtr.bottomList.contains(BottomControlType.episode)) { + vdCtr.bottomList.removeAt(3); + } } }); } @@ -260,7 +283,6 @@ class _VideoDetailPageState extends State @override Widget build(BuildContext context) { - // final double videoHeight = MediaQuery.sizeOf(context).width * 9 / 16; final sizeContext = MediaQuery.sizeOf(context); final _context = MediaQuery.of(context); late double defaultVideoHeight = sizeContext.width * 9 / 16; @@ -292,15 +314,19 @@ class _VideoDetailPageState extends State () { return !vdCtr.autoPlay.value ? const SizedBox() - : PLVideoPlayer( - controller: plPlayerController!, - headerControl: vdCtr.headerControl, - danmuWidget: Obx( - () => PlDanmaku( + : Obx( + () => PLVideoPlayer( + controller: plPlayerController!, + headerControl: vdCtr.headerControl, + danmuWidget: PlDanmaku( key: Key(vdCtr.danmakuCid.value.toString()), cid: vdCtr.danmakuCid.value, playerController: plPlayerController!, ), + bottomList: vdCtr.bottomList, + showEposideCb: () => vdCtr.videoType == SearchType.video + ? videoIntroController.showEposideHandler() + : bangumiIntroController.showEposideHandler(), ), ); }, @@ -351,6 +377,18 @@ class _VideoDetailPageState extends State child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ + Obx(() => AnimatedOpacity( + opacity: playerStatus.value != PlayerStatus.playing + ? 1 + : 0, + duration: const Duration(milliseconds: 100), + child: const Icon( + Icons.drag_handle_rounded, + size: 20, + color: Colors.grey, + ), + )), + const SizedBox(width: 8), SizedBox( height: 32, child: TextButton( @@ -366,27 +404,44 @@ class _VideoDetailPageState extends State width: 38, height: 38, child: Obx( - () => IconButton( - onPressed: () { - plPlayerController?.isOpenDanmu.value = - !(plPlayerController?.isOpenDanmu.value ?? - false); - }, - icon: !(plPlayerController?.isOpenDanmu.value ?? - false) - ? SvgPicture.asset( + () => !vdCtr.isShowCover.value + ? IconButton( + onPressed: () { + plPlayerController?.isOpenDanmu.value = + !(plPlayerController + ?.isOpenDanmu.value ?? + false); + }, + icon: + !(plPlayerController?.isOpenDanmu.value ?? + false) + ? SvgPicture.asset( + 'assets/images/video/danmu_close.svg', + // ignore: deprecated_member_use + color: Theme.of(context) + .colorScheme + .outline, + ) + : SvgPicture.asset( + 'assets/images/video/danmu_open.svg', + // ignore: deprecated_member_use + color: Theme.of(context) + .colorScheme + .primary, + ), + ) + : IconButton( + icon: SvgPicture.asset( 'assets/images/video/danmu_close.svg', - ) - : SvgPicture.asset( - 'assets/images/video/danmu_open.svg', // ignore: deprecated_member_use color: - Theme.of(context).colorScheme.primary, + Theme.of(context).colorScheme.outline, ), - ), + onPressed: () {}, + ), ), ), - const SizedBox(width: 14), + const SizedBox(width: 18), ], ), )), @@ -539,7 +594,7 @@ class _VideoDetailPageState extends State Orientation.landscape || plPlayerController?.isFullScreen.value == true ? MediaQuery.sizeOf(context).height - : playerStatus != PlayerStatus.playing + : playerStatus.value != PlayerStatus.playing ? kToolbarHeight : pinnedHeaderHeight; }, @@ -600,13 +655,13 @@ class _VideoDetailPageState extends State /// 重新进入会刷新 // 播放完成/暂停播放 StreamBuilder( - stream: appbarStream.stream, + stream: appbarStream.stream.distinct(), initialData: 0, builder: ((context, snapshot) { return ScrollAppBar( snapshot.data!.toDouble(), () => continuePlay(), - playerStatus, + playerStatus.value, null, ); }), diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index b0b7db17..e6a324cb 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -11,12 +11,14 @@ import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:pilipala/http/user.dart'; import 'package:pilipala/models/video/play/quality.dart'; import 'package:pilipala/models/video/play/url.dart'; +import 'package:pilipala/pages/dlna/index.dart'; import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/models/play_repeat.dart'; import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/services/shutdown_timer_service.dart'; +import '../../../../http/danmaku.dart'; import '../../../../models/common/search_type.dart'; import '../../../../models/video_detail_res.dart'; import '../introduction/index.dart'; @@ -52,7 +54,7 @@ class _HeaderControlState extends State { final Box videoStorage = GStrorage.video; late List speedsList; double buttonSpace = 8; - bool showTitle = false; + RxBool isFullScreen = false.obs; late String heroTag; late VideoIntroController videoIntroController; late VideoDetailData videoDetail; @@ -69,13 +71,8 @@ class _HeaderControlState extends State { } void fullScreenStatusListener() { - widget.videoDetailCtr!.plPlayerController.isFullScreen - .listen((bool isFullScreen) { - if (isFullScreen) { - showTitle = true; - } else { - showTitle = false; - } + widget.videoDetailCtr!.plPlayerController.isFullScreen.listen((bool val) { + isFullScreen.value = val; /// TODO setState() called after dispose() if (mounted) { @@ -162,7 +159,7 @@ class _HeaderControlState extends State { dense: true, leading: const Icon(Icons.hourglass_top_outlined, size: 20), - title: const Text('定时关闭(测试)', style: titleStyle), + title: const Text('定时关闭', style: titleStyle), ), ListTile( onTap: () => {Get.back(), showSetVideoQa()}, @@ -218,6 +215,87 @@ class _HeaderControlState extends State { ); } + /// 发送弹幕 + void showShootDanmakuSheet() { + final TextEditingController textController = TextEditingController(); + bool isSending = false; // 追踪是否正在发送 + showDialog( + context: Get.context!, + builder: (BuildContext context) { + // TODO: 支持更多类型和颜色的弹幕 + return AlertDialog( + title: const Text('发送弹幕(测试)'), + content: StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return TextField( + controller: textController, + ); + }), + actions: [ + TextButton( + onPressed: () => Get.back(), + child: Text( + '取消', + style: TextStyle(color: Theme.of(context).colorScheme.outline), + ), + ), + StatefulBuilder( + builder: (BuildContext context, StateSetter setState) { + return TextButton( + onPressed: isSending + ? null + : () async { + final String msg = textController.text; + if (msg.isEmpty) { + SmartDialog.showToast('弹幕内容不能为空'); + return; + } else if (msg.length > 100) { + SmartDialog.showToast('弹幕内容不能超过100个字符'); + return; + } + setState(() { + isSending = true; // 开始发送,更新状态 + }); + //修改按钮文字 + final dynamic res = await DanmakaHttp.shootDanmaku( + oid: widget.videoDetailCtr!.cid.value, + msg: textController.text, + bvid: widget.videoDetailCtr!.bvid, + progress: + widget.controller!.position.value.inMilliseconds, + type: 1, + ); + setState(() { + isSending = false; // 发送结束,更新状态 + }); + if (res['status']) { + SmartDialog.showToast('发送成功'); + // 发送成功,自动预览该弹幕,避免重新请求 + // TODO: 暂停状态下预览弹幕仍会移动与计时,可考虑添加到dmSegList或其他方式实现 + widget.controller!.danmakuController!.addItems([ + DanmakuItem( + msg, + color: Colors.white, + time: widget + .controller!.position.value.inMilliseconds, + type: DanmakuItemType.scroll, + isSend: true, + ) + ]); + Get.back(); + } else { + SmartDialog.showToast('发送失败,错误信息为${res['msg']}'); + } + }, + child: Text(isSending ? '发送中...' : '发送'), + ); + }) + ], + ); + }, + ); + } + /// 定时关闭 void scheduleExit() async { const List scheduleTimeChoices = [ @@ -653,9 +731,12 @@ class _HeaderControlState extends State { margin: const EdgeInsets.all(12), child: Column( children: [ - SizedBox( - height: 45, - child: Center(child: Text('选择解码格式', style: titleStyle))), + const SizedBox( + height: 45, + child: Center( + child: Text('选择解码格式', style: titleStyle), + ), + ), Expanded( child: Material( child: ListView( @@ -1000,9 +1081,12 @@ class _HeaderControlState extends State { margin: const EdgeInsets.all(12), child: Column( children: [ - SizedBox( - height: 45, - child: Center(child: Text('选择播放顺序', style: titleStyle))), + const SizedBox( + height: 45, + child: Center( + child: Text('选择播放顺序', style: titleStyle), + ), + ), Expanded( child: Material( child: ListView( @@ -1079,7 +1163,7 @@ class _HeaderControlState extends State { }, ), SizedBox(width: buttonSpace), - if (showTitle && + if (isFullScreen.value && isLandscape && widget.videoType == SearchType.video) ...[ Column( @@ -1087,11 +1171,13 @@ class _HeaderControlState extends State { children: [ ConstrainedBox( constraints: const BoxConstraints(maxWidth: 200), - child: Text( - videoIntroController.videoDetail.value.title ?? '', - style: const TextStyle( - color: Colors.white, - fontSize: 16, + child: Obx( + () => Text( + videoIntroController.videoDetail.value.title ?? '', + style: const TextStyle( + color: Colors.white, + fontSize: 16, + ), ), ), ), @@ -1131,6 +1217,59 @@ class _HeaderControlState extends State { // ), // fuc: () => _.screenshot(), // ), + ComBtn( + icon: const Icon( + Icons.cast, + size: 19, + color: Colors.white, + ), + fuc: () async { + showDialog( + context: context, + builder: (BuildContext context) { + return LiveDlnaPage( + datasource: widget.videoDetailCtr!.videoUrl); + }, + ); + }, + ), + if (isFullScreen.value) ...[ + SizedBox( + width: 56, + height: 34, + child: TextButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () => showShootDanmakuSheet(), + child: const Text( + '发弹幕', + style: textStyle, + ), + ), + ), + SizedBox( + width: 34, + height: 34, + child: Obx( + () => IconButton( + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + onPressed: () { + _.isOpenDanmu.value = !_.isOpenDanmu.value; + }, + icon: Icon( + _.isOpenDanmu.value + ? Icons.subtitles_outlined + : Icons.subtitles_off_outlined, + size: 19, + color: Colors.white, + ), + ), + ), + ), + ], SizedBox(width: buttonSpace), if (Platform.isAndroid) ...[ SizedBox( diff --git a/lib/pages/video/detail/widgets/right_drawer.dart b/lib/pages/video/detail/widgets/right_drawer.dart new file mode 100644 index 00000000..ca0d34ef --- /dev/null +++ b/lib/pages/video/detail/widgets/right_drawer.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class RightDrawer extends StatefulWidget { + const RightDrawer({super.key}); + + @override + State createState() => _RightDrawerState(); +} + +class _RightDrawerState extends State { + @override + Widget build(BuildContext context) { + return Drawer( + shadowColor: Colors.transparent, + elevation: 0, + backgroundColor: + Theme.of(context).colorScheme.surface.withOpacity(0.8)); + } +} diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index b385fca8..be72eccb 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -101,7 +101,7 @@ class PlPlayerController { bool _isFirstTime = true; Timer? _timer; - Timer? _timerForSeek; + late Timer? _timerForSeek; Timer? _timerForVolume; Timer? _timerForShowingVolume; Timer? _timerForGettingVolume; @@ -473,17 +473,17 @@ class PlPlayerController { } // 字幕 - if (dataSource.subFiles != '' && dataSource.subFiles != null) { - await pp.setProperty( - 'sub-files', - UniversalPlatform.isWindows - ? dataSource.subFiles!.replaceAll(';', '\\;') - : dataSource.subFiles!.replaceAll(':', '\\:'), - ); - await pp.setProperty("subs-with-matching-audio", "no"); - await pp.setProperty("sub-forced-only", "yes"); - await pp.setProperty("blend-subtitles", "video"); - } + // if (dataSource.subFiles != '' && dataSource.subFiles != null) { + // await pp.setProperty( + // 'sub-files', + // UniversalPlatform.isWindows + // ? dataSource.subFiles!.replaceAll(';', '\\;') + // : dataSource.subFiles!.replaceAll(':', '\\:'), + // ); + // await pp.setProperty("subs-with-matching-audio", "no"); + // await pp.setProperty("sub-forced-only", "yes"); + // await pp.setProperty("blend-subtitles", "video"); + // } _videoController = _videoController ?? VideoController( @@ -522,7 +522,22 @@ class PlPlayerController { Duration seekTo = Duration.zero, Duration? duration, }) async { - // 设置倍速 + getVideoFit(); + // if (_looping) { + // await setLooping(_looping); + // } + + /// 跳转播放 + if (seekTo != Duration.zero) { + await this.seekTo(seekTo); + } + + /// 自动播放 + if (_autoPlay) { + await play(duration: duration); + } + + /// 设置倍速 if (videoType.value == 'live') { await setPlaybackSpeed(1.0); } else { @@ -532,20 +547,6 @@ class PlPlayerController { await setPlaybackSpeed(1.0); } } - getVideoFit(); - // if (_looping) { - // await setLooping(_looping); - // } - - // 跳转播放 - if (seekTo != Duration.zero) { - await this.seekTo(seekTo); - } - - // 自动播放 - if (_autoPlay) { - await play(duration: duration); - } } List subscriptions = []; @@ -603,7 +604,9 @@ class PlPlayerController { makeHeartBeat(event.inSeconds); }), videoPlayerController!.stream.duration.listen((event) { - duration.value = event; + if (event > Duration.zero) { + duration.value = event; + } }), videoPlayerController!.stream.buffer.listen((event) { _buffered.value = event; @@ -646,43 +649,38 @@ class PlPlayerController { /// 跳转至指定位置 Future seekTo(Duration position, {type = 'seek'}) async { - // if (position >= duration.value) { - // position = duration.value - const Duration(milliseconds: 100); - // } - if (position < Duration.zero) { - position = Duration.zero; - } - _position.value = position; - updatePositionSecond(); - _heartDuration = position.inSeconds; - if (duration.value.inSeconds != 0) { - if (type != 'slider') { - /// 拖动进度条调节时,不等待第一帧,防止抖动 - await _videoPlayerController?.stream.buffer.first; + try { + if (position < Duration.zero) { + position = Duration.zero; } - await _videoPlayerController?.seek(position); - // if (playerStatus.stopped) { - // play(); - // } - } else { - print('seek duration else'); - _timerForSeek?.cancel(); - _timerForSeek = - Timer.periodic(const Duration(milliseconds: 200), (Timer t) async { - //_timerForSeek = null; - if (duration.value.inSeconds != 0) { - await _videoPlayerController!.stream.buffer.first; - await _videoPlayerController?.seek(position); - // if (playerStatus.status.value == PlayerStatus.paused) { - // play(); - // } - t.cancel(); - _timerForSeek = null; + _position.value = position; + updatePositionSecond(); + _heartDuration = position.inSeconds; + if (duration.value.inSeconds != 0) { + if (type != 'slider') { + await _videoPlayerController?.stream.buffer.first; } - }); + await _videoPlayerController?.seek(position); + } else { + _timerForSeek?.cancel(); + _timerForSeek ??= _startSeekTimer(position); + } + } catch (err) { + print('Error while seeking: $err'); } } + Timer? _startSeekTimer(Duration position) { + return Timer.periodic(const Duration(milliseconds: 200), (Timer t) async { + if (duration.value.inSeconds != 0) { + await _videoPlayerController!.stream.buffer.first; + await _videoPlayerController?.seek(position); + t.cancel(); + _timerForSeek = null; + } + }); + } + /// 设置倍速 Future setPlaybackSpeed(double speed) async { /// TODO _duration.value丢失 @@ -719,11 +717,10 @@ class PlPlayerController { await seekTo(Duration.zero); } await _videoPlayerController?.play(); - + playerStatus.status.value = PlayerStatus.playing; await getCurrentVolume(); await getCurrentBrightness(); - playerStatus.status.value = PlayerStatus.playing; // screenManager.setOverlays(false); /// 临时fix _duration.value丢失 diff --git a/lib/plugin/pl_player/models/bottom_control_type.dart b/lib/plugin/pl_player/models/bottom_control_type.dart index 739e1d38..d724c5de 100644 --- a/lib/plugin/pl_player/models/bottom_control_type.dart +++ b/lib/plugin/pl_player/models/bottom_control_type.dart @@ -4,6 +4,7 @@ enum BottomControlType { next, time, space, + episode, fit, speed, fullscreen, diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index be24b105..6a5f22ec 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -37,6 +37,7 @@ class PLVideoPlayer extends StatefulWidget { this.bottomList, this.customWidget, this.customWidgets, + this.showEposideCb, super.key, }); @@ -49,6 +50,7 @@ class PLVideoPlayer extends StatefulWidget { final Widget? customWidget; final List? customWidgets; + final Function? showEposideCb; @override State createState() => _PLVideoPlayerState(); @@ -214,8 +216,8 @@ class _PLVideoPlayerState extends State /// 上一集 BottomControlType.pre: ComBtn( icon: const Icon( - Icons.skip_previous_outlined, - size: 15, + Icons.skip_previous_rounded, + size: 21, color: Colors.white, ), fuc: () {}, @@ -229,8 +231,8 @@ class _PLVideoPlayerState extends State /// 下一集 BottomControlType.next: ComBtn( icon: const Icon( - Icons.last_page_outlined, - size: 15, + Icons.skip_next_rounded, + size: 21, color: Colors.white, ), fuc: () {}, @@ -239,6 +241,7 @@ class _PLVideoPlayerState extends State /// 时间进度 BottomControlType.time: Row( children: [ + const SizedBox(width: 8), Obx(() { return Text( _.durationSeconds.value >= 3600 @@ -266,6 +269,24 @@ class _PLVideoPlayerState extends State /// 空白占位 BottomControlType.space: const Spacer(), + /// 选集 + BottomControlType.episode: SizedBox( + height: 30, + width: 30, + child: TextButton( + onPressed: () { + widget.showEposideCb?.call(); + }, + style: ButtonStyle( + padding: MaterialStateProperty.all(EdgeInsets.zero), + ), + child: const Text( + '选集', + style: TextStyle(color: Colors.white, fontSize: 13), + ), + ), + ), + /// 画面比例 BottomControlType.fit: SizedBox( height: 30, diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index ebb71b54..35e7792a 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -68,91 +68,8 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { ); }, ), - Row( - children: [...buildBottomControl!], - ), - // Row( - // children: [ - // PlayOrPauseButton( - // controller: _, - // ), - // const SizedBox(width: 4), - // // 播放时间 - // Obx(() { - // return Text( - // _.durationSeconds.value >= 3600 - // ? printDurationWithHours( - // Duration(seconds: _.positionSeconds.value)) - // : printDuration( - // Duration(seconds: _.positionSeconds.value)), - // style: textStyle, - // ); - // }), - // const SizedBox(width: 2), - // const Text('/', style: textStyle), - // const SizedBox(width: 2), - // Obx( - // () => Text( - // _.durationSeconds.value >= 3600 - // ? printDurationWithHours( - // Duration(seconds: _.durationSeconds.value)) - // : printDuration( - // Duration(seconds: _.durationSeconds.value)), - // style: textStyle, - // ), - // ), - // const Spacer(), - // // 倍速 - // // Obx( - // // () => SizedBox( - // // width: 45, - // // height: 34, - // // child: TextButton( - // // style: ButtonStyle( - // // padding: MaterialStateProperty.all(EdgeInsets.zero), - // // ), - // // onPressed: () { - // // _.togglePlaybackSpeed(); - // // }, - // // child: Text( - // // '${_.playbackSpeed.toString()}X', - // // style: textStyle, - // // ), - // // ), - // // ), - // // ), - // SizedBox( - // height: 30, - // child: TextButton( - // onPressed: () => _.toggleVideoFit(), - // style: ButtonStyle( - // padding: MaterialStateProperty.all(EdgeInsets.zero), - // ), - // child: Obx( - // () => Text( - // _.videoFitDEsc.value, - // style: const TextStyle(color: Colors.white, fontSize: 13), - // ), - // ), - // ), - // ), - // const SizedBox(width: 10), - // // 全屏 - // Obx( - // () => ComBtn( - // icon: Icon( - // _.isFullScreen.value - // ? FontAwesomeIcons.compress - // : FontAwesomeIcons.expand, - // size: 15, - // color: Colors.white, - // ), - // fuc: () => triggerFullScreen!(), - // ), - // ), - // ], - // ), - const SizedBox(height: 12), + Row(children: [...buildBottomControl!]), + const SizedBox(height: 10), ], ), ); diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 1f1ea31e..7fda1bd8 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -39,6 +39,7 @@ import '../pages/setting/pages/color_select.dart'; import '../pages/setting/pages/display_mode.dart'; import '../pages/setting/pages/font_size_select.dart'; import '../pages/setting/pages/home_tabbar_set.dart'; +import '../pages/setting/pages/navigation_bar_set.dart'; import '../pages/setting/pages/play_gesture_set.dart'; import '../pages/setting/pages/play_speed_set.dart'; import '../pages/setting/recommend_setting.dart'; @@ -170,6 +171,9 @@ class Routes { // 播放器手势 CustomGetPage( name: '/playerGestureSet', page: () => const PlayGesturePage()), + // navigation bar + CustomGetPage( + name: '/navbarSetting', page: () => const NavigationBarSetPage()), ]; } diff --git a/lib/utils/download.dart b/lib/utils/download.dart index a9c56ec0..2aff8999 100644 --- a/lib/utils/download.dart +++ b/lib/utils/download.dart @@ -1,5 +1,7 @@ +import 'dart:io'; import 'dart:typed_data'; +import 'package:device_info_plus/device_info_plus.dart'; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -70,9 +72,20 @@ class DownloadUtils { static Future downloadImg(String imgUrl, {String imgType = 'cover'}) async { try { - if (!await requestPhotoPer()) { + if (!Platform.isAndroid || !await requestPhotoPer()) { return false; } + final androidInfo = await DeviceInfoPlugin().androidInfo; + if (androidInfo.version.sdkInt <= 32) { + if (!await requestStoragePer()) { + return false; + } + } else { + if (!await requestPhotoPer()) { + return false; + } + } + SmartDialog.showLoading(msg: '保存中'); var response = await Dio() .get(imgUrl, options: Options(responseType: ResponseType.bytes)); diff --git a/lib/utils/drawer.dart b/lib/utils/drawer.dart new file mode 100644 index 00000000..f4aa17f8 --- /dev/null +++ b/lib/utils/drawer.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +class DrawerUtils { + static void showRightDialog({ + required Widget child, + double width = 400, + bool useSystem = false, + }) { + SmartDialog.show( + alignment: Alignment.topRight, + animationBuilder: (controller, child, animationParam) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(controller.view), + child: child, + ); + }, + useSystem: useSystem, + maskColor: Colors.black.withOpacity(0.5), + animationTime: const Duration(milliseconds: 200), + builder: (context) => Container( + width: width, + color: Theme.of(context).scaffoldBackgroundColor, + child: SafeArea( + left: false, + right: false, + bottom: false, + child: MediaQuery( + data: const MediaQueryData(padding: EdgeInsets.zero), + child: child, + ), + ), + ), + ); + } +} diff --git a/lib/utils/em.dart b/lib/utils/em.dart index 733f5c35..2c5af8ba 100644 --- a/lib/utils/em.dart +++ b/lib/utils/em.dart @@ -19,15 +19,7 @@ class Em { return regCate(matchStr); }, onNonMatch: (String str) { if (str != '') { - str = str - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll(''', "'") - .replaceAll('"', '"') - .replaceAll(''', "'") - .replaceAll(' ', " ") - .replaceAll('&', "&"); + str = decodeHtmlEntities(str); Map map = {'type': 'text', 'text': str}; res.add(map); } @@ -35,4 +27,17 @@ class Em { }); return res; } + + static String decodeHtmlEntities(String title) { + return title + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll('"', '"') + .replaceAll(''', "'") + .replaceAll(' ', " ") + .replaceAll('&', "&") + .replaceAll(''', "'"); + } } diff --git a/lib/utils/global_data.dart b/lib/utils/global_data.dart index ef3daf21..29791210 100644 --- a/lib/utils/global_data.dart +++ b/lib/utils/global_data.dart @@ -1,10 +1,16 @@ +import 'package:hive/hive.dart'; +import 'package:pilipala/utils/storage.dart'; import '../models/common/index.dart'; +Box setting = GStrorage.setting; + class GlobalData { int imgQuality = 10; FullScreenGestureMode fullScreenGestureMode = FullScreenGestureMode.values.last; bool enablePlayerControlAnimation = true; + final bool enableMYBar = + setting.get(SettingBoxKey.enableMYBar, defaultValue: true); // 私有构造函数 GlobalData._(); diff --git a/lib/utils/main_stream.dart b/lib/utils/main_stream.dart new file mode 100644 index 00000000..e63248f8 --- /dev/null +++ b/lib/utils/main_stream.dart @@ -0,0 +1,32 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/rendering.dart'; +import 'package:get/get.dart'; + +import '../pages/home/index.dart'; +import '../pages/main/index.dart'; + +void handleScrollEvent(ScrollController scrollController) { + StreamController mainStream = + Get.find().bottomBarStream; + StreamController searchBarStream = + Get.find().searchBarStream; + EasyThrottle.throttle( + 'stream-throttler', + const Duration(milliseconds: 300), + () { + try { + final ScrollDirection direction = + scrollController.position.userScrollDirection; + if (direction == ScrollDirection.forward) { + mainStream.add(true); + searchBarStream.add(true); + } else if (direction == ScrollDirection.reverse) { + mainStream.add(false); + searchBarStream.add(false); + } + } catch (_) {} + }, + ); +} diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index a82972e0..29cf1846 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -148,7 +148,8 @@ class SettingBoxKey { hideTabBar = 'hideTabBar', // 收起底栏 tabbarSort = 'tabbarSort', // 首页tabbar dynamicBadgeMode = 'dynamicBadgeMode', - enableGradientBg = 'enableGradientBg'; + enableGradientBg = 'enableGradientBg', + navBarSort = 'navBarSort'; } class LocalCacheKey { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index adcc7b5a..987f57c1 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -51,7 +51,7 @@ class Utils { } if (time < 3600) { if (time == 0) { - return time; + return '00:00'; } final int minute = time ~/ 60; final double res = time / 60; @@ -208,17 +208,38 @@ class Utils { static int findClosestNumber(int target, List numbers) { int minDiff = 127; - late int closestNumber; + int closestNumber = 0; // 初始化为0,表示没有找到比目标值小的整数 + + if (numbers.contains(target)) { + return target; + } + // 向下查找 try { for (int number in numbers) { - int diff = (number - target).abs(); - - if (diff < minDiff) { - minDiff = diff; - closestNumber = number; + if (number < target) { + int diff = target - number; // 计算目标值与当前整数的差值 + if (diff < minDiff) { + minDiff = diff; + closestNumber = number; + return closestNumber; + } } } } catch (_) {} + + // 向上查找 + if (closestNumber == 0) { + try { + for (int number in numbers) { + int diff = (number - target).abs(); + + if (diff < minDiff) { + minDiff = diff; + closestNumber = number; + } + } + } catch (_) {} + } return closestNumber; } diff --git a/pubspec.lock b/pubspec.lock index 695505d7..a64c85c0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -409,6 +409,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.0.2" + dlna_dart: + dependency: "direct main" + description: + name: dlna_dart + sha256: ae07c1c53077bbf58756fa589f936968719b0085441981d33e74f82f89d1d281 + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.0.8" dynamic_color: dependency: "direct main" description: @@ -433,6 +441,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "5.0.3" + expandable: + dependency: "direct main" + description: + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" + url: "https://pub.flutter-io.cn" + source: hosted + version: "5.0.1" extended_image: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 5e19b56b..27b8b720 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -142,6 +142,10 @@ dependencies: path: 1.8.3 # 电池优化 disable_battery_optimization: ^1.1.1 + # 展开/收起 + expandable: ^5.0.1 + # 投屏 + dlna_dart: ^0.0.8 dev_dependencies: flutter_test: