Skip to content

Commit de50c6d

Browse files
mikimiki-totefu
authored andcommitted
bruig: show progress of file upload
1 parent 7f07e7b commit de50c6d

File tree

14 files changed

+325
-24
lines changed

14 files changed

+325
-24
lines changed

bruig/flutterui/bruig/lib/components/attach_file.dart

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:bruig/components/empty_widget.dart';
99
import 'package:bruig/components/text.dart';
1010
import 'package:bruig/models/client.dart';
1111
import 'package:bruig/models/snackbar.dart';
12+
import 'package:bruig/models/uploads.dart';
1213
import 'package:bruig/screens/compress.dart';
1314
import 'package:bruig/screens/send_file.dart';
1415
import 'package:bruig/theme_manager.dart';
@@ -196,6 +197,7 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
196197

197198
void loadFile() async {
198199
var snackbar = SnackBarModel.of(context);
200+
var uploads = UploadsModel.of(context, listen: false);
199201
if (_debounce?.isActive ?? false) _debounce!.cancel();
200202
_debounce = Timer(const Duration(milliseconds: 500), () async {
201203
try {
@@ -209,7 +211,7 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
209211
filePath = filePath.trim();
210212
if (filePath == "") return;
211213
await showSendFileScreen(context,
212-
chat: widget.chat, file: File(filePath));
214+
chat: widget.chat, file: File(filePath), uploads: uploads);
213215
widget.closeAttachScreen(); // File screen already does the sending.
214216
} catch (exception) {
215217
snackbar.error("Unable to attach file: $exception");
@@ -247,6 +249,7 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
247249
required BuildContext context,
248250
}) async {
249251
var snackbar = SnackBarModel.of(context);
252+
var uploads = UploadsModel.of(context, listen: false);
250253
try {
251254
var mimeType = lookupMimeType(filePath);
252255
if (mimeType == null) {
@@ -272,7 +275,7 @@ class _AttachFileScreenState extends State<AttachFileScreen> {
272275
// Compression was insufficient to reduce size. This needs to be sent
273276
// as a file.
274277
await showSendFileScreen(context,
275-
chat: widget.chat, file: File(filePath));
278+
chat: widget.chat, file: File(filePath), uploads: uploads);
276279
return;
277280
}
278281

bruig/flutterui/bruig/lib/components/chat/events.dart

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:bruig/components/text.dart';
66
import 'package:bruig/components/snackbars.dart';
77
import 'package:bruig/models/realtimechat.dart';
88
import 'package:bruig/models/uistate.dart';
9+
import 'package:bruig/models/uploads.dart';
910
import 'package:bruig/screens/feed.dart';
1011
import 'package:flutter/gestures.dart';
1112
import 'package:flutter/material.dart';
@@ -29,11 +30,13 @@ import 'package:file_icon/file_icon.dart';
2930
import 'package:bruig/components/interactive_avatar.dart';
3031
import 'package:bruig/components/user_context_menu.dart';
3132
import 'package:bruig/theme_manager.dart';
33+
import 'package:path/path.dart' as path;
3234

3335
class ServerEvent extends StatelessWidget {
3436
final Widget? child;
3537
final String? msg;
36-
const ServerEvent({this.child, this.msg, super.key});
38+
final SurfaceColor? bgColor;
39+
const ServerEvent({this.child, this.msg, this.bgColor, super.key});
3740

3841
@override
3942
Widget build(BuildContext context) {
@@ -42,7 +45,7 @@ class ServerEvent extends StatelessWidget {
4245
return Box(
4346
padding: const EdgeInsets.only(left: 41, top: 5, bottom: 5),
4447
margin: const EdgeInsets.all(5),
45-
color: SurfaceColor.surfaceContainer,
48+
color: bgColor ?? SurfaceColor.surfaceContainer,
4649
child: child ?? Txt.S(msg!));
4750
}
4851
}
@@ -1299,6 +1302,64 @@ class _RTDTInviteWState extends State<RTDTInviteW> {
12991302
}
13001303
}
13011304

1305+
class _FileUploadEvent extends StatefulWidget {
1306+
final FileUploadModel uploadModel;
1307+
const _FileUploadEvent(this.uploadModel);
1308+
1309+
@override
1310+
State<_FileUploadEvent> createState() => __FileUploadEventState();
1311+
}
1312+
1313+
class __FileUploadEventState extends State<_FileUploadEvent> {
1314+
FileUploadModel get uploadModel => widget.uploadModel;
1315+
1316+
void uploadUpdated() {
1317+
setState(() {});
1318+
}
1319+
1320+
@override
1321+
void initState() {
1322+
super.initState();
1323+
uploadModel.addListener(uploadUpdated);
1324+
}
1325+
1326+
@override
1327+
void didUpdateWidget(covariant _FileUploadEvent oldWidget) {
1328+
super.didUpdateWidget(oldWidget);
1329+
oldWidget.uploadModel.removeListener(uploadUpdated);
1330+
uploadModel.addListener(uploadUpdated);
1331+
}
1332+
1333+
@override
1334+
void dispose() {
1335+
uploadModel.removeListener(uploadUpdated);
1336+
super.dispose();
1337+
}
1338+
1339+
@override
1340+
Widget build(BuildContext context) {
1341+
Widget child;
1342+
SurfaceColor? bgColor;
1343+
if (uploadModel.error != null) {
1344+
bgColor = SurfaceColor.errorContainer;
1345+
child = Txt("Error during upload - ${uploadModel.error}");
1346+
} else if (uploadModel.sent) {
1347+
child = Txt.S("✓ Sent file ${uploadModel.filepath}");
1348+
} else {
1349+
child = Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
1350+
Txt.S("Sending file ${path.basename(uploadModel.filepath)}"),
1351+
const SizedBox(height: 5),
1352+
LinearProgressIndicator(value: uploadModel.progress)
1353+
]);
1354+
}
1355+
1356+
return ServerEvent(
1357+
bgColor: bgColor,
1358+
child: child,
1359+
);
1360+
}
1361+
}
1362+
13021363
class Event extends StatelessWidget {
13031364
final ChatEventModel event;
13041365
final ChatModel chat;
@@ -1431,6 +1492,10 @@ class Event extends StatelessWidget {
14311492
RealtimeChatModel.of(context, listen: false), chat);
14321493
}
14331494

1495+
if (event.event is FileUploadModel) {
1496+
return _FileUploadEvent(event.event as FileUploadModel);
1497+
}
1498+
14341499
return const Box(
14351500
color: SurfaceColor.errorContainer,
14361501
child: Text("Unknonwn chat event type"));

bruig/flutterui/bruig/lib/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:bruig/models/menus.dart';
1111
import 'package:bruig/models/payments.dart';
1212
import 'package:bruig/models/realtimechat.dart';
1313
import 'package:bruig/models/resources.dart';
14+
import 'package:bruig/models/uploads.dart';
1415
import 'package:bruig/models/wallet.dart';
1516
import 'package:bruig/models/shutdown.dart';
1617
import 'package:bruig/notification_service.dart';
@@ -190,6 +191,7 @@ Future<void> runMainApp(Config cfg) async {
190191
ChangeNotifierProvider.value(value: rtc.active),
191192
ChangeNotifierProvider.value(value: rtc.liveSessions),
192193
ChangeNotifierProvider(create: (c) => RealtimeChatRTTModel()),
194+
ChangeNotifierProvider(create: (c) => UploadsModel()),
193195
],
194196
child: App(cfg, globalLogModel, globalShutdownModel),
195197
));

bruig/flutterui/bruig/lib/models/menus.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import 'package:bruig/models/log.dart';
1313
import 'package:bruig/models/notifications.dart';
1414
import 'package:bruig/models/realtimechat.dart';
1515
import 'package:bruig/models/resources.dart';
16+
import 'package:bruig/models/uploads.dart';
1617
import 'package:bruig/screens/chat/new_gc_screen.dart';
1718
import 'package:bruig/screens/chat/new_message_screen.dart';
1819
import 'package:bruig/screens/chats.dart';
@@ -296,7 +297,9 @@ List<ChatMenuItem> buildUserChatMenu(ChatModel chat) {
296297
filePath = filePath.trim();
297298
if (filePath == "") return;
298299

299-
await showSendFileScreen(context, chat: chat, file: File(filePath));
300+
var uploads = UploadsModel.of(context, listen: false);
301+
await showSendFileScreen(context,
302+
chat: chat, file: File(filePath), uploads: uploads);
300303
}
301304

302305
void listUserPosts(BuildContext context, ClientModel client) async {
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import 'package:collection/collection.dart';
2+
import 'package:flutter/material.dart';
3+
import 'package:golib_plugin/definitions.dart';
4+
import 'package:golib_plugin/golib_plugin.dart';
5+
import 'package:provider/provider.dart';
6+
7+
class FileUploadModel extends ChatEvent with ChangeNotifier {
8+
final String uid;
9+
final String filepath;
10+
FileUploadModel({required this.uid, required this.filepath})
11+
: super(uid, 'Upload of $filepath');
12+
13+
int _sentChunks = 0;
14+
int get sentChunks => _sentChunks;
15+
16+
int _totalChunks = 0;
17+
int get totalChunks => _totalChunks;
18+
19+
double get progress =>
20+
_totalChunks == 0 ? 0 : _sentChunks.toDouble() / _totalChunks.toDouble();
21+
22+
String? _error;
23+
String? get error => _error;
24+
void _setError(String error) {
25+
if (_error != null) {
26+
_error = error;
27+
notifyListeners();
28+
}
29+
}
30+
31+
bool _sent = false;
32+
bool get sent => _sent;
33+
void _markSent() {
34+
if (!_sent) {
35+
_sent = true;
36+
notifyListeners();
37+
}
38+
}
39+
40+
void _updateProgress(SendProgress progress) {
41+
_sentChunks = progress.sent;
42+
_totalChunks = progress.total;
43+
_error ??= progress.error;
44+
notifyListeners();
45+
}
46+
}
47+
48+
class UploadsModel extends ChangeNotifier {
49+
static UploadsModel of(BuildContext context, {bool listen = false}) =>
50+
Provider.of<UploadsModel>(context, listen: listen);
51+
52+
UploadsModel() {
53+
_handleSendProgress();
54+
}
55+
56+
final List<FileUploadModel> _uploads = [];
57+
FileUploadModel? _findUploadByArgs(SendFileArgs args) {
58+
return _uploads.firstWhereOrNull(
59+
(fu) => fu.uid == args.uid && fu.filepath == args.filepath);
60+
}
61+
62+
FileUploadModel sendFile(String uid, String filepath) {
63+
var model = FileUploadModel(uid: uid, filepath: filepath);
64+
_uploads.add(model);
65+
(() async {
66+
try {
67+
await Golib.sendFile(uid, filepath);
68+
} catch (exception) {
69+
model._setError("$exception");
70+
} finally {
71+
model._markSent();
72+
}
73+
})();
74+
return model;
75+
}
76+
77+
void _handleSendProgress() async {
78+
await for (var update in Golib.sendFileProgress()) {
79+
var fu = _findUploadByArgs(update.args);
80+
if (fu == null) {
81+
continue;
82+
}
83+
84+
fu._updateProgress(update.progress);
85+
}
86+
}
87+
}

bruig/flutterui/bruig/lib/screens/send_file.dart

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import 'package:bruig/components/buttons.dart';
55
import 'package:bruig/components/snackbars.dart';
66
import 'package:bruig/components/text.dart';
77
import 'package:bruig/models/client.dart';
8+
import 'package:bruig/models/uploads.dart';
89
import 'package:bruig/screens/startupscreen.dart';
910
import 'package:bruig/util.dart';
1011
import 'package:flutter/material.dart';
@@ -23,6 +24,7 @@ Future<SendFileScreenResult?> showSendFileScreen(
2324
BuildContext context, {
2425
required ChatModel chat,
2526
required File file,
27+
required UploadsModel uploads,
2628
}) async {
2729
if (chat.isGC) {
2830
var mimeType = lookupMimeType(file.path) ?? "binary/octet-stream";
@@ -35,15 +37,16 @@ Future<SendFileScreenResult?> showSendFileScreen(
3537
useRootNavigator: true,
3638
context: context,
3739
builder: (context) {
38-
final Widget child = SendFileScreen(file, chat);
40+
final Widget child = SendFileScreen(file, chat, uploads);
3941
return Dialog.fullscreen(child: child);
4042
});
4143
}
4244

4345
class SendFileScreen extends StatefulWidget {
4446
final File file;
4547
final ChatModel chat;
46-
const SendFileScreen(this.file, this.chat, {super.key});
48+
final UploadsModel uploads;
49+
const SendFileScreen(this.file, this.chat, this.uploads, {super.key});
4750

4851
@override
4952
State<SendFileScreen> createState() => _SendFileScreenState();
@@ -55,6 +58,7 @@ class _SendFileScreenState extends State<SendFileScreen> {
5558
String filename = "";
5659
int fileSize = 0;
5760
bool sending = false;
61+
FileUploadModel? uploadModel;
5862

5963
void cancel() {
6064
Navigator.of(context).pop();
@@ -74,18 +78,12 @@ class _SendFileScreenState extends State<SendFileScreen> {
7478
}
7579

7680
Future<void> sendAsFileTransfer() async {
77-
var chatMsg =
78-
SynthChatEvent("Sending file \"$filename\" to user", SCE_sending);
79-
chat.append(ChatEventModel(chatMsg, null), false);
80-
81-
try {
82-
await Golib.sendFile(chat.id, file.absolute.path);
83-
chatMsg.state = SCE_sent;
84-
showSuccessSnackbar(this, "Sent file \"$filename\" to ${chat.nick}");
85-
} catch (exception) {
86-
chatMsg.error = Exception(exception);
87-
showErrorSnackbar(this, "Unable to send file: $exception");
88-
}
81+
var model = widget.uploads.sendFile(chat.id, file.absolute.path);
82+
chat.append(ChatEventModel(model, null), false);
83+
setState(() {
84+
uploadModel = model;
85+
model.addListener(uploadModelChanged);
86+
});
8987
}
9088

9189
void send() async {
@@ -112,6 +110,10 @@ class _SendFileScreenState extends State<SendFileScreen> {
112110
if (mounted) Navigator.pop(context);
113111
}
114112

113+
void uploadModelChanged() {
114+
setState(() {});
115+
}
116+
115117
@override
116118
void initState() {
117119
super.initState();
@@ -124,6 +126,12 @@ class _SendFileScreenState extends State<SendFileScreen> {
124126
})();
125127
}
126128

129+
@override
130+
void dispose() {
131+
uploadModel?.removeListener(uploadModelChanged);
132+
super.dispose();
133+
}
134+
127135
@override
128136
Widget build(BuildContext context) {
129137
return StartupScreen(hideAboutButton: true, [
@@ -133,6 +141,9 @@ class _SendFileScreenState extends State<SendFileScreen> {
133141
const SizedBox(height: 5),
134142
Txt.M("Size: ${humanReadableSize(fileSize)}"),
135143
// const Expanded(child: Empty()),
144+
if (uploadModel != null && uploadModel?.totalChunks != 0) ...[
145+
LinearProgressIndicator(value: uploadModel?.progress)
146+
],
136147
const SizedBox(height: 20),
137148
Wrap(spacing: 5, children: [
138149
ElevatedButton(onPressed: sending ? null : send, child: Text("Send")),

0 commit comments

Comments
 (0)