import 'package:flutter/material.dart'; import 'package:forui/forui.dart'; import '../../../core/contracts/library.dart'; import '../../../shared/constants/breakpoints.dart'; import '../../../shared/widgets/frosted_panel.dart'; import '../../../shared/widgets/hover_scrollable_row.dart'; class MediaInfoSection extends StatelessWidget { final EmbyRawItem item; final int selectedSourceIndex; final EmbyRawMediaSource? selectedSource; final bool embedded; const MediaInfoSection({ super.key, required this.item, this.selectedSourceIndex = 0, this.selectedSource, this.embedded = false, }); static bool hasVisibleContent({ required EmbyRawItem item, int selectedSourceIndex = 0, EmbyRawMediaSource? selectedSource, }) { if (item.Type == 'Series') return false; final EmbyRawMediaSource source; if (selectedSource != null) { source = selectedSource; } else { final sources = mediaSourcesOfItem(item); if (sources.isEmpty) return false; source = sources[selectedSourceIndex.clamp(0, sources.length - 1)]; } final streams = source.MediaStreams ?? []; if (streams.isEmpty) return false; return streams.any( (stream) => stream.Type == 'Video' || stream.Type == 'Audio' || stream.Type == 'Subtitle', ); } @override Widget build(BuildContext context) { if (!hasVisibleContent( item: item, selectedSourceIndex: selectedSourceIndex, selectedSource: selectedSource, )) { return const SizedBox.shrink(); } final EmbyRawMediaSource source; if (selectedSource != null) { source = selectedSource!; } else { final sources = mediaSourcesOfItem(item); if (sources.isEmpty) return const SizedBox.shrink(); source = sources[selectedSourceIndex.clamp(0, sources.length - 1)]; } final streams = source.MediaStreams ?? []; if (streams.isEmpty) return const SizedBox.shrink(); final hPad = embedded ? 0.0 : (MediaQuery.sizeOf(context).width < Breakpoints.detail ? 16.0 : 32.0); final defaultAudioIdx = source.DefaultAudioStreamIndex; final cards = []; for (final s in streams.where((s) => s.Type == 'Video')) { cards.add(_StreamCard(stream: s, kind: _Kind.video)); } for (final s in streams.where((s) => s.Type == 'Audio')) { final isDefault = (s.Index != null && s.Index == defaultAudioIdx) || (s.IsDefault ?? false); cards.add( _StreamCard(stream: s, kind: _Kind.audio, isDefault: isDefault), ); } for (final s in streams.where((s) => s.Type == 'Subtitle')) { cards.add( _StreamCard( stream: s, kind: _Kind.subtitle, isDefault: s.IsDefault ?? false, ), ); } if (cards.isEmpty) return const SizedBox.shrink(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const FDivider(style: .delta(color: Color(0x1AFFFFFF), padding: .value(EdgeInsets.symmetric(vertical: 24)))), Padding( padding: EdgeInsets.fromLTRB(hPad, 0, hPad, 16), child: Text( '媒体信息', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w600, color: Colors.white.withValues(alpha: 0.9), ), ), ), SizedBox( height: 280, child: HoverScrollableRow( builder: (_, controller) => ListView.builder( controller: controller, scrollDirection: Axis.horizontal, padding: EdgeInsets.symmetric(horizontal: hPad), itemCount: cards.length, itemBuilder: (_, i) => Padding( padding: EdgeInsets.only(right: i < cards.length - 1 ? 12 : 0), child: cards[i], ), ), ), ), ], ); } } enum _Kind { video, audio, subtitle } class _StreamCard extends StatelessWidget { final EmbyRawMediaStream stream; final _Kind kind; final bool isDefault; const _StreamCard({ required this.stream, required this.kind, this.isDefault = false, }); List<(String label, String value)> _fields() { final ex = stream.extra; final rows = <(String, String)>[]; switch (kind) { case _Kind.video: _add(rows, '编码', stream.Codec); final w = ex['Width'] as int?; final h = ex['Height'] as int?; if (w != null && h != null) rows.add(('分辨率', '${w}x$h')); final fr = (ex['RealFrameRate'] as num?) ?? (ex['AverageFrameRate'] as num?); if (fr != null) rows.add(('帧率', fr.toDouble().toStringAsFixed(1))); final br = ex['BitRate'] as int?; if (br != null) rows.add(('比特率', '${(br / 1000).round()}kbps')); _add(rows, '动态范围', ex['VideoRange'] as String?); _add(rows, '配置', ex['Profile'] as String?); case _Kind.audio: _add(rows, '标题', stream.Title); _add(rows, '内嵌标题', stream.DisplayTitle); _add(rows, '语言', stream.Language); _add(rows, '布局', ex['ChannelLayout'] as String?); if (stream.Channels != null) rows.add(('声道', '${stream.Channels}')); _add(rows, '编码', stream.Codec); final br = ex['BitRate'] as int?; if (br != null) rows.add(('比特率', '${(br / 1000).round()}kbps')); final sr = ex['SampleRate'] as int?; if (sr != null) rows.add(('采样率', '${sr}Hz')); rows.add(('外部', (stream.IsExternal ?? false) ? '是' : '否')); rows.add(('默认', (stream.IsDefault ?? false) ? '是' : '否')); case _Kind.subtitle: _add(rows, '标题', stream.Title); _add(rows, '内嵌标题', stream.DisplayTitle); _add(rows, '语言', stream.Language); _add(rows, '格式', stream.Codec); rows.add(('外部', (stream.IsExternal ?? false) ? '是' : '否')); if (stream.IsForced ?? false) rows.add(('强制', '是')); rows.add(('默认', (stream.IsDefault ?? false) ? '是' : '否')); } return rows; } static void _add(List<(String, String)> rows, String label, String? value) { if (value != null && value.isNotEmpty) rows.add((label, value)); } @override Widget build(BuildContext context) { final fields = _fields(); final (IconData icon, String name) = switch (kind) { _Kind.video => (Icons.videocam_outlined, '视频'), _Kind.audio => (Icons.music_note_outlined, '音频'), _Kind.subtitle => (Icons.subtitles_outlined, '字幕'), }; return SizedBox( width: 260, height: 280, child: FrostedPanel( radius: 12, padding: const EdgeInsets.all(16), blurSigma: 15, border: Border.all(color: Colors.white.withValues(alpha: 0.08)), color: Colors.black26, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(icon, size: 16, color: Colors.white), const SizedBox(width: 6), Text( name, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), ), if (isDefault) ...[ const SizedBox(width: 6), Icon( Icons.check, size: 14, color: Colors.white.withValues(alpha: 0.7), ), ], ], ), const SizedBox(height: 12), Expanded( child: SingleChildScrollView( child: Column( children: [ for (final (label, value) in fields) Padding( padding: const EdgeInsets.only(bottom: 5), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 60, child: Text( label, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, color: Colors.white.withValues(alpha: 0.55), ), ), ), Expanded( child: Text( value, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w400, color: Colors.white.withValues(alpha: 0.85), ), ), ), ], ), ), ], ), ), ), ], ), ), ); } }