Files
2026-07-14 11:11:36 +08:00

250 lines
7.7 KiB
Dart

import 'dart:ui' show ImageFilter;
import 'package:flutter/material.dart';
import 'package:forui/forui.dart';
import '../../../shared/widgets/app_loading_ring.dart';
import '../../../shared/widgets/smart_image.dart';
import '../../../shared/widgets/top_drag_area.dart';
import '../../../shared/widgets/window_controls.dart';
class PlayerLoadingBackdrop extends StatelessWidget {
const PlayerLoadingBackdrop({super.key, this.backdropUrl});
final String? backdropUrl;
@override
Widget build(BuildContext context) {
final url = backdropUrl;
if (url == null || url.isEmpty) {
return const Positioned.fill(child: ColoredBox(color: Colors.black));
}
return Positioned.fill(
child: Stack(
fit: StackFit.expand,
children: [
const ColoredBox(color: Colors.black),
RepaintBoundary(
child: Transform.scale(
scale: 1.08,
child: ImageFiltered(
imageFilter: ImageFilter.blur(sigmaX: 24, sigmaY: 24),
child: SmartImage(
url: url,
fit: BoxFit.cover,
borderRadius: 0,
fallbackIcon: null,
memCacheWidth: 480,
placeholder: const ColoredBox(color: Colors.black),
),
),
),
),
const ColoredBox(color: Colors.black54),
],
),
);
}
}
class PlayerLoadingIndicator extends StatelessWidget {
const PlayerLoadingIndicator({super.key});
@override
Widget build(BuildContext context) {
return const Center(child: AppLoadingRing(size: 44, color: Colors.white));
}
}
class PlayerStatusTopBar extends StatelessWidget {
const PlayerStatusTopBar({
super.key,
required this.title,
required this.onBack,
required this.topInset,
required this.leftInset,
required this.showWindowControls,
this.enableWindowDrag = false,
});
final String title;
final VoidCallback onBack;
final double topInset;
final double leftInset;
final bool showWindowControls;
final bool enableWindowDrag;
@override
Widget build(BuildContext context) {
final trimmed = title.trim();
final shown = trimmed.isEmpty ? '播放器' : trimmed;
final Widget bar = SafeArea(
bottom: false,
child: Row(
children: [
IconButton(
onPressed: onBack,
icon: const Icon(Icons.arrow_back, color: Colors.white),
tooltip: '返回',
),
const SizedBox(width: 4),
Expanded(
child: Text(
shown,
style: const TextStyle(color: Colors.white, fontSize: 15),
overflow: TextOverflow.ellipsis,
),
),
if (showWindowControls) const WindowControls.forDarkSurface(),
],
),
);
return Positioned(
top: topInset,
left: leftInset,
right: 12,
child: enableWindowDrag ? TopDragArea(child: bar) : bar,
);
}
}
class PlayerErrorCard extends StatelessWidget {
const PlayerErrorCard({
super.key,
required this.message,
required this.onBack,
required this.onRetry,
});
final String message;
final VoidCallback onBack;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: Material(
color: const Color(0xFF1C1F26),
borderRadius: BorderRadius.circular(16),
elevation: 8,
shadowColor: Colors.black,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 20, 12, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
width: 36,
height: 36,
decoration: BoxDecoration(
color: const Color(
0xFFFFB020,
).withValues(alpha: 0.14),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.error_outline,
color: Color(0xFFFFC46B),
size: 22,
),
),
const SizedBox(width: 14),
const Expanded(
child: Text(
'无法播放此媒体',
style: TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
),
IconButton(
onPressed: onBack,
icon: const Icon(Icons.close, color: Colors.white70),
tooltip: '关闭播放器',
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 12, 24, 0),
child: ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 140),
child: SingleChildScrollView(
child: SelectableText(
message,
style: const TextStyle(
color: Colors.white70,
fontSize: 13,
height: 1.45,
),
),
),
),
),
const FDivider(
style: .delta(
color: Colors.white12,
padding: .value(EdgeInsets.only(top: 20)),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton.icon(
onPressed: onBack,
icon: const Icon(Icons.arrow_back, size: 18),
label: const Text('返回详情'),
style: TextButton.styleFrom(
foregroundColor: Colors.white70,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
const SizedBox(width: 10),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh, size: 18),
label: const Text('重试'),
style: FilledButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(6),
),
),
),
],
),
),
],
),
),
),
),
);
}
}