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, ), ), ], ), ), ), ); } }