diff --git a/lib/pages/info/info_page.dart b/lib/pages/info/info_page.dart index 4c66e23ec..9c1a39460 100644 --- a/lib/pages/info/info_page.dart +++ b/lib/pages/info/info_page.dart @@ -359,22 +359,28 @@ class _InfoPageState extends State with TickerProviderStateMixin { onPressed: () async { showModalBottomSheet( isScrollControlled: true, - constraints: BoxConstraints( - maxHeight: (MediaQuery.sizeOf(context).height >= - LayoutBreakpoint.compact['height']!) - ? MediaQuery.of(context).size.height * 3 / 4 - : MediaQuery.of(context).size.height, - maxWidth: (MediaQuery.sizeOf(context).width >= - LayoutBreakpoint.medium['width']!) - ? MediaQuery.of(context).size.width * 9 / 16 - : MediaQuery.of(context).size.width, - ), clipBehavior: Clip.antiAlias, backgroundColor: Theme.of(context).scaffoldBackgroundColor, showDragHandle: true, context: context, builder: (context) { - return SourceSheet(tabController: sourceTabController, infoController: infoController); + final double minChildSize = 0.3; + return DraggableScrollableSheet( + initialChildSize: (MediaQuery.sizeOf(context).height >= LayoutBreakpoint.compact['height']!) + ? 0.75 + : 0.90, + minChildSize: minChildSize, + maxChildSize: 1.0, + expand: false, + builder: (context, scrollController) { + return SourceSheet( + tabController: sourceTabController, + infoController: infoController, + scrollController: scrollController, + tabGridHeight: minChildSize*0.8, + ); + }, + ); }, ); }, diff --git a/lib/pages/info/source_sheet.dart b/lib/pages/info/source_sheet.dart index e72fd815f..2c5fdaf4b 100644 --- a/lib/pages/info/source_sheet.dart +++ b/lib/pages/info/source_sheet.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'dart:async'; import 'package:kazumi/utils/utils.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_modular/flutter_modular.dart'; import 'package:kazumi/pages/info/info_controller.dart'; @@ -12,27 +14,72 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:kazumi/request/query_manager.dart'; import 'package:kazumi/pages/collect/collect_controller.dart'; import 'package:kazumi/bean/widget/error_widget.dart'; +import 'package:kazumi/utils/storage.dart'; +import 'package:flutter/services.dart'; class SourceSheet extends StatefulWidget { const SourceSheet({ super.key, required this.tabController, required this.infoController, + required this.scrollController, + required this.tabGridHeight, }); final TabController tabController; final InfoController infoController; + final ScrollController scrollController; + final double tabGridHeight; @override State createState() => _SourceSheetState(); } -class _SourceSheetState extends State - with SingleTickerProviderStateMixin { +class _SourceSheetState extends State with SingleTickerProviderStateMixin { + bool _showOnlySuccess = false; + final tabBarHeight = 48.0; + bool expandedByDrag = false; + final defaultShowSelector = GStorage.setting.get(SettingBoxKey.defaultShowSelector, defaultValue: false); + final autoLock = GStorage.setting.get(SettingBoxKey.autoLockSourceSheet, defaultValue: true); + final autoShowSuccess = GStorage.setting.get(SettingBoxKey.autoshowSuccessed, defaultValue: false); + void _maybeExpandTabGridOnListViewHeight(BoxConstraints constraints) { + final screenHeight = MediaQuery.of(context).size.height; + final dragHandleHeight = 48.0; + final bufferHeight = 40; //40像素作为下拉时的缓冲高度,防止误触回弹 + final hideHeight = screenHeight - tabBarHeight - dragHandleHeight - 1; + final showHeight = screenHeight * ( 1 - widget.tabGridHeight ) - tabBarHeight - dragHandleHeight - 1; + if (constraints.maxHeight >= hideHeight && !_showTabGrid) { + if (!_isLocked && !expandedByDrag) { //当面板触顶且未锁定、未通过拖拽展开时自动展开面板。防止点击番源按钮收起面板后立刻展开面板。 + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _showTabGrid = true; + if(autoShowSuccess){_showOnlySuccess = true;} + }); + }); + expandedByDrag = true; + } + } else if (constraints.maxHeight < showHeight - bufferHeight) { + if (!_isLocked && expandedByDrag) { //禁用点击按钮且解锁时面板高度触发的自动收起,防止用户点击解锁按钮后就立刻收起。 + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() { + _showTabGrid = false; + }); + }); + } + if (!_showTabGrid){ + expandedByDrag = false; //重置拖拽展开状态,允许后续触顶时展开面板, + } + } + } + final ScrollController _tabGridScrollController = ScrollController(); + bool _showTabGrid = false; + Timer? _scrollWaitTimer; + bool _isLocked = false; final VideoPageController videoPageController = Modular.get(); final CollectController collectController = Modular.get(); final PluginsController pluginsController = Modular.get(); + final Map _menuControllers = {}; late String keyword; /// Concurrent query manager @@ -46,10 +93,21 @@ class _SourceSheetState extends State queryManager = QueryManager(infoController: widget.infoController); queryManager?.queryAllSource(keyword); super.initState(); + if (defaultShowSelector == true) { + _showTabGrid = true; + if(autoShowSuccess){_showOnlySuccess = true;} + if(autoLock){ _isLocked = true; } + } + widget.tabController.addListener(() { + if (!mounted) return; + setState(() {}); + }); } @override void dispose() { + _tabGridScrollController.dispose(); + _scrollWaitTimer?.cancel(); queryManager?.cancel(); queryManager = null; super.dispose(); @@ -187,116 +245,543 @@ class _SourceSheetState extends State child: Scaffold( body: Column( children: [ - Row( - children: [ - Expanded( - child: TabBar( - isScrollable: true, - tabAlignment: TabAlignment.center, - dividerHeight: 0, - controller: widget.tabController, - tabs: pluginsController.pluginList - .map( - (plugin) => Observer( - builder: (context) { - bool isSuccessButEmpty = false; - if (widget.infoController - .pluginSearchStatus[plugin.name] == - 'success') { - bool hasContent = false; - for (var searchResponse in widget - .infoController.pluginSearchResponseList) { - if (searchResponse.pluginName == - plugin.name && - searchResponse.data.isNotEmpty) { - hasContent = true; - break; - } - } - isSuccessButEmpty = !hasContent; - } + Listener( + onPointerSignal: (event) { + if (event is PointerScrollEvent) { + if (event.scrollDelta.dy.abs() < 8) return; + if (_scrollWaitTimer?.isActive ?? false) return; + _scrollWaitTimer?.cancel(); + _scrollWaitTimer = Timer(Duration(milliseconds: 750), () {}); //防抖 - return Tab( - child: Row( - children: [ - Text( - plugin.name, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleMedium! - .fontSize, - color: Theme.of(context) - .colorScheme - .onSurface), + if (!_showTabGrid) { + setState(() { + _showTabGrid = true; + if(autoShowSuccess){_showOnlySuccess = true;} + + }); + } else { + // 仅当面板内部无需滚动时才允许收起 + final maxScroll = _tabGridScrollController.hasClients + ? _tabGridScrollController.position.maxScrollExtent + : 0.0; + if (maxScroll == 0.0) { + setState(() { + _showTabGrid = false; + }); + } + } + } + }, + child: AnimatedContainer( + duration: const Duration(milliseconds: 250), + height: _showTabGrid ? MediaQuery.of(context).size.height * widget.tabGridHeight : tabBarHeight, + child: Stack( + children: [ + // TabBar(收起时显示) + AnimatedOpacity( + opacity: _showTabGrid ? 0 : 1, + duration: const Duration(milliseconds: 200), + child: SizedBox( + height: tabBarHeight, + child: Stack( + children: [ + Positioned.fill( + child: Observer( + builder: (context) => TabBar( + isScrollable: true, + tabAlignment: TabAlignment.center, + dividerHeight: 0, + controller: widget.tabController, + labelPadding: const EdgeInsets.symmetric(horizontal: 10.0), + indicatorColor: (() { + final list = pluginsController.pluginList; + final idx = widget.tabController.index; + if (idx < 0 || idx >= list.length) { + return Theme.of(context).colorScheme.secondary; + } + final status = widget.infoController.pluginSearchStatus[list[idx].name]; + return status == 'success' + ? Theme.of(context).colorScheme.onSurface + : Color.lerp( + Theme.of(context).colorScheme.secondary, + status == 'pending' + ? Colors.blueGrey + : status == 'noresult' + ? Colors.orange + : Colors.red, + Theme.of(context).brightness == Brightness.dark ? 0.5 : 0.8, + )!; + })(), + tabs: pluginsController.pluginList + .asMap() + .entries + .map((entry) { + final index = entry.key; + final plugin = entry.value; + final status = widget.infoController.pluginSearchStatus[plugin.name]; + final isLast = index == pluginsController.pluginList.length - 1; + return Tab( + child: Padding( + padding: isLast ? const EdgeInsets.only(right: 40) : EdgeInsets.zero, + child: Text( + plugin.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize, + fontWeight: FontWeight.bold, + color: status == 'success' + ? Theme.of(context).colorScheme.onSurface + : Color.lerp( + Theme.of(context).colorScheme.onSurface, + status == 'pending' + ? Colors.blueGrey + : status == 'noresult' + ? Colors.orange + : Colors.red, + Theme.of(context).brightness == Brightness.dark ? 0.5 : 0.8,) + ), + ), + ), + ); + }).toList(), + ), + ), + ), + // Fading background behind the expand button to increase contrast + Positioned( + right: 0, + top: 0, + bottom: 0, + width: 50, + child: IgnorePointer( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + // left: gradual fade (transparent -> semi), right: fully opaque + colors: [ + Theme.of(context).colorScheme.surface.withAlpha(0), + Theme.of(context).colorScheme.surface.withAlpha(230), + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.surface, + ], + stops: const [0.0, 0.3, 0.5, 1.0], ), - const SizedBox(width: 5.0), - Container( - width: 8.0, - height: 8.0, - decoration: BoxDecoration( - color: isSuccessButEmpty - ? Colors.orange - : (widget.infoController - .pluginSearchStatus[ - plugin.name] == - 'success' - ? Colors.green - : (widget.infoController - .pluginSearchStatus[ - plugin.name] == - 'pending') - ? Colors.grey - : Colors.red), - shape: BoxShape.circle, + ), + ), + ), + ), + Positioned( + right: 2, + top: 0, + bottom: 0, + child: IconButton( + onPressed: () { + setState(() { + _showTabGrid = true; + if(autoLock){ _isLocked = true;} + if(autoShowSuccess){_showOnlySuccess = true;} + }); + }, + icon: const Icon(Icons.keyboard_arrow_down), + tooltip: '展开', + ), + ), + ], + ), + ), + ), + // 展开时显示的按钮网格 + AnimatedOpacity( + opacity: _showTabGrid ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: _showTabGrid + ? Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children:[ + Row( + children:[ + Row( + children: [ + SizedBox(width : 16), + Tooltip( + message:"点击打开规则管理", + child: TextButton( + onPressed: () { + Modular.to.pushNamed('/settings/plugin/'); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(0, 0), + ), + child: Text( + '番源', + style: TextStyle( + fontSize: 18, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + ) + ), + ] + ), + Spacer(), + Row( + children: [ + IconButton( + onPressed: () { + setState(() { + expandedByDrag = false; //避免触顶展开>锁上>拉回>解锁时自动收起 + _isLocked = !_isLocked; + }); + }, + icon: Icon(_isLocked ? Icons.lock : Icons.lock_open), + tooltip: '锁定/解锁', + ), + IconButton( + onPressed: () { + setState(() { + _showOnlySuccess = !_showOnlySuccess; + }); + }, + icon: Icon(_showOnlySuccess ? Icons.filter_alt : Icons.filter_alt_outlined,), + tooltip: '筛选有结果项', + ), + IconButton( + onPressed: () { + setState(() { + _showTabGrid = false; + });}, + icon: const Icon(Icons.keyboard_arrow_up), + tooltip: '收起', + ), + SizedBox(width: 2), + ] + ), + ] + ), + Expanded( + child: ScrollConfiguration( + behavior: const ScrollBehavior().copyWith(scrollbars: false), + child:SingleChildScrollView( + controller: _tabGridScrollController, + physics: const ClampingScrollPhysics(), + padding: const EdgeInsets.fromLTRB(16, 2, 16, 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Observer( + builder: (_) { + // 根据筛选条件生成要显示的插件列表 + final visiblePluginsWithIndex = pluginsController.pluginList + .asMap() + .entries + .where((entry) { + final plugin = entry.value; + final status = widget.infoController.pluginSearchStatus[plugin.name]; + if (_showOnlySuccess) return status == 'success'; + return true; + }) + .toList(); // entry.key = 原始索引 + + return Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + children: visiblePluginsWithIndex.map((entry){ + final originalIndex = entry.key; + final plugin = entry.value; + final status = widget.infoController.pluginSearchStatus[plugin.name]; + + return DragTarget( + onAcceptWithDetails: (details) async { + final fromIndex = details.data; + // 如果拖放到自身,则弹出菜单而不是进行排序 + if (fromIndex == originalIndex) { + widget.tabController.index = originalIndex; + final controller = _menuControllers[originalIndex]; + if (controller != null) { + controller.open(); + return; + } + } + final targetIndex = originalIndex; + setState(() { + final item = pluginsController.pluginList.removeAt(fromIndex); + pluginsController.pluginList.insert(targetIndex, item); + // menu controllers keyed by indices may now be stale; clear so they'll be re-cached + _menuControllers.clear(); + }); + // Persist the new plugin order so it survives restarts + pluginsController.savePlugins(); + widget.tabController.index = targetIndex; + }, + builder: (context, candidateData, rejectedData) { + return MenuAnchor( + menuChildren: [ + MenuItemButton( + onPressed: () { + queryManager?.querySource(keyword, plugin.name); + }, + child: Row( + children: [ + Icon(Icons.refresh), + SizedBox(width: 8), + Text('重新检索'), + ], + ), + ), + MenuItemButton( + onPressed: () { + showAliasSearchDialog(pluginsController.pluginList[widget.tabController.index].name); + }, + child: Row( + children: [ + Icon(Icons.saved_search_rounded), + SizedBox(width: 8), + Text('别名检索'), + ], + ), + ), + MenuItemButton( + onPressed: () { + showCustomSearchDialog(pluginsController.pluginList[widget.tabController.index].name); + }, + child: Row( + children: [ + Icon(Icons.search_rounded), + SizedBox(width: 8), + Text('手动检索'), + ], + ), + ), + MenuItemButton( + onPressed: () { + launchUrl( + Uri.parse(pluginsController.pluginList[widget.tabController.index].searchURL.replaceFirst('@keyword', keyword)), + mode: LaunchMode.externalApplication, + ); + }, + child: Row( + children: [ + Icon(Icons.open_in_browser_rounded), + SizedBox(width: 8), + Text('打开网页'), + ], + ), + ), + MenuItemButton( + onPressed: () { + Modular.to.pushNamed('/settings/plugin/'); + }, + child: Row( + children: [ + Icon(Icons.extension), + SizedBox(width: 8), + Text('规则管理'), + ], + ), + ), + ], + builder: (context, controller, child) { + // cache controller so we can open the same menu on drop + _menuControllers[originalIndex] = controller; + return GestureDetector( + onSecondaryTap: () { + widget.tabController.index = originalIndex; + controller.open(); + }, + child: child, + ); + }, + child: LongPressDraggable( + data: originalIndex, + feedback: Material( + color: Colors.transparent, + child: ActionChip( + label: Text( + plugin.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + color: status == 'success' + ? Theme.of(context).colorScheme.onSurface + : Color.lerp( + Theme.of(context).colorScheme.onSurface, + status == 'pending' + ? Colors.blueGrey + : status == 'noresult' + ? Colors.orange + : Colors.red, + Theme.of(context).brightness == Brightness.dark ? 0.5 : 0.8,) + ), + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9), + side: BorderSide( + color: status == 'success' + ? Color.lerp(Theme.of(context).colorScheme.outlineVariant, Theme.of(context).colorScheme.secondary, 0.15)! + : Color.lerp( + Theme.of(context).colorScheme.outlineVariant, + status == 'pending' ? Colors.blueGrey : status == 'noresult' ? Colors.orange : Colors.red, + 0.15, + )!, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + childWhenDragging: Opacity( + opacity: 0.4, + child: ActionChip( + label: Text( + plugin.name, + overflow: TextOverflow.ellipsis, + style: TextStyle(fontSize: 15), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + child: ActionChip( + label: Text( + plugin.name, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 15, + color: widget.tabController.index == originalIndex + ? status == 'success' + ? Theme.of(context).colorScheme.surface + : Color.lerp( + Theme.of(context).colorScheme.surface, + status == 'pending' ? Colors.blueGrey : status == 'noresult' ? Colors.orange : Colors.red, + 0.15, + ) + : null, + ), + ), + backgroundColor: widget.tabController.index == originalIndex + ? status == 'success' + ? Theme.of(context).colorScheme.onSurface + : Color.lerp( + Theme.of(context).colorScheme.onSurface, + status == 'pending' ? Colors.blueGrey : status == 'noresult' ? Colors.orange : Colors.red, + Theme.of(context).brightness == Brightness.dark ? 0.5 : 0.8, + ) + : status == 'success' + ? null + : Color.lerp( + null, + status == 'pending' ? Colors.blueGrey : status == 'noresult' ? Colors.orange : Colors.red, + 0.075, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(9), + side: BorderSide( + color: status == 'success' + ? Color.lerp(Theme.of(context).colorScheme.outlineVariant, Theme.of(context).colorScheme.secondary, 0.15)! + : Color.lerp( + Theme.of(context).colorScheme.outlineVariant, + status == 'pending' ? Colors.blueGrey : status == 'noresult' ? Colors.orange : Colors.red, + 0.15, + )!, + ), + ), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + onPressed: () { + widget.tabController.index = originalIndex; + if (!_isLocked) { + setState(() { + _showTabGrid = false; + }); + } + }, + ), + ), + ); + }, + ); + }).toList(), + ); + } + ) + ], + ) ), ), - ], - ), - ); - }, - ), - ) - .toList(), - ), - ), - IconButton( - onPressed: () { - int currentIndex = widget.tabController.index; - launchUrl( - Uri.parse(pluginsController - .pluginList[currentIndex].searchURL - .replaceFirst('@keyword', keyword)), - mode: LaunchMode.externalApplication, - ); - }, - icon: const Icon(Icons.open_in_browser_rounded), + ), + ] + ), + ) + : const SizedBox.shrink(), + ), + ], ), - const SizedBox(width: 4), - ], + ), ), const Divider(height: 1), Expanded( - child: Observer( - builder: (context) => TabBarView( - controller: widget.tabController, - children: List.generate(pluginsController.pluginList.length, - (pluginIndex) { - var plugin = pluginsController.pluginList[pluginIndex]; - var cardList = []; - for (var searchResponse - in widget.infoController.pluginSearchResponseList) { - if (searchResponse.pluginName == plugin.name) { - for (var searchItem in searchResponse.data) { - cardList.add( - Card( - elevation: 0, - margin: const EdgeInsets.only( - left: 10, right: 10, top: 10), - child: InkWell( - borderRadius: BorderRadius.circular(12), - onTap: () async { + child: LayoutBuilder( + builder: (context, constraints) { + _maybeExpandTabGridOnListViewHeight(constraints); + return Observer( + builder: (context) { + return Focus( + autofocus: true, + onKeyEvent: (FocusNode node, KeyEvent event) { + if (event is KeyDownEvent || event is KeyRepeatEvent) { + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { + final cur = widget.tabController.index; + if (cur > 0) { + widget.tabController.animateTo(cur - 1); + } + return KeyEventResult.handled; + } + if (event.logicalKey == LogicalKeyboardKey.arrowRight) { + final cur = widget.tabController.index; + if (cur < widget.tabController.length - 1) { + widget.tabController.animateTo(cur + 1); + } + return KeyEventResult.handled; + } + } + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.slash) { + if(!_showTabGrid){ + setState(() { + if(autoLock){ _isLocked = true;} + if(autoShowSuccess){_showOnlySuccess = true;} + }); + } + setState(() {_showTabGrid = !_showTabGrid;}); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: TabBarView( + controller: widget.tabController, + children: List.generate(pluginsController.pluginList.length, + (pluginIndex) { + var plugin = pluginsController.pluginList[pluginIndex]; + var cardList = []; + for (var searchResponse + in widget.infoController.pluginSearchResponseList) { + if (searchResponse.pluginName == plugin.name) { + for (var searchItem in searchResponse.data) { + cardList.add( + Card( + elevation: 0, + margin: const EdgeInsets.only( + left: 10, right: 10, top: 10), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () async { KazumiDialog.showLoading( msg: '获取中', barrierDismissible: Utils.isDesktop(), @@ -304,79 +789,115 @@ class _SourceSheetState extends State videoPageController.cancelQueryRoads(); }, ); - videoPageController.bangumiItem = - widget.infoController.bangumiItem; - videoPageController.currentPlugin = plugin; - videoPageController.title = searchItem.name; - videoPageController.src = searchItem.src; - try { - await videoPageController.queryRoads( - searchItem.src, plugin.name); - KazumiDialog.dismiss(); - Modular.to.pushNamed('/video/'); - } catch (_) { - KazumiLogger() - .log(Level.warning, "获取视频播放列表失败"); - KazumiDialog.dismiss(); - } - }, - child: Padding( - padding: const EdgeInsets.all(20), - child: Text(searchItem.name), + videoPageController.bangumiItem = + widget.infoController.bangumiItem; + videoPageController.currentPlugin = plugin; + videoPageController.title = searchItem.name; + videoPageController.src = searchItem.src; + try { + await videoPageController.queryRoads( + searchItem.src, plugin.name); + KazumiDialog.dismiss(); + Modular.to.pushNamed('/video/'); + } catch (_) { + KazumiLogger() + .log(Level.warning, "获取视频播放列表失败"); + KazumiDialog.dismiss(); + } + }, + child: Padding( + padding: const EdgeInsets.all(20), + child: Text(searchItem.name), + ), + ), ), - ), - ), - ); + ); + } + } } - } - } - return widget.infoController - .pluginSearchStatus[plugin.name] == - 'pending' - ? const Center(child: CircularProgressIndicator()) - : (widget.infoController + return widget.infoController .pluginSearchStatus[plugin.name] == - 'error' - ? GeneralErrorWidget( - errMsg: '${plugin.name} 检索失败 重试或左右滑动以切换到其他视频来源', - actions: [ - GeneralErrorButton( - onPressed: () { - queryManager?.querySource( - keyword, plugin.name); - }, - text: '重试', + 'pending' + ? SingleChildScrollView( + controller: widget.scrollController, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height * (1 - widget.tabGridHeight) * 0.75, + ), + child: const Center( + child: CircularProgressIndicator(), ), - ], + ), ) - : cardList.isEmpty - ? GeneralErrorWidget( - errMsg: - '${plugin.name} 无结果 使用别名或左右滑动以切换到其他视频来源', - actions: [ - GeneralErrorButton( - onPressed: () { - showAliasSearchDialog( - plugin.name, - ); - }, - text: '别名检索', + : (widget.infoController + .pluginSearchStatus[plugin.name] == + 'error' + ? SingleChildScrollView( + controller: widget.scrollController, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height * (1 - widget.tabGridHeight) * 0.75, ), - GeneralErrorButton( - onPressed: () { - showCustomSearchDialog( - plugin.name, - ); - }, - text: '手动检索', + child: Center( + child: GeneralErrorWidget( + errMsg: '${plugin.name} 检索失败 重试或左右滑动以切换到其他视频来源', + actions: [ + GeneralErrorButton( + onPressed: () { + queryManager?.querySource(keyword, plugin.name); + }, + text: '重试', + ), + ], + ), ), - ], + ), ) - : ListView(children: cardList)); - }), - ), + + : (widget.infoController + .pluginSearchStatus[plugin.name] == + 'noresult' + ? SingleChildScrollView( + controller: widget.scrollController, + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: MediaQuery.of(context).size.height * (1 - widget.tabGridHeight) * 0.75, + ), + child: Center( + child: GeneralErrorWidget( + errMsg: '${plugin.name} 无结果 使用别名或左右滑动以切换到其他视频来源', + actions: [ + GeneralErrorButton( + onPressed: () { + showAliasSearchDialog(plugin.name); + }, + text: '别名检索', + ), + GeneralErrorButton( + onPressed: () { + showCustomSearchDialog(plugin.name); + }, + text: '手动检索', + ), + ], + ), + ), + ), + ) + : ListView( + controller: widget.scrollController, + children: cardList, + ) + ) + ); + }), // end List.generate + ), // end TabBarView + ); // end Focus (returned from builder) + }, // end Observer builder + ); + }, ), - ) + ), ], ), ), diff --git a/lib/pages/settings/theme_settings_page.dart b/lib/pages/settings/theme_settings_page.dart index f3e683c98..d6ae3d59d 100644 --- a/lib/pages/settings/theme_settings_page.dart +++ b/lib/pages/settings/theme_settings_page.dart @@ -28,6 +28,9 @@ class _ThemeSettingsPageState extends State { late dynamic defaultThemeColor; late bool oledEnhance; late bool useDynamicColor; + late bool defaultShowSelector; + late bool autoLockSourceSheet; + late bool autoshowSuccessed; late bool showWindowButton; late bool useSystemFont; final PopularController popularController = Modular.get(); @@ -42,6 +45,9 @@ class _ThemeSettingsPageState extends State { defaultThemeColor = setting.get(SettingBoxKey.themeColor, defaultValue: 'default'); oledEnhance = setting.get(SettingBoxKey.oledEnhance, defaultValue: false); + defaultShowSelector = setting.get(SettingBoxKey.defaultShowSelector, defaultValue: false); + autoLockSourceSheet = setting.get(SettingBoxKey.autoLockSourceSheet, defaultValue: true); + autoshowSuccessed = setting.get(SettingBoxKey.autoshowSuccessed, defaultValue: false); useDynamicColor = setting.get(SettingBoxKey.useDynamicColor, defaultValue: false); showWindowButton = @@ -268,6 +274,17 @@ class _ThemeSettingsPageState extends State { ], ), ), + SettingsTile.switchTile( + onToggle: (value) async { + oledEnhance = value ?? !oledEnhance; + await setting.put(SettingBoxKey.oledEnhance, oledEnhance); + updateOledEnhance(); + setState(() {}); + }, + title: const Text('OLED优化'), + description: const Text('深色模式下使用纯黑背景'), + initialValue: oledEnhance, + ), SettingsTile.navigation( enabled: !useDynamicColor, onPressed: (_) async { @@ -327,7 +344,8 @@ class _ThemeSettingsPageState extends State { themeProvider.setDynamic(useDynamicColor); setState(() {}); }, - title: Text('动态配色', style: TextStyle(fontFamily: fontFamily)), + title: const Text('动态配色'), + description: const Text('仅支持安卓12及以上和桌面平台'), initialValue: useDynamicColor, ), SettingsTile.switchTile( @@ -349,21 +367,49 @@ class _ThemeSettingsPageState extends State { initialValue: useSystemFont, ), ], - bottomInfo: Text('动态配色仅支持安卓12及以上和桌面平台', style: TextStyle(fontFamily: fontFamily)), ), SettingsSection( + title: const Text('番源选择器'), tiles: [ SettingsTile.switchTile( onToggle: (value) async { - oledEnhance = value ?? !oledEnhance; - await setting.put(SettingBoxKey.oledEnhance, oledEnhance); - updateOledEnhance(); + defaultShowSelector = value ?? !defaultShowSelector; + if (defaultShowSelector) { + autoLockSourceSheet = true; + await setting.put(SettingBoxKey.autoLockSourceSheet, autoLockSourceSheet); + } + await setting.put(SettingBoxKey.defaultShowSelector, defaultShowSelector); setState(() {}); }, - title: Text('OLED优化', style: TextStyle(fontFamily: fontFamily)), - description: Text('深色模式下使用纯黑背景', style: TextStyle(fontFamily: fontFamily)), - initialValue: oledEnhance, + title: const Text('默认展开'), + initialValue: defaultShowSelector, ), + SettingsTile.switchTile( + enabled: !defaultShowSelector, + onToggle: (value) async { + autoLockSourceSheet = value ?? !autoLockSourceSheet; + await setting.put(SettingBoxKey.autoLockSourceSheet, autoLockSourceSheet); + setState(() {}); + }, + title: const Text('自动锁定'), + description: const Text('点击按钮展开时自动锁定面板'), + initialValue: autoLockSourceSheet, + ), + SettingsTile.switchTile( + onToggle: (value) async { + autoshowSuccessed = value ?? !autoshowSuccessed; + await setting.put(SettingBoxKey.autoshowSuccessed, autoshowSuccessed); + setState(() {}); + }, + title: const Text('自动筛选'), + description: const Text('自动筛选有结果项'), + initialValue: autoshowSuccessed, + ), + ], + ), + SettingsSection( + tiles: [ + ], ), if (Utils.isDesktop()) diff --git a/lib/request/query_manager.dart b/lib/request/query_manager.dart index c99a23d34..cb38348f6 100644 --- a/lib/request/query_manager.dart +++ b/lib/request/query_manager.dart @@ -33,9 +33,11 @@ class QueryManager { return; } - infoController.pluginSearchStatus[plugin.name] = 'success'; if (result.data.isNotEmpty) { + infoController.pluginSearchStatus[plugin.name] = 'success'; pluginsController.validityTracker.markSearchValid(plugin.name); + } else { + infoController.pluginSearchStatus[plugin.name] = 'noresult'; } infoController.pluginSearchResponseList.add(result); }).catchError((error) { @@ -65,9 +67,11 @@ class QueryManager { return; } - infoController.pluginSearchStatus[plugin.name] = 'success'; if (result.data.isNotEmpty) { + infoController.pluginSearchStatus[plugin.name] = 'success'; pluginsController.validityTracker.markSearchValid(plugin.name); + } else { + infoController.pluginSearchStatus[plugin.name] = 'noresult'; } _controller?.add(result); }).catchError((error) { diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 45f917b3a..58ea85f0b 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -239,6 +239,9 @@ class SettingBoxKey { playResume = 'playResume', showPlayerError = 'showPlayerError', oledEnhance = 'oledEnhance', + defaultShowSelector = 'defaultShowSelector', + autoLockSourceSheet = 'autoLockSourceSheet', + autoshowSuccessed = 'autoshowSuccessed', displayMode = 'displayMode', enableGitProxy = 'enableGitProxy', enableSystemProxy = 'enableSystemProxy',