Initial commit
This commit is contained in:
@@ -0,0 +1,249 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user