mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
at main 170 lines 4.9 kB view raw
1import 'dart:io'; 2 3import 'package:flutter/material.dart'; 4 5import '../../domain/draft.dart'; 6 7/// Displays upload progress for a single media attachment. 8class UploadProgressTile extends StatelessWidget { 9 const UploadProgressTile({ 10 required this.filename, 11 required this.status, 12 this.thumbnailPath, 13 this.progress = 0.0, 14 this.onRetry, 15 this.onCancel, 16 super.key, 17 }); 18 19 /// Display filename for the media. 20 final String filename; 21 22 /// Local path for thumbnail preview. 23 final String? thumbnailPath; 24 25 /// Current upload status. 26 final DraftMediaStatus status; 27 28 /// Upload progress from 0.0 to 1.0. 29 final double progress; 30 31 /// Callback for retry action on failed uploads. 32 final VoidCallback? onRetry; 33 34 /// Callback for cancel action during uploads. 35 final VoidCallback? onCancel; 36 37 @override 38 Widget build(BuildContext context) { 39 final theme = Theme.of(context); 40 final colorScheme = theme.colorScheme; 41 42 return Padding( 43 padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), 44 child: Row( 45 children: [ 46 ClipRRect( 47 borderRadius: BorderRadius.circular(4), 48 child: SizedBox( 49 width: 48, 50 height: 48, 51 child: thumbnailPath != null 52 ? Image.file( 53 File(thumbnailPath!), 54 fit: BoxFit.cover, 55 errorBuilder: (_, _, _) => _PlaceholderThumbnail(colorScheme: colorScheme), 56 ) 57 : _PlaceholderThumbnail(colorScheme: colorScheme), 58 ), 59 ), 60 const SizedBox(width: 12), 61 Expanded( 62 child: Column( 63 crossAxisAlignment: CrossAxisAlignment.start, 64 mainAxisSize: MainAxisSize.min, 65 children: [ 66 Text( 67 filename, 68 style: theme.textTheme.bodyMedium, 69 maxLines: 1, 70 overflow: TextOverflow.ellipsis, 71 ), 72 const SizedBox(height: 4), 73 if (status == DraftMediaStatus.uploading) 74 LinearProgressIndicator( 75 value: progress, 76 backgroundColor: colorScheme.surfaceContainerHighest, 77 color: colorScheme.primary, 78 ) 79 else 80 _StatusLabel(status: status, colorScheme: colorScheme), 81 ], 82 ), 83 ), 84 const SizedBox(width: 8), 85 _StatusAction( 86 status: status, 87 colorScheme: colorScheme, 88 onRetry: onRetry, 89 onCancel: onCancel, 90 ), 91 ], 92 ), 93 ); 94 } 95} 96 97class _PlaceholderThumbnail extends StatelessWidget { 98 const _PlaceholderThumbnail({required this.colorScheme}); 99 100 final ColorScheme colorScheme; 101 102 @override 103 Widget build(BuildContext context) { 104 return Container( 105 color: colorScheme.surfaceContainerHighest, 106 child: Icon(Icons.image, color: colorScheme.onSurfaceVariant, size: 24), 107 ); 108 } 109} 110 111class _StatusLabel extends StatelessWidget { 112 const _StatusLabel({required this.status, required this.colorScheme}); 113 114 final DraftMediaStatus status; 115 final ColorScheme colorScheme; 116 117 @override 118 Widget build(BuildContext context) { 119 final (text, color) = switch (status) { 120 DraftMediaStatus.pending => ('Pending', colorScheme.outline), 121 DraftMediaStatus.uploading => ('Uploading', colorScheme.primary), 122 DraftMediaStatus.uploaded => ('Uploaded', Colors.green), 123 DraftMediaStatus.failed => ('Failed', colorScheme.error), 124 }; 125 126 return Text(text, style: Theme.of(context).textTheme.bodySmall?.copyWith(color: color)); 127 } 128} 129 130class _StatusAction extends StatelessWidget { 131 const _StatusAction({ 132 required this.status, 133 required this.colorScheme, 134 this.onRetry, 135 this.onCancel, 136 }); 137 138 final DraftMediaStatus status; 139 final ColorScheme colorScheme; 140 final VoidCallback? onRetry; 141 final VoidCallback? onCancel; 142 143 @override 144 Widget build(BuildContext context) { 145 return switch (status) { 146 DraftMediaStatus.pending => Icon( 147 Icons.hourglass_empty, 148 color: colorScheme.outline, 149 size: 20, 150 ), 151 DraftMediaStatus.uploading => IconButton( 152 onPressed: onCancel, 153 icon: Icon(Icons.close, color: colorScheme.outline), 154 iconSize: 20, 155 padding: EdgeInsets.zero, 156 constraints: const BoxConstraints(), 157 tooltip: 'Cancel upload', 158 ), 159 DraftMediaStatus.uploaded => const Icon(Icons.check_circle, color: Colors.green, size: 20), 160 DraftMediaStatus.failed => IconButton( 161 onPressed: onRetry, 162 icon: Icon(Icons.refresh, color: colorScheme.error), 163 iconSize: 20, 164 padding: EdgeInsets.zero, 165 constraints: const BoxConstraints(), 166 tooltip: 'Retry upload', 167 ), 168 }; 169 } 170}