part of 'danmaku_search_dialog.dart'; extension _DanmakuSearchDialogView on _DanmakuSearchDialogState { Widget _slideFade(Widget child, Animation animation) { final curved = CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic, ); return FadeTransition( opacity: curved, child: SlideTransition( position: Tween( begin: _transitionOffset, end: Offset.zero, ).animate(curved), child: child, ), ); } Widget _buildHeader() { return Padding( padding: const EdgeInsets.fromLTRB(16, 12, 8, 10), child: Row( children: [ const Icon(Icons.search, color: Colors.white, size: 20), const SizedBox(width: 8), const Text( '搜索弹幕源', style: TextStyle( color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600, ), ), const Spacer(), IconButton( iconSize: 18, visualDensity: VisualDensity.compact, onPressed: _dismiss, icon: const Icon(Icons.close, color: Colors.white70), tooltip: '关闭', ), ], ), ); } Widget _buildSearchField() { return SizedBox( height: 44, child: TextField( controller: _queryCtrl, focusNode: _focusNode, autofocus: true, enabled: !_busy, style: const TextStyle(color: Colors.white, fontSize: 13), cursorColor: Colors.lightBlueAccent, textInputAction: TextInputAction.search, decoration: InputDecoration( filled: true, fillColor: Colors.white.withValues(alpha: 0.08), hintText: '输入番剧名后按 Enter 搜索', hintStyle: const TextStyle(color: Colors.white38, fontSize: 13), prefixIcon: const Icon(Icons.search, color: Colors.white54, size: 18), suffixIcon: IconButton( onPressed: _busy ? null : _submit, icon: const Icon(Icons.arrow_forward, size: 18), color: Colors.white70, tooltip: '搜索', ), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 0, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _kDivider), ), disabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _kDivider), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: _kAccent), ), ), onSubmitted: (_) => _submit(), ), ); } Widget _buildBody() { switch (_phase) { case _Phase.idle: return _buildStateBody( key: const ValueKey('idle'), icon: Icons.search, title: '输入番剧名,按 Enter 搜索', subtitle: '可手动选择具体集数', showMessage: true, ); case _Phase.searching: return _buildLoadingBody( key: const ValueKey('searching'), text: '正在搜索…', ); case _Phase.results: return _buildResultsBody( key: const ValueKey('results'), disabled: false, ); case _Phase.empty: return _buildStateBody( key: const ValueKey('empty'), icon: Icons.search_off, title: '未找到匹配的弹幕源', subtitle: '换个剧名试试', ); case _Phase.error: return _buildStateBody( key: const ValueKey('error'), icon: Icons.error_outline, title: _messageText ?? '搜索失败', subtitle: '请检查弹幕服务地址', ); case _Phase.episodesLoading: return _buildEpisodesLoadingBody(); case _Phase.episodes: return _buildEpisodesBody( key: const ValueKey('episodes'), disabled: false, ); case _Phase.applying: return _buildApplyingBody(); } } Widget _buildStateBody({ required Key key, required IconData icon, required String title, String? subtitle, bool showMessage = false, }) { return Center( key: key, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 280), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, color: Colors.white24, size: 32), const SizedBox(height: 12), Text( title, textAlign: TextAlign.center, style: const TextStyle( color: Colors.white70, fontSize: 13, fontWeight: FontWeight.w500, ), ), if (subtitle != null) ...[ const SizedBox(height: 6), Text( subtitle, textAlign: TextAlign.center, style: const TextStyle(color: Colors.white54, fontSize: 12), ), ], if (showMessage && _messageText != null) ...[ const SizedBox(height: 16), _buildMessageBanner(), ], ], ), ), ); } Widget _buildLoadingBody({required Key key, required String text}) { return Center( key: key, child: Column( mainAxisSize: MainAxisSize.min, children: [ const AppLoadingRing(size: 22, color: Colors.white), const SizedBox(height: 12), Text( text, style: const TextStyle(color: Colors.white54, fontSize: 12), ), ], ), ); } Widget _buildResultsBody({required Key key, required bool disabled}) { return Column( key: key, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '搜索结果 ${_results.length} 条', style: const TextStyle( color: Colors.white70, fontSize: 14, fontWeight: FontWeight.w600, ), ), if (_messageText != null) ...[ const SizedBox(height: 12), _buildMessageBanner(), ], const SizedBox(height: 12), Expanded( child: ListView.separated( padding: EdgeInsets.zero, itemCount: _results.length, separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (_, i) => _buildResultTile(_results[i], disabled), ), ), ], ); } Widget _buildEpisodesLoadingBody() { return Column( key: const ValueKey('episodes-loading'), crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildEpisodesHeader(disabled: false), const SizedBox(height: 16), const Expanded( child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ AppLoadingRing(size: 22, color: Colors.white), SizedBox(height: 12), Text( '正在加载分集…', style: TextStyle(color: Colors.white54, fontSize: 12), ), ], ), ), ), ], ); } Widget _buildEpisodesBody({ required Key key, required bool disabled, bool useScrollController = true, }) { return Column( key: key, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildEpisodesHeader(disabled: disabled), if (_messageText != null) ...[ const SizedBox(height: 12), _buildMessageBanner(), ], const SizedBox(height: 12), Expanded( child: ListView.separated( controller: useScrollController ? _episodesScrollCtrl : null, padding: EdgeInsets.zero, itemCount: _episodes.length, separatorBuilder: (_, _) => const SizedBox(height: 8), itemBuilder: (_, i) => _buildEpisodeTile(_episodes[i], i, disabled), ), ), ], ); } Widget _buildApplyingBody() { final base = _activeMatch != null && _episodes.isNotEmpty ? _buildEpisodesBody( key: const ValueKey('applying-episodes'), disabled: true, useScrollController: false, ) : _buildResultsBody( key: const ValueKey('applying-results'), disabled: true, ); return Stack( key: const ValueKey('applying'), fit: StackFit.expand, children: [ base, Positioned.fill( child: ColoredBox(color: Colors.black.withValues(alpha: 0.18)), ), const Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ AppLoadingRing(size: 22, color: Colors.white), SizedBox(height: 12), Text( '正在加载弹幕…', style: TextStyle(color: Colors.white70, fontSize: 12), ), ], ), ), ], ); } Widget _buildEpisodesHeader({required bool disabled}) { return Row( children: [ IconButton( onPressed: disabled ? null : _backToResults, iconSize: 18, visualDensity: VisualDensity.compact, icon: const Icon(Icons.arrow_back, color: Colors.white70), tooltip: '返回', ), const SizedBox(width: 4), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( _activeMatch?.animeTitle ?? '选择分集', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), const Text( '选择要加载的集数', style: TextStyle(color: Colors.white54, fontSize: 12), ), ], ), ), ], ); } Widget _buildMessageBanner() { final color = _messageIsError ? Colors.redAccent : _kAccent; final icon = _messageIsError ? Icons.error_outline : Icons.info_outline; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: color.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(10), border: Border.all(color: color.withValues(alpha: 0.35)), ), child: Row( children: [ Icon(icon, size: 16, color: color), const SizedBox(width: 8), Expanded( child: Text( _messageText ?? '', style: const TextStyle(color: Colors.white70, fontSize: 12), ), ), ], ), ); } Widget _buildResultTile(DanmakuMatchCandidate match, bool disabled) { final highlighted = _flashEpisodeId == match.episodeId && _flashSource?.id == match.source.id; final canDrill = match.animeId > 0; final subtitle = match.episodeTitle.isNotEmpty ? match.episodeTitle : canDrill ? '进入分集列表后选择具体集数' : '直接加载当前搜索结果'; final showTargetHint = widget.targetEpisode != null && canDrill; return InkWell( onTap: disabled ? null : () { if (canDrill) { _openAnime(match); } else { _applyDirect(match); } }, borderRadius: BorderRadius.circular(10), child: AnimatedContainer( duration: const Duration(milliseconds: 100), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: highlighted ? Colors.white12 : Colors.white.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(10), border: Border.all( color: highlighted ? Colors.lightBlueAccent.withValues(alpha: 0.7) : Colors.white.withValues(alpha: 0.08), ), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( match.animeTitle, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 4), Text( subtitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white54, fontSize: 12, height: 1.35, ), ), const SizedBox(height: 4), Text( match.source.displayName, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(color: Colors.white38, fontSize: 11), ), if (showTargetHint) ...[ const SizedBox(height: 4), Row( children: [ const Icon( Icons.adjust, size: 12, color: Colors.white38, ), const SizedBox(width: 4), Expanded( child: Text( '目标:${formatEpisodeNumber(widget.targetEpisode)}(点击选择)', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white38, fontSize: 11, ), ), ), ], ), ], ], ), ), const SizedBox(width: 12), _buildResultAffordance(canDrill: canDrill), ], ), ), ); } Widget _buildResultAffordance({required bool canDrill}) { if (canDrill) { return const Row( mainAxisSize: MainAxisSize.min, children: [ Text('选择集数', style: TextStyle(color: Colors.white54, fontSize: 11)), SizedBox(width: 2), Icon(Icons.chevron_right, size: 16, color: Colors.white54), ], ); } return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: _kAccent.withValues(alpha: 0.16), borderRadius: BorderRadius.circular(999), border: Border.all(color: _kAccent.withValues(alpha: 0.4)), ), child: const Text( '直接加载', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w600, ), ), ); } Widget _buildEpisodeTile( DanmakuBangumiEpisode episode, int index, bool disabled, ) { final highlighted = _flashEpisodeId == episode.episodeId; final epNum = episode.episodeNumber.trim(); final episodeLabel = epNum.isEmpty ? formatEpisodeNumber(index + 1)! : '第$epNum集'; return InkWell( onTap: disabled || _activeMatch == null ? null : () => _applyEpisode(_activeMatch!, episode), borderRadius: BorderRadius.circular(10), child: AnimatedContainer( duration: const Duration(milliseconds: 100), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), decoration: BoxDecoration( color: highlighted ? Colors.white12 : Colors.white.withValues(alpha: 0.04), borderRadius: BorderRadius.circular(10), border: Border.all( color: highlighted ? Colors.lightBlueAccent.withValues(alpha: 0.7) : Colors.white.withValues(alpha: 0.08), ), ), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( episodeLabel, style: const TextStyle( color: Colors.white, fontSize: 13, fontWeight: FontWeight.w600, ), ), if (episode.episodeTitle.isNotEmpty) ...[ const SizedBox(height: 4), Text( episode.episodeTitle, maxLines: 2, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.white54, fontSize: 12, height: 1.35, ), ), ], ], ), ), const SizedBox(width: 12), const Text( '点击加载', style: TextStyle(color: Colors.white54, fontSize: 11), ), ], ), ), ); } }