Files

233 lines
6.7 KiB
Dart
Raw Permalink Normal View History

2026-07-14 11:11:36 +08:00
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<EmbyRawSeason> seasons;
final String selectedId;
final ValueChanged<String> 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<String>.rich(
control: FSelectControl<String>.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<String>.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;
}
}