import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../../core/contracts/library.dart'; import '../../../shared/theme/app_theme.dart'; import '../../../shared/utils/emby_ticks.dart'; import '../../../shared/utils/time_bucket.dart'; import '../../../shared/widgets/adaptive_modal.dart'; import '../../aggregated/aggregated_grouping.dart'; Future showHistorySourcePicker( BuildContext context, { required AggregatedEntry entry, DateTime? now, }) { return showAdaptiveModal( context: context, maxWidth: 520, maxHeight: 640, builder: (modalContext) => HistorySourcePickerContent( entry: entry, now: now ?? DateTime.now(), onSourceSelected: (source) { Navigator.of(modalContext).pop(source); }, ), ); } class HistorySourcePickerContent extends StatelessWidget { final AggregatedEntry entry; final DateTime now; final ValueChanged onSourceSelected; const HistorySourcePickerContent({ super.key, required this.entry, required this.now, required this.onSourceSelected, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final desiredHeight = 104.0 + (entry.sources.length * 92.0); final contentHeight = math.min(desiredHeight, 560.0); return SizedBox( height: contentHeight, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 8, 8, 12), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '选择服务器', style: theme.textTheme.titleMedium?.copyWith( fontWeight: FontWeight.w600, ), ), const SizedBox(height: 2), Text( entry.item.SeriesName ?? entry.item.Name, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: context.appTokens.mutedForeground, ), ), ], ), ), IconButton( tooltip: '关闭', onPressed: () => Navigator.of(context).pop(), icon: const Icon(Icons.close, size: 20), ), ], ), ), Divider(height: 1, color: context.appTokens.separatorColor), Expanded( child: ListView.separated( padding: const EdgeInsets.symmetric(vertical: 8), itemCount: entry.sources.length, separatorBuilder: (_, _) => const SizedBox(height: 2), itemBuilder: (context, index) { final source = entry.sources[index]; return _HistorySourceRow( source: source, now: now, isPrimary: index == 0, onTap: () => onSourceSelected(source), ); }, ), ), ], ), ); } } class _HistorySourceRow extends StatelessWidget { final AggregatedEntrySource source; final DateTime now; final bool isPrimary; final VoidCallback onTap; const _HistorySourceRow({ required this.source, required this.now, required this.isPrimary, required this.onTap, }); @override Widget build(BuildContext context) { final theme = Theme.of(context); final tokens = context.appTokens; final progress = _HistorySourceProgress.fromItem(source.item); final lastPlayedLabel = relativeTimeLabel(source.dateFor(GroupAxis.watchedTime), now) ?? '未记录播放时间'; return Material( color: Colors.transparent, child: InkWell( onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), child: Row( children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Flexible( child: Text( source.source.serverName, maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), ), ), if (isPrimary) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric( horizontal: 7, vertical: 2, ), decoration: BoxDecoration( color: theme.colorScheme.primary.withValues( alpha: 0.12, ), borderRadius: BorderRadius.circular(999), ), child: Text( '最近播放', style: theme.textTheme.labelSmall?.copyWith( color: theme.colorScheme.primary, fontWeight: FontWeight.w600, ), ), ), ], ], ), const SizedBox(height: 7), ClipRRect( borderRadius: BorderRadius.circular(999), child: LinearProgressIndicator( value: progress.value ?? 0, minHeight: 4, backgroundColor: theme.colorScheme.onSurface.withValues( alpha: 0.08, ), valueColor: AlwaysStoppedAnimation( theme.colorScheme.primary, ), ), ), const SizedBox(height: 6), Text( '${progress.label} · $lastPlayedLabel', maxLines: 1, overflow: TextOverflow.ellipsis, style: theme.textTheme.bodySmall?.copyWith( color: tokens.mutedForeground, ), ), ], ), ), const SizedBox(width: 12), Icon( Icons.chevron_right_rounded, size: 20, color: tokens.mutedForeground, ), ], ), ), ), ); } } class _HistorySourceProgress { final double? value; final String label; const _HistorySourceProgress({required this.value, required this.label}); factory _HistorySourceProgress.fromItem(EmbyRawItem item) { final userData = item.UserData; if (userData?.Played == true) { return const _HistorySourceProgress(value: 1, label: '已看完'); } final positionTicks = userData?.PlaybackPositionTicks; final runtimeTicks = item.RunTimeTicks; if (positionTicks == null || runtimeTicks == null || runtimeTicks <= 0) { return const _HistorySourceProgress(value: null, label: '进度未知'); } final progress = (positionTicks / runtimeTicks).clamp(0.0, 1.0); final watchedPercent = (progress * 100).round(); final remainingTicks = runtimeTicks - positionTicks; if (remainingTicks <= 0) { return const _HistorySourceProgress(value: 1, label: '已看完'); } final remainingMinutes = (remainingTicks / kTicksPerMinute).ceil(); return _HistorySourceProgress( value: progress, label: '已看 $watchedPercent% · 剩余 $remainingMinutes 分钟', ); } }