From a70e56ba3fbff93cb1b8d26e279bb62429ea255e Mon Sep 17 00:00:00 2001 From: Pratik Canopas <109139581+cp-pratik-k@users.noreply.github.com> Date: Fri, 19 Apr 2024 14:38:19 +0530 Subject: [PATCH] Google drive api support (#31) * Process termination * Use google drive api V3 to terminate process * Add upload button in preview * Fix lint * Fix error builder in network image --- .DS_Store | Bin 6148 -> 8196 bytes .idea/libraries/Dart_Packages.xml | 8 ++ .idea/libraries/Flutter_Plugins.xml | 6 +- app/assets/locales/app_en.arb | 1 + app/ios/Podfile.lock | 2 +- app/ios/Runner/Base.lproj/Main.storyboard | 13 +- .../accounts/accounts_screen_view_model.dart | 11 +- .../components/settings_action_list.dart | 11 ++ .../ui/flow/home/components/hint_view.dart | 24 ++-- app/lib/ui/flow/home/home_screen.dart | 2 + .../ui/flow/home/home_screen_view_model.dart | 28 +++- .../components/image_preview_screen.dart | 6 +- .../network_image_preview.dart | 17 ++- .../network_image_preview_view_model.dart | 59 ++++---- ...work_image_preview_view_model.freezed.dart | 126 +++++------------- .../media_preview/components/top_bar.dart | 20 ++- .../media_preview/media_preview_screen.dart | 14 +- .../media_preview_view_model.dart | 4 + .../components/transfer_item.dart | 1 - app/pubspec.lock | 10 +- app/pubspec.yaml | 2 + data/.flutter-plugins-dependencies | 2 +- .../google_drive/google_drive_endpoint.dart | 98 ++++++++++++++ data/lib/apis/network/base_url.dart | 4 + data/lib/apis/network/client.dart | 73 ++++++++++ data/lib/apis/network/endpoint.dart | 37 +++++ .../interceptors/auth_interceptor.dart | 22 +++ data/lib/errors/app_error.dart | 26 +++- data/lib/models/app_process/app_process.dart | 3 + data/lib/models/media/media_extension.dart | 9 +- .../google_drive_process_repo.dart | 75 +++++++++-- data/lib/services/google_drive_service.dart | 69 ++++++---- data/lib/services/local_media_service.dart | 51 ++----- data/lib/storage/app_preferences.dart | 5 + data/pubspec.yaml | 3 +- 35 files changed, 587 insertions(+), 255 deletions(-) create mode 100644 data/lib/apis/google_drive/google_drive_endpoint.dart create mode 100644 data/lib/apis/network/base_url.dart create mode 100644 data/lib/apis/network/client.dart create mode 100644 data/lib/apis/network/endpoint.dart create mode 100644 data/lib/apis/network/interceptors/auth_interceptor.dart diff --git a/.DS_Store b/.DS_Store index dd497050836dc453f08ca68f0a410870135c4807..6d56fc50212028c2943cc11d83de6f9bde772748 100644 GIT binary patch literal 8196 zcmeHM&ubGw6n>MWWyg~ zfPsx+dLFwe#{8^JSWM0=Arj07ubwY$&}M5QQcq|Fv;tZIt$y4Xa)XD1=#vvVq<7Y3{Iv#a!&R9;QkG%c*>T=Xy-Phl0D9;9VElh>vur4&93r?gu7%wLyR1QDyE@eMF z;T-l{r%A_5W=BQ%h{a|O`M6Mp8rTsoF^erSidW^av%2}{lh7g(G3GEmB2qv00t-MK zY-mC2nnyRJ(rhtX!~bu-muUtDBU1 z&Twoyx7=ENEr*KmoMBXZ+cCiZn0a#ZA@-=a9F$^^$&5R+>fd`WVcU5y{hW# zPq!cMeBs|4C12HjdW5Px e#}P+=7~ + + + + + + @@ -1145,6 +1152,7 @@ + diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index da28e2f..36f37f9 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -15,20 +15,20 @@ + + + - - - diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index 205015b..c251037 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -40,6 +40,7 @@ "load_local_media_button_text": "Load local media", "theme_text": "Theme", + "notification_text": "Notification", "light_theme_text": "Light", "dark_theme_text": "Dark", "system_theme_text": "System", diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 4be63c7..c87fc7a 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -138,4 +138,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 -COCOAPODS: 1.14.3 +COCOAPODS: 1.15.2 diff --git a/app/ios/Runner/Base.lproj/Main.storyboard b/app/ios/Runner/Base.lproj/Main.storyboard index f3c2851..04bdd8d 100644 --- a/app/ios/Runner/Base.lproj/Main.storyboard +++ b/app/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/app/lib/ui/flow/accounts/accounts_screen_view_model.dart b/app/lib/ui/flow/accounts/accounts_screen_view_model.dart index 0227ad7..2b4b013 100644 --- a/app/lib/ui/flow/accounts/accounts_screen_view_model.dart +++ b/app/lib/ui/flow/accounts/accounts_screen_view_model.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:data/services/auth_service.dart'; import 'package:data/services/device_service.dart'; +import 'package:data/storage/app_preferences.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:google_sign_in/google_sign_in.dart'; @@ -12,6 +13,7 @@ final accountsStateNotifierProvider = (ref) => AccountsStateNotifier( ref.read(deviceServiceProvider), ref.read(authServiceProvider), + ref.read(AppPreferences.canTakeAutoBackUpInGoogleDrive.notifier), ), ); @@ -19,11 +21,11 @@ class AccountsStateNotifier extends StateNotifier { final DeviceService _deviceService; final AuthService _authService; StreamSubscription? _googleAccountSubscription; + StateController canTakeAutoBackUpInGoogleDrive; - AccountsStateNotifier( - this._deviceService, - this._authService, - ) : super(AccountsState(googleAccount: _authService.googleAccount)); + AccountsStateNotifier(this._deviceService, this._authService, + this.canTakeAutoBackUpInGoogleDrive) + : super(AccountsState(googleAccount: _authService.googleAccount)); Future init() async { _getAppVersion(); @@ -54,6 +56,7 @@ class AccountsStateNotifier extends StateNotifier { Future signOutWithGoogle() async { try { await _authService.signOutWithGoogle(); + canTakeAutoBackUpInGoogleDrive.state = false; } catch (e) { state = state.copyWith(error: e); } diff --git a/app/lib/ui/flow/accounts/components/settings_action_list.dart b/app/lib/ui/flow/accounts/components/settings_action_list.dart index a914518..eaca3b6 100644 --- a/app/lib/ui/flow/accounts/components/settings_action_list.dart +++ b/app/lib/ui/flow/accounts/components/settings_action_list.dart @@ -4,6 +4,7 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:style/buttons/buttons_list.dart'; import 'package:style/buttons/segmented_button.dart'; +import 'package:style/buttons/switch.dart'; class SettingsActionList extends ConsumerWidget { const SettingsActionList({super.key}); @@ -11,7 +12,17 @@ class SettingsActionList extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final isDarkMode = ref.watch(AppPreferences.isDarkMode); + final notifications = ref.watch(AppPreferences.notifications); return ActionList(buttons: [ + ActionListButton( + title: context.l10n.notification_text, + trailing: AppSwitch( + value: notifications, + onChanged: (value) { + ref.read(AppPreferences.notifications.notifier).state = value; + }, + ), + ), ActionListButton( title: context.l10n.theme_text, trailing: AppSegmentedButton( diff --git a/app/lib/ui/flow/home/components/hint_view.dart b/app/lib/ui/flow/home/components/hint_view.dart index 45b45c7..74bbed5 100644 --- a/app/lib/ui/flow/home/components/hint_view.dart +++ b/app/lib/ui/flow/home/components/hint_view.dart @@ -30,11 +30,11 @@ class HintView extends StatelessWidget { child: Column( children: [ Row( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Padding( - padding: const EdgeInsets.only(top: 16, left: 16), + padding: const EdgeInsets.only(top: 4, left: 16), child: Text( title, style: AppTextStyles.subtitle2.copyWith( @@ -43,14 +43,18 @@ class HintView extends StatelessWidget { ), ), ), - ActionButton( - backgroundColor: context.colorScheme.containerNormal, - size: 28, - onPressed: onClose, - icon: Icon( - CupertinoIcons.xmark, - color: context.colorScheme.textSecondary, - size: 18, + Padding( + padding: const EdgeInsets.all(8.0).copyWith(bottom: 4), + child: ActionButton( + backgroundColor: context.colorScheme.containerNormal, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + size: 28, + onPressed: onClose, + icon: Icon( + CupertinoIcons.xmark, + color: context.colorScheme.textSecondary, + size: 18, + ), ), ), ], diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart index fdf9d6b..a2af761 100644 --- a/app/lib/ui/flow/home/home_screen.dart +++ b/app/lib/ui/flow/home/home_screen.dart @@ -69,6 +69,7 @@ class _HomeScreenState extends ConsumerState { child: Padding( padding: const EdgeInsets.only(right: 8.0), child: ActionButton( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, size: 36, backgroundColor: context.colorScheme.containerNormal, onPressed: () { @@ -87,6 +88,7 @@ class _HomeScreenState extends ConsumerState { ActionButton( size: 36, backgroundColor: context.colorScheme.containerNormal, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, onPressed: () { AppRouter.accounts.push(context); }, diff --git a/app/lib/ui/flow/home/home_screen_view_model.dart b/app/lib/ui/flow/home/home_screen_view_model.dart index 8daa41f..9ba17aa 100644 --- a/app/lib/ui/flow/home/home_screen_view_model.dart +++ b/app/lib/ui/flow/home/home_screen_view_model.dart @@ -8,6 +8,7 @@ import 'package:data/repositories/google_drive_process_repo.dart'; import 'package:data/services/auth_service.dart'; import 'package:data/services/google_drive_service.dart'; import 'package:data/services/local_media_service.dart'; +import 'package:data/storage/app_preferences.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:google_sign_in/google_sign_in.dart'; @@ -19,16 +20,23 @@ part 'home_screen_view_model.freezed.dart'; final homeViewStateNotifier = StateNotifierProvider.autoDispose( (ref) { - return HomeViewStateNotifier( + final homeView = HomeViewStateNotifier( ref.read(localMediaServiceProvider), ref.read(googleDriveServiceProvider), ref.read(authServiceProvider), ref.read(googleDriveProcessRepoProvider), + ref.read(AppPreferences.canTakeAutoBackUpInGoogleDrive), ); + + ref.listen(AppPreferences.canTakeAutoBackUpInGoogleDrive, (previous, next) { + homeView.updateAutoBackUpStatus(next); + }); + return homeView; }); class HomeViewStateNotifier extends StateNotifier with HomeViewModelHelperMixin { + bool _autoBackUpStatus; final AuthService _authService; final GoogleDriveService _googleDriveService; final GoogleDriveProcessRepo _googleDriveProcessRepo; @@ -43,12 +51,26 @@ class HomeViewStateNotifier extends StateNotifier bool _isMaxLocalMediaLoaded = false; HomeViewStateNotifier(this._localMediaService, this._googleDriveService, - this._authService, this._googleDriveProcessRepo) + this._authService, this._googleDriveProcessRepo, this._autoBackUpStatus) : super(const HomeViewState()) { _listenUserGoogleAccount(); _googleDriveProcessRepo.setBackUpFolderId(_backUpFolderId); _googleDriveProcessRepo.addListener(_listenGoogleDriveProcess); _loadInitialMedia(); + _checkAutoBackUp(); + } + + void updateAutoBackUpStatus(bool status) { + _autoBackUpStatus = status; + _checkAutoBackUp(); + } + + void _checkAutoBackUp() { + if (_autoBackUpStatus) { + _googleDriveProcessRepo.uploadMediasInGoogleDrive( + medias: state.medias.valuesWhere((element) => element.isLocalStored), + ); + } } void _listenUserGoogleAccount() { @@ -115,7 +137,7 @@ class HomeViewStateNotifier extends StateNotifier _googleDriveProcessRepo.downloadQueue.isNotEmpty); } - void _loadInitialMedia() async { + Future _loadInitialMedia() async { state = state.copyWith(loading: true, error: null); final hasAccess = await _localMediaService.requestPermission(); state = state.copyWith(hasLocalMediaAccess: hasAccess, loading: false); diff --git a/app/lib/ui/flow/media_preview/components/image_preview_screen.dart b/app/lib/ui/flow/media_preview/components/image_preview_screen.dart index c66b552..9fef3a4 100644 --- a/app/lib/ui/flow/media_preview/components/image_preview_screen.dart +++ b/app/lib/ui/flow/media_preview/components/image_preview_screen.dart @@ -3,6 +3,7 @@ import 'package:cloud_gallery/components/app_page.dart'; import 'package:cloud_gallery/components/error_view.dart'; import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; import 'package:data/models/media/media.dart'; +import 'package:data/models/media/media_extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../../domain/extensions/widget_extensions.dart'; @@ -28,8 +29,9 @@ class _ImagePreviewScreenState extends ConsumerState { void initState() { if (!widget.media.sources.contains(AppMediaSource.local)) { notifier = ref.read(networkImagePreviewStateNotifierProvider.notifier); - runPostFrame(() { - notifier.loadImage(widget.media.id); + runPostFrame(() async { + await notifier.loadImageFromGoogleDrive( + id: widget.media.id, extension: widget.media.extension); }); } super.initState(); diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart index b794206..53389e5 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart @@ -1,9 +1,10 @@ -import 'dart:typed_data'; +import 'dart:io'; import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; import 'package:data/models/media/media.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:style/indicators/circular_progress_indicator.dart'; +import '../../../../../components/app_page.dart'; import '../../../../../components/error_view.dart'; import 'network_image_preview_view_model.dart'; @@ -18,11 +19,17 @@ class NetworkImagePreview extends ConsumerWidget { if (state.loading) { return Center(child: AppCircularProgressIndicator(value: state.progress)); - } else if (state.mediaBytes != null) { + } else if (state.filePath != null) { return Hero( tag: media, - child: Image.memory(Uint8List.fromList(state.mediaBytes!), - fit: BoxFit.fitWidth), + child: Image.file(File(state.filePath!), fit: BoxFit.fitWidth, + errorBuilder: (context, error, stackTrace) { + return AppPage( + body: ErrorView( + title: context.l10n.unable_to_load_media_error, + message: context.l10n.unable_to_load_media_message, + )); + }), ); } else if (state.error != null) { return ErrorView( @@ -30,6 +37,6 @@ class NetworkImagePreview extends ConsumerWidget { message: context.l10n.unable_to_load_media_message, ); } - return const Placeholder(); + return const SizedBox(); } } diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart index dbff95e..d22f556 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart @@ -1,13 +1,14 @@ import 'dart:async'; - -import 'package:data/models/media_content/media_content.dart'; +import 'dart:io'; import 'package:data/services/google_drive_service.dart'; +import 'package:dio/dio.dart' show CancelToken; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:path_provider/path_provider.dart'; part 'network_image_preview_view_model.freezed.dart'; -final networkImagePreviewStateNotifierProvider = StateNotifierProvider< +final networkImagePreviewStateNotifierProvider = StateNotifierProvider.autoDispose< NetworkImagePreviewStateNotifier, NetworkImagePreviewState>((ref) { return NetworkImagePreviewStateNotifier(ref.read(googleDriveServiceProvider)); }); @@ -15,40 +16,32 @@ final networkImagePreviewStateNotifierProvider = StateNotifierProvider< class NetworkImagePreviewStateNotifier extends StateNotifier { final GoogleDriveService _googleDriveServices; - late StreamSubscription _subscription; NetworkImagePreviewStateNotifier(this._googleDriveServices) : super(const NetworkImagePreviewState()); - Future loadImage(String mediaId) async { + File? tempFile; + CancelToken? cancelToken; + + Future loadImageFromGoogleDrive( + {required String id, required String extension}) async { try { state = state.copyWith(loading: true, error: null); - final mediaContent = await _googleDriveServices.fetchMediaBytes(mediaId); - final mediaByte = []; - final length = mediaContent.length ?? 0; - - _subscription = mediaContent.stream.listen( - (byteChunk) { - mediaByte.addAll(byteChunk); - state = state.copyWith( - progress: length <= 0 ? 0 : mediaByte.length / length); - }, - onDone: () { - state = state.copyWith( - mediaContent: mediaContent, - mediaBytes: mediaByte, - loading: false, - ); - _subscription.cancel(); - }, - onError: (error) { - state = state.copyWith( - error: error, - loading: false, - ); - _subscription.cancel(); + cancelToken = CancelToken(); + final dir = await getTemporaryDirectory(); + tempFile = File('${dir.path}/$id.$extension'); + await _googleDriveServices.downloadFromGoogleDrive( + id: id, + saveLocation: tempFile!.path, + cancelToken: cancelToken, + onProgress: (progress, total) { + state = state.copyWith(progress: total <= 0 ? 0 : progress / total); }, ); + state = state.copyWith( + loading: false, + filePath: tempFile?.path, + ); } catch (error) { state = state.copyWith( error: error, @@ -59,7 +52,8 @@ class NetworkImagePreviewStateNotifier @override void dispose() { - _subscription.cancel(); + tempFile?.deleteSync(); + cancelToken?.cancel(); super.dispose(); } } @@ -68,9 +62,8 @@ class NetworkImagePreviewStateNotifier class NetworkImagePreviewState with _$NetworkImagePreviewState { const factory NetworkImagePreviewState({ @Default(false) bool loading, - AppMediaContent? mediaContent, - List? mediaBytes, - @Default(0.0) double progress, + double? progress, + String? filePath, Object? error, }) = _NetworkImagePreviewState; } diff --git a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart index a1a47a9..c8479e5 100644 --- a/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart +++ b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart @@ -17,9 +17,8 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$NetworkImagePreviewState { bool get loading => throw _privateConstructorUsedError; - AppMediaContent? get mediaContent => throw _privateConstructorUsedError; - List? get mediaBytes => throw _privateConstructorUsedError; - double get progress => throw _privateConstructorUsedError; + double? get progress => throw _privateConstructorUsedError; + String? get filePath => throw _privateConstructorUsedError; Object? get error => throw _privateConstructorUsedError; @JsonKey(ignore: true) @@ -33,14 +32,7 @@ abstract class $NetworkImagePreviewStateCopyWith<$Res> { $Res Function(NetworkImagePreviewState) then) = _$NetworkImagePreviewStateCopyWithImpl<$Res, NetworkImagePreviewState>; @useResult - $Res call( - {bool loading, - AppMediaContent? mediaContent, - List? mediaBytes, - double progress, - Object? error}); - - $AppMediaContentCopyWith<$Res>? get mediaContent; + $Res call({bool loading, double? progress, String? filePath, Object? error}); } /// @nodoc @@ -58,9 +50,8 @@ class _$NetworkImagePreviewStateCopyWithImpl<$Res, @override $Res call({ Object? loading = null, - Object? mediaContent = freezed, - Object? mediaBytes = freezed, - Object? progress = null, + Object? progress = freezed, + Object? filePath = freezed, Object? error = freezed, }) { return _then(_value.copyWith( @@ -68,33 +59,17 @@ class _$NetworkImagePreviewStateCopyWithImpl<$Res, ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - mediaContent: freezed == mediaContent - ? _value.mediaContent - : mediaContent // ignore: cast_nullable_to_non_nullable - as AppMediaContent?, - mediaBytes: freezed == mediaBytes - ? _value.mediaBytes - : mediaBytes // ignore: cast_nullable_to_non_nullable - as List?, - progress: null == progress + progress: freezed == progress ? _value.progress : progress // ignore: cast_nullable_to_non_nullable - as double, + as double?, + filePath: freezed == filePath + ? _value.filePath + : filePath // ignore: cast_nullable_to_non_nullable + as String?, error: freezed == error ? _value.error : error, ) as $Val); } - - @override - @pragma('vm:prefer-inline') - $AppMediaContentCopyWith<$Res>? get mediaContent { - if (_value.mediaContent == null) { - return null; - } - - return $AppMediaContentCopyWith<$Res>(_value.mediaContent!, (value) { - return _then(_value.copyWith(mediaContent: value) as $Val); - }); - } } /// @nodoc @@ -106,15 +81,7 @@ abstract class _$$NetworkImagePreviewStateImplCopyWith<$Res> __$$NetworkImagePreviewStateImplCopyWithImpl<$Res>; @override @useResult - $Res call( - {bool loading, - AppMediaContent? mediaContent, - List? mediaBytes, - double progress, - Object? error}); - - @override - $AppMediaContentCopyWith<$Res>? get mediaContent; + $Res call({bool loading, double? progress, String? filePath, Object? error}); } /// @nodoc @@ -131,9 +98,8 @@ class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res> @override $Res call({ Object? loading = null, - Object? mediaContent = freezed, - Object? mediaBytes = freezed, - Object? progress = null, + Object? progress = freezed, + Object? filePath = freezed, Object? error = freezed, }) { return _then(_$NetworkImagePreviewStateImpl( @@ -141,18 +107,14 @@ class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res> ? _value.loading : loading // ignore: cast_nullable_to_non_nullable as bool, - mediaContent: freezed == mediaContent - ? _value.mediaContent - : mediaContent // ignore: cast_nullable_to_non_nullable - as AppMediaContent?, - mediaBytes: freezed == mediaBytes - ? _value._mediaBytes - : mediaBytes // ignore: cast_nullable_to_non_nullable - as List?, - progress: null == progress + progress: freezed == progress ? _value.progress : progress // ignore: cast_nullable_to_non_nullable - as double, + as double?, + filePath: freezed == filePath + ? _value.filePath + : filePath // ignore: cast_nullable_to_non_nullable + as String?, error: freezed == error ? _value.error : error, )); } @@ -162,37 +124,21 @@ class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res> class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { const _$NetworkImagePreviewStateImpl( - {this.loading = false, - this.mediaContent, - final List? mediaBytes, - this.progress = 0.0, - this.error}) - : _mediaBytes = mediaBytes; + {this.loading = false, this.progress, this.filePath, this.error}); @override @JsonKey() final bool loading; @override - final AppMediaContent? mediaContent; - final List? _mediaBytes; + final double? progress; @override - List? get mediaBytes { - final value = _mediaBytes; - if (value == null) return null; - if (_mediaBytes is EqualUnmodifiableListView) return _mediaBytes; - // ignore: implicit_dynamic_type - return EqualUnmodifiableListView(value); - } - - @override - @JsonKey() - final double progress; + final String? filePath; @override final Object? error; @override String toString() { - return 'NetworkImagePreviewState(loading: $loading, mediaContent: $mediaContent, mediaBytes: $mediaBytes, progress: $progress, error: $error)'; + return 'NetworkImagePreviewState(loading: $loading, progress: $progress, filePath: $filePath, error: $error)'; } @override @@ -201,22 +147,15 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { (other.runtimeType == runtimeType && other is _$NetworkImagePreviewStateImpl && (identical(other.loading, loading) || other.loading == loading) && - (identical(other.mediaContent, mediaContent) || - other.mediaContent == mediaContent) && - const DeepCollectionEquality() - .equals(other._mediaBytes, _mediaBytes) && (identical(other.progress, progress) || other.progress == progress) && + (identical(other.filePath, filePath) || + other.filePath == filePath) && const DeepCollectionEquality().equals(other.error, error)); } @override - int get hashCode => Object.hash( - runtimeType, - loading, - mediaContent, - const DeepCollectionEquality().hash(_mediaBytes), - progress, + int get hashCode => Object.hash(runtimeType, loading, progress, filePath, const DeepCollectionEquality().hash(error)); @JsonKey(ignore: true) @@ -230,19 +169,16 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState { abstract class _NetworkImagePreviewState implements NetworkImagePreviewState { const factory _NetworkImagePreviewState( {final bool loading, - final AppMediaContent? mediaContent, - final List? mediaBytes, - final double progress, + final double? progress, + final String? filePath, final Object? error}) = _$NetworkImagePreviewStateImpl; @override bool get loading; @override - AppMediaContent? get mediaContent; - @override - List? get mediaBytes; + double? get progress; @override - double get progress; + String? get filePath; @override Object? get error; @override diff --git a/app/lib/ui/flow/media_preview/components/top_bar.dart b/app/lib/ui/flow/media_preview/components/top_bar.dart index 908fe39..c4ddf76 100644 --- a/app/lib/ui/flow/media_preview/components/top_bar.dart +++ b/app/lib/ui/flow/media_preview/components/top_bar.dart @@ -50,18 +50,28 @@ class PreviewTopBar extends StatelessWidget { ), if(media.isGoogleDriveStored) ActionButton( + padding: const EdgeInsets.all(4), onPressed: () { notifier.downloadMediaFromGoogleDrive(media: media); }, - icon: Padding( - padding: const EdgeInsets.all(4.0), - child: Icon( - CupertinoIcons.cloud_download, + icon: Icon( + CupertinoIcons.cloud_download, + color: context.colorScheme.textSecondary, + size: 22, + ), + ), + if(media.isLocalStored) + ActionButton( + padding: const EdgeInsets.all(4), + onPressed: () { + notifier.uploadMediaInGoogleDrive(media: media); + }, + icon: Icon( + CupertinoIcons.cloud_upload, color: context.colorScheme.textSecondary, size: 22, ), ), - ), ActionButton( onPressed: () async { if (media.isCommonStored && media.driveMediaRefId != null) { diff --git a/app/lib/ui/flow/media_preview/media_preview_screen.dart b/app/lib/ui/flow/media_preview/media_preview_screen.dart index 5e70c34..a34973b 100644 --- a/app/lib/ui/flow/media_preview/media_preview_screen.dart +++ b/app/lib/ui/flow/media_preview/media_preview_screen.dart @@ -121,14 +121,16 @@ class _MediaPreviewState extends ConsumerState { Widget build(BuildContext context) { _observeError(); _updateVideoControllerOnMediaChange(); - final medias = ref.watch(_provider.select((state) => state.medias)); - final showActions = - ref.watch(_provider.select((state) => state.showActions)); + final ({List medias, bool showActions}) state = + ref.watch(_provider.select((state) => ( + medias: state.medias, + showActions: state.showActions, + ))); return DismissiblePage( backgroundColor: context.colorScheme.surface, onProgress: (progress) { - if (progress > 0 && showActions) { + if (progress > 0 && state.showActions) { notifier.toggleActionVisibility(); } }, @@ -146,9 +148,9 @@ class _MediaPreviewState extends ConsumerState { child: PageView.builder( onPageChanged: notifier.changeVisibleMediaIndex, controller: _pageController, - itemCount: medias.length, + itemCount: state.medias.length, itemBuilder: (context, index) => - _preview(context: context, media: medias[index]), + _preview(context: context, media: state.medias[index]), ), ), PreviewTopBar(provider: _provider), diff --git a/app/lib/ui/flow/media_preview/media_preview_view_model.dart b/app/lib/ui/flow/media_preview/media_preview_view_model.dart index efef08f..584b236 100644 --- a/app/lib/ui/flow/media_preview/media_preview_view_model.dart +++ b/app/lib/ui/flow/media_preview/media_preview_view_model.dart @@ -103,6 +103,10 @@ class MediaPreviewStateNotifier extends StateNotifier { _googleDriveProcessRepo.downloadMediasFromGoogleDrive(medias: [media]); } + Future uploadMediaInGoogleDrive({required AppMedia media})async { + _googleDriveProcessRepo.uploadMediasInGoogleDrive(medias: [media]); + } + void updateVideoPosition(Duration position) { if (state.videoPosition == position) return; state = state.copyWith(videoPosition: position); diff --git a/app/lib/ui/flow/media_transfer/components/transfer_item.dart b/app/lib/ui/flow/media_transfer/components/transfer_item.dart index 017336f..f6c45e9 100644 --- a/app/lib/ui/flow/media_transfer/components/transfer_item.dart +++ b/app/lib/ui/flow/media_transfer/components/transfer_item.dart @@ -96,7 +96,6 @@ class _ProcessItemState extends State { ], ), ), - if (widget.process.status.isWaiting) ActionButton( onPressed: widget.onCancelTap, icon: const Icon(CupertinoIcons.xmark), diff --git a/app/pubspec.lock b/app/pubspec.lock index 6ddf9e0..b29b141 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -272,6 +272,14 @@ packages: relative: true source: path version: "0.0.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + url: "https://pub.dev" + source: hosted + version: "5.4.3+1" extension_google_sign_in_as_googleapis_auth: dependency: transitive description: @@ -717,7 +725,7 @@ packages: source: hosted version: "1.0.1" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 3989ba8..bacd4c1 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -47,6 +47,8 @@ dependencies: # core firebase_core: ^2.24.2 collection: ^1.18.0 + path_provider: ^2.1.2 + dio: ^5.4.3+1 # storage shared_preferences: ^2.2.2 diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index 52c68b3..04e3d87 100644 --- a/data/.flutter-plugins-dependencies +++ b/data/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.3.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.21/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.2/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.2.1/","native_build":true,"dependencies":[]}],"macos":[{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.3.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.3.2/","native_build":false,"dependencies":["path_provider_linux"]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.3.2/","native_build":false,"dependencies":["path_provider_windows"]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.3+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.2.2/","dependencies":[]}]},"dependencyGraph":[{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2024-04-04 11:45:07.647168","version":"3.19.3"} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.3.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"google_sign_in_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.21/","native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.2/","native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0/","native_build":true,"dependencies":[]},{"name":"shared_preferences_android","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.2.1/","native_build":true,"dependencies":[]}],"macos":[{"name":"google_sign_in_ios","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.4/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":true,"dependencies":[]},{"name":"path_provider_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0/","native_build":true,"dependencies":[]},{"name":"shared_preferences_foundation","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.3.5/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":false,"dependencies":[]},{"name":"path_provider_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_linux","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.3.2/","native_build":false,"dependencies":["path_provider_linux"]}],"windows":[{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","native_build":false,"dependencies":[]},{"name":"path_provider_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/","native_build":false,"dependencies":[]},{"name":"shared_preferences_windows","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.3.2/","native_build":false,"dependencies":["path_provider_windows"]}],"web":[{"name":"google_sign_in_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.3+3/","dependencies":[]},{"name":"package_info_plus","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.2.2/","dependencies":[]}]},"dependencyGraph":[{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"package_info_plus","dependencies":[]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":[]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"photo_manager","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]}],"date_created":"2024-04-18 14:12:20.264184","version":"3.19.3"} \ No newline at end of file diff --git a/data/lib/apis/google_drive/google_drive_endpoint.dart b/data/lib/apis/google_drive/google_drive_endpoint.dart new file mode 100644 index 0000000..56f9f71 --- /dev/null +++ b/data/lib/apis/google_drive/google_drive_endpoint.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'package:data/apis/network/base_url.dart'; +import 'package:data/apis/network/endpoint.dart'; +import 'package:data/models/media_content/media_content.dart'; +import 'package:dio/dio.dart'; +import 'package:googleapis/drive/v3.dart' as drive; +import 'package:http_parser/http_parser.dart'; + +class UploadGoogleDriveFile extends Endpoint { + final drive.File request; + final AppMediaContent content; + final CancelToken? cancellationToken; + final void Function(int chunk, int length)? onProgress; + + const UploadGoogleDriveFile({ + required this.request, + required this.content, + this.cancellationToken, + this.onProgress, + }); + + @override + String get baseUrl => BaseURL.googleDriveUpload; + + @override + CancelToken? get cancelToken => cancellationToken; + + @override + HttpMethod get method => HttpMethod.post; + + @override + Map get headers => { + 'Content-Type': 'multipart/related', + 'Content-Length': content.length.toString(), + }; + + @override + Object? get data => FormData.fromMap( + { + 'metadata': MultipartFile.fromString( + json.encode(request.toJson()), + contentType: MediaType.parse("application/json; charset=UTF-8"), + ), + 'media': MultipartFile.fromStream( + () => content.stream, + content.length ?? 0, + ), + }, + ); + + @override + String get path => '/files'; + + @override + Map? get queryParameters => { + 'uploadType': 'multipart', + }; + + @override + void Function(int p1, int p2)? get onSendProgress => onProgress; +} + +class DownloadGoogleDriveFileContent extends DownloadEndpoint { + final String id; + + final void Function(int received, int total)? onProgress; + + final String saveLocation; + + final CancelToken? cancellationToken; + + const DownloadGoogleDriveFileContent({ + required this.id, + this.cancellationToken, + this.onProgress, + required this.saveLocation, + }); + + @override + String get baseUrl => BaseURL.googleDrive; + + @override + String get path => '/files/$id'; + + @override + Map? get queryParameters => { + 'alt': 'media', + }; + + @override + CancelToken? get cancelToken => cancellationToken; + + @override + void Function(int p1, int p2)? get onReceiveProgress => onProgress; + + @override + String? get storePath => saveLocation; +} diff --git a/data/lib/apis/network/base_url.dart b/data/lib/apis/network/base_url.dart new file mode 100644 index 0000000..72aafd4 --- /dev/null +++ b/data/lib/apis/network/base_url.dart @@ -0,0 +1,4 @@ +class BaseURL { + static const googleDriveUpload = 'https://www.googleapis.com/upload/drive/v3'; + static const googleDrive = 'https://www.googleapis.com/drive/v3'; +} \ No newline at end of file diff --git a/data/lib/apis/network/client.dart b/data/lib/apis/network/client.dart new file mode 100644 index 0000000..68f187b --- /dev/null +++ b/data/lib/apis/network/client.dart @@ -0,0 +1,73 @@ +import 'package:data/apis/network/interceptors/auth_interceptor.dart'; +import 'package:data/errors/app_error.dart'; +import 'package:data/services/auth_service.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'endpoint.dart'; + +final googleAuthenticatedDioProvider = Provider((ref) { + return Dio() + ..options.connectTimeout = const Duration(seconds: 60) + ..options.sendTimeout = const Duration(seconds: 60) + ..options.receiveTimeout = const Duration(seconds: 60) + ..interceptors.add( + GoogleDriveAuthInterceptor( + googleSignIn: ref.read(googleSignInProvider), + ), + ); +}); + +final rawDioProvider = Provider((ref) { + return Dio() + ..options.connectTimeout = const Duration(seconds: 60) + ..options.sendTimeout = const Duration(seconds: 60) + ..options.receiveTimeout = const Duration(seconds: 60); +}); + +extension DioExtensions on Dio { + Future> req(Endpoint endpoint) async { + try { + return await request( + endpoint.baseUrl + endpoint.path, + queryParameters: endpoint.queryParameters, + options: Options( + method: endpoint.method.name, + headers: endpoint.headers, + responseType: endpoint.responseType, + contentType: endpoint.contentType, + validateStatus: (status) => + status != null && status >= 200 && status < 300, + ), + data: endpoint.data, + cancelToken: endpoint.cancelToken, + onReceiveProgress: endpoint.onReceiveProgress, + onSendProgress: endpoint.onSendProgress, + ); + } catch (error) { + throw AppError.fromError(error); + } + } + + Future downloadReq(DownloadEndpoint endpoint) async { + try { + return await download( + endpoint.baseUrl + endpoint.path, + endpoint.storePath, + queryParameters: endpoint.queryParameters, + options: Options( + method: endpoint.method.name, + headers: endpoint.headers, + responseType: endpoint.responseType, + contentType: endpoint.contentType, + validateStatus: (status) => + status != null && status >= 200 && status < 300, + ), + data: endpoint.data, + cancelToken: endpoint.cancelToken, + onReceiveProgress: endpoint.onReceiveProgress, + ); + } catch (e) { + throw AppError.fromError(e); + } + } +} diff --git a/data/lib/apis/network/endpoint.dart b/data/lib/apis/network/endpoint.dart new file mode 100644 index 0000000..4748731 --- /dev/null +++ b/data/lib/apis/network/endpoint.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; + +enum HttpMethod { get, post, put, delete, patch } + +abstract class Endpoint { + const Endpoint(); + + HttpMethod get method => HttpMethod.get; + + String get path; + + String get baseUrl; + + Map? get queryParameters => null; + + Map get headers => const {}; + + Object? get data => null; + + String? get contentType => null; + + ResponseType get responseType => ResponseType.json; + + CancelToken? get cancelToken => null; + + void Function(int, int)? get onReceiveProgress => null; + + void Function(int, int)? get onSendProgress => null; +} + +abstract class DownloadEndpoint extends Endpoint { + const DownloadEndpoint(); + @override + ResponseType get responseType => ResponseType.stream; + + String? get storePath; +} diff --git a/data/lib/apis/network/interceptors/auth_interceptor.dart b/data/lib/apis/network/interceptors/auth_interceptor.dart new file mode 100644 index 0000000..b4501c6 --- /dev/null +++ b/data/lib/apis/network/interceptors/auth_interceptor.dart @@ -0,0 +1,22 @@ +import 'package:dio/dio.dart'; +import 'package:google_sign_in/google_sign_in.dart'; + +class GoogleDriveAuthInterceptor extends Interceptor { + final GoogleSignIn googleSignIn; + + GoogleDriveAuthInterceptor({ + required this.googleSignIn, + }); + + @override + Future onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + final authHeaders = await googleSignIn.currentUser?.authHeaders; + if (authHeaders != null) { + options.headers.addAll(authHeaders); + } + handler.next(options); + } +} diff --git a/data/lib/errors/app_error.dart b/data/lib/errors/app_error.dart index 46f196e..e418569 100644 --- a/data/lib/errors/app_error.dart +++ b/data/lib/errors/app_error.dart @@ -1,11 +1,11 @@ import 'dart:io'; - import 'package:data/errors/l10n_error_codes.dart'; +import 'package:dio/dio.dart' show DioException, DioExceptionType; class AppError implements Exception { final String? message; final String? l10nCode; - final String? statusCode; + final int? statusCode; const AppError({this.message, this.statusCode, this.l10nCode}); @@ -19,6 +19,14 @@ class AppError implements Exception { return error; } else if (error is SocketException) { return const NoConnectionError(); + } else if (error is DioException) { + if (error.type == DioExceptionType.cancel) { + return const RequestCancelledByUser(); + } + return SomethingWentWrongError( + message: error.message, + statusCode: error.response?.statusCode, + ); } else { return const SomethingWentWrongError(); } @@ -41,6 +49,13 @@ class UserGoogleSignInAccountNotFound extends AppError { "User google signed in account not found. Please sign in again"); } +class RequestCancelledByUser extends AppError { + const RequestCancelledByUser() + : super( + message: "Request cancelled.", + ); +} + class BackUpFolderNotFound extends AppError { const BackUpFolderNotFound() : super( @@ -48,8 +63,13 @@ class BackUpFolderNotFound extends AppError { message: "Back up folder not found"); } +class UnableToSaveFileInGallery extends AppError { + const UnableToSaveFileInGallery() + : super(message: "Unable to save file in gallery"); +} + class SomethingWentWrongError extends AppError { - const SomethingWentWrongError({String? message, String? statusCode}) + const SomethingWentWrongError({String? message, int? statusCode}) : super( l10nCode: AppErrorL10nCodes.somethingWentWrongError, message: message, diff --git a/data/lib/models/app_process/app_process.dart b/data/lib/models/app_process/app_process.dart index 8ae5c4f..c819bc4 100644 --- a/data/lib/models/app_process/app_process.dart +++ b/data/lib/models/app_process/app_process.dart @@ -10,6 +10,7 @@ enum AppProcessStatus { deleting, downloading, success, + terminated, failed; bool get isProcessing => @@ -22,6 +23,8 @@ enum AppProcessStatus { bool get isSuccess => this == AppProcessStatus.success; bool get isFailed => this == AppProcessStatus.failed; + + bool get isTerminated => this == AppProcessStatus.terminated; } @freezed diff --git a/data/lib/models/media/media_extension.dart b/data/lib/models/media/media_extension.dart index 8707149..1877b2b 100644 --- a/data/lib/models/media/media_extension.dart +++ b/data/lib/models/media/media_extension.dart @@ -31,7 +31,7 @@ extension AppMediaExtension on AppMedia { ); } - AppMedia mergeGoogleDriveMedia(AppMedia media){ + AppMedia mergeGoogleDriveMedia(AppMedia media) { return copyWith( thumbnailLink: media.thumbnailLink, driveMediaRefId: media.driveMediaRefId, @@ -46,6 +46,13 @@ extension AppMediaExtension on AppMedia { sources.contains(AppMediaSource.local) && sources.length == 1; bool get isCommonStored => sources.length > 1; + + String get extension { + if (mimeType?.trim().isNotEmpty ?? false) return mimeType!.split('/').last; + if (type.isVideo) return 'mp4'; + if (type.isImage) return 'jpg'; + return ''; + } } class ThumbNailParameter { diff --git a/data/lib/repositories/google_drive_process_repo.dart b/data/lib/repositories/google_drive_process_repo.dart index 3431d77..6ae9345 100644 --- a/data/lib/repositories/google_drive_process_repo.dart +++ b/data/lib/repositories/google_drive_process_repo.dart @@ -1,12 +1,15 @@ import 'dart:async'; +import 'dart:io'; import 'package:collection/collection.dart'; import 'package:data/extensions/iterable_extension.dart'; import 'package:data/models/app_process/app_process.dart'; import 'package:data/models/media/media_extension.dart'; import 'package:data/services/google_drive_service.dart'; import 'package:data/services/local_media_service.dart'; +import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; import '../errors/app_error.dart'; import '../models/media/media.dart'; @@ -71,10 +74,19 @@ class GoogleDriveProcessRepo extends ChangeNotifier { _backUpFolderID ??= await _googleDriveService.getBackupFolderId(); + final cancelToken = CancelToken(); + final res = await _googleDriveService.uploadInGoogleDrive( folderID: _backUpFolderID!, media: process.media, - onProgress: (total, chunk) { + onProgress: (chunk, total) { + if (_uploadQueue + .firstWhereOrNull((element) => element.id == process.id) + ?.status + .isTerminated ?? + true) { + cancelToken.cancel(); + } _uploadQueue.updateWhere( where: (element) => element.id == process.id, update: (element) => element.copyWith( @@ -82,6 +94,7 @@ class GoogleDriveProcessRepo extends ChangeNotifier { ); notifyListeners(); }, + cancelToken: cancelToken, ); _uploadQueue.updateWhere( where: (element) => element.id == process.id, @@ -91,7 +104,9 @@ class GoogleDriveProcessRepo extends ChangeNotifier { ), ); } catch (error) { - if (error is BackUpFolderNotFound) { + if(error is RequestCancelledByUser){ + return; + } else if (error is BackUpFolderNotFound) { _backUpFolderID = await _googleDriveService.getBackupFolderId(); _uploadInGoogleDrive(process); return; @@ -163,6 +178,7 @@ class GoogleDriveProcessRepo extends ChangeNotifier { } Future _downloadFromGoogleDrive(AppProcess process) async { + String? tempFileLocation; try { _downloadQueue.updateWhere( where: (element) => element.id == process.id, @@ -171,39 +187,66 @@ class GoogleDriveProcessRepo extends ChangeNotifier { ); notifyListeners(); - final mediaContent = await _googleDriveService - .fetchMediaBytes(process.media.driveMediaRefId!); + final tempDir = await getTemporaryDirectory(); + tempFileLocation = + "${tempDir.path}/${process.media.id}.${process.media.extension}"; - final localMedia = await _localMediaService.saveMedia( - content: mediaContent, - onProgress: (total, chunk) { + final cancelToken = CancelToken(); + + await _googleDriveService.downloadFromGoogleDrive( + id: process.media.driveMediaRefId!, + saveLocation: tempFileLocation, + onProgress: (received, total) { + if (_downloadQueue + .firstWhereOrNull((element) => element.id == process.id) + ?.status + .isTerminated ?? + true) { + cancelToken.cancel(); + } _downloadQueue.updateWhere( where: (element) => element.id == process.id, update: (element) => element.copyWith( - progress: AppProcessProgress(total: total, chunk: chunk)), + progress: AppProcessProgress(total: total, chunk: received)), ); notifyListeners(); }, - mimeType: process.media.mimeType, + cancelToken: cancelToken, + ); + + final localMedia = await _localMediaService.saveInGallery( + saveFromLocation: tempFileLocation, type: process.media.type, ); + if (localMedia == null) { + throw const UnableToSaveFileInGallery(); + } + final updatedMedia = await _googleDriveService.updateMediaDescription( - process.media.id, localMedia?.path ?? ""); + process.media.id, + localMedia.id, + ); _downloadQueue.updateWhere( where: (element) => element.id == process.id, update: (element) => element.copyWith( status: AppProcessStatus.success, - response: localMedia?.mergeGoogleDriveMedia(updatedMedia)), + response: localMedia.mergeGoogleDriveMedia(updatedMedia)), ); } catch (error) { + if(error is RequestCancelledByUser){ + return; + } _downloadQueue.updateWhere( where: (element) => element.id == process.id, update: (element) => element.copyWith(status: AppProcessStatus.failed), ); } finally { notifyListeners(); + if (tempFileLocation != null) { + await File(tempFileLocation).delete(); + } } } @@ -215,7 +258,10 @@ class GoogleDriveProcessRepo extends ChangeNotifier { } void terminateUploadProcess(String id) { - _uploadQueue.removeWhere((element) => element.id == id); + _uploadQueue.updateWhere( + where: (element) => element.id == id, + update: (element) => + element.copyWith(status: AppProcessStatus.terminated)); notifyListeners(); } @@ -225,7 +271,10 @@ class GoogleDriveProcessRepo extends ChangeNotifier { } void terminateDownloadProcess(String id) { - _downloadQueue.removeWhere((element) => element.id == id); + _uploadQueue.updateWhere( + where: (element) => element.id == id, + update: (element) => + element.copyWith(status: AppProcessStatus.terminated)); notifyListeners(); } } diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart index d4fc2f7..fed602b 100644 --- a/data/lib/services/google_drive_service.dart +++ b/data/lib/services/google_drive_service.dart @@ -1,6 +1,10 @@ +import 'dart:async'; import 'dart:io'; +import 'package:data/apis/google_drive/google_drive_endpoint.dart'; +import 'package:data/apis/network/client.dart'; import 'package:data/models/media/media.dart'; import 'package:data/models/media_content/media_content.dart'; +import 'package:dio/dio.dart'; import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_sign_in/google_sign_in.dart'; @@ -9,15 +13,18 @@ import '../errors/app_error.dart'; import 'auth_service.dart'; final googleDriveServiceProvider = Provider( - (ref) => GoogleDriveService(ref.read(googleSignInProvider)), + (ref) => GoogleDriveService( + ref.read(googleSignInProvider), + ref.read(googleAuthenticatedDioProvider), + ), ); class GoogleDriveService { final String _backUpFolderName = "Cloud Gallery Backup"; - + final Dio _client; final GoogleSignIn _googleSignIn; - const GoogleDriveService(this._googleSignIn); + GoogleDriveService(this._googleSignIn, this._client); Future _getGoogleDriveAPI() async { if (_googleSignIn.currentUser == null) { @@ -96,41 +103,55 @@ class GoogleDriveService { Future uploadInGoogleDrive( {required String folderID, required AppMedia media, - void Function(int total, int chunk)? onProgress}) async { + CancelToken? cancelToken, + void Function(int chunk, int total)? onProgress}) async { final localFile = File(media.path); try { - final driveApi = await _getGoogleDriveAPI(); - final file = drive.File( name: media.name ?? localFile.path.split('/').last, + mimeType: media.mimeType, description: media.id, parents: [folderID], ); - final fileLength = localFile.lengthSync(); - int chunk = 0; - final googleDriveFile = await driveApi.files.create( - file, - uploadMedia: drive.Media( - localFile.openRead().map((event) { - chunk += event.length; - onProgress?.call(fileLength, chunk); - return event; - }), - fileLength), + + final res = await _client.req( + UploadGoogleDriveFile( + request: file, + content: AppMediaContent( + stream: localFile.openRead(), + length: localFile.lengthSync(), + contentType: 'application/octet-stream', + ), + onProgress: onProgress, + cancellationToken: cancelToken, + ), ); - return AppMedia.fromGoogleDriveFile(googleDriveFile); + + return AppMedia.fromGoogleDriveFile(drive.File.fromJson(res.data)); } catch (error) { - if (error is drive.DetailedApiRequestError && error.status == 404) { + if (error is AppError && error.statusCode == 404) { throw const BackUpFolderNotFound(); } throw AppError.fromError(error); } } - Future fetchMediaBytes(String mediaId) async { - final api = await _getGoogleDriveAPI(); - final media = await api.files.get(mediaId, - downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media; - return AppMediaContent.fromGoogleDrive(media); + Future downloadFromGoogleDrive( + {required String id, + required String saveLocation, + void Function(int chunk, int total)? onProgress, + CancelToken? cancelToken}) async { + try { + await _client.downloadReq( + DownloadGoogleDriveFileContent( + id: id, + cancellationToken: cancelToken, + saveLocation: saveLocation, + onProgress: onProgress, + ), + ); + } catch (e) { + throw AppError.fromError(e); + } } } diff --git a/data/lib/services/local_media_service.dart b/data/lib/services/local_media_service.dart index 7a1dac0..d4d6a4e 100644 --- a/data/lib/services/local_media_service.dart +++ b/data/lib/services/local_media_service.dart @@ -2,9 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:data/models/media/media.dart'; -import 'package:data/models/media_content/media_content.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:photo_manager/photo_manager.dart'; import '../errors/app_error.dart'; @@ -15,9 +13,10 @@ final localMediaServiceProvider = Provider( class LocalMediaService { const LocalMediaService(); - - Future isLocalFileExist({required AppMediaType type, required String id}) async { - return await AssetEntity(id: id, typeInt: type.index, width: 0, height: 0).isLocallyAvailable(); + Future isLocalFileExist( + {required AppMediaType type, required String id}) async { + return await AssetEntity(id: id, typeInt: type.index, width: 0, height: 0) + .isLocallyAvailable(); } Future requestPermission() async { @@ -62,52 +61,28 @@ class LocalMediaService { } } - Future saveMedia({ + Future saveInGallery({ + required String saveFromLocation, required AppMediaType type, - required String? mimeType, - required AppMediaContent content, - required void Function(int total, int chunk) onProgress, }) async { + AssetEntity? asset; try { - final extension = mimeType?.trim().isNotEmpty ?? false - ? mimeType!.split('/').last - : type.isVideo - ? 'mp4' - : 'jpg'; - - AssetEntity? asset; - - final tempDir = await getTemporaryDirectory(); - final tempFile = File( - '${tempDir.path}${DateTime.now()}_gd_cloud_gallery_temp.$extension'); - await tempFile.create(); - - int chunkLength = 0; - - StreamSubscription> subscription = - content.stream.listen((chunk) { - chunkLength += chunk.length; - onProgress(content.length ?? 0, chunkLength); - tempFile.writeAsBytesSync(chunk, mode: FileMode.append); - }); - await subscription.asFuture(); - subscription.cancel(); - if (type.isVideo) { asset = await PhotoManager.editor.saveVideo( - tempFile, - title: "${DateTime.now()}_gd_cloud_gallery.$extension", + File(saveFromLocation), + title: saveFromLocation.split('/').last, ); } else if (type.isImage) { asset = await PhotoManager.editor.saveImageWithPath( - tempFile.path, - title: "${DateTime.now()}_gd_cloud_gallery.$extension", + saveFromLocation, + title: saveFromLocation.split('/').last, ); } - await tempFile.delete(); return asset != null ? AppMedia.fromAssetEntity(asset) : null; } catch (e) { throw AppError.fromError(e); } } } + + diff --git a/data/lib/storage/app_preferences.dart b/data/lib/storage/app_preferences.dart index 35c6b4d..0bb6818 100644 --- a/data/lib/storage/app_preferences.dart +++ b/data/lib/storage/app_preferences.dart @@ -12,6 +12,11 @@ class AppPreferences { defaultValue: null, ); + static StateProvider notifications = createPrefProvider( + prefKey: "show_notifications", + defaultValue: true, + ); + static StateProvider canTakeAutoBackUpInGoogleDrive = createPrefProvider( prefKey: "google_drive_auto_backup", diff --git a/data/pubspec.yaml b/data/pubspec.yaml index b429eb3..c7f0d99 100644 --- a/data/pubspec.yaml +++ b/data/pubspec.yaml @@ -12,7 +12,8 @@ dependencies: # services googleapis: ^12.0.0 - http: ^1.2.0 + http_parser: ^4.0.2 + dio: ^5.4.3+1 photo_manager: ^3.0.0-dev.5 package_info_plus: ^5.0.1 path_provider: ^2.1.2