diff --git a/lib/common/widgets/list_sheet.dart b/lib/common/widgets/list_sheet.dart index 862012c8..882c4899 100644 --- a/lib/common/widgets/list_sheet.dart +++ b/lib/common/widgets/list_sheet.dart @@ -57,11 +57,7 @@ class ListSheet { } SmartDialog.showToast('切换到:$title'); bottomSheetController.close(); - print(episode.runtimeType.toString()); if (episode.runtimeType.toString() == "EpisodeItem") { - print(episode.bvid); - print(episode.cid); - print(episode.aid); changeFucCall(episode.bvid, episode.cid, episode.aid); } else { changeFucCall(bvid!, episode.cid, aid!); diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 32ac2bb6..aeeedae3 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -41,7 +41,6 @@ class NetworkImgLayer extends StatelessWidget { final int defaultImgQuality = GlobalData().imgQuality; final String imageUrl = '${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? defaultImgQuality}q.webp'; - print(imageUrl); int? memCacheWidth, memCacheHeight; if (width > height || (origAspectRatio != null && origAspectRatio! > 1)) { diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index d62de12f..a81436b2 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -28,7 +28,6 @@ class DynamicsHttp { 'data': DynamicsDataModel.fromJson(res.data['data']), }; } catch (err) { - print(err); return { 'status': false, 'data': [], diff --git a/lib/http/init.dart b/lib/http/init.dart index 97defc8d..7f706d32 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -105,7 +105,7 @@ class Request { String jsonData = json.encode({ '3064': 1, - '39c8': '${spmPrefix}.fp.risk', + '39c8': '$spmPrefix.fp.risk', '3c43': { 'adca': 'Linux', 'bfe9': rand_png_end.substring(rand_png_end.length - 50), @@ -138,9 +138,9 @@ class Request { enableSystemProxy = setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false) as bool; systemProxyHost = - localCache.get(LocalCacheKey.systemProxyHost, defaultValue: ''); + setting.get(SettingBoxKey.systemProxyHost, defaultValue: ''); systemProxyPort = - localCache.get(LocalCacheKey.systemProxyPort, defaultValue: ''); + setting.get(SettingBoxKey.systemProxyPort, defaultValue: ''); dio = Dio(options); diff --git a/lib/http/search.dart b/lib/http/search.dart index 5194bca8..6e00ce3e 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -89,8 +89,10 @@ class SearchHttp { try { switch (searchType) { case SearchType.video: - List blackMidsList = - setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + List blackMidsList = setting + .get(SettingBoxKey.blackMidsList, defaultValue: [-1]) + .map((i) => i as int) + .toList(); for (var i in res.data['data']['result']) { // 屏蔽推广和拉黑用户 i['available'] = !blackMidsList.contains(i['mid']); diff --git a/lib/http/video.dart b/lib/http/video.dart index abceee95..b83dbfe7 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -45,8 +45,10 @@ class VideoHttp { ); if (res.data['code'] == 0) { List list = []; - List blackMidsList = - setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + List blackMidsList = setting + .get(SettingBoxKey.blackMidsList, defaultValue: [-1]) + .map((e) => e as int) + .toList(); for (var i in res.data['data']['item']) { //过滤掉live与ad,以及拉黑用户 if (i['goto'] == 'av' && @@ -91,8 +93,10 @@ class VideoHttp { ); if (res.data['code'] == 0) { List list = []; - List blackMidsList = - setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + List blackMidsList = setting + .get(SettingBoxKey.blackMidsList, defaultValue: [-1]) + .map((e) => e as int) + .toList(); for (var i in res.data['data']['items']) { // 屏蔽推广和拉黑用户 if (i['card_goto'] != 'ad_av' && @@ -123,8 +127,10 @@ class VideoHttp { ); if (res.data['code'] == 0) { List list = []; - List blackMidsList = - setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + List blackMidsList = setting + .get(SettingBoxKey.blackMidsList, defaultValue: [-1]) + .map((e) => e as int) + .toList(); for (var i in res.data['data']['list']) { if (!blackMidsList.contains(i['owner']['mid'])) { list.add(HotVideoItemModel.fromJson(i)); @@ -377,12 +383,12 @@ class VideoHttp { } static Future replyDel({ - required int type,//replyType + required int type, //replyType required int oid, required int rpid, }) async { var res = await Request().post(Api.replyDel, queryParameters: { - 'type': type,//type.index + 'type': type, //type.index 'oid': oid, 'rpid': rpid, 'csrf': await Request.getCsrf(), @@ -615,8 +621,10 @@ class VideoHttp { var res = await Request().get(rankApi); if (res.data['code'] == 0) { List list = []; - List blackMidsList = - setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]); + List blackMidsList = setting + .get(SettingBoxKey.blackMidsList, defaultValue: [-1]) + .map((e) => e as int) + .toList(); for (var i in res.data['data']['list']) { if (!blackMidsList.contains(i['owner']['mid'])) { list.add(HotVideoItemModel.fromJson(i)); diff --git a/lib/main.dart b/lib/main.dart index 20ee19c9..fdd8ee8c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -30,10 +30,11 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); await GStrorage.init(); - if (GStrorage.setting.get(SettingBoxKey.autoClearCache, defaultValue: false)) { + if (GStrorage.setting.get(SettingBoxKey.autoClearCache, defaultValue: true)) { await CacheManage.clearLibraryCache(); } - if (GStrorage.setting.get(SettingBoxKey.horizontalScreen, defaultValue: false)) { + if (GStrorage.setting + .get(SettingBoxKey.horizontalScreen, defaultValue: false)) { await SystemChrome.setPreferredOrientations( //支持竖屏与横屏 [ @@ -115,21 +116,18 @@ class MyApp extends StatelessWidget { // 强制设置高帧率 if (Platform.isAndroid) { - try { - late List modes; - FlutterDisplayMode.supported.then((value) { - modes = value; - var storageDisplay = setting.get(SettingBoxKey.displayMode); - DisplayMode f = DisplayMode.auto; - if (storageDisplay != null) { - f = modes.firstWhere((e) => e.toString() == storageDisplay); - } - DisplayMode preferred = modes.toList().firstWhere((el) => el == f); - FlutterDisplayMode.setPreferredMode(preferred); - }); - } catch (e) { - SmartDialog.showToast('设置帧率失败:$e', displayTime: const Duration(milliseconds: 500)); - } + late List modes; + FlutterDisplayMode.supported.then((value) { + modes = value; + var storageDisplay = setting.get(SettingBoxKey.displayMode); + DisplayMode f = DisplayMode.auto; + if (storageDisplay != null) { + f = modes.firstWhere((e) => e.toString() == storageDisplay, + orElse: () => f); + } + DisplayMode preferred = modes.toList().firstWhere((el) => el == f); + FlutterDisplayMode.setPreferredMode(preferred); + }); } return DynamicColorBuilder( diff --git a/lib/models/video/play/quality.dart b/lib/models/video/play/quality.dart index 96077364..6cae84cc 100644 --- a/lib/models/video/play/quality.dart +++ b/lib/models/video/play/quality.dart @@ -94,18 +94,18 @@ extension AudioQualityDesc on AudioQuality { enum VideoDecodeFormats { DVH1, - AVC, - HEVC, AV1, + HEVC, + AVC, } extension VideoDecodeFormatsDesc on VideoDecodeFormats { - static final List _descList = ['DVH1', 'AVC', 'HEVC', 'AV1']; + static final List _descList = ['DVH1', 'AV1', 'HEVC', 'AVC']; get description => _descList[index]; } extension VideoDecodeFormatsCode on VideoDecodeFormats { - static final List _codeList = ['dvh1', 'avc1', 'hev1', 'av01']; + static final List _codeList = ['dvh1', 'av01', 'hev1', 'avc1']; get code => _codeList[index]; static VideoDecodeFormats? fromCode(String code) { diff --git a/lib/pages/about/index.dart b/lib/pages/about/index.dart index b24a36f9..615a7054 100644 --- a/lib/pages/about/index.dart +++ b/lib/pages/about/index.dart @@ -84,6 +84,7 @@ class _AboutPageState extends State { () => ListTile( onTap: () => _aboutController.tapOnVersion(), title: const Text('当前版本'), + leading: const Icon(Icons.commit_outlined), trailing: Text(_aboutController.currentVersion.value, style: subTitleStyle), ), @@ -92,6 +93,7 @@ class _AboutPageState extends State { () => ListTile( onTap: () => _aboutController.onUpdate(), title: const Text('最新版本'), + leading: const Icon(Icons.flag_outlined), trailing: Text( _aboutController.isLoading.value ? '正在获取' @@ -117,6 +119,7 @@ class _AboutPageState extends State { ), ListTile( onTap: () => _aboutController.githubUrl(), + leading: const Icon(Icons.star_outline_outlined), title: const Text('Github开源仓库'), trailing: Text( 'github.com/orz12/pilipala', @@ -125,6 +128,7 @@ class _AboutPageState extends State { ), ListTile( onTap: () => _aboutController.feedback(), + leading: const Icon(Icons.feedback_outlined), title: const Text('问题反馈'), trailing: Icon( Icons.arrow_forward_ios, @@ -134,33 +138,37 @@ class _AboutPageState extends State { ), ListTile( onTap: () => _aboutController.qqGroup(), - title: const Text('QQ群:392176105'), - trailing: Icon( - Icons.arrow_forward_ios, - size: 16, - color: outline, + leading: const Icon(Icons.group_add_outlined), + title: const Text('QQ群'), + trailing: Text( + '392176105', + style: subTitleStyle, ), ), ListTile( - onTap: () => _aboutController.tgChanel(), + onTap: () => _aboutController.tgChannel(), + leading: const Icon(Icons.group_add_outlined), title: const Text('TG频道'), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), ), ListTile( onTap: () => _aboutController.webSiteUrl(), - title: const Text('访问官网'), + leading: const Icon(Icons.language), + title: const Text('官网'), trailing: Text( - 'https://pilipalanet.mysxl.cn/pilipala-x', + 'pilipalanet.mysxl.cn/pilipala-x', style: subTitleStyle, ), ), ListTile( onTap: () => _aboutController.aPay(), + leading: const Icon(Icons.wallet_giftcard_outlined), title: const Text('赞赏'), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), ), ListTile( onTap: () => _aboutController.logs(), + leading: const Icon(Icons.bug_report_outlined), title: const Text('错误日志'), trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline), ), @@ -169,11 +177,78 @@ class _AboutPageState extends State { await CacheManage().clearCacheAll(); getCacheSize(); }, + leading: const Icon(Icons.delete_outline), title: const Text('清除缓存'), subtitle: Text('图片及网络缓存 $cacheSize', style: subTitleStyle), ), + ListTile( + title: const Text('导入/导出设置'), + dense: false, + leading: const Icon(Icons.import_export_outlined), + onTap: () { + SmartDialog.show( + useSystem: true, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('导入/导出设置'), + children: [ + ListTile( + title: const Text('导出设置至剪贴板'), + onTap: () async { + SmartDialog.dismiss(); + String data = await GStrorage.exportAllSettings(); + Clipboard.setData(ClipboardData(text: data)); + SmartDialog.showToast('已复制到剪贴板'); + }, + ), + ListTile( + title: const Text('从剪贴板导入设置'), + onTap: () async { + SmartDialog.dismiss(); + ClipboardData? data = await Clipboard.getData('text/plain'); + if (data == null || data.text == null || data.text!.isEmpty) { + SmartDialog.showToast('剪贴板无数据'); + return; + } + SmartDialog.show( + useSystem: true, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('是否导入如下设置?'), + content: Text(data.text!), + actions: [ + TextButton( + onPressed: () { + SmartDialog.dismiss(); + }, + child: const Text('取消'), + ), + TextButton( + onPressed: () async { + SmartDialog.dismiss(); + try{ + await GStrorage.importAllSettings(data.text!); + SmartDialog.showToast('导入成功'); + } catch (e) { + SmartDialog.showToast('导入失败:$e'); + } + }, + child: const Text('确定'), + ), + ], + ); + }, + ); + }, + ), + ], + ); + }, + ); + }), ListTile( title: const Text('重置所有设置'), + leading: const Icon(Icons.settings_backup_restore_outlined), onTap: () { SmartDialog.show( useSystem: true, @@ -191,6 +266,8 @@ class _AboutPageState extends State { TextButton( onPressed: () { GStrorage.setting.clear(); + GStrorage.localCache.clear(); + GStrorage.video.clear(); SmartDialog.showToast('重置成功'); SmartDialog.dismiss(); }, @@ -312,24 +389,24 @@ class AboutController extends GetxController { useSystem: true, animationType: SmartAnimationType.centerFade_otherSlide, builder: (BuildContext context) { - return AlertDialog( + return SimpleDialog( title: const Text('问题反馈'), - actions: [ - ElevatedButton( - onPressed: () => launchUrl( + children: [ + ListTile( + title: const Text('GitHub Issue'), + onTap: () => launchUrl( Uri.parse('https://github.com/orz12/pilipala/issues'), // 系统自带浏览器打开 mode: LaunchMode.externalApplication, ), - child: const Text('GitHub Issue'), ), - ElevatedButton( - onPressed: () => launchUrl( + ListTile( + title: const Text('腾讯兔小巢'), + onTap: () => launchUrl( Uri.parse('https://support.qq.com/embed/phone/637735'), // 系统自带浏览器打开 mode: LaunchMode.externalApplication, ), - child: const Text('腾讯兔小巢'), ), ], ); @@ -355,7 +432,7 @@ class AboutController extends GetxController { } // tg频道 - tgChanel() { + tgChannel() { Clipboard.setData( const ClipboardData(text: 'https://t.me/+162zlPtZlT9hNWVl'), ); diff --git a/lib/pages/history_search/view.dart b/lib/pages/history_search/view.dart index f75dee02..efb602fa 100644 --- a/lib/pages/history_search/view.dart +++ b/lib/pages/history_search/view.dart @@ -1,9 +1,6 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:PiliPalaX/common/skeleton/video_card_h.dart'; -import 'package:PiliPalaX/common/widgets/http_error.dart'; -import 'package:PiliPalaX/common/widgets/no_data.dart'; import 'package:PiliPalaX/pages/history/widgets/item.dart'; import '../../common/constants.dart'; diff --git a/lib/pages/home/controller.dart b/lib/pages/home/controller.dart index b267abbf..ea0c03de 100644 --- a/lib/pages/home/controller.dart +++ b/lib/pages/home/controller.dart @@ -68,7 +68,7 @@ class HomeController extends GetxController with GetTickerProviderStateMixin { void setTabConfig() async { defaultTabs = [...tabsConfig]; tabbarSort = settingStorage.get(SettingBoxKey.tabbarSort, - defaultValue: ['live', 'rcmd', 'hot', 'bangumi']); + defaultValue: ['live', 'rcmd', 'hot', 'bangumi']).map((i) => i.toString()).toList(); defaultTabs.retainWhere( (item) => tabbarSort.contains((item['type'] as TabType).id)); defaultTabs.sort((a, b) => tabbarSort diff --git a/lib/pages/home/view.dart b/lib/pages/home/view.dart index 9db320a8..da460d4f 100644 --- a/lib/pages/home/view.dart +++ b/lib/pages/home/view.dart @@ -184,12 +184,14 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { duration: const Duration(milliseconds: 500), height: snapshot.data ? top + 52 : top, padding: EdgeInsets.fromLTRB(14, top + 6, 14, 0), - child: UserInfoWidget( - top: top, - ctr: ctr, - userLogin: isUserLoggedIn, - userFace: ctr?.userFace.value, - callback: () => callback!(), + child: Obx( + () => UserInfoWidget( + top: top, + ctr: ctr, + userLogin: isUserLoggedIn, + userFace: ctr?.userFace.value, + callback: () => callback!(), + ), ), ), ); @@ -219,18 +221,18 @@ class UserInfoWidget extends StatelessWidget { return Row( children: [ SearchBar(ctr: ctr), - if (userLogin.value) ...[ - const SizedBox(width: 4), - ClipRect( - child: IconButton( - tooltip: '消息', - onPressed: () => Get.toNamed('/whisper'), - icon: const Icon( - Icons.notifications_none, - ), - ), - ) - ], + const SizedBox(width: 4), + Obx(() => userLogin.value + ? ClipRect( + child: IconButton( + tooltip: '消息', + onPressed: () => Get.toNamed('/whisper'), + icon: const Icon( + Icons.notifications_none, + ), + ), + ) + : const SizedBox.shrink()), const SizedBox(width: 8), Semantics( label: "我的", diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 7bd7ffe5..f9454fc4 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -1,6 +1,5 @@ import 'dart:io'; -import 'package:PiliPalaX/utils/storage.dart'; import 'package:floating/floating.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 863b6775..99d1df6b 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -31,7 +31,7 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { int? _lastSelectTime; //上次点击时间 Box setting = GStrorage.setting; late bool enableMYBar; - late bool horizontalScreen; + late bool adaptiveNavBar; @override void initState() { @@ -40,7 +40,8 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { _mainController.pageController = PageController(initialPage: _mainController.selectedIndex); enableMYBar = setting.get(SettingBoxKey.enableMYBar, defaultValue: true); - horizontalScreen = setting.get(SettingBoxKey.horizontalScreen, defaultValue: false); + adaptiveNavBar = + setting.get(SettingBoxKey.adaptiveNavBar, defaultValue: false); } void setIndex(int value) async { @@ -112,134 +113,137 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { onPopInvoked: (bool didPop) async { _mainController.onBackPressed(context); }, - child: horizontalScreen - ? AdaptiveScaffold( - body: (_) => PageView( - physics: const NeverScrollableScrollPhysics(), - controller: _mainController.pageController, - onPageChanged: (index) { - _mainController.selectedIndex = index; - setState(() {}); - }, - children: _mainController.pages, - ), - destinations: _mainController.navigationBars.map((e) => NavigationDestination( - icon: Badge( - label: _mainController.dynamicBadgeType == - DynamicBadgeMode.number - ? Text(e['count'].toString()) - : null, - padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: - _mainController.dynamicBadgeType != - DynamicBadgeMode.hidden && - e['count'] > 0, - child: e['icon'], - backgroundColor: - Theme.of(context).colorScheme.primary, - textColor: Theme.of(context) - .colorScheme - .onInverseSurface, - ), - selectedIcon: e['selectIcon'], - label: e['label'], - )).toList(), - onSelectedIndexChange: (value) => setIndex(value), - selectedIndex: _mainController.selectedIndex, - useDrawer: false - ) - : Scaffold( - extendBody: true, - body: PageView( - physics: const NeverScrollableScrollPhysics(), - controller: _mainController.pageController, - onPageChanged: (index) { - _mainController.selectedIndex = index; - setState(() {}); - }, - 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: enableMYBar - ? NavigationBar( - onDestinationSelected: (value) => setIndex(value), - selectedIndex: _mainController.selectedIndex, - destinations: [ - ..._mainController.navigationBars.map((e) { - return NavigationDestination( - icon: Badge( - label: _mainController.dynamicBadgeType == - DynamicBadgeMode.number - ? Text(e['count'].toString()) - : null, - padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: - _mainController.dynamicBadgeType != - DynamicBadgeMode.hidden && - e['count'] > 0, - child: e['icon'], - backgroundColor: - Theme.of(context).colorScheme.primary, - textColor: Theme.of(context) - .colorScheme - .onInverseSurface, - ), - selectedIcon: e['selectIcon'], - label: e['label'], - ); - }).toList(), - ], - ) - : BottomNavigationBar( - currentIndex: _mainController.selectedIndex, - onTap: (value) => setIndex(value), - iconSize: 16, - selectedFontSize: 12, - unselectedFontSize: 12, - type: BottomNavigationBarType.fixed, - // selectedItemColor: - // Theme.of(context).colorScheme.primary, // 选中项的颜色 - // unselectedItemColor: - // Theme.of(context).colorScheme.onSurface, - items: [ - ..._mainController.navigationBars.map((e) { - return BottomNavigationBarItem( - icon: Badge( - label: _mainController.dynamicBadgeType == - DynamicBadgeMode.number - ? Text(e['count'].toString()) - : null, - padding: const EdgeInsets.fromLTRB(6, 0, 6, 0), - isLabelVisible: - _mainController.dynamicBadgeType != - DynamicBadgeMode.hidden && - e['count'] > 0, - child: e['icon'], - backgroundColor: - Theme.of(context).colorScheme.primary, - textColor: Theme.of(context) - .colorScheme - .onInverseSurface, - ), - activeIcon: e['selectIcon'], - label: e['label'], - ); - }).toList(), - ], - ), - ); - }, - ), - ), + child: adaptiveNavBar + ? AdaptiveScaffold( + body: (_) => PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _mainController.pageController, + onPageChanged: (index) { + _mainController.selectedIndex = index; + setState(() {}); + }, + children: _mainController.pages, + ), + destinations: _mainController.navigationBars + .map((e) => NavigationDestination( + icon: Badge( + label: _mainController.dynamicBadgeType == + DynamicBadgeMode.number + ? Text(e['count'].toString()) + : null, + padding: const EdgeInsets.fromLTRB(2, 0, 2, 0), + isLabelVisible: _mainController.dynamicBadgeType != + DynamicBadgeMode.hidden && + e['count'] > 0, + child: e['icon'], + backgroundColor: + Theme.of(context).colorScheme.primary, + textColor: + Theme.of(context).colorScheme.onInverseSurface, + ), + selectedIcon: e['selectIcon'], + label: e['label'], + )) + .toList(), + onSelectedIndexChange: (value) => setIndex(value), + selectedIndex: _mainController.selectedIndex, + extendedNavigationRailWidth: 180, + transitionDuration: const Duration(milliseconds: 500), + useDrawer: true) + : Scaffold( + extendBody: true, + body: PageView( + physics: const NeverScrollableScrollPhysics(), + controller: _mainController.pageController, + onPageChanged: (index) { + _mainController.selectedIndex = index; + setState(() {}); + }, + 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: enableMYBar + ? NavigationBar( + onDestinationSelected: (value) => setIndex(value), + selectedIndex: _mainController.selectedIndex, + destinations: [ + ..._mainController.navigationBars.map((e) { + return NavigationDestination( + icon: Badge( + label: _mainController.dynamicBadgeType == + DynamicBadgeMode.number + ? Text(e['count'].toString()) + : null, + padding: + const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: + _mainController.dynamicBadgeType != + DynamicBadgeMode.hidden && + e['count'] > 0, + child: e['icon'], + backgroundColor: + Theme.of(context).colorScheme.primary, + textColor: Theme.of(context) + .colorScheme + .onInverseSurface, + ), + selectedIcon: e['selectIcon'], + label: e['label'], + ); + }).toList(), + ], + ) + : BottomNavigationBar( + currentIndex: _mainController.selectedIndex, + onTap: (value) => setIndex(value), + iconSize: 16, + selectedFontSize: 12, + unselectedFontSize: 12, + type: BottomNavigationBarType.fixed, + // selectedItemColor: + // Theme.of(context).colorScheme.primary, // 选中项的颜色 + // unselectedItemColor: + // Theme.of(context).colorScheme.onSurface, + items: [ + ..._mainController.navigationBars.map((e) { + return BottomNavigationBarItem( + icon: Badge( + label: _mainController.dynamicBadgeType == + DynamicBadgeMode.number + ? Text(e['count'].toString()) + : null, + padding: + const EdgeInsets.fromLTRB(6, 0, 6, 0), + isLabelVisible: + _mainController.dynamicBadgeType != + DynamicBadgeMode.hidden && + e['count'] > 0, + child: e['icon'], + backgroundColor: + Theme.of(context).colorScheme.primary, + textColor: Theme.of(context) + .colorScheme + .onInverseSurface, + ), + activeIcon: e['selectIcon'], + label: e['label'], + ); + }).toList(), + ], + ), + ); + }, + ), + ), ); } } diff --git a/lib/pages/media/view.dart b/lib/pages/media/view.dart index c310e9ff..85844779 100644 --- a/lib/pages/media/view.dart +++ b/lib/pages/media/view.dart @@ -79,6 +79,16 @@ class _MediaPageState extends State ), ), ), + trailing: IconButton( + tooltip: '设置', + onPressed: () { + Get.toNamed('/setting'); + }, + icon: const Icon( + Icons.settings_outlined, + size: 20, + ), + ) ), for (var i in mediaController.list) ...[ ListTile( diff --git a/lib/pages/rank/controller.dart b/lib/pages/rank/controller.dart index 2473456e..53d43b5a 100644 --- a/lib/pages/rank/controller.dart +++ b/lib/pages/rank/controller.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; diff --git a/lib/pages/rank/view.dart b/lib/pages/rank/view.dart index 3a20bfa3..b203186f 100644 --- a/lib/pages/rank/view.dart +++ b/lib/pages/rank/view.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'package:PiliPalaX/common/constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -28,8 +27,6 @@ class _RankPageState extends State TabController(vsync: this, length: _rankController.tabs.length); _selectedTabIndex = _rankController.initialIndex.value; _rankController.tabController.addListener(() { - print("_rankController.tabController.index"); - print(_rankController.tabController.index); if (!_rankController.tabController.indexIsChanging) { // _rankController.onRefresh(); setState(() { diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart index da5a6c16..c2fb2a80 100644 --- a/lib/pages/setting/extra_setting.dart +++ b/lib/pages/setting/extra_setting.dart @@ -21,7 +21,6 @@ class ExtraSetting extends StatefulWidget { class _ExtraSettingState extends State { Box setting = GStrorage.setting; final SettingController settingController = Get.put(SettingController()); - static Box localCache = GStrorage.localCache; late dynamic defaultReplySort; late dynamic defaultDynamicType; late dynamic enableSystemProxy; @@ -45,9 +44,9 @@ class _ExtraSettingState extends State { enableSystemProxy = setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false); defaultSystemProxyHost = - localCache.get(LocalCacheKey.systemProxyHost, defaultValue: ''); + setting.get(SettingBoxKey.systemProxyHost, defaultValue: ''); defaultSystemProxyPort = - localCache.get(LocalCacheKey.systemProxyPort, defaultValue: ''); + setting.get(SettingBoxKey.systemProxyPort, defaultValue: ''); } // 设置代理 @@ -111,8 +110,8 @@ class _ExtraSettingState extends State { ), TextButton( onPressed: () async { - localCache.put(LocalCacheKey.systemProxyHost, systemProxyHost); - localCache.put(LocalCacheKey.systemProxyPort, systemProxyPort); + setting.put(SettingBoxKey.systemProxyHost, systemProxyHost); + setting.put(SettingBoxKey.systemProxyPort, systemProxyPort); SmartDialog.dismiss(); // Request.dio; }, @@ -143,9 +142,10 @@ class _ExtraSettingState extends State { body: ListView( children: [ Obx( - () => ListTile( + () => ListTile( enableFeedback: true, onTap: () => settingController.onOpenFeedBack(), + leading: const Icon(Icons.vibration_outlined), title: Text('震动反馈', style: titleStyle), subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle), trailing: Transform.scale( @@ -153,13 +153,13 @@ class _ExtraSettingState extends State { scale: 0.8, child: Switch( thumbIcon: MaterialStateProperty.resolveWith( - (Set states) { - if (states.isNotEmpty && - states.first == MaterialState.selected) { - return const Icon(Icons.done); - } - return null; // All other states will use the default thumbIcon. - }), + (Set states) { + if (states.isNotEmpty && + states.first == MaterialState.selected) { + return const Icon(Icons.done); + } + return null; // All other states will use the default thumbIcon. + }), value: settingController.feedBackEnable.value, onChanged: (value) => settingController.onOpenFeedBack()), ), @@ -168,12 +168,14 @@ class _ExtraSettingState extends State { const SetSwitchItem( title: '大家都在搜', subTitle: '是否展示「大家都在搜」', + leading: Icon(Icons.data_thresholding_outlined), setKey: SettingBoxKey.enableHotKey, defaultVal: true, ), SetSwitchItem( title: '搜索默认词', subTitle: '是否展示搜索框默认词', + leading: const Icon(Icons.whatshot_outlined), setKey: SettingBoxKey.enableSearchWord, defaultVal: true, callFn: (val) { @@ -183,30 +185,35 @@ class _ExtraSettingState extends State { const SetSwitchItem( title: '快速收藏', subTitle: '点按收藏至默认,长按选择文件夹', + leading: Icon(Icons.bookmark_add_outlined), setKey: SettingBoxKey.enableQuickFav, defaultVal: false, ), const SetSwitchItem( title: '评论区搜索关键词', subTitle: '展示评论区搜索关键词', + leading: Icon(Icons.search_outlined), setKey: SettingBoxKey.enableWordRe, defaultVal: false, ), const SetSwitchItem( title: '启用ai总结', subTitle: '视频详情页开启ai总结', + leading: Icon(Icons.engineering_outlined), setKey: SettingBoxKey.enableAi, defaultVal: true, ), const SetSwitchItem( title: '消息页禁用“收到的赞”功能', subTitle: '禁止打开入口,降低网络社交依赖', + leading: Icon(Icons.beach_access_outlined), setKey: SettingBoxKey.disableLikeMsg, defaultVal: false, ), ListTile( dense: false, title: Text('评论展示', style: titleStyle), + leading: const Icon(Icons.whatshot_outlined), subtitle: Text( '当前优先展示「${ReplySortType.values[defaultReplySort].titles}」', style: subTitleStyle, @@ -233,6 +240,7 @@ class _ExtraSettingState extends State { ListTile( dense: false, title: Text('动态展示', style: titleStyle), + leading: const Icon(Icons.dynamic_feed_outlined), subtitle: Text( '当前优先展示「${DynamicsType.values[defaultDynamicType].labels}」', style: subTitleStyle, @@ -259,6 +267,7 @@ class _ExtraSettingState extends State { ListTile( enableFeedback: true, onTap: () => twoFADialog(), + leading: const Icon(Icons.airplane_ticket_outlined), title: Text('设置代理', style: titleStyle), subtitle: Text('设置代理 host:port', style: subTitleStyle), trailing: Transform.scale( @@ -287,12 +296,14 @@ class _ExtraSettingState extends State { const SetSwitchItem( title: '自动清除缓存', subTitle: '每次启动时清除缓存', + leading: Icon(Icons.auto_delete_outlined), setKey: SettingBoxKey.autoClearCache, - defaultVal: false, + defaultVal: true, ), const SetSwitchItem( title: '检查更新', subTitle: '每次启动时检查是否需要更新', + leading: Icon(Icons.system_update_alt_outlined), setKey: SettingBoxKey.autoUpdate, defaultVal: false, ), diff --git a/lib/pages/setting/hidden_settings.dart b/lib/pages/setting/hidden_settings.dart index c6167b19..fed22c1f 100644 --- a/lib/pages/setting/hidden_settings.dart +++ b/lib/pages/setting/hidden_settings.dart @@ -22,10 +22,6 @@ class _HiddenSettingState extends State { @override Widget build(BuildContext context) { TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!; - TextStyle subTitleStyle = Theme.of(context) - .textTheme - .labelMedium! - .copyWith(color: Theme.of(context).colorScheme.outline); return Scaffold( appBar: AppBar( centerTitle: false, diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart index 547b493f..2239a00b 100644 --- a/lib/pages/setting/play_setting.dart +++ b/lib/pages/setting/play_setting.dart @@ -65,30 +65,35 @@ class _PlaySettingState extends State { const SetSwitchItem( title: '弹幕开关', subTitle: '是否展示弹幕', + leading: Icon(Icons.comment_outlined), setKey: SettingBoxKey.enableShowDanmaku, defaultVal: false, ), ListTile( dense: false, onTap: () => Get.toNamed('/playSpeedSet'), + leading: const Icon(Icons.speed_outlined), title: Text('倍速设置', style: titleStyle), subtitle: Text('设置视频播放速度', style: subTitleStyle), ), const SetSwitchItem( title: '自动播放', subTitle: '进入详情页自动播放', + leading: Icon(Icons.motion_photos_auto_outlined), setKey: SettingBoxKey.autoPlayEnable, defaultVal: true, ), const SetSwitchItem( title: '双击快退/快进', subTitle: '左侧双击快退,右侧双击快进', + leading: Icon(Icons.touch_app_outlined), setKey: SettingBoxKey.enableQuickDouble, defaultVal: true, ), ListTile( dense: false, title: Text('自动启用字幕', style: titleStyle), + leading: const Icon(Icons.closed_caption_outlined), subtitle: Text( '当前选择偏好:' '${SubtitlePreferenceCode.fromCode(defaultSubtitlePreference)!.description}', @@ -116,30 +121,35 @@ class _PlaySettingState extends State { const SetSwitchItem( title: '竖屏扩大展示', subTitle: '小屏竖屏视频宽高比由16:9扩大至4:5(!暂不支持临时收起)', + leading: Icon(Icons.expand_outlined), setKey: SettingBoxKey.enableVerticalExpand, defaultVal: false, ), const SetSwitchItem( title: '自动全屏', subTitle: '视频开始播放时进入全屏', + leading: Icon(Icons.fullscreen_outlined), setKey: SettingBoxKey.enableAutoEnter, defaultVal: false, ), const SetSwitchItem( title: '自动退出全屏', subTitle: '视频结束播放时退出全屏', + leading: Icon(Icons.fullscreen_exit_outlined), setKey: SettingBoxKey.enableAutoExit, defaultVal: true, ), const SetSwitchItem( title: '全向旋转', - subTitle: '非全屏时可受重力转为临时全屏,若系统锁定旋转仍异常触发请关闭,无异常可保持开启', + subTitle: '小屏可受重力转为临时全屏,若系统锁定旋转仍触发请关闭,关闭会影响横屏适配', + leading: Icon(Icons.screen_rotation_alt_outlined), setKey: SettingBoxKey.allowRotateScreen, defaultVal: true, ), const SetSwitchItem( title: '后台播放', subTitle: '进入后台时继续播放', + leading: Icon(Icons.motion_photos_pause_outlined), setKey: SettingBoxKey.continuePlayInBackground, defaultVal: true, ), @@ -147,6 +157,7 @@ class _PlaySettingState extends State { SetSwitchItem( title: '后台画中画', subTitle: '进入后台时以小窗形式(PiP)播放', + leading: const Icon(Icons.picture_in_picture_outlined), setKey: SettingBoxKey.autoPiP, defaultVal: false, callFn: (val) { @@ -160,26 +171,30 @@ class _PlaySettingState extends State { const SetSwitchItem( title: '画中画不加载弹幕', subTitle: '当弹幕开关开启时,小窗屏蔽弹幕以获得较好的体验', + leading: Icon(Icons.comments_disabled_outlined), setKey: SettingBoxKey.pipNoDanmaku, defaultVal: true, ), const SetSwitchItem( title: '全屏手势反向', subTitle: '默认播放器中部向上滑动进入全屏,向下退出\n开启后向下全屏,向上退出', + leading: Icon(Icons.swap_vert_outlined), setKey: SettingBoxKey.fullScreenGestureReverse, defaultVal: false, ), const SetSwitchItem( title: '观看人数', subTitle: '展示同时在看人数', + leading: Icon(Icons.people_outlined), setKey: SettingBoxKey.enableOnlineTotal, defaultVal: false, ), ListTile( dense: false, - title: Text('默认全屏方式', style: titleStyle), + title: Text('默认全屏方向', style: titleStyle), + leading: const Icon(Icons.open_with_outlined), subtitle: Text( - '当前全屏方式:${FullScreenModeCode.fromCode(defaultFullScreenMode)!.description}', + '当前全屏方向:${FullScreenModeCode.fromCode(defaultFullScreenMode)!.description}', style: subTitleStyle, ), onTap: () async { @@ -187,7 +202,7 @@ class _PlaySettingState extends State { context: context, builder: (context) { return SelectDialog( - title: '默认全屏方式', + title: '默认全屏方向', value: defaultFullScreenMode, values: FullScreenMode.values.map((e) { return {'title': e.description, 'value': e.code}; @@ -204,6 +219,7 @@ class _PlaySettingState extends State { ListTile( dense: false, title: Text('底部进度条展示', style: titleStyle), + leading: const Icon(Icons.border_bottom_outlined), subtitle: Text( '当前展示方式:${BtmProgresBehaviorCode.fromCode(defaultBtmProgressBehavior)!.description}', style: subTitleStyle, @@ -230,6 +246,7 @@ class _PlaySettingState extends State { const SetSwitchItem( title: '后台音频服务', subTitle: '避免画中画没有播放暂停功能', + leading: Icon(Icons.volume_up_outlined), setKey: SettingBoxKey.enableBackgroundPlay, defaultVal: true, ), diff --git a/lib/pages/setting/privacy_setting.dart b/lib/pages/setting/privacy_setting.dart index 9965a506..146fa0cb 100644 --- a/lib/pages/setting/privacy_setting.dart +++ b/lib/pages/setting/privacy_setting.dart @@ -1,4 +1,6 @@ +import 'package:PiliPalaX/utils/cookie.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; @@ -6,6 +8,11 @@ import 'package:PiliPalaX/http/interceptor_anonymity.dart'; import 'package:PiliPalaX/http/member.dart'; import 'package:PiliPalaX/utils/storage.dart'; +import '../../http/user.dart'; +import '../../models/user/info.dart'; +import '../../utils/login.dart'; +import '../home/controller.dart'; +import '../media/controller.dart'; import '../mine/controller.dart'; class PrivacySetting extends StatefulWidget { @@ -18,13 +25,16 @@ class PrivacySetting extends StatefulWidget { class _PrivacySettingState extends State { bool userLogin = false; Box userInfoCache = GStrorage.userInfo; - var userInfo; + UserInfoData? userInfo; + late bool hiddenSettingUnlocked; @override void initState() { super.initState(); userInfo = userInfoCache.get('userInfoCache'); userLogin = userInfo != null; + hiddenSettingUnlocked = GStrorage.setting + .get(SettingBoxKey.hiddenSettingUnlocked, defaultValue: false); } @override @@ -56,6 +66,7 @@ class _PrivacySettingState extends State { dense: false, title: Text('黑名单管理', style: titleStyle), subtitle: Text('已拉黑用户', style: subTitleStyle), + leading: const Icon(Icons.block), ), ListTile( onTap: () { @@ -66,15 +77,33 @@ class _PrivacySettingState extends State { }, dense: false, title: Text('刷新access_key', style: titleStyle), + leading: const Icon(Icons.perm_device_info_outlined), subtitle: Text( - 'access_key是app端的用户凭证,用于推荐接口。刷新将使用cookie请求新的access_key,小概率导致其他设备下线。若发现app端推荐内容不是个性化内容,可尝试刷新', + '用于app端推荐接口的用户凭证。刷新有小概率导致其他设备下线。若app端未推荐个性化内容,可尝试刷新或清除本app数据后重新登录', style: subTitleStyle), ), + if (hiddenSettingUnlocked) + ListTile( + title: Text( + '导入/导出cookie', + style: titleStyle, + ), + subtitle: Text( + 'cookie代表您的登录状态,仅供高级用户使用', + style: subTitleStyle, + ), + leading: const Icon(Icons.cookie_outlined), + dense: false, + onTap: () { + import_export_cookies(titleStyle, subTitleStyle); + }, + ), ListTile( onTap: () { MineController.onChangeAnonymity(context); setState(() {}); }, + leading: const Icon(Icons.privacy_tip_outlined), dense: false, title: Text(MineController.anonymity ? '退出无痕模式' : '进入无痕模式', style: titleStyle), @@ -104,6 +133,7 @@ class _PrivacySettingState extends State { }, ); }, + leading: const Icon(Icons.flag_outlined), dense: false, title: Text('了解无痕模式', style: titleStyle), subtitle: Text('查看无痕模式作用的API列表', style: subTitleStyle), @@ -112,4 +142,146 @@ class _PrivacySettingState extends State { ), ); } + + void import_export_cookies(TextStyle titleStyle, TextStyle subTitleStyle) { + SmartDialog.show( + useSystem: true, + builder: (BuildContext context) { + return SimpleDialog( + title: const Text('导入/导出cookie', style: TextStyle(color: Colors.red)), + children: [ + ListTile( + title: Text( + '导出cookie至剪贴板', + style: titleStyle.copyWith(color: Colors.red), + ), + leading: const Icon( + Icons.warning_amber, + color: Colors.red, + ), + subtitle: Text( + '泄露账号cookie等同于绕过账号密码与验证码直接登录,可导致隐私泄露、风控、毁号、盗号等各类问题。\n' + '你应妥善保管该cookie且仅供自己使用。你承诺,不会利用本服务进行任何违法或不当的活动。你承诺,对所进行的一切活动' + '(包括但不限于网上点击同意或提交各类规则协议或购买服务、分享资讯或图片等)负全部责任。\n' + '你承诺、理解、同意并确认,在你的账户遭到未获授权的使用,或者发生其他任何安全问题时,' + '作者不对上述情形产生的任何直接或间接的遗失或损害承担责任。', + style: subTitleStyle.copyWith(color: Colors.redAccent), + ), + dense: false, + onTap: () async { + await SmartDialog.dismiss(); + if (!userLogin) { + SmartDialog.showToast('请先登录'); + return; + } + final String cookie = await CookieTool.exportCookie(); + await SmartDialog.show( + builder: (context) { + return AlertDialog( + title: const Text('导出cookie(危险)', + style: TextStyle(color: Colors.red)), + content: Text(cookie), + actions: [ + TextButton( + onPressed: () async { + await SmartDialog.dismiss(); + await Clipboard.setData( + ClipboardData(text: cookie)); + }, + child: const Text('复制(危险)', + style: TextStyle(color: Colors.red)), + ), + TextButton( + onPressed: () async { + await SmartDialog.dismiss(); + }, + child: const Text('取消'), + ), + ], + ); + }, + ); + }), + ListTile( + title: Text( + '从剪贴板导入cookie', + style: titleStyle, + ), + leading: const Icon( + Icons.warning_amber, + color: Colors.red, + ), + subtitle: Text( + '导入将覆盖当前登录状态,你应自行对利用服务从事的所有行为及结果承担责任,请慎用', + style: subTitleStyle, + ), + dense: false, + onTap: () async { + await SmartDialog.dismiss(); + ClipboardData? data = await Clipboard.getData('text/plain'); + if (data == null || data.text == null || data.text == '') { + SmartDialog.showToast('未检测到剪贴板内容'); + return; + } + await SmartDialog.show( + builder: (context) { + return AlertDialog( + title: const Text('导入剪贴板中的cookie'), + content: Text(data.text!), + actions: [ + TextButton( + onPressed: () async { + await SmartDialog.dismiss(); + }, + child: const Text('取消'), + ), + TextButton( + onPressed: () async { + await SmartDialog.dismiss(); + final String cookie = data.text!; + try { + await CookieTool.importCookie(cookie); + await SmartDialog.showToast('已导入'); + await CookieTool.onSet(); + final result = await UserHttp.userInfo(); + if (result['status'] && + result['data'].isLogin) { + SmartDialog.showToast('登录成功,当前采用「' + '${GStrorage.setting.get(SettingBoxKey.defaultRcmdType, defaultValue: 'web')}' + '端」推荐'); + Box userInfoCache = GStrorage.userInfo; + await userInfoCache.put( + 'userInfoCache', result['data']); + final HomeController homeCtr = + Get.find(); + homeCtr.updateLoginStatus(true); + homeCtr.userFace.value = result['data'].face; + final MediaController mediaCtr = + Get.find(); + mediaCtr.mid = result['data'].mid; + await LoginUtils.refreshLoginStatus(true); + Get.back(); + } else { + // 获取用户信息失败 + SmartDialog.showNotify( + msg: + '登录失败,请检查cookie是否正确,${result['message']}', + notifyType: NotifyType.warning); + } + } catch (e) { + SmartDialog.showToast('导入失败:$e'); + } + }, + child: const Text('确认'), + ), + ], + ); + }, + ); + }), + ], + ); + }, + ); + } } diff --git a/lib/pages/setting/recommend_setting.dart b/lib/pages/setting/recommend_setting.dart index 57a6195f..f7cd1f1e 100644 --- a/lib/pages/setting/recommend_setting.dart +++ b/lib/pages/setting/recommend_setting.dart @@ -57,7 +57,7 @@ class _RecommendSettingState extends State { centerTitle: false, titleSpacing: 0, title: Text( - '推荐设置', + '推荐流设置', style: Theme.of(context).textTheme.titleMedium, ), ), @@ -66,6 +66,7 @@ class _RecommendSettingState extends State { ListTile( dense: false, title: Text('首页推荐类型', style: titleStyle), + leading: const Icon(Icons.model_training_outlined), subtitle: Text( '当前使用「$defaultRcmdType端」推荐¹', style: subTitleStyle, @@ -131,12 +132,14 @@ class _RecommendSettingState extends State { const SetSwitchItem( title: '推荐动态', subTitle: '是否在推荐内容中展示动态(仅app端)', + leading: Icon(Icons.motion_photos_on_outlined), setKey: SettingBoxKey.enableRcmdDynamic, defaultVal: true, ), const SetSwitchItem( title: '首页推荐刷新', subTitle: '下拉刷新时保留上次内容', + leading: Icon(Icons.refresh), setKey: SettingBoxKey.enableSaveLastData, defaultVal: false, ), @@ -144,6 +147,7 @@ class _RecommendSettingState extends State { const Divider(height: 1), ListTile( dense: false, + leading: const Icon(Icons.thumb_up_outlined), title: Text('点赞率过滤', style: titleStyle), subtitle: Text( '过滤掉点赞数/播放量「小于$minLikeRatioForRecommend%」的推荐视频(仅web端)', @@ -172,6 +176,7 @@ class _RecommendSettingState extends State { ListTile( dense: false, title: Text('视频时长过滤', style: titleStyle), + leading: const Icon(Icons.timelapse_outlined), subtitle: Text( '过滤掉时长「小于$minDurationForRcmd秒」的推荐视频', style: subTitleStyle, @@ -199,6 +204,7 @@ class _RecommendSettingState extends State { SetSwitchItem( title: '已关注Up豁免推荐过滤', subTitle: '推荐中已关注用户发布的内容不会被过滤', + leading: const Icon(Icons.favorite_border_outlined), setKey: SettingBoxKey.exemptFilterForFollowed, defaultVal: true, callFn: (_) => {RecommendFilter.update}, @@ -234,6 +240,7 @@ class _RecommendSettingState extends State { SetSwitchItem( title: '过滤器也应用于相关视频', subTitle: '视频详情页的相关视频也进行过滤²', + leading: const Icon(Icons.explore_outlined), setKey: SettingBoxKey.applyFilterToRelatedVideos, defaultVal: true, callFn: (_) => {RecommendFilter.update}, diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart index 460af815..174c776f 100644 --- a/lib/pages/setting/style_setting.dart +++ b/lib/pages/setting/style_setting.dart @@ -63,7 +63,8 @@ class _StyleSettingState extends State { children: [ SetSwitchItem( title: '横屏适配', - subTitle: '开启该项启用横屏布局与逻辑(测试)', + subTitle: '启用横屏布局与逻辑,适用于平板等设备', + leading: const Icon(Icons.phonelink_outlined), setKey: SettingBoxKey.horizontalScreen, defaultVal: false, callFn: (value) { @@ -75,15 +76,24 @@ class _StyleSettingState extends State { SmartDialog.showToast('已关闭横屏适配'); } }), + const SetSwitchItem( + title: '自适应底栏/侧边栏', + subTitle: '横竖屏自动切换(其它底栏设置失效)', + leading: Icon(Icons.chrome_reader_mode_outlined), + setKey: SettingBoxKey.adaptiveNavBar, + defaultVal: false, + ), const SetSwitchItem( title: 'MD3样式底栏', - subTitle: '符合Material You设计规范的底栏,关闭可使底栏变窄', + subTitle: 'Material You设计规范底栏,关闭可变窄', + leading: Icon(Icons.design_services_outlined), setKey: SettingBoxKey.enableMYBar, defaultVal: true, ), const SetSwitchItem( title: '首页顶栏收起', subTitle: '首页列表滑动时,收起顶栏', + leading: Icon(Icons.vertical_align_top_outlined), setKey: SettingBoxKey.hideSearchBar, defaultVal: false, needReboot: true, @@ -91,6 +101,7 @@ class _StyleSettingState extends State { const SetSwitchItem( title: '首页底栏收起', subTitle: '首页列表滑动时,收起底栏', + leading: Icon(Icons.vertical_align_bottom_outlined), setKey: SettingBoxKey.hideTabBar, defaultVal: false, needReboot: true, @@ -98,6 +109,7 @@ class _StyleSettingState extends State { const SetSwitchItem( title: '首页背景渐变', setKey: SettingBoxKey.enableGradientBg, + leading: Icon(Icons.gradient_outlined), defaultVal: true, needReboot: true, ), @@ -123,11 +135,12 @@ class _StyleSettingState extends State { setState(() {}); } }, + leading: const Icon(Icons.calendar_view_week_outlined), dense: false, - title: Text('最大列宽(dp)基准', style: titleStyle), + title: Text('列表宽度(dp)上限', style: titleStyle), subtitle: Text( - '当前:${maxRowWidth.toInt()}dp,屏幕宽度:${MediaQuery.of(context).size.width.toPrecision(2)}dp。' - '该值决定列表在不同屏宽下的列数,部分列表会根据系数折算宽度', + '当前:${maxRowWidth.toInt()}dp,屏幕宽度:${MediaQuery.of(context).size.width.toPrecision(2)}dp。' + '宽度越小列数越多,横条、大卡会2倍折算', style: subTitleStyle, ), ), @@ -187,6 +200,7 @@ class _StyleSettingState extends State { }, title: Text('图片质量', style: titleStyle), subtitle: Text('选择合适的图片清晰度,上限100%', style: subTitleStyle), + leading: const Icon(Icons.image_outlined), trailing: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Obx( @@ -218,8 +232,9 @@ class _StyleSettingState extends State { setting.put(SettingBoxKey.defaultToastOp, result); } }, - title: Text('Toast不透明度', style: titleStyle), - subtitle: Text('自定义Toast不透明度', style: subTitleStyle), + leading: const Icon(Icons.opacity_outlined), + title: Text('气泡提示不透明度', style: titleStyle), + subtitle: Text('自定义气泡提示(Toast)不透明度', style: subTitleStyle), ), ListTile( dense: false, @@ -242,6 +257,7 @@ class _StyleSettingState extends State { Get.forceAppUpdate(); } }, + leading: const Icon(Icons.flashlight_on_outlined), title: Text('主题模式', style: titleStyle), subtitle: Obx(() => Text( '当前模式:${settingController.themeType.value.description}', @@ -251,6 +267,7 @@ class _StyleSettingState extends State { dense: false, onTap: () => settingController.setDynamicBadgeMode(context), title: Text('动态未读标记', style: titleStyle), + leading: const Icon(Icons.motion_photos_on_outlined), subtitle: Obx(() => Text( '当前标记样式:${settingController.dynamicBadgeType.value.description}', style: subTitleStyle)), @@ -258,6 +275,7 @@ class _StyleSettingState extends State { ListTile( dense: false, onTap: () => Get.toNamed('/colorSetting'), + leading: const Icon(Icons.color_lens_outlined), title: Text('应用主题', style: titleStyle), subtitle: Obx(() => Text( '当前主题:${colorSelectController.type.value == 0 ? '动态取色' : '指定颜色'}', @@ -266,12 +284,14 @@ class _StyleSettingState extends State { const SetSwitchItem( title: '默认展示评论区', subTitle: '在视频详情页默认切换至评论区页', + leading: Icon(Icons.mode_comment_outlined), setKey: SettingBoxKey.defaultShowComment, defaultVal: false, ), ListTile( dense: false, onTap: () => settingController.seteDefaultHomePage(context), + leading: const Icon(Icons.home_outlined), title: Text('默认启动页', style: titleStyle), subtitle: Obx(() => Text( '当前启动页:${defaultNavigationBars.firstWhere((e) => e['id'] == settingController.defaultHomePage.value)['label']}', @@ -281,18 +301,21 @@ class _StyleSettingState extends State { dense: false, onTap: () => Get.toNamed('/fontSizeSetting'), title: Text('字体大小', style: titleStyle), + leading: const Icon(Icons.format_size_outlined), ), ListTile( dense: false, onTap: () => Get.toNamed('/tabbarSetting'), title: Text('首页标签页', style: titleStyle), subtitle: Text('删除或调换首页标签页', style: subTitleStyle), + leading: const Icon(Icons.toc_outlined), ), if (Platform.isAndroid) ListTile( dense: false, onTap: () => Get.toNamed('/displayModeSetting'), title: Text('屏幕帧率', style: titleStyle), + leading: const Icon(Icons.autofps_select_outlined), ) ], ), diff --git a/lib/pages/setting/video_setting.dart b/lib/pages/setting/video_setting.dart index d3c8b239..9f136b0e 100644 --- a/lib/pages/setting/video_setting.dart +++ b/lib/pages/setting/video_setting.dart @@ -21,6 +21,8 @@ class _VideoSettingState extends State { late dynamic defaultAudioQa; late dynamic defaultDecode; late dynamic secondDecode; + late dynamic hardwareDecoding; + late dynamic videoSync; @override void initState() { @@ -33,6 +35,10 @@ class _VideoSettingState extends State { defaultValue: VideoDecodeFormats.values.last.code); secondDecode = setting.get(SettingBoxKey.secondDecode, defaultValue: VideoDecodeFormats.values[1].code); + hardwareDecoding = setting.get(SettingBoxKey.hardwareDecoding, + defaultValue: Platform.isAndroid ? 'auto-safe' : 'auto'); + videoSync = + setting.get(SettingBoxKey.videoSync, defaultValue: 'display-resample'); } @override @@ -55,31 +61,36 @@ class _VideoSettingState extends State { children: [ const SetSwitchItem( title: '开启硬解', - subTitle: '以较低功耗播放视频,若遇异常卡死,请尝试关闭', + subTitle: '以较低功耗播放视频,若异常卡死请关闭', + leading: Icon(Icons.flash_on_outlined), setKey: SettingBoxKey.enableHA, defaultVal: true, ), const SetSwitchItem( title: '亮度记忆', subTitle: '返回时自动调整视频亮度', + leading: Icon(Icons.brightness_6_outlined), setKey: SettingBoxKey.enableAutoBrightness, defaultVal: false, ), const SetSwitchItem( title: '免登录1080P', subTitle: '免登录查看1080P视频', + leading: Icon(Icons.hd_outlined), setKey: SettingBoxKey.p1080, defaultVal: true, ), const SetSwitchItem( title: 'CDN优化', subTitle: '使用优质CDN线路', + leading: Icon(Icons.network_check_outlined), setKey: SettingBoxKey.enableCDN, defaultVal: true, ), ListTile( dense: false, title: Text('默认画质', style: titleStyle), + leading: const Icon(Icons.video_settings_outlined), subtitle: Text( '当前画质:${VideoQualityCode.fromCode(defaultVideoQa)!.description!}', style: subTitleStyle, @@ -106,6 +117,7 @@ class _VideoSettingState extends State { ListTile( dense: false, title: Text('默认音质', style: titleStyle), + leading: const Icon(Icons.audiotrack_outlined), subtitle: Text( '当前音质:${AudioQualityCode.fromCode(defaultAudioQa)!.description!}', style: subTitleStyle, @@ -132,6 +144,7 @@ class _VideoSettingState extends State { ListTile( dense: false, title: Text('首选解码格式', style: titleStyle), + leading: const Icon(Icons.movie_creation_outlined), subtitle: Text( '首选解码格式:${VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!},请根据设备支持情况与需求调整', style: subTitleStyle, @@ -162,6 +175,7 @@ class _VideoSettingState extends State { '非杜比视频次选:${VideoDecodeFormatsCode.fromCode(secondDecode)!.description!},仍无则选择首个提供的解码格式', style: subTitleStyle, ), + leading: const Icon(Icons.swap_horizontal_circle_outlined), onTap: () async { String? result = await showDialog( context: context, @@ -184,16 +198,84 @@ class _VideoSettingState extends State { if (Platform.isAndroid) const SetSwitchItem( title: '优先使用 OpenSL ES 输出音频', + leading: Icon(Icons.speaker_outlined), subTitle: '关闭则优先使用AudioTrack输出音频(此项即mpv的--ao)', setKey: SettingBoxKey.useOpenSLES, defaultVal: true, ), const SetSwitchItem( title: '扩大缓冲区', - subTitle: '默认缓冲区为视频5MB/直播32MB,开启后为视频32MB/直播64MB,但会延长首次加载时间', + leading: Icon(Icons.storage_outlined), + subTitle: '默认缓冲区为视频5MB/直播32MB,开启后为32MB/64MB,加载时间变长', setKey: SettingBoxKey.expandBuffer, defaultVal: false, ), + //video-sync + ListTile( + dense: false, + title: Text('视频同步', style: titleStyle), + leading: const Icon(Icons.view_timeline_outlined), + subtitle: Text( + '当前:$videoSync(此项即mpv的--video-sync)', + style: subTitleStyle, + ), + onTap: () async { + String? result = await showDialog( + context: context, + builder: (context) { + return SelectDialog( + title: '视频同步', + value: videoSync, + values: [ + 'audio', + 'display-resample', + 'display-resample-vdrop', + 'display-resample-desync', + 'display-tempo', + 'display-vdrop', + 'display-adrop', + 'display-desync', + 'desync' + ].map((e) { + return {'title': e, 'value': e}; + }).toList()); + }, + ); + if (result != null) { + setting.put(SettingBoxKey.videoSync, result); + videoSync = result; + setState(() {}); + } + }, + ), + ListTile( + dense: false, + title: Text('硬解模式', style: titleStyle), + leading: const Icon(Icons.memory_outlined), + subtitle: Text( + '当前:$hardwareDecoding(此项即mpv的--hwdec)', + style: subTitleStyle, + ), + onTap: () async { + String? result = await showDialog( + context: context, + builder: (context) { + return SelectDialog( + title: '硬解模式', + value: hardwareDecoding, + values: ['auto', 'auto-copy', 'auto-safe', 'no', 'yes'] + .map((e) { + return {'title': e, 'value': e}; + }).toList()); + }, + ); + if (result != null) { + setting.put(SettingBoxKey.hardwareDecoding, result); + hardwareDecoding = result; + setState(() {}); + } + }, + ), ], ), ); diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index bf7e4a4c..400083a6 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -69,7 +69,7 @@ class SettingPage extends StatelessWidget { () => Visibility( visible: settingController.hiddenSettingUnlocked.value, child: ListTile( - leading: const Icon(Icons.developer_mode_outlined), + leading: const Icon(Icons.developer_board_outlined), onTap: () => Get.toNamed('/hiddenSetting'), dense: false, title: const Text('开发人员选项'), diff --git a/lib/pages/setting/widgets/switch_item.dart b/lib/pages/setting/widgets/switch_item.dart index 5d4b1ccc..e32970f6 100644 --- a/lib/pages/setting/widgets/switch_item.dart +++ b/lib/pages/setting/widgets/switch_item.dart @@ -11,6 +11,7 @@ class SetSwitchItem extends StatefulWidget { final bool? defaultVal; final Function? callFn; final bool? needReboot; + final Widget? leading; const SetSwitchItem({ this.title, @@ -19,6 +20,7 @@ class SetSwitchItem extends StatefulWidget { this.defaultVal, this.callFn, this.needReboot, + this.leading, Key? key, }) : super(key: key); @@ -66,6 +68,7 @@ class _SetSwitchItemState extends State { subtitle: widget.subTitle != null ? Text(widget.subTitle!, style: subTitleStyle) : null, + leading: widget.leading, trailing: Transform.scale( alignment: Alignment.centerRight, // 缩放Switch的大小后保持右侧对齐, 避免右侧空隙过大 scale: 0.8, diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 1ec5f337..4ea2c55e 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -56,6 +56,7 @@ class VideoDetailController extends GetxController RxBool isShowCover = true.obs; // 硬解 RxBool enableHA = true.obs; + RxString hwdec = 'auto-safe'.obs; /// 本地存储 Box userInfoCache = GStrorage.userInfo; @@ -117,7 +118,8 @@ class VideoDetailController extends GetxController autoPlay.value = setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true); enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true); - + hwdec.value = setting.get(SettingBoxKey.hardwareDecoding, + defaultValue: Platform.isAndroid ? 'auto-safe' : 'auto'); if (userInfo == null || localCache.get(LocalCacheKey.historyPause) == true) { enableHeart = false; @@ -270,6 +272,7 @@ class VideoDetailController extends GetxController ), // 硬解 enableHA: enableHA.value, + hwdec: hwdec.value, seekTo: seekToTime ?? defaultST, duration: duration ?? data.timeLength == null ? null @@ -300,109 +303,112 @@ class VideoDetailController extends GetxController '该视频为专属视频,仅提供试看', displayTime: const Duration(seconds: 3), ); + } + if (data.dash == null && data.durl != null) { videoUrl = data.durl!.first.url!; audioUrl = ''; defaultST = Duration.zero; - firstVideo = VideoItem(); + // 实际为FLV/MP4格式,但已被淘汰,这里仅做兜底处理 + firstVideo = VideoItem( + id: data.quality!, + baseUrl: videoUrl, + codecs: 'avc1', + quality: VideoQualityCode.fromCode(data.quality!)! + ); + currentDecodeFormats = VideoDecodeFormatsCode.fromString('avc1')!; + currentVideoQa = VideoQualityCode.fromCode(data.quality!)!; if (autoPlay.value) { await playerInit(); isShowCover.value = false; } return result; } - final List allVideosList = data.dash!.video!; - try { - // 当前可播放的最高质量视频 - int currentHighVideoQa = allVideosList.first.quality!.code; - // 预设的画质为null,则当前可用的最高质量 - cacheVideoQa ??= currentHighVideoQa; - int resVideoQa = currentHighVideoQa; - if (cacheVideoQa! <= currentHighVideoQa) { - // 如果预设的画质低于当前最高 - final List numbers = data.acceptQuality! - .where((e) => e <= currentHighVideoQa) - .toList(); - resVideoQa = Utils.findClosestNumber(cacheVideoQa!, numbers); - } - currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!; - - /// 取出符合当前画质的videoList - final List videosList = - allVideosList.where((e) => e.quality!.code == resVideoQa).toList(); - - /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 - final List supportFormats = data.supportFormats!; - // 根据画质选编码格式 - final List supportDecodeFormats = - supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!; - // 默认从设置中取AV1 - currentDecodeFormats = VideoDecodeFormatsCode.fromString(cacheDecode)!; - VideoDecodeFormats secondDecodeFormats = - VideoDecodeFormatsCode.fromString(cacheSecondDecode)!; - try { - // 当前视频没有对应格式返回第一个 - int flag = 0; - for (var i in supportDecodeFormats) { - if (i.startsWith(currentDecodeFormats.code)) { - flag = 1; - break; - } else if (i.startsWith(secondDecodeFormats.code)) { - flag = 2; - } - } - if (flag == 2) { - currentDecodeFormats = secondDecodeFormats; - } else if (flag == 0) { - currentDecodeFormats = - VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!; - } - } catch (err) { - SmartDialog.showToast('DecodeFormats error: $err'); - } - - /// 取出符合当前解码格式的videoItem - try { - firstVideo = videosList.firstWhere( - (e) => e.codecs!.startsWith(currentDecodeFormats.code)); - } catch (_) { - firstVideo = videosList.first; - } - videoUrl = enableCDN - ? VideoUtils.getCdnUrl(firstVideo) - : (firstVideo.backupUrl ?? firstVideo.baseUrl!); - } catch (err) { - SmartDialog.showToast('firstVideo error: $err'); + if (data.dash == null) { + SmartDialog.showToast('视频资源不存在'); + isShowCover.value = false; + return result; } + final List allVideosList = data.dash!.video!; + print("allVideosList:${allVideosList}"); + // 当前可播放的最高质量视频 + int currentHighVideoQa = allVideosList.first.quality!.code; + // 预设的画质为null,则当前可用的最高质量 + cacheVideoQa ??= currentHighVideoQa; + int resVideoQa = currentHighVideoQa; + if (cacheVideoQa! <= currentHighVideoQa) { + // 如果预设的画质低于当前最高 + final List numbers = + data.acceptQuality!.where((e) => e <= currentHighVideoQa).toList(); + resVideoQa = Utils.findClosestNumber(cacheVideoQa!, numbers); + } + currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!; + + /// 取出符合当前画质的videoList + final List videosList = + allVideosList.where((e) => e.quality!.code == resVideoQa).toList(); + + /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 + final List supportFormats = data.supportFormats!; + // 根据画质选编码格式 + final List supportDecodeFormats = supportFormats + .firstWhere((e) => e.quality == resVideoQa, + orElse: () => supportFormats.first) + .codecs!; + // 默认从设置中取AV1 + currentDecodeFormats = VideoDecodeFormatsCode.fromString(cacheDecode)!; + VideoDecodeFormats secondDecodeFormats = + VideoDecodeFormatsCode.fromString(cacheSecondDecode)!; + // 当前视频没有对应格式返回第一个 + int flag = 0; + for (var i in supportDecodeFormats) { + if (i.startsWith(currentDecodeFormats.code)) { + flag = 1; + break; + } else if (i.startsWith(secondDecodeFormats.code)) { + flag = 2; + } + } + if (flag == 2) { + currentDecodeFormats = secondDecodeFormats; + } else if (flag == 0) { + currentDecodeFormats = + VideoDecodeFormatsCode.fromString(supportDecodeFormats.first)!; + } + + /// 取出符合当前解码格式的videoItem + firstVideo = videosList.firstWhere( + (e) => e.codecs!.startsWith(currentDecodeFormats.code), + orElse: () => videosList.first); + + videoUrl = enableCDN + ? VideoUtils.getCdnUrl(firstVideo) + : (firstVideo.backupUrl ?? firstVideo.baseUrl!); /// 优先顺序 设置中指定质量 -> 当前可选的最高质量 late AudioItem? firstAudio; final List audiosList = data.dash!.audio!; - try { - if (data.dash!.dolby?.audio?.isNotEmpty == true) { - // 杜比 - audiosList.insert(0, data.dash!.dolby!.audio!.first); - } + if (data.dash!.dolby?.audio != null && data.dash!.dolby!.audio!.isNotEmpty) { + // 杜比 + audiosList.insert(0, data.dash!.dolby!.audio!.first); + } - if (data.dash!.flac?.audio != null) { - // 无损 - audiosList.insert(0, data.dash!.flac!.audio!); - } + if (data.dash!.flac?.audio != null) { + // 无损 + audiosList.insert(0, data.dash!.flac!.audio!); + } - 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)) { - closestNumber = 30280; - } - firstAudio = audiosList.firstWhere((e) => e.id == closestNumber); - } else { - firstAudio = AudioItem(); + 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)) { + closestNumber = 30280; } - } catch (err) { - firstAudio = audiosList.isNotEmpty ? audiosList.first : AudioItem(); - SmartDialog.showToast('firstAudio error: $err'); + firstAudio = audiosList.firstWhere((e) => e.id == closestNumber, + orElse: () => audiosList.first); + } else { + firstAudio = AudioItem(); } audioUrl = enableCDN diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index e86eed6f..6ff37ba0 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -18,7 +18,6 @@ import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:PiliPalaX/utils/utils.dart'; -import '../../../../utils/id_utils.dart'; import 'widgets/action_item.dart'; import 'widgets/action_row_item.dart'; import 'widgets/fav_panel.dart'; diff --git a/lib/pages/video/detail/introduction/widgets/action_item.dart b/lib/pages/video/detail/introduction/widgets/action_item.dart index b68324b7..46a23841 100644 --- a/lib/pages/video/detail/introduction/widgets/action_item.dart +++ b/lib/pages/video/detail/introduction/widgets/action_item.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:PiliPalaX/common/constants.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; class ActionItem extends StatelessWidget { diff --git a/lib/pages/video/detail/introduction/widgets/season.dart b/lib/pages/video/detail/introduction/widgets/season.dart index 390c50aa..fc71003a 100644 --- a/lib/pages/video/detail/introduction/widgets/season.dart +++ b/lib/pages/video/detail/introduction/widgets/season.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPalaX/models/video_detail_res.dart'; import 'package:PiliPalaX/pages/video/detail/index.dart'; -import 'package:PiliPalaX/utils/id_utils.dart'; class SeasonPanel extends StatefulWidget { const SeasonPanel({ diff --git a/lib/pages/video/detail/reply_new/view.dart b/lib/pages/video/detail/reply_new/view.dart index f7066f38..18d3fba4 100644 --- a/lib/pages/video/detail/reply_new/view.dart +++ b/lib/pages/video/detail/reply_new/view.dart @@ -109,7 +109,6 @@ class _VideoReplyNewDialogState extends State @override void didChangeMetrics() { super.didChangeMetrics(); - final String routePath = Get.currentRoute; if (!mounted) return; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index 0b0acc95..82b35244 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -66,8 +66,6 @@ class _VideoDetailPageState extends State RxBool isFullScreen = false.obs; late StreamSubscription fullScreenStatusListener; late final MethodChannel onUserLeaveHintListener; - late AnimationController _animationController; - late Animation _animation; StreamSubscription? _bufferedListener; @override @@ -292,29 +290,33 @@ class _VideoDetailPageState extends State if (mounted) { setState(() => {}); } + super.didPopNext(); videoDetailController.isFirstTime = false; final bool autoplay = autoPlayEnable; - videoDetailController.playerInit(autoplay: autoplay); + await videoDetailController.playerInit(autoplay: autoplay); /// 未开启自动播放时,未播放跳转下一页返回/播放后跳转下一页返回 videoDetailController.autoPlay.value = !videoDetailController.isShowCover.value; videoIntroController.isPaused = false; - if (autoplay) { - // await Future.delayed(const Duration(milliseconds: 300)); - if (plPlayerController?.buffered.value == Duration.zero) { - _bufferedListener = plPlayerController?.buffered.listen((p0) { - if (p0 > Duration.zero) { - _bufferedListener!.cancel(); - plPlayerController?.seekTo(videoDetailController.defaultST); - plPlayerController?.play(); - } - }); - } else { - plPlayerController?.seekTo(videoDetailController.defaultST); - plPlayerController?.play(); - } - } + // if (autoplay) { + // // await Future.delayed(const Duration(milliseconds: 300)); + // print(plPlayerController); + // if (plPlayerController?.buffered.value == Duration.zero) { + // _bufferedListener = plPlayerController?.buffered.listen((p0) { + // print("p0"); + // print(p0); + // if (p0 > Duration.zero) { + // _bufferedListener!.cancel(); + // plPlayerController?.seekTo(videoDetailController.defaultST); + // plPlayerController?.play(); + // } + // }); + // } else { + // plPlayerController?.seekTo(videoDetailController.defaultST); + // plPlayerController?.play(); + // } + // } Future.delayed(const Duration(milliseconds: 600), () { AutoOrientation.fullAutoMode(); }); @@ -322,7 +324,6 @@ class _VideoDetailPageState extends State if (plPlayerController != null) { listenFullScreenStatus(); } - super.didPopNext(); } @override @@ -409,6 +410,9 @@ class _VideoDetailPageState extends State child: AppBar( backgroundColor: Colors.transparent, elevation: 0, + // systemOverlayStyle: const SystemUiOverlayStyle( + // statusBarColor: Colors.transparent, + // statusBarIconBrightness: Brightness.light), ), ), body: Column( @@ -417,10 +421,9 @@ class _VideoDetailPageState extends State () { double videoheight = context.width * 9 / 16; final double videowidth = context.width; - print(videoDetailController.tabCtr.index); + // print(videoDetailController.tabCtr.index); if (enableVerticalExpand && - plPlayerController?.direction.value == 'vertical' && - videoDetailController.tabCtr.index != 1) { + plPlayerController?.direction.value == 'vertical') { videoheight = context.width * 5 / 4; } if (MediaQuery.of(context).orientation == @@ -812,6 +815,9 @@ class _VideoDetailPageState extends State child: AppBar( backgroundColor: Colors.transparent, elevation: 0, + // systemOverlayStyle: const SystemUiOverlayStyle( + // statusBarColor: Colors.transparent, + // statusBarIconBrightness: Brightness.dark), ), ), body: childWhenDisabledLandscapeInner) diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 3077b1d6..b0636819 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -20,10 +20,8 @@ import 'package:PiliPalaX/utils/storage.dart'; import 'package:PiliPalaX/http/danmaku.dart'; import 'package:PiliPalaX/services/shutdown_timer_service.dart'; import '../../../../models/video_detail_res.dart'; -import '../../../../services/service_locator.dart'; import '../introduction/index.dart'; import 'package:marquee/marquee.dart'; -import '../../../danmaku/controller.dart'; class HeaderControl extends StatefulWidget implements PreferredSizeWidget { const HeaderControl({ @@ -440,6 +438,10 @@ class _HeaderControlState extends State { /// 选择画质 void showSetVideoQa() { + if (videoInfo.dash == null) { + SmartDialog.showToast('当前视频不支持选择画质'); + return; + } final List videoFormat = videoInfo.supportFormats!; final VideoQuality currentVideoQa = widget.videoDetailCtr!.currentVideoQa; @@ -634,9 +636,13 @@ class _HeaderControlState extends State { final VideoItem firstVideo = widget.videoDetailCtr!.firstVideo; // 当前视频可用的解码格式 final List videoFormat = videoInfo.supportFormats!; - final List list = videoFormat + final List? list = videoFormat .firstWhere((FormatItem e) => e.quality == firstVideo.quality!.code) - .codecs!; + .codecs; + if (list == null) { + SmartDialog.showToast('当前视频不支持选择解码格式'); + return; + } showModalBottomSheet( context: context, diff --git a/lib/pages/webview/controller.dart b/lib/pages/webview/controller.dart index 8909e005..2e71514d 100644 --- a/lib/pages/webview/controller.dart +++ b/lib/pages/webview/controller.dart @@ -118,7 +118,7 @@ class WebviewController extends GetxController { content = '${content + url}; \n'; } try { - await SetCookie.onSet(); + await CookieTool.onSet(); final result = await UserHttp.userInfo(); if (result['status'] && result['data'].isLogin) { SmartDialog.showToast('登录成功,当前采用「' diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 5d662191..243dff74 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -116,7 +116,7 @@ class PlPlayerController { static List> videoFitType = [ {'attr': BoxFit.contain, 'desc': '自动', 'toast': '缩放至播放器尺寸,保留黑边'}, {'attr': BoxFit.cover, 'desc': '裁剪', 'toast': '缩放至填满播放器,裁剪超出部分'}, - {'attr': BoxFit.fill, 'desc': '拉伸', 'toast': '拉伸至播放器尺寸,将产生变形'}, + {'attr': BoxFit.fill, 'desc': '拉伸', 'toast': '拉伸至播放器尺寸,将产生变形(竖屏改为自动)'}, {'attr': BoxFit.none, 'desc': '原始', 'toast': '不缩放,以视频原始尺寸显示'}, {'attr': BoxFit.fitHeight, 'desc': '等高', 'toast': '缩放至撑满播放器高度'}, {'attr': BoxFit.fitWidth, 'desc': '等宽', 'toast': '缩放至撑满播放器宽度'}, @@ -285,21 +285,21 @@ class PlPlayerController { isOpenDanmu.value = setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: false); danmakuWeight.value = - localCache.get(LocalCacheKey.danmakuWeight, defaultValue: 0); + setting.get(SettingBoxKey.danmakuWeight, defaultValue: 0); blockTypes = - localCache.get(LocalCacheKey.danmakuBlockType, defaultValue: []); - showArea = localCache.get(LocalCacheKey.danmakuShowArea, defaultValue: 0.5); + setting.get(SettingBoxKey.danmakuBlockType, defaultValue: []); + showArea = setting.get(SettingBoxKey.danmakuShowArea, defaultValue: 0.5); // 不透明度 opacityVal = - localCache.get(LocalCacheKey.danmakuOpacity, defaultValue: 1.0); + setting.get(SettingBoxKey.danmakuOpacity, defaultValue: 1.0); // 字体大小 fontSizeVal = - localCache.get(LocalCacheKey.danmakuFontScale, defaultValue: 1.0); + setting.get(SettingBoxKey.danmakuFontScale, defaultValue: 1.0); // 弹幕时间 danmakuDurationVal = - localCache.get(LocalCacheKey.danmakuDuration, defaultValue: 4.0); + setting.get(SettingBoxKey.danmakuDuration, defaultValue: 4.0); // 描边粗细 - strokeWidth = localCache.get(LocalCacheKey.strokeWidth, defaultValue: 1.5); + strokeWidth = setting.get(SettingBoxKey.strokeWidth, defaultValue: 1.5); playRepeat = PlayRepeat.values.toList().firstWhere( (e) => e.value == @@ -356,6 +356,7 @@ class PlPlayerController { double speed = 1.0, // 硬件加速 bool enableHA = true, + String? hwdec, double? width, double? height, Duration? duration, @@ -390,7 +391,7 @@ class PlPlayerController { } // 配置Player 音轨、字幕等等 _videoPlayerController = await _createVideoController( - dataSource, _looping, enableHA, width, height); + dataSource, _looping, enableHA, hwdec, width, height); // 获取视频时长 00:00 _duration.value = duration ?? _videoPlayerController!.state.duration; updateDurationSecond(); @@ -401,7 +402,7 @@ class PlPlayerController { if (!_listenersInitialized) { startListeners(); } - await _initializePlayer(seekTo: seekTo, duration: _duration.value); + await _initializePlayer(seekTo: seekTo); if (videoType.value != 'live' && _cid != 0) { refreshSubtitles().then((value) { if (_vttSubtitles.isNotEmpty) { @@ -432,6 +433,7 @@ class PlPlayerController { DataSource dataSource, PlaylistMode looping, bool enableHA, + String? hwdec, double? width, double? height, ) async { @@ -468,6 +470,9 @@ class PlPlayerController { : "audiotrack,opensles"; await pp.setProperty("ao", ao); } + // video-sync=display-resample + await pp.setProperty("video-sync", + setting.get(SettingBoxKey.videoSync, defaultValue: 'display-resample')); // // vo=gpu-next & gpu-context=android & gpu-api=opengl // await pp.setProperty("vo", "gpu-next"); // await pp.setProperty("gpu-context", "android"); @@ -510,6 +515,7 @@ class PlPlayerController { configuration: VideoControllerConfiguration( enableHardwareAcceleration: enableHA, androidAttachSurfaceAfterVideoParameters: false, + hwdec: hwdec, ), ); @@ -519,15 +525,16 @@ class PlPlayerController { final assetUrl = dataSource.videoSource!.startsWith("asset://") ? dataSource.videoSource! : "asset://${dataSource.videoSource!}"; - player.open( + await player.open( Media(assetUrl, httpHeaders: dataSource.httpHeaders), play: false, ); + } else { + await player.open( + Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders), + play: false, + ); } - player.open( - Media(dataSource.videoSource!, httpHeaders: dataSource.httpHeaders), - play: false, - ); // 音轨 // player.setAudioTrack( // AudioTrack.uri(dataSource.audioSource!), @@ -764,6 +771,12 @@ class PlPlayerController { if (repeat) { await seekTo(Duration.zero); } + + /// 临时fix _duration.value丢失 + if (duration != null) { + _duration.value = duration; + updateDurationSecond(); + } await _videoPlayerController?.play(); await getCurrentVolume(); @@ -772,11 +785,6 @@ class PlPlayerController { playerStatus.status.value = PlayerStatus.playing; // screenManager.setOverlays(false); - /// 临时fix _duration.value丢失 - if (duration != null) { - _duration.value = duration; - updateDurationSecond(); - } audioSessionHandler.setActive(true); } @@ -957,7 +965,7 @@ class PlPlayerController { Future getVideoFit() async { int fitValue = videoStorage.get(VideoBoxKey.cacheVideoFit, defaultValue: 0); var attr = videoFitType[fitValue]['attr']; - // 由于none与scaleDown涉及视频原始尺寸,需要等待视频加载后再设置,否则尺寸会变为0,出现错误 + // 由于none与scaleDown涉及视频原始尺寸,需要等待视频加载后再设置,否则尺寸会变为0,出现错误; if (attr == BoxFit.none || attr == BoxFit.scaleDown) { if (buffered.value == Duration.zero) { attr = BoxFit.contain; @@ -973,6 +981,9 @@ class PlPlayerController { } }); } + // fill不应该在竖屏视频生效 + } else if (attr == BoxFit.fill && direction.value == 'vertical') { + attr = BoxFit.contain; } _videoFit.value = attr; _videoFitDesc.value = videoFitType[fitValue]['desc']; @@ -1132,13 +1143,13 @@ class PlPlayerController { } void putDanmakuSettings() { - localCache.put(LocalCacheKey.danmakuWeight, danmakuWeight.value); - localCache.put(LocalCacheKey.danmakuBlockType, blockTypes); - localCache.put(LocalCacheKey.danmakuShowArea, showArea); - localCache.put(LocalCacheKey.danmakuOpacity, opacityVal); - localCache.put(LocalCacheKey.danmakuFontScale, fontSizeVal); - localCache.put(LocalCacheKey.danmakuDuration, danmakuDurationVal); - localCache.put(LocalCacheKey.strokeWidth, strokeWidth); + setting.put(SettingBoxKey.danmakuWeight, danmakuWeight.value); + setting.put(SettingBoxKey.danmakuBlockType, blockTypes); + setting.put(SettingBoxKey.danmakuShowArea, showArea); + setting.put(SettingBoxKey.danmakuOpacity, opacityVal); + setting.put(SettingBoxKey.danmakuFontScale, fontSizeVal); + setting.put(SettingBoxKey.danmakuDuration, danmakuDurationVal); + setting.put(SettingBoxKey.strokeWidth, strokeWidth); } Future dispose({String type = 'single'}) async { diff --git a/lib/plugin/pl_player/view.dart b/lib/plugin/pl_player/view.dart index 6c0e854f..5d5f76bf 100644 --- a/lib/plugin/pl_player/view.dart +++ b/lib/plugin/pl_player/view.dart @@ -17,7 +17,6 @@ import 'package:PiliPalaX/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:PiliPalaX/plugin/pl_player/utils.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; import 'package:PiliPalaX/utils/storage.dart'; -import 'package:media_kit_video/media_kit_video_controls/src/controls/methods/video_state.dart'; import 'package:screen_brightness/screen_brightness.dart'; import '../../common/widgets/audio_video_progress_bar.dart'; @@ -496,7 +495,7 @@ class _PLVideoPlayerState extends State children: [ Obx( () => Video( - key: ValueKey('${_.videoFit.value}${_.continuePlayInBackground.value}'), + key: ValueKey('${_.videoFit.value}'), controller: videoController, controls: NoVideoControls, pauseUponEnteringBackgroundMode: !_.continuePlayInBackground.value, diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index eb6f3dc1..39016d5a 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -5,11 +5,9 @@ import 'package:flutter/rendering.dart'; import 'package:get/get.dart'; import 'package:nil/nil.dart'; import 'package:PiliPalaX/plugin/pl_player/index.dart'; -import 'package:PiliPalaX/plugin/pl_player/widgets/play_pause_btn.dart'; import 'package:PiliPalaX/utils/feed_back.dart'; import '../../../common/widgets/audio_video_progress_bar.dart'; -import '../../../utils/utils.dart'; class BottomControl extends StatelessWidget implements PreferredSizeWidget { final PlPlayerController? controller; @@ -27,10 +25,6 @@ class BottomControl extends StatelessWidget implements PreferredSizeWidget { Widget build(BuildContext context) { Color colorTheme = Theme.of(context).colorScheme.primary; final _ = controller!; - const textStyle = TextStyle( - color: Colors.white, - fontSize: 11, - ); //阅读器限制 Timer? _accessibilityDebounce; double _lastAnnouncedValue = -1; diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index b4c3aee9..7c696bbf 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -2,7 +2,6 @@ import 'package:appscheme/appscheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import '../http/dynamics.dart'; import '../http/search.dart'; import '../models/common/search_type.dart'; import 'id_utils.dart'; diff --git a/lib/utils/cookie.dart b/lib/utils/cookie.dart index 8f9a3295..b6107273 100644 --- a/lib/utils/cookie.dart +++ b/lib/utils/cookie.dart @@ -1,8 +1,35 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:PiliPalaX/http/constants.dart'; import 'package:PiliPalaX/http/init.dart'; import 'package:webview_cookie_manager/webview_cookie_manager.dart'; -class SetCookie { +class CookieTool { + static exportCookie() async { + Map allCookies = {}; + List Urls = [HttpString.baseUrl, HttpString.apiBaseUrl, HttpString.tUrl]; + for (var url in Urls) { + allCookies[url] = await WebviewCookieManager().getCookies(url) + .then((cookies) => cookies.map((cookie) => '${cookie.name}=${cookie.value}').join('; ')); + } + return jsonEncode(allCookies); + } + static importCookie(String cookie) async { + var allCookies = jsonDecode(cookie); + for (var url in allCookies.keys) { + List cookiesStringList = allCookies[url]!.split('; '); + List cookies = []; + for (var c in cookiesStringList) { + List kv = c.split('='); + cookies.add(Cookie(kv[0], kv[1])); + } + await Request.cookieManager.cookieJar.saveFromResponse(Uri.parse(url), cookies); + if (url == HttpString.baseUrl) { + Request.dio.options.headers['cookie'] = allCookies[url]; + } + } + } static onSet() async { var cookies = await WebviewCookieManager().getCookies(HttpString.baseUrl); await Request.cookieManager.cookieJar diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index d1377df3..e9020abf 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'dart:io'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; @@ -47,6 +48,21 @@ class GStrorage { setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10); // 设置全局变量 } + static Future exportAllSettings() async { + return jsonEncode({ + setting.name: setting.toMap(), + video.name: video.toMap(), + }); + } + + static Future importAllSettings(String data) async { + final Map map = jsonDecode(data); + await setting.clear(); + await video.clear(); + await setting.putAll(map[setting.name]); + await video.putAll(map[video.name]); + } + static void regAdapter() { Hive.registerAdapter(OwnerAdapter()); Hive.registerAdapter(UserInfoDataAdapter()); @@ -89,6 +105,8 @@ class SettingBoxKey { enableHA = 'enableHA', useOpenSLES = 'useOpenSLES', expandBuffer = 'expandBuffer', + hardwareDecoding = 'hardwareDecoding', + videoSync = 'videoSync', enableVerticalExpand = 'enableVerticalExpand', enableOnlineTotal = 'enableOnlineTotal', enableAutoBrightness = 'enableAutoBrightness', @@ -136,7 +154,20 @@ class SettingBoxKey { enableSystemProxy = 'enableSystemProxy', enableAi = 'enableAi', disableLikeMsg = 'disableLikeMsg', - defaultHomePage = 'defaultHomePage'; + defaultHomePage = 'defaultHomePage', + + // 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 + danmakuWeight = 'danmakuWeight', + danmakuBlockType = 'danmakuBlockType', + danmakuShowArea = 'danmakuShowArea', + danmakuOpacity = 'danmakuOpacity', + danmakuFontScale = 'danmakuFontScale', + danmakuDuration = 'danmakuDuration', + strokeWidth = 'strokeWidth', + + // 代理host port + systemProxyHost = 'systemProxyHost', + systemProxyPort = 'systemProxyPort'; /// 外观 static const String themeMode = 'themeMode', @@ -146,6 +177,7 @@ class SettingBoxKey { enableSingleRow = 'enableSingleRow', // 首页单列 displayMode = 'displayMode', maxRowWidth = 'maxRowWidth', // 首页列最大宽度(dp) + adaptiveNavBar = 'adaptiveNavBar', enableMYBar = 'enableMYBar', hideSearchBar = 'hideSearchBar', // 收起顶栏 hideTabBar = 'hideTabBar', // 收起底栏 @@ -164,20 +196,7 @@ class LocalCacheKey { // wbiKeys = 'wbiKeys', - timeStamp = 'timeStamp', - - // 弹幕相关设置 权重(云屏蔽) 屏蔽类型 显示区域 透明度 字体大小 弹幕时间 描边粗细 - danmakuWeight = 'danmakuWeight', - danmakuBlockType = 'danmakuBlockType', - danmakuShowArea = 'danmakuShowArea', - danmakuOpacity = 'danmakuOpacity', - danmakuFontScale = 'danmakuFontScale', - danmakuDuration = 'danmakuDuration', - strokeWidth = 'strokeWidth', - - // 代理host port - systemProxyHost = 'systemProxyHost', - systemProxyPort = 'systemProxyPort'; + timeStamp = 'timeStamp'; } class VideoBoxKey { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 6a8e6068..fd39a9bf 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -4,7 +4,6 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'dart:ui'; import 'dart:math'; import 'package:PiliPalaX/utils/storage.dart'; import 'package:crypto/crypto.dart'; @@ -193,7 +192,6 @@ class Utils { toInt: true, ); } - print('distance: $distance'); if (distance <= 60) { return '刚刚'; } else if (distance <= 3600) {