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

321 lines
9.4 KiB
Dart

import 'dart:io' show Platform;
import 'package:flutter/material.dart';
import '../utils/format_utils.dart';
import 'adaptive_modal.dart';
import 'app_loading_ring.dart';
import 'app_snack_bar.dart';
import 'version_filter.dart';
import 'version_grouping.dart';
import 'version_source_card.dart';
const int kCompactInlineVersionLimit = 6;
const int kDesktopInlineVersionLimit = 8;
int inlineVersionLimit() {
return Platform.isAndroid
? kCompactInlineVersionLimit
: kDesktopInlineVersionLimit;
}
Future<void> showAllVersionsSheet({
required BuildContext context,
required List<VersionSourceGroup> groups,
String title = '全部版本',
}) async {
final isSingleVersionPicker = groups.length == 1;
await showAdaptiveModal<void>(
context: context,
maxWidth: isSingleVersionPicker ? 520 : 720,
maxHeight: isSingleVersionPicker ? 300 : 640,
compactMaxHeightFactor: isSingleVersionPicker ? 0.55 : 0.92,
compactHeightFactor: isSingleVersionPicker ? null : 0.82,
builder: (modalContext) => AllVersionsSheet(groups: groups, title: title),
);
}
class AllVersionsSheet extends StatelessWidget {
final List<VersionSourceGroup> groups;
final String title;
const AllVersionsSheet({
super.key,
required this.groups,
this.title = '全部版本',
});
@override
Widget build(BuildContext context) {
final groupsByBucket = <String, List<VersionSourceGroup>>{};
for (final group in groups) {
final bucket = versionBucketOf(group.representative.resolutionLabel);
groupsByBucket.putIfAbsent(bucket, () => []).add(group);
}
final concreteSourceCount = groups.fold<int>(
0,
(count, group) => count + group.members.length,
);
return Material(
color: const Color(0xFF171A20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 8, 10),
child: Row(
children: [
const Icon(
Icons.video_library_outlined,
color: Colors.white70,
size: 20,
),
const SizedBox(width: 10),
Text(
title,
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: 8),
Text(
'$concreteSourceCount 个来源',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 12,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white60),
tooltip: '关闭',
),
],
),
),
Divider(height: 1, color: Colors.white.withValues(alpha: 0.08)),
Flexible(
fit: FlexFit.loose,
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 24),
children: [
for (final bucket in kVersionBucketOrder)
if (groupsByBucket[bucket] case final bucketGroups?) ...[
_BucketHeader(label: bucket, count: bucketGroups.length),
const SizedBox(height: 8),
for (
var index = 0;
index < bucketGroups.length;
index++
) ...[
_VersionGroupRow(group: bucketGroups[index]),
if (index < bucketGroups.length - 1)
const SizedBox(height: 10),
],
const SizedBox(height: 20),
],
],
),
),
],
),
);
}
}
class _BucketHeader extends StatelessWidget {
final String label;
final int count;
const _BucketHeader({required this.label, required this.count});
@override
Widget build(BuildContext context) {
return Row(
children: [
Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
const SizedBox(width: 8),
Text(
'$count 个版本',
style: TextStyle(
color: Colors.white.withValues(alpha: 0.5),
fontSize: 12,
),
),
],
);
}
}
class _VersionGroupRow extends StatelessWidget {
final VersionSourceGroup group;
const _VersionGroupRow({required this.group});
@override
Widget build(BuildContext context) {
final representative = group.representative;
final sizeBytes = representative.sizeBytes;
final bitrate = representative.bitrate;
final details = <String>[
if (sizeBytes != null && sizeBytes > 0) formatFileSize(sizeBytes),
if (bitrate != null && bitrate > 0)
'${(bitrate / 1e6).toStringAsFixed(2)}Mbps',
];
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.06),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
representative.topLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w700,
),
),
),
if (details.isNotEmpty)
Text(
details.join(' · '),
style: TextStyle(
color: Colors.white.withValues(alpha: 0.58),
fontSize: 12,
),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
for (final member in group.members)
_ServerSourceChip(data: member),
],
),
],
),
);
}
}
class _ServerSourceChip extends StatefulWidget {
final VersionSourceCardData data;
const _ServerSourceChip({required this.data});
@override
State<_ServerSourceChip> createState() => _ServerSourceChipState();
}
class _ServerSourceChipState extends State<_ServerSourceChip> {
bool _loading = false;
Future<void> _selectSource() async {
final data = widget.data;
if (_loading || data.isCurrent || data.selected || data.onTap == null) {
return;
}
setState(() => _loading = true);
try {
await data.onTap!();
if (mounted) Navigator.of(context).pop();
} catch (_) {
if (mounted) {
setState(() => _loading = false);
showAppSnackBar(context, '切换服务器失败', tone: AppSnackTone.error);
}
}
}
@override
Widget build(BuildContext context) {
final data = widget.data;
final active = data.isCurrent || data.selected;
return Tooltip(
message: active ? '${data.serverName}(当前选择)' : '切换到 ${data.serverName}',
child: InkWell(
onTap: active ? null : _selectSource,
borderRadius: BorderRadius.circular(999),
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
decoration: BoxDecoration(
color: active
? const Color(0xFF4F8DFF).withValues(alpha: 0.24)
: Colors.white.withValues(alpha: 0.07),
borderRadius: BorderRadius.circular(999),
border: Border.all(
color: active
? const Color(0xFF4F8DFF)
: Colors.white.withValues(alpha: 0.12),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_loading)
const AppLoadingRing(size: 12, color: Colors.white)
else
Icon(
active ? Icons.check_circle : Icons.dns_outlined,
size: 14,
color: active ? const Color(0xFF77A7FF) : Colors.white60,
),
const SizedBox(width: 6),
Text(
data.serverName,
style: TextStyle(
color: Colors.white.withValues(alpha: active ? 1 : 0.78),
fontSize: 12,
fontWeight: active ? FontWeight.w600 : FontWeight.w500,
),
),
if (data.isCurrent) ...[
const SizedBox(width: 6),
const Text(
'当前',
style: TextStyle(
color: Color(0xFF9FC0FF),
fontSize: 10,
fontWeight: FontWeight.w600,
),
),
],
],
),
),
),
);
}
}