Files
sm-emby-share/lib/features/player/widgets/android/android_gesture_feedback.dart
T

255 lines
7.2 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'package:flutter/material.dart';
import '../../../../shared/utils/format_utils.dart';
import '../player_hold_speed_prompt.dart';
enum GestureFeedbackKind {
none,
brightness,
volume,
seek,
doubleTapSeek,
longPressSpeed,
}
class GestureFeedbackData {
final GestureFeedbackKind kind;
final double value;
final Duration seekTarget;
final Duration totalDuration;
final bool seekIsForward;
final int doubleTapSeconds;
final bool doubleTapIsLeft;
final double speedRate;
const GestureFeedbackData({
this.kind = GestureFeedbackKind.none,
this.value = 0,
this.seekTarget = Duration.zero,
this.totalDuration = Duration.zero,
this.seekIsForward = true,
this.doubleTapSeconds = 0,
this.doubleTapIsLeft = true,
this.speedRate = 1.0,
});
const GestureFeedbackData.none() : this();
}
class AndroidGestureFeedback extends StatelessWidget {
final GestureFeedbackData data;
const AndroidGestureFeedback({super.key, required this.data});
@override
Widget build(BuildContext context) {
if (data.kind == GestureFeedbackKind.none) {
return const IgnorePointer(child: SizedBox.shrink());
}
return IgnorePointer(
child: LayoutBuilder(
builder: (context, constraints) {
final screenWidth = constraints.maxWidth;
final screenHeight = constraints.maxHeight;
return Stack(children: [_buildContent(screenWidth, screenHeight)]);
},
),
);
}
Widget _buildContent(double screenWidth, double screenHeight) {
switch (data.kind) {
case GestureFeedbackKind.brightness:
case GestureFeedbackKind.volume:
return _buildTopIndicator();
case GestureFeedbackKind.seek:
return _buildSeekPreview();
case GestureFeedbackKind.longPressSpeed:
return _buildLongPressSpeed();
case GestureFeedbackKind.doubleTapSeek:
return _buildDoubleTapSeek(screenWidth, screenHeight);
case GestureFeedbackKind.none:
return const SizedBox.shrink();
}
}
Widget _buildTopIndicator() {
final isBrightness = data.kind == GestureFeedbackKind.brightness;
final icon = isBrightness
? Icons.brightness_6
: (data.value == 0
? Icons.volume_off
: (data.value < 0.5 ? Icons.volume_down : Icons.volume_up));
final clamped = data.value.clamp(0.0, 1.0);
return Positioned(
top: 64,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xE8181818),
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, color: Colors.white, size: 20),
const SizedBox(width: 10),
_buildHorizontalProgress(clamped),
const SizedBox(width: 10),
SizedBox(
width: 36,
child: Text(
'${(clamped * 100).round()}%',
textAlign: TextAlign.right,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()],
),
),
),
],
),
),
),
);
}
Widget _buildHorizontalProgress(double value) {
return SizedBox(
width: 120,
height: 4,
child: LinearProgressIndicator(
value: value,
color: Colors.white,
backgroundColor: Colors.white24,
borderRadius: BorderRadius.circular(2),
),
);
}
Widget _buildSeekPreview() {
final isForward = data.seekIsForward;
return Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
decoration: BoxDecoration(
color: const Color(0xE8181818),
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
isForward ? Icons.fast_forward : Icons.fast_rewind,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
formatDurationClock(
data.seekTarget < Duration.zero
? Duration.zero
: data.seekTarget,
),
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w500,
fontFeatures: [FontFeature.tabularFigures()],
),
),
const Text(
' / ',
style: TextStyle(color: Colors.white54, fontSize: 15),
),
Text(
formatDurationClock(data.totalDuration),
style: const TextStyle(
color: Colors.white54,
fontSize: 15,
fontFeatures: [FontFeature.tabularFigures()],
),
),
],
),
),
);
}
Widget _buildLongPressSpeed() {
return Positioned(
top: 64,
left: 0,
right: 0,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: holdSpeedPromptBackgroundColor,
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.fast_forward, color: Colors.white, size: 18),
const SizedBox(width: 6),
Text(
'${data.speedRate}x',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
Widget _buildDoubleTapSeek(double screenWidth, double screenHeight) {
return Positioned(
top: 0,
bottom: 0,
left: data.doubleTapIsLeft ? screenWidth * 0.15 : null,
right: !data.doubleTapIsLeft ? screenWidth * 0.15 : null,
child: Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xE8181818),
borderRadius: BorderRadius.circular(999),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
data.doubleTapIsLeft ? Icons.fast_rewind : Icons.fast_forward,
color: Colors.white,
size: 20,
),
const SizedBox(width: 6),
Text(
'${data.doubleTapSeconds}s',
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
);
}
}