Skip to content

Commit 3ca9821

Browse files
feat: handle media file uploads states in previews widgets
1 parent 63f088a commit 3ca9821

File tree

2 files changed

+189
-59
lines changed

2 files changed

+189
-59
lines changed

lib/ui/chat/widgets/chat_input_media_preview.dart

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ import 'dart:io';
22

33
import 'package:flutter/material.dart';
44
import 'package:flutter_screenutil/flutter_screenutil.dart';
5+
import 'package:whitenoise/domain/models/media_file_upload.dart';
56
import 'package:whitenoise/ui/chat/widgets/media_thumbnail.dart';
67
import 'package:whitenoise/ui/core/themes/assets.dart';
78
import 'package:whitenoise/ui/core/themes/src/extensions.dart';
89
import 'package:whitenoise/ui/core/ui/wn_icon_button.dart';
10+
import 'package:whitenoise/ui/core/ui/wn_image.dart';
911

1012
class ChatInputMediaPreview extends StatefulWidget {
1113
const ChatInputMediaPreview({
1214
super.key,
13-
required this.imagePaths,
15+
required this.mediaItems,
1416
required this.onRemoveImage,
1517
required this.onAddMore,
1618
this.isReply = false,
1719
});
1820

19-
final List<String> imagePaths;
21+
final List<MediaFileUpload> mediaItems;
2022
final void Function(int index) onRemoveImage;
2123
final VoidCallback onAddMore;
2224
final bool isReply;
@@ -64,7 +66,7 @@ class _ChatInputMediaPreviewState extends State<ChatInputMediaPreview> {
6466

6567
@override
6668
Widget build(BuildContext context) {
67-
if (widget.imagePaths.isEmpty) return const SizedBox.shrink();
69+
if (widget.mediaItems.isEmpty) return const SizedBox.shrink();
6870

6971
return Container(
7072
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: widget.isReply ? 8.h : 16.h),
@@ -75,18 +77,73 @@ class _ChatInputMediaPreviewState extends State<ChatInputMediaPreview> {
7577
ListView.separated(
7678
controller: _scrollController,
7779
scrollDirection: Axis.horizontal,
78-
itemCount: widget.imagePaths.length,
80+
itemCount: widget.mediaItems.length,
7981
separatorBuilder: (context, index) => SizedBox(width: _imageSpacing.w),
8082
itemBuilder: (context, index) {
81-
final imagePath = widget.imagePaths[index];
82-
return ClipRRect(
83-
child: Image.file(
84-
File(imagePath),
85-
height: _imageHeight.h,
86-
width: _imageWidth.w,
87-
fit: BoxFit.cover,
88-
errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(),
89-
),
83+
final mediaItem = widget.mediaItems[index];
84+
return mediaItem.when(
85+
uploading:
86+
(filePath) => Stack(
87+
children: [
88+
ClipRRect(
89+
child: Image.file(
90+
File(filePath),
91+
height: _imageHeight.h,
92+
width: _imageWidth.w,
93+
fit: BoxFit.cover,
94+
),
95+
),
96+
Positioned.fill(
97+
child: Container(
98+
color: context.colors.solidNeutralBlack.withValues(alpha: 0.5),
99+
child: Center(
100+
child: SizedBox(
101+
width: 32,
102+
height: 32,
103+
child: CircularProgressIndicator(
104+
strokeWidth: 2,
105+
color: context.colors.solidNeutralWhite,
106+
),
107+
),
108+
),
109+
),
110+
),
111+
],
112+
),
113+
uploaded:
114+
(file, originalFilePath) => ClipRRect(
115+
child: Image.file(
116+
File(originalFilePath),
117+
height: _imageHeight.h,
118+
width: _imageWidth.w,
119+
fit: BoxFit.cover,
120+
),
121+
),
122+
failed:
123+
(filePath, error) => Stack(
124+
children: [
125+
ClipRRect(
126+
child: Image.file(
127+
File(filePath),
128+
height: _imageHeight.h,
129+
width: _imageWidth.w,
130+
fit: BoxFit.cover,
131+
),
132+
),
133+
Positioned.fill(
134+
child: Container(
135+
color: context.colors.solidNeutralBlack.withValues(alpha: 0.5),
136+
child: Center(
137+
child: WnImage(
138+
AssetsPaths.icErrorFilled,
139+
color: context.colors.destructive,
140+
size: 48.w,
141+
),
142+
),
143+
),
144+
),
145+
],
146+
),
90147
);
91148
},
92149
),
@@ -98,7 +155,7 @@ class _ChatInputMediaPreviewState extends State<ChatInputMediaPreview> {
98155
height: 32.h,
99156
child: ListView.separated(
100157
scrollDirection: Axis.horizontal,
101-
itemCount: widget.imagePaths.length + 1,
158+
itemCount: widget.mediaItems.length + 1,
102159
separatorBuilder: (context, index) => SizedBox(width: _thumbnailSpacing.w),
103160
itemBuilder: (context, index) {
104161
if (index == 0) {
@@ -112,12 +169,13 @@ class _ChatInputMediaPreviewState extends State<ChatInputMediaPreview> {
112169
iconColor: context.colors.primary,
113170
);
114171
}
115-
final imageIndex = index - 1;
116-
final imagePath = widget.imagePaths[imageIndex];
172+
final itemIndex = index - 1;
173+
final mediaItem = widget.mediaItems[itemIndex];
174+
117175
return MediaThumbnail(
118-
path: imagePath,
119-
isActive: _activeThumbIndex == imageIndex,
120-
onTap: () => _handleThumbnailTap(imageIndex),
176+
mediaItem: mediaItem,
177+
isActive: _activeThumbIndex == itemIndex,
178+
onTap: () => _handleThumbnailTap(itemIndex),
121179
);
122180
},
123181
),

lib/ui/chat/widgets/media_thumbnail.dart

Lines changed: 112 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,140 @@ import 'dart:io';
22

33
import 'package:flutter/material.dart';
44
import 'package:flutter_screenutil/flutter_screenutil.dart';
5+
import 'package:whitenoise/domain/models/media_file_upload.dart';
56
import 'package:whitenoise/ui/core/themes/assets.dart';
67
import 'package:whitenoise/ui/core/themes/src/extensions.dart';
78
import 'package:whitenoise/ui/core/ui/wn_image.dart';
89

910
class MediaThumbnail extends StatelessWidget {
1011
const MediaThumbnail({
1112
super.key,
12-
required this.path,
13+
required this.mediaItem,
1314
required this.isActive,
1415
required this.onTap,
1516
});
1617

17-
final String path;
18+
final MediaFileUpload mediaItem;
1819
final bool isActive;
1920
final VoidCallback onTap;
2021

2122
@override
2223
Widget build(BuildContext context) {
2324
return GestureDetector(
24-
onTap: onTap,
25-
child: Stack(
26-
children: [
27-
Container(
28-
decoration: BoxDecoration(
29-
border: Border.all(
30-
color: context.colors.mutedForeground,
31-
width: 1.w,
32-
),
25+
onTap: mediaItem.isUploading ? null : onTap,
26+
child: mediaItem.when(
27+
uploading:
28+
(filePath) => _buildThumbnail(
29+
context,
30+
filePath: filePath,
31+
overlay: _uploadingOverlay(context),
3332
),
34-
child: ClipRRect(
35-
child: Image.file(
36-
File(path),
37-
height: 32.h,
38-
width: 32.w,
39-
fit: BoxFit.cover,
40-
errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(),
41-
),
33+
uploaded:
34+
(file, originalFilePath) => _buildThumbnail(
35+
context,
36+
filePath: originalFilePath,
37+
overlay: isActive ? _uploadedOverlay(context) : null,
4238
),
39+
failed:
40+
(filePath, error) => _buildThumbnail(
41+
context,
42+
filePath: filePath,
43+
overlay: _failedOverlay(context),
44+
),
45+
),
46+
);
47+
}
48+
49+
Widget _buildThumbnail(
50+
BuildContext context, {
51+
required String filePath,
52+
Widget? overlay,
53+
}) {
54+
return Stack(
55+
children: [
56+
Container(
57+
decoration: BoxDecoration(
58+
border: Border.all(
59+
color: context.colors.mutedForeground,
60+
width: 1.w,
61+
),
62+
),
63+
child: ClipRRect(
64+
child: Image.file(
65+
File(filePath),
66+
height: 32.h,
67+
width: 32.w,
68+
fit: BoxFit.cover,
69+
errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(),
70+
),
71+
),
72+
),
73+
if (overlay != null) overlay,
74+
],
75+
);
76+
}
77+
78+
Widget _uploadedOverlay(BuildContext context) {
79+
return Positioned.fill(
80+
child: Center(
81+
child: Container(
82+
width: 32.w,
83+
height: 32.h,
84+
decoration: BoxDecoration(
85+
color: context.colors.solidNeutralBlack.withValues(alpha: 0.5),
4386
),
44-
if (isActive)
45-
Positioned.fill(
46-
child: Center(
47-
child: Container(
48-
width: 32.w,
49-
height: 32.h,
50-
decoration: BoxDecoration(
51-
color: context.colors.solidNeutralBlack.withValues(alpha: 0.5),
52-
),
53-
child: Center(
54-
child: SizedBox(
55-
width: 18.w,
56-
height: 18.h,
57-
child: WnImage(
58-
AssetsPaths.icTrashCan,
59-
color: context.colors.solidNeutralWhite,
60-
),
61-
),
62-
),
63-
),
87+
child: Center(
88+
child: SizedBox(
89+
width: 18.w,
90+
height: 18.h,
91+
child: WnImage(
92+
AssetsPaths.icTrashCan,
93+
color: context.colors.solidNeutralWhite,
6494
),
6595
),
66-
],
96+
),
97+
),
98+
),
99+
);
100+
}
101+
102+
Widget _uploadingOverlay(BuildContext context) {
103+
return Positioned.fill(
104+
child: Container(
105+
width: 32.w,
106+
height: 32.h,
107+
decoration: BoxDecoration(
108+
color: context.colors.solidNeutralBlack.withValues(alpha: 0.5),
109+
),
110+
child: Center(
111+
child: SizedBox(
112+
width: 18,
113+
height: 18,
114+
child: CircularProgressIndicator(
115+
strokeWidth: 2,
116+
color: context.colors.solidNeutralWhite,
117+
),
118+
),
119+
),
120+
),
121+
);
122+
}
123+
124+
Widget _failedOverlay(BuildContext context) {
125+
return Positioned.fill(
126+
child: Container(
127+
width: 32.w,
128+
height: 32.h,
129+
decoration: BoxDecoration(
130+
color: context.colors.solidNeutralBlack.withValues(alpha: 0.5),
131+
),
132+
child: Center(
133+
child: WnImage(
134+
AssetsPaths.icErrorFilled,
135+
color: context.colors.destructive,
136+
size: 14.w,
137+
),
138+
),
67139
),
68140
);
69141
}

0 commit comments

Comments
 (0)