import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; import '../../core/contracts/library.dart'; const Color _defaultSeasonAccentColor = Color(0xFF4F8DFF); const double _inlineSeasonHorizontalPadding = 14; const double _inlineSeasonVerticalPadding = 6; const double _inlineSeasonGap = 8; const double _inlineWidthSafetyMargin = 2; const TextStyle _inlineSeasonTextStyle = TextStyle(fontSize: 13); class SeasonSelector extends StatelessWidget { final List seasons; final String selectedId; final ValueChanged onSelect; final Color accentColor; final Color? backgroundColor; const SeasonSelector({ super.key, required this.seasons, required this.selectedId, required this.onSelect, this.accentColor = _defaultSeasonAccentColor, this.backgroundColor, }); EmbyRawSeason _resolveSelectedSeason() { for (final season in seasons) { if (season.Id == selectedId) return season; } return seasons.first; } TextStyle _inlineTextStyle(bool selected) { return _inlineSeasonTextStyle.copyWith( fontWeight: selected ? FontWeight.w600 : FontWeight.normal, color: selected ? Colors.white : Colors.white.withValues(alpha: 0.7), ); } double _calculateInlineWidth(BuildContext context) { final textDirection = Directionality.of(context); final textScaler = MediaQuery.textScalerOf(context); var requiredWidth = 0.0; for (var index = 0; index < seasons.length; index++) { final season = seasons[index]; final textPainter = TextPainter( text: TextSpan( text: season.Name, style: _inlineTextStyle(season.Id == selectedId), ), maxLines: 1, textDirection: textDirection, textScaler: textScaler, )..layout(); requiredWidth += textPainter.width + (_inlineSeasonHorizontalPadding * 2); if (index > 0) requiredWidth += _inlineSeasonGap; } return requiredWidth + _inlineWidthSafetyMargin; } @override Widget build(BuildContext context) { if (seasons.isEmpty) return const SizedBox.shrink(); final selectedSeason = _resolveSelectedSeason(); return LayoutBuilder( builder: (context, constraints) { final inlineWidth = _calculateInlineWidth(context); final hasEnoughInlineSpace = !constraints.hasBoundedWidth || inlineWidth <= constraints.maxWidth; final selector = hasEnoughInlineSpace ? _buildInlineOptions() : _buildDropdownSelect(context, selectedSeason); return Align( alignment: Alignment.centerLeft, widthFactor: 1, child: selector, ); }, ); } Widget _buildInlineOptions() { return Row( mainAxisSize: MainAxisSize.min, children: [ for (var index = 0; index < seasons.length; index++) ...[ if (index > 0) const SizedBox(width: _inlineSeasonGap), _buildInlineOption(seasons[index]), ], ], ); } Widget _buildInlineOption(EmbyRawSeason season) { final selected = season.Id == selectedId; return Material( color: Colors.transparent, child: InkWell( borderRadius: BorderRadius.circular(20), onTap: selected ? null : () => onSelect(season.Id), child: AnimatedContainer( duration: const Duration(milliseconds: 150), padding: const EdgeInsets.symmetric( horizontal: _inlineSeasonHorizontalPadding, vertical: _inlineSeasonVerticalPadding, ), decoration: BoxDecoration( color: selected ? accentColor.withValues(alpha: 0.28) : backgroundColor ?? Colors.white.withValues(alpha: 0.08), borderRadius: BorderRadius.circular(20), ), child: Text(season.Name, style: _inlineTextStyle(selected)), ), ), ); } Widget _buildDropdownSelect( BuildContext context, EmbyRawSeason selectedSeason, ) { final controlWidth = _calculateControlWidth(context, selectedSeason); final menuWidth = _calculateMenuWidth(context, controlWidth); return SizedBox( width: controlWidth, child: FSelect.rich( control: FSelectControl.lifted( value: selectedId, onChange: (seasonId) { if (seasonId != null && seasonId != selectedId) { onSelect(seasonId); } }, ), format: _seasonNameForId, contentConstraints: FPortalConstraints( minWidth: menuWidth, maxWidth: menuWidth, maxHeight: 360, ), children: [ for (final season in seasons) FSelectItem.item( title: Text(season.Name), subtitle: season.ChildCount == null ? null : Text('${season.ChildCount} 集'), value: season.Id, ), ], ), ); } double _calculateControlWidth( BuildContext context, EmbyRawSeason selectedSeason, ) { final selectedTextWidth = _measureTextWidth( context, selectedSeason.Name, _inlineSeasonTextStyle.copyWith(fontWeight: FontWeight.w600), ); return (selectedTextWidth + 64).clamp(104.0, 200.0).toDouble(); } double _calculateMenuWidth(BuildContext context, double controlWidth) { var widestContent = 0.0; for (final season in seasons) { final seasonNameWidth = _measureTextWidth( context, season.Name, const TextStyle(fontSize: 14), ); final episodeCountWidth = season.ChildCount == null ? 0.0 : _measureTextWidth( context, '${season.ChildCount} 集', const TextStyle(fontSize: 12), ); widestContent = widestContent.clamp( seasonNameWidth > episodeCountWidth ? seasonNameWidth : episodeCountWidth, double.infinity, ); } final screenWidth = MediaQuery.sizeOf(context).width; final maximumSafeWidth = (screenWidth - 32).clamp( controlWidth, double.infinity, ); return (widestContent + 72) .clamp(controlWidth, maximumSafeWidth) .toDouble(); } double _measureTextWidth(BuildContext context, String text, TextStyle style) { final textPainter = TextPainter( text: TextSpan(text: text, style: style), maxLines: 1, textDirection: Directionality.of(context), textScaler: MediaQuery.textScalerOf(context), )..layout(); return textPainter.width; } String _seasonNameForId(String seasonId) { for (final season in seasons) { if (season.Id == seasonId) return season.Name; } return _resolveSelectedSeason().Name; } }