Spaces:
Paused
Paused
| // lib/widgets/video_player/ui_components.dart | |
| import 'package:flutter/material.dart'; | |
| import 'package:tikslop/theme/colors.dart'; | |
| import 'package:tikslop/widgets/ai_content_disclaimer.dart'; | |
| import 'package:tikslop/config/config.dart'; | |
| /// Builds a placeholder widget for when video is not loaded | |
| Widget buildPlaceholder(String? initialThumbnailUrl) { | |
| // Use our new AI Content Disclaimer widget as the placeholder | |
| if (initialThumbnailUrl?.isEmpty ?? true) { | |
| // Set isInteractive to true as we generate content on-the-fly | |
| return const AiContentDisclaimer(isInteractive: true); | |
| } | |
| try { | |
| if (initialThumbnailUrl!.startsWith('data:image')) { | |
| final uri = Uri.parse(initialThumbnailUrl); | |
| final base64Data = uri.data?.contentAsBytes(); | |
| if (base64Data == null) { | |
| throw Exception('Invalid image data'); | |
| } | |
| return Image.memory( | |
| base64Data, | |
| fit: BoxFit.cover, | |
| errorBuilder: (_, __, ___) => buildErrorPlaceholder(), | |
| ); | |
| } | |
| return Image.network( | |
| initialThumbnailUrl, | |
| fit: BoxFit.cover, | |
| errorBuilder: (_, __, ___) => buildErrorPlaceholder(), | |
| ); | |
| } catch (e) { | |
| return buildErrorPlaceholder(); | |
| } | |
| } | |
| /// Builds an error placeholder for when image loading fails | |
| Widget buildErrorPlaceholder() { | |
| return const Center( | |
| child: Icon( | |
| Icons.broken_image, | |
| size: 64, | |
| color: TikSlopColors.onSurfaceVariant, | |
| ), | |
| ); | |
| } | |
| /// Builds a buffer status indicator widget | |
| Widget buildBufferStatus({ | |
| required bool showDuringLoading, | |
| required bool isLoading, | |
| required List<dynamic> clipBuffer, | |
| }) { | |
| final readyOrPlayingClips = clipBuffer.where((c) => c.isReady || c.isPlaying).length; | |
| final totalClips = clipBuffer.length; | |
| final bufferPercentage = (readyOrPlayingClips / totalClips * 100).round(); | |
| // since we are playing clips at a reduced speed, they each last "longer" | |
| // eg a video playing back at 0.5 speed will see its duration multiplied by 2 | |
| final actualDurationPerClip = Configuration.instance.actualClipPlaybackDuration; | |
| final remainingSeconds = readyOrPlayingClips * actualDurationPerClip.inSeconds; | |
| // Don't show 0% during initial loading | |
| if (!showDuringLoading && bufferPercentage == 0) { | |
| return const SizedBox.shrink(); | |
| } | |
| return Positioned( | |
| right: 16, | |
| top: 16, | |
| child: Container( | |
| padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), | |
| decoration: BoxDecoration( | |
| color: Colors.black.withOpacity(0.6), | |
| borderRadius: BorderRadius.circular(16), | |
| ), | |
| child: Row( | |
| mainAxisSize: MainAxisSize.min, | |
| children: [ | |
| Icon( | |
| _getBufferIcon(bufferPercentage), | |
| color: _getBufferStatusColor(bufferPercentage), | |
| size: 16, | |
| ), | |
| const SizedBox(width: 4), | |
| Text( | |
| isLoading | |
| ? 'Buffering $bufferPercentage%' | |
| : '$bufferPercentage% (${remainingSeconds}s)', | |
| style: const TextStyle( | |
| color: Colors.white, | |
| fontSize: 12, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ); | |
| } | |
| /// Get icon for buffer status based on percentage | |
| IconData _getBufferIcon(int percentage) { | |
| if (percentage >= 40) return Icons.network_wifi; | |
| if (percentage >= 30) return Icons.network_wifi_3_bar; | |
| if (percentage >= 20) return Icons.network_wifi_2_bar; | |
| return Icons.network_wifi_1_bar; | |
| } | |
| /// Get color for buffer status based on percentage | |
| Color _getBufferStatusColor(int percentage) { | |
| if (percentage >= 30) return Colors.green; | |
| if (percentage >= 20) return Colors.orange; | |
| return Colors.red; | |
| } | |
| /// Formats queue statistics for display | |
| String formatQueueStats(dynamic queueManager) { | |
| final stats = queueManager.getBufferStats(); | |
| final currentClipInfo = queueManager.currentClip != null | |
| ? '\nPlaying: ${queueManager.currentClip!.seed}' | |
| : ''; | |
| final nextClipInfo = stats['nextControllerReady'] == true | |
| ? '\nNext clip preloaded' | |
| : ''; | |
| return 'Queue: ${stats['readyClips']}/${stats['bufferSize']} ready\n' | |
| 'Gen: ${stats['activeGenerations']} active' | |
| '$currentClipInfo$nextClipInfo'; | |
| } | |
| /// Builds a play/pause button overlay | |
| Widget buildPlayPauseButton({ | |
| required bool isPlaying, | |
| required VoidCallback onTap, | |
| }) { | |
| return Positioned( | |
| left: 16, | |
| bottom: 16, | |
| child: GestureDetector( | |
| onTap: onTap, | |
| child: Container( | |
| padding: const EdgeInsets.all(8), | |
| decoration: BoxDecoration( | |
| color: Colors.black.withOpacity(0.6), | |
| borderRadius: BorderRadius.circular(24), | |
| ), | |
| child: Icon( | |
| isPlaying ? Icons.pause : Icons.play_arrow, | |
| color: Colors.white, | |
| size: 24, | |
| ), | |
| ), | |
| ), | |
| ); | |
| } |