Skip to content
This repository was archived by the owner on Sep 18, 2025. It is now read-only.

Commit 3117a21

Browse files
authored
Implement resumable upload (#117)
* Implement resumable upload for google drive * Implement resumable-upload * Resumable upload * Minor improvement * Fix dropbox resumable upload not working if file size is less then 256kb * Push ios build * Implement resumable-upload * Implement resumable-upload * Resolve conflict * Fix google drive permission error * Show log-out message * Minor change * Minor improvement
1 parent 8908a7b commit 3117a21

20 files changed

+903
-181
lines changed

app/assets/locales/app_en.arb

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"back_up_folder_not_found_error": "Back up folder not found!",
4141
"auth_session_expired_error": "Your session has expired. Please log in again to continue using the app.",
4242
"save_media_in_gallery_error": "There was an issue saving your media to the gallery. Please try again, we’ll have it sorted out shortly",
43+
"no_google_drive_access_error": "It seems we don’t have the required permissions to access your Google Drive. Please sign in again and grant the necessary permissions.",
4344

4445
"@_MEDIA_ACTIONS":{},
4546
"upload_to_google_drive_title": "Upload to Google Drive",
@@ -97,18 +98,21 @@
9798
"clear_cache_succeed_message": "Cache cleared successfully",
9899
"sign_in_with_google_drive_message": "Connect to unlock your Google Drive treasure chest.",
99100
"sign_in_with_dropbox_message": "Drop into your Dropbox cloud and make it yours.",
101+
"successfully_sign_out_from_google_drive": "You have successfully signed out of Google Drive.",
102+
"successfully_sign_out_from_dropbox": "You have successfully signed out of Dropbox",
100103

101104
"@_UPLOAD_ITEM":{},
102-
"upload_status_waiting": "Upload is in the queue",
103-
"upload_status_success": "Upload completed successfully",
105+
"upload_status_waiting": "Upload is in the queue.",
106+
"upload_status_success": "Upload completed successfully.",
104107
"upload_status_failed": "Upload failed. Please try again.",
105-
"upload_status_cancelled": "Upload was cancelled",
108+
"upload_status_cancelled": "Upload was cancelled.",
109+
"upload_status_paused": "Upload paused.",
106110

107111
"@_DOWNLOAD_ITEM":{},
108-
"download_status_waiting": "Download is in the queue",
109-
"download_status_success": "Download completed successfully",
112+
"download_status_waiting": "Download is in the queue.",
113+
"download_status_success": "Download completed successfully.",
110114
"download_status_failed": "Download failed. Please try again.",
111-
"download_status_cancelled": "Download was cancelled",
115+
"download_status_cancelled": "Download was cancelled.",
112116

113117
"@_TRANSFER":{},
114118
"transfer_screen_title": "Transfer",

app/lib/domain/extensions/app_error_extensions.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ extension AppErrorExtensions on Object {
2121
return context.l10n.auth_session_expired_error;
2222
case AppErrorL10nCodes.unableToSaveFileInGalleryError:
2323
return context.l10n.save_media_in_gallery_error;
24+
case AppErrorL10nCodes.noGoogleDriveAccessError:
25+
return context.l10n.no_google_drive_access_error;
2426
default:
2527
return context.l10n.something_went_wrong_error;
2628
}

app/lib/ui/flow/accounts/accounts_screen.dart

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,21 @@ class _AccountsScreenState extends ConsumerState<AccountsScreen>
141141
),
142142
),
143143
title: context.l10n.sign_out_title,
144-
onPressed: notifier.signOutWithGoogle,
144+
onPressed: () async {
145+
await notifier.signOutWithGoogle();
146+
if (context.mounted) {
147+
showSnackBar(
148+
context: context,
149+
text:
150+
context.l10n.successfully_sign_out_from_google_drive,
151+
icon: SvgPicture.asset(
152+
Assets.images.icGoogleDrive,
153+
height: 22,
154+
width: 22,
155+
),
156+
);
157+
}
158+
},
145159
),
146160
],
147161
backgroundColor: AppColors.googleDriveColor,
@@ -216,7 +230,20 @@ class _AccountsScreenState extends ConsumerState<AccountsScreen>
216230
),
217231
),
218232
title: context.l10n.sign_out_title,
219-
onPressed: notifier.signOutWithDropbox,
233+
onPressed: () async {
234+
await notifier.signOutWithDropbox();
235+
if (context.mounted) {
236+
showSnackBar(
237+
context: context,
238+
text: context.l10n.successfully_sign_out_from_dropbox,
239+
icon: SvgPicture.asset(
240+
Assets.images.icDropbox,
241+
height: 22,
242+
width: 22,
243+
),
244+
);
245+
}
246+
},
220247
),
221248
],
222249
backgroundColor: AppColors.dropBoxColor,

app/lib/ui/flow/media_transfer/components/transfer_item.dart

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,16 @@ class UploadProcessItem extends StatelessWidget {
1111
final UploadMediaProcess process;
1212
final void Function() onCancelTap;
1313
final void Function() onRemoveTap;
14+
final void Function() onPausedTap;
15+
final void Function() onResumeTap;
1416

1517
const UploadProcessItem({
1618
super.key,
1719
required this.process,
1820
required this.onCancelTap,
1921
required this.onRemoveTap,
22+
required this.onPausedTap,
23+
required this.onResumeTap,
2024
});
2125

2226
@override
@@ -77,7 +81,27 @@ class UploadProcessItem extends StatelessWidget {
7781
],
7882
),
7983
),
80-
if (process.status.isRunning || process.status.isWaiting)
84+
if (process.status.isPaused)
85+
ActionButton(
86+
onPressed: onResumeTap,
87+
icon: Icon(
88+
CupertinoIcons.play,
89+
color: context.colorScheme.textPrimary,
90+
size: 20,
91+
),
92+
),
93+
if (process.status.isRunning)
94+
ActionButton(
95+
onPressed: onPausedTap,
96+
icon: Icon(
97+
CupertinoIcons.pause,
98+
color: context.colorScheme.textPrimary,
99+
size: 20,
100+
),
101+
),
102+
if (process.status.isRunning ||
103+
process.status.isWaiting ||
104+
process.status.isPaused)
81105
ActionButton(
82106
onPressed: onCancelTap,
83107
icon: Icon(
@@ -86,6 +110,15 @@ class UploadProcessItem extends StatelessWidget {
86110
size: 20,
87111
),
88112
),
113+
if (process.status.isFailed)
114+
ActionButton(
115+
onPressed: onResumeTap,
116+
icon: Icon(
117+
CupertinoIcons.refresh,
118+
color: context.colorScheme.textSecondary,
119+
size: 20,
120+
),
121+
),
89122
if (process.status.isTerminated ||
90123
process.status.isFailed ||
91124
process.status.isCompleted)
@@ -105,6 +138,8 @@ class UploadProcessItem extends StatelessWidget {
105138
String _getUploadMessage(BuildContext context) {
106139
if (process.status.isWaiting) {
107140
return context.l10n.upload_status_waiting;
141+
} else if (process.status.isPaused) {
142+
return context.l10n.upload_status_paused;
108143
} else if (process.status.isFailed) {
109144
return context.l10n.upload_status_failed;
110145
} else if (process.status.isCompleted) {

app/lib/ui/flow/media_transfer/media_transfer_screen.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ class _MediaTransferScreenState extends ConsumerState<MediaTransferScreen> {
122122
itemBuilder: (context, index) => UploadProcessItem(
123123
key: ValueKey(uploadProcesses[index].id),
124124
process: uploadProcesses[index],
125+
onResumeTap: () {
126+
notifier.onResumeUploadProcess(uploadProcesses[index].id);
127+
},
128+
onPausedTap: () {
129+
notifier.onPauseUploadProcess(uploadProcesses[index].id);
130+
},
125131
onRemoveTap: () {
126132
notifier.onRemoveUploadProcess(uploadProcesses[index].id);
127133
},

app/lib/ui/flow/media_transfer/media_transfer_view_model.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ class MediaTransferStateNotifier extends StateNotifier<MediaTransferState> {
3838
_mediaProcessRepo.removeItemFromUploadQueue(id);
3939
}
4040

41+
void onPauseUploadProcess(String id) {
42+
_mediaProcessRepo.pauseUploadProcess(id);
43+
}
44+
45+
void onResumeUploadProcess(String id) {
46+
_mediaProcessRepo.resumeUploadProcess(id);
47+
}
48+
4149
void onRemoveDownloadProcess(String id) {
4250
_mediaProcessRepo.removeItemFromDownloadQueue(id);
4351
}

data/.flutter-plugins-dependencies

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

data/lib/apis/dropbox/dropbox_content_endpoints.dart

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ class DropboxUploadEndpoint extends Endpoint {
147147
}
148148
],
149149
}),
150-
'Content-Type': content.contentType,
150+
'Content-Type': content.type,
151151
'Content-Length': content.length,
152152
};
153153

@@ -161,6 +161,165 @@ class DropboxUploadEndpoint extends Endpoint {
161161
void Function(int p1, int p2)? get onSendProgress => onProgress;
162162
}
163163

164+
class DropboxStartUploadEndpoint extends Endpoint {
165+
final AppMediaContent content;
166+
final void Function(int chunk, int length)? onProgress;
167+
final CancelToken? cancellationToken;
168+
169+
const DropboxStartUploadEndpoint({
170+
this.cancellationToken,
171+
this.onProgress,
172+
required this.content,
173+
});
174+
175+
@override
176+
String get baseUrl => BaseURL.dropboxContentV2;
177+
178+
@override
179+
HttpMethod get method => HttpMethod.post;
180+
181+
@override
182+
String get path => '/files/upload_session/start';
183+
184+
@override
185+
Map<String, dynamic> get headers => {
186+
'Content-Type': content.type,
187+
};
188+
189+
@override
190+
Object? get data => content.stream;
191+
192+
@override
193+
CancelToken? get cancelToken => cancellationToken;
194+
195+
@override
196+
void Function(int p1, int p2)? get onSendProgress => onProgress;
197+
}
198+
199+
class DropboxAppendUploadEndpoint extends Endpoint {
200+
final String sessionId;
201+
final int offset;
202+
final AppMediaContent content;
203+
final void Function(int chunk, int length)? onProgress;
204+
final CancelToken? cancellationToken;
205+
206+
const DropboxAppendUploadEndpoint({
207+
required this.sessionId,
208+
required this.offset,
209+
this.cancellationToken,
210+
this.onProgress,
211+
required this.content,
212+
});
213+
214+
@override
215+
String get baseUrl => BaseURL.dropboxContentV2;
216+
217+
@override
218+
HttpMethod get method => HttpMethod.post;
219+
220+
@override
221+
String get path => '/files/upload_session/append_v2';
222+
223+
@override
224+
Map<String, dynamic> get headers => {
225+
'Dropbox-API-Arg': jsonEncode({
226+
'cursor': {
227+
'session_id': sessionId,
228+
'offset': offset,
229+
},
230+
}),
231+
'Content-Type': content.type,
232+
};
233+
234+
@override
235+
Object? get data => content.stream;
236+
237+
@override
238+
CancelToken? get cancelToken => cancellationToken;
239+
240+
@override
241+
void Function(int p1, int p2)? get onSendProgress => onProgress;
242+
}
243+
244+
class DropboxFinishUploadEndpoint extends Endpoint {
245+
final String? appPropertyTemplateId;
246+
final String filePath;
247+
final String? localRefId;
248+
final String mode;
249+
final bool autoRename;
250+
final bool mute;
251+
final bool strictConflict;
252+
253+
final String sessionId;
254+
final int offset;
255+
final AppMediaContent content;
256+
final void Function(int chunk, int length)? onProgress;
257+
final CancelToken? cancellationToken;
258+
259+
const DropboxFinishUploadEndpoint({
260+
this.appPropertyTemplateId,
261+
required this.filePath,
262+
this.mode = 'add',
263+
this.autoRename = true,
264+
this.mute = false,
265+
this.localRefId,
266+
this.strictConflict = false,
267+
this.cancellationToken,
268+
this.onProgress,
269+
required this.content,
270+
required this.sessionId,
271+
required this.offset,
272+
});
273+
274+
@override
275+
String get baseUrl => BaseURL.dropboxContentV2;
276+
277+
@override
278+
HttpMethod get method => HttpMethod.post;
279+
280+
@override
281+
String get path => '/files/upload_session/finish';
282+
283+
@override
284+
Map<String, dynamic> get headers => {
285+
'Dropbox-API-Arg': jsonEncode({
286+
"commit": {
287+
"autorename": autoRename,
288+
"mode": mode,
289+
"mute": mute,
290+
"path": filePath,
291+
"strict_conflict": strictConflict,
292+
if (appPropertyTemplateId != null && localRefId != null)
293+
'property_groups': [
294+
{
295+
"fields": [
296+
{
297+
"name": ProviderConstants.localRefIdKey,
298+
"value": localRefId ?? '',
299+
},
300+
],
301+
"template_id": appPropertyTemplateId,
302+
}
303+
],
304+
},
305+
"cursor": {
306+
"offset": offset,
307+
"session_id": sessionId,
308+
},
309+
}),
310+
'Content-Type': content.type,
311+
};
312+
313+
@override
314+
Object? get data => content.stream;
315+
316+
@override
317+
CancelToken? get cancelToken => cancellationToken;
318+
319+
@override
320+
void Function(int p1, int p2)? get onSendProgress => onProgress;
321+
}
322+
164323
class DropboxDownloadEndpoint extends DownloadEndpoint {
165324
final String filePath;
166325
final String? storagePath;

0 commit comments

Comments
 (0)