Files
sm-emby-share/lib/features/player/widgets/version_panel.dart
T
2026-07-14 11:11:36 +08:00

226 lines
7.4 KiB
Dart

import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import 'package:forui/forui.dart' as forui;
import '../../../shared/widgets/all_versions_sheet.dart';
import '../../../shared/widgets/hover_scrollable_row.dart';
import '../../../shared/widgets/version_filter.dart';
import '../../../shared/widgets/version_grouping.dart';
import '../../../shared/widgets/version_source_card.dart';
import 'player_panel_shell.dart';
class VersionPanel extends StatefulWidget {
final List<VersionSourceCardData> entries;
final VoidCallback onClose;
const VersionPanel({super.key, required this.entries, required this.onClose});
@override
State<VersionPanel> createState() => _VersionPanelState();
}
class _VersionPanelState extends State<VersionPanel> {
final ScrollController _scrollController = ScrollController();
String? _selectedBucket;
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final entries = widget.entries;
final allGroups = groupVersionEntries(entries);
final bucketsPresent = <String>{
for (final e in entries) versionBucketOf(e.resolutionLabel),
};
final buckets = kVersionBucketOrder
.where(bucketsPresent.contains)
.toList(growable: false);
final activeEntry = entries
.where((entry) => entry.isCurrent || entry.selected)
.firstOrNull;
final defaultBucket = activeEntry == null
? (buckets.isNotEmpty ? buckets.first : null)
: versionBucketOf(activeEntry.resolutionLabel);
final selected =
_selectedBucket != null && bucketsPresent.contains(_selectedBucket)
? _selectedBucket!
: defaultBucket;
if (selected != _selectedBucket) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
setState(() => _selectedBucket = selected);
});
}
final filteredGroups = selected == null
? allGroups
: allGroups
.where(
(group) =>
versionBucketOf(group.representative.resolutionLabel) ==
selected,
)
.toList(growable: false);
final inlineLimit = inlineVersionLimit();
final inlineGroups = filteredGroups
.take(inlineLimit)
.toList(growable: true);
final activeGroupIndex = filteredGroups.indexWhere(
(group) => group.hasActiveMember,
);
if (activeGroupIndex >= inlineLimit && inlineGroups.isNotEmpty) {
inlineGroups[inlineGroups.length - 1] = filteredGroups[activeGroupIndex];
}
final hasMergedGroup = allGroups.any((group) => group.members.length > 1);
final showAllVersionsButton =
filteredGroups.length > inlineLimit || hasMergedGroup;
final itemCount = inlineGroups.length;
return Material(
color: Colors.transparent,
child: Container(
decoration: BoxDecoration(
color: kPanelBg,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_header(
buckets,
selected,
allGroups: allGroups,
sourceCount: entries.length,
showAllVersionsButton: showAllVersionsButton,
),
panelDivider(),
Padding(
padding: EdgeInsets.only(
top: Platform.isAndroid ? 8 : 12,
bottom: Platform.isAndroid ? 10 : 14,
),
child: SizedBox(
height: Platform.isAndroid ? 56 : 120,
child: HoverScrollableRow(
controller: _scrollController,
builder: (_, controller) => ListView.builder(
controller: controller,
scrollDirection: Axis.horizontal,
padding: EdgeInsets.symmetric(
horizontal: Platform.isAndroid ? 10 : 16,
),
itemCount: itemCount,
itemBuilder: (_, i) => Padding(
padding: EdgeInsets.only(
right: i < itemCount - 1
? (Platform.isAndroid ? 6 : 12)
: 0,
),
child: VersionSourceCard(
data: inlineGroups[i].displayData,
compact: Platform.isAndroid,
onMergedServersTap: inlineGroups[i].members.length > 1
? () => showAllVersionsSheet(
context: context,
groups: [inlineGroups[i]],
title: '选择服务器',
)
: null,
),
),
),
),
),
),
],
),
),
);
}
Widget _header(
List<String> buckets,
String? selected, {
required List<VersionSourceGroup> allGroups,
required int sourceCount,
required bool showAllVersionsButton,
}) {
final isAndroid = Platform.isAndroid;
return Padding(
padding: isAndroid
? const EdgeInsets.fromLTRB(16, 10, 8, 8)
: const EdgeInsets.fromLTRB(16, 12, 8, 10),
child: Row(
children: [
const Icon(
Icons.video_file_outlined,
color: Colors.white70,
size: 18,
),
const SizedBox(width: 8),
Text(
'切换版本',
style: TextStyle(
color: Colors.white,
fontSize: isAndroid ? 14 : 15,
fontWeight: FontWeight.w600,
),
),
if (showAllVersionsButton) ...[
const SizedBox(width: 10),
forui.FButton(
variant: forui.FButtonVariant.secondary,
size: forui.FButtonSizeVariant.xs,
mainAxisSize: MainAxisSize.min,
onPress: () =>
showAllVersionsSheet(context: context, groups: allGroups),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text('全部 $sourceCount 个版本'),
const SizedBox(width: 2),
const Icon(Icons.chevron_right_rounded, size: 16),
],
),
),
],
const SizedBox(width: 16),
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
for (var i = 0; i < buckets.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
VersionFilterChip(
label: buckets[i],
selected: selected == buckets[i],
showDot: true,
onTap: () => setState(() => _selectedBucket = buckets[i]),
),
],
],
),
),
),
const SizedBox(width: 8),
IconButton(
icon: const Icon(Icons.close, color: Colors.white54, size: 18),
onPressed: widget.onClose,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 32, minHeight: 32),
),
],
),
);
}
}