250 lines
7.7 KiB
Dart
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),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|