diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml
index 5371787..da28e2f 100644
--- a/.idea/libraries/Flutter_Plugins.xml
+++ b/.idea/libraries/Flutter_Plugins.xml
@@ -13,22 +13,22 @@
-
+
+
+
+
-
+
+
+
-
-
+
-
-
-
-
-
+
diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb
index 28f1489..81d8549 100644
--- a/app/assets/locales/app_en.arb
+++ b/app/assets/locales/app_en.arb
@@ -14,11 +14,22 @@
"common_yesterday": "Yesterday",
"common_tomorrow": "Tomorrow",
+ "common_upload":"Upload",
+ "common_download":"Download",
+ "common_delete": "Delete",
+ "common_delete_from_google_drive": "Delete from Google Drive",
+ "common_delete_from_device": "Delete from Device",
+ "common_cancel": "Cancel",
+
+
"no_internet_connection_error": "No internet connection! Please check your network and try again.",
"something_went_wrong_error": "Something went wrong! Please try again later.",
"user_google_sign_in_account_not_found_error": "You haven't signed in with Google account yet. Please sign in with Google account and try again.",
"back_up_folder_not_found_error": "Back up folder not found!",
- "unable_to_open_attachment_error": "Unable to open attachment!",
+
+ "unable_to_load_media_error": "Unable to load media!",
+
+ "unable_to_load_media_message": "Oops! It looks like we're having trouble loading the media right now. Please try again later.",
"on_board_description": "Effortlessly move, share, and organize your photos and videos in a breeze. Access all your clouds in one friendly place. Your moments, your way, simplified for you! 🚀",
@@ -41,6 +52,25 @@
"hint_google_sign_in_message": "Sign in with Google and effortlessly link your Google Drive to your Cloud Gallery. Enjoy quick access to all your awesome content in one spot",
"hint_google_auto_backup_message": "Enable Auto Back Up to Google Drive and never lose your precious memories. Your photos and videos will be automatically backed up to your Google Drive",
- "hint_action_auto_backup": "Enable Auto Back Up"
+ "hint_action_auto_backup": "Enable Auto Back Up",
+
+ "delete_media_from_device_confirmation_message": "Are you sure you want to delete this media? It will be permanently deleted from your device.",
+ "delete_media_from_google_drive_confirmation_message": "Are you sure you want to delete this media? It will be permanently deleted from your Google Drive.",
+
+ "waiting_in_queue_text": "Waiting in Queue...",
+ "waiting_in_download_queue_message": "Your video download is in queue. We appreciate your patience!",
+
+ "transfer_screen_title": "Transfer",
+
+ "empty_upload_title":"No Files Being Uploads",
+ "empty_upload_message": "No uploads are happening right now. If you have files to upload, go ahead and start uploading.",
+
+ "empty_download_title":"No Files Being Downloads",
+ "empty_download_message": "No downloads are happening right now. If you have files to download, go ahead and start downloading.",
+
+ "download_in_progress_text": "Download in progress",
+
+ "download_require_text": "Download required",
+ "download_require_message": "To watch the video, simply download it first. Tap the download button to begin downloading the video."
}
\ No newline at end of file
diff --git a/app/lib/components/action_sheet.dart b/app/lib/components/action_sheet.dart
index 872f817..4a0f359 100644
--- a/app/lib/components/action_sheet.dart
+++ b/app/lib/components/action_sheet.dart
@@ -22,7 +22,7 @@ class AppSheetAction extends StatelessWidget {
return OnTapScale(
onTap: onPressed,
child: SizedBox(
- height: 45,
+ height: 50,
width: double.infinity,
child: Row(
children: [
diff --git a/app/lib/components/app_dialog.dart b/app/lib/components/app_dialog.dart
new file mode 100644
index 0000000..c85af95
--- /dev/null
+++ b/app/lib/components/app_dialog.dart
@@ -0,0 +1,83 @@
+import 'dart:io';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/text/app_text_style.dart';
+
+class AppAlertAction {
+ final String title;
+ final bool isDestructiveAction;
+ final VoidCallback onPressed;
+
+ const AppAlertAction({
+ required this.title,
+ this.isDestructiveAction = false,
+ required this.onPressed,
+ });
+}
+
+Future showAppAlertDialog({
+ required BuildContext context,
+ required String title,
+ required String message,
+ required List actions,
+}) {
+ return showAdaptiveDialog(
+ context: context,
+ builder: (context) {
+ if (Platform.isIOS || Platform.isMacOS) {
+ return CupertinoAlertDialog(
+ title: Text(title,
+ style: AppTextStyles.body
+ .copyWith(color: context.colorScheme.textPrimary)),
+ content: Text(
+ message,
+ style: AppTextStyles.caption
+ .copyWith(color: context.colorScheme.textSecondary),
+ ),
+ actions: [
+ for (final action in actions)
+ CupertinoDialogAction(
+ onPressed: action.onPressed,
+ child: Text(
+ action.title,
+ style: AppTextStyles.button.copyWith(
+ color: action.isDestructiveAction
+ ? context.colorScheme.alert
+ : context.colorScheme.textPrimary),
+ ),
+ ),
+ ],
+ );
+ }
+
+ return AlertDialog(
+ backgroundColor: context.colorScheme.containerNormalOnSurface,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(22),
+ ),
+ title: Text(title,
+ style: AppTextStyles.body
+ .copyWith(color: context.colorScheme.textPrimary)),
+ content: Text(
+ message,
+ style: AppTextStyles.caption
+ .copyWith(color: context.colorScheme.textSecondary),
+ ),
+ actions: [
+ for (final action in actions)
+ TextButton(
+ onPressed: action.onPressed,
+ child: Text(
+ action.title,
+ style: AppTextStyles.button.copyWith(
+ color: action.isDestructiveAction
+ ? context.colorScheme.alert
+ : context.colorScheme.textPrimary),
+ ),
+ ),
+ ],
+ );
+ },
+ );
+}
diff --git a/app/lib/components/app_page.dart b/app/lib/components/app_page.dart
index e91d72f..0c0c643 100644
--- a/app/lib/components/app_page.dart
+++ b/app/lib/components/app_page.dart
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
+import 'package:style/extensions/context_extensions.dart';
class AppPage extends StatelessWidget {
final String? title;
@@ -145,11 +146,16 @@ class AdaptiveAppBar extends StatelessWidget {
children: actions!,
),
)
- : AppBar(
- leading: leading,
- actions: actions,
- automaticallyImplyLeading: automaticallyImplyLeading,
- title: Text(text),
- );
+ : Column(
+ children: [
+ AppBar(
+ backgroundColor: context.colorScheme.barColor,
+ leading: leading,
+ actions: actions,
+ automaticallyImplyLeading: automaticallyImplyLeading,
+ title: Text(text),
+ ),
+ ],
+ );
}
}
diff --git a/app/lib/components/error_view.dart b/app/lib/components/error_view.dart
new file mode 100644
index 0000000..b6e3609
--- /dev/null
+++ b/app/lib/components/error_view.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/cupertino.dart';
+import 'package:style/buttons/primary_button.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/text/app_text_style.dart';
+
+class ErrorViewAction {
+ final String title;
+ final VoidCallback onPressed;
+
+ const ErrorViewAction({required this.title, required this.onPressed});
+}
+
+class ErrorView extends StatelessWidget {
+ final Widget? icon;
+ final String title;
+ final String message;
+ final Color? foregroundColor;
+ final ErrorViewAction? action;
+
+ const ErrorView({
+ super.key,
+ this.icon,
+ required this.title,
+ this.foregroundColor,
+ this.message = '',
+ this.action,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return SafeArea(
+ child: Padding(
+ padding: const EdgeInsets.all(30),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const SizedBox(width: double.infinity),
+ icon ??
+ Icon(
+ CupertinoIcons.exclamationmark_circle,
+ color: context.colorScheme.containerHighOnSurface,
+ size: 100,
+ ),
+ const SizedBox(height: 40),
+ Text(title,
+ style: AppTextStyles.subtitle1.copyWith(
+ color: foregroundColor?? context.colorScheme.textPrimary,
+ )),
+ const SizedBox(height: 20),
+ Text(
+ message,
+ style: AppTextStyles.subtitle2.copyWith(
+ color: foregroundColor ??context.colorScheme.textSecondary,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ if (action != null) ...[
+ const SizedBox(height: 20),
+ PrimaryButton(
+ onPressed: action!.onPressed,
+ child: Text(action!.title, style: AppTextStyles.button,),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/app/lib/domain/extensions/media_list_extension.dart b/app/lib/domain/extensions/media_list_extension.dart
new file mode 100644
index 0000000..49900ce
--- /dev/null
+++ b/app/lib/domain/extensions/media_list_extension.dart
@@ -0,0 +1,56 @@
+import 'package:data/extensions/iterable_extension.dart';
+import 'package:data/models/app_process/app_process.dart';
+import 'package:data/models/media/media.dart';
+import 'package:data/models/media/media_extension.dart';
+
+extension MediaListExtension on List {
+ void removeGoogleDriveRefFromMedias({List? removeFromIds}) {
+ for (int index = 0; index < length; index++) {
+ if (this[index].isGoogleDriveStored &&
+ (removeFromIds?.contains(this[index].id) ?? true)) {
+ removeAt(index);
+ } else if (this[index].isCommonStored &&
+ (removeFromIds?.contains(this[index].id) ?? true)) {
+ this[index] = this[index].copyWith(
+ sources: this[index].sources.toList()
+ ..remove(AppMediaSource.googleDrive),
+ thumbnailLink: null,
+ driveMediaRefId: null,
+ );
+ }
+ }
+ }
+
+ void addGoogleDriveRefInMedias(
+ {required List process, List? processIds}) {
+ processIds ??= process.map((e) => e.id).toList();
+ updateWhere(
+ where: (media) => processIds?.contains(media.id) ?? false,
+ update: (media) {
+ final res = process
+ .where((element) => element.id == media.id)
+ .first
+ .response as AppMedia?;
+ if (res == null) return media;
+ return media.mergeGoogleDriveMedia(res);
+ },
+ );
+ }
+
+ void replaceMediaRefInMedias(
+ {required List process, List? processIds}) {
+ processIds ??= process.map((e) => e.id).toList();
+ updateWhere(
+ where: (media) => processIds?.contains(media.id) ?? false,
+ update: (media) {
+ final res = process
+ .where((element) => element.id == media.id)
+ .first
+ .response as AppMedia?;
+
+ if (res == null) return media;
+ return res;
+ },
+ );
+ }
+}
diff --git a/app/lib/domain/formatter/byte_formatter.dart b/app/lib/domain/formatter/byte_formatter.dart
new file mode 100644
index 0000000..4b73b51
--- /dev/null
+++ b/app/lib/domain/formatter/byte_formatter.dart
@@ -0,0 +1,11 @@
+import 'dart:math';
+
+extension BytesFormatter on int {
+ String get formatBytes {
+ const List suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
+ if (this == 0) return '0 ${suffixes[0]}';
+ final i = (this == 0) ? 0 : (log(this) / log(1024)).floor();
+ final formattedValue = (this / pow(1024, i)).toStringAsFixed(2);
+ return '${formattedValue.endsWith('.00') ? formattedValue.substring(0, formattedValue.length - 3) : formattedValue} ${suffixes[i]}';
+ }
+}
diff --git a/app/lib/ui/flow/accounts/accounts_screen.dart b/app/lib/ui/flow/accounts/accounts_screen.dart
index 3fdf06c..4b14557 100644
--- a/app/lib/ui/flow/accounts/accounts_screen.dart
+++ b/app/lib/ui/flow/accounts/accounts_screen.dart
@@ -13,6 +13,7 @@ import 'package:style/text/app_text_style.dart';
import 'package:style/theme/colors.dart';
import 'package:style/buttons/buttons_list.dart';
import 'package:style/buttons/switch.dart';
+import '../../../components/snack_bar.dart';
import 'components/account_tab.dart';
class AccountsScreen extends ConsumerStatefulWidget {
@@ -32,11 +33,20 @@ class _AccountsScreenState extends ConsumerState {
runPostFrame(() => notifier.init());
}
+ void _errorObserver() {
+ ref.listen(accountsStateNotifierProvider.select((value) => value.error),
+ (previous, next) {
+ if (next != null) {
+ showErrorSnackBar(context: context, error: next);
+ }
+ });
+ }
+
@override
Widget build(BuildContext context) {
+ _errorObserver();
final googleAccount = ref.watch(
accountsStateNotifierProvider.select((value) => value.googleAccount));
-
return AppPage(
title: context.l10n.common_accounts,
bodyBuilder: (context) {
diff --git a/app/lib/ui/flow/home/components/app_media_item.dart b/app/lib/ui/flow/home/components/app_media_item.dart
index c7e3b03..12efc36 100644
--- a/app/lib/ui/flow/home/components/app_media_item.dart
+++ b/app/lib/ui/flow/home/components/app_media_item.dart
@@ -2,9 +2,10 @@ import 'dart:async';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:cloud_gallery/domain/formatter/duration_formatter.dart';
+import 'package:data/models/app_process/app_process.dart';
import 'package:data/models/media/media.dart';
+import 'package:data/models/media/media_extension.dart';
import 'package:flutter/cupertino.dart';
-import 'package:flutter/foundation.dart';
import 'package:flutter_svg/svg.dart';
import 'package:style/extensions/context_extensions.dart';
import 'package:style/indicators/circular_progress_indicator.dart';
@@ -17,7 +18,7 @@ class AppMediaItem extends StatefulWidget {
final void Function()? onTap;
final void Function()? onLongTap;
final bool isSelected;
- final UploadStatus? status;
+ final AppProcess? process;
const AppMediaItem({
super.key,
@@ -25,7 +26,7 @@ class AppMediaItem extends StatefulWidget {
this.onTap,
this.onLongTap,
this.isSelected = false,
- this.status,
+ this.process,
});
@override
@@ -39,15 +40,11 @@ class _AppMediaItemState extends State
@override
void initState() {
if (widget.media.sources.contains(AppMediaSource.local)) {
- _loadImage();
+ thumbnailByte = widget.media.loadThumbnail();
}
super.initState();
}
- _loadImage() async {
- thumbnailByte = widget.media.thumbnailDataWithSize(const Size(300, 300));
- }
-
@override
Widget build(BuildContext context) {
super.build(context);
@@ -120,7 +117,7 @@ class _AppMediaItemState extends State
return Hero(
tag: widget.media,
child: CachedNetworkImage(
- imageUrl: widget.media.thumbnailLink!,
+ imageUrl: widget.media.thumbnailLink ?? '',
width: constraints.maxWidth,
height: constraints.maxHeight,
fit: BoxFit.cover,
@@ -171,14 +168,29 @@ class _AppMediaItemState extends State
],
),
),
- if (widget.status == UploadStatus.uploading)
+ if (widget.process?.status.isProcessing ?? false)
_BackgroundContainer(
- child: AppCircularProgressIndicator(
- size: 16,
- color: context.colorScheme.surfaceInverse,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ AppCircularProgressIndicator(
+ size: 16,
+ value: widget.process?.progress?.percentageInPoint,
+ color: context.colorScheme.surfaceInverse,
+ ),
+ if (widget.process?.progress != null) ...[
+ const SizedBox(width: 4),
+ Text(
+ '${widget.process?.progress?.percentage.toStringAsFixed(0)}%',
+ style: AppTextStyles.caption.copyWith(
+ color: context.colorScheme.surfaceInverse,
+ ),
+ ),
+ ]
+ ],
),
),
- if (widget.status == UploadStatus.waiting)
+ if (widget.process?.status.isWaiting ?? false)
_BackgroundContainer(
child: Icon(
CupertinoIcons.time,
diff --git a/app/lib/ui/flow/home/components/hint_view.dart b/app/lib/ui/flow/home/components/hint_view.dart
new file mode 100644
index 0000000..45b45c7
--- /dev/null
+++ b/app/lib/ui/flow/home/components/hint_view.dart
@@ -0,0 +1,88 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:style/buttons/action_button.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/text/app_text_style.dart';
+
+class HintView extends StatelessWidget {
+ final String title;
+ final String hint;
+ final String actionTitle;
+ final VoidCallback? onActionTap;
+ final VoidCallback onClose;
+
+ const HintView(
+ {super.key,
+ required this.hint,
+ required this.onClose,
+ this.actionTitle = '',
+ this.onActionTap,
+ required this.title});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: context.colorScheme.containerNormalOnSurface,
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Column(
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Expanded(
+ child: Padding(
+ padding: const EdgeInsets.only(top: 16, left: 16),
+ child: Text(
+ title,
+ style: AppTextStyles.subtitle2.copyWith(
+ color: context.colorScheme.textPrimary,
+ ),
+ ),
+ ),
+ ),
+ ActionButton(
+ backgroundColor: context.colorScheme.containerNormal,
+ size: 28,
+ onPressed: onClose,
+ icon: Icon(
+ CupertinoIcons.xmark,
+ color: context.colorScheme.textSecondary,
+ size: 18,
+ ),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16),
+ child: Text(
+ hint,
+ style: AppTextStyles.body2.copyWith(
+ color: context.colorScheme.textSecondary,
+ ),
+ ),
+ ),
+ if (onActionTap != null)
+ FilledButton(
+ onPressed: onActionTap,
+ style: FilledButton.styleFrom(
+ backgroundColor: context.colorScheme.containerNormal,
+ foregroundColor: context.colorScheme.textPrimary,
+ tapTargetSize: MaterialTapTargetSize.shrinkWrap,
+ minimumSize: const Size(double.maxFinite, 40),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ child: Text(
+ actionTitle,
+ style: AppTextStyles.button,
+ ),
+ )
+ ],
+ ),
+ );
+ }
+}
diff --git a/app/lib/ui/flow/home/components/hints.dart b/app/lib/ui/flow/home/components/hints.dart
index 677338f..8970c6f 100644
--- a/app/lib/ui/flow/home/components/hints.dart
+++ b/app/lib/ui/flow/home/components/hints.dart
@@ -3,8 +3,7 @@ import 'package:cloud_gallery/ui/flow/home/home_screen_view_model.dart';
import 'package:data/storage/app_preferences.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:style/extensions/context_extensions.dart';
-import 'package:style/text/app_text_style.dart';
+import 'hint_view.dart';
class HomeScreenHints extends ConsumerWidget {
const HomeScreenHints({super.key});
@@ -62,87 +61,3 @@ class HomeScreenHints extends ConsumerWidget {
}
}
-class HintView extends StatelessWidget {
- final String title;
- final String hint;
- final String actionTitle;
- final VoidCallback? onActionTap;
- final VoidCallback onClose;
-
- const HintView(
- {super.key,
- required this.hint,
- required this.onClose,
- this.actionTitle = '',
- this.onActionTap,
- required this.title});
-
- @override
- Widget build(BuildContext context) {
- return Container(
- margin: const EdgeInsets.all(16),
- decoration: BoxDecoration(
- color: context.colorScheme.containerNormalOnSurface,
- borderRadius: BorderRadius.circular(12),
- ),
- child: Column(
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Expanded(
- child: Padding(
- padding: const EdgeInsets.only(top: 16, left: 16),
- child: Text(
- title,
- style: AppTextStyles.subtitle2.copyWith(
- color: context.colorScheme.textPrimary,
- ),
- ),
- ),
- ),
- IconButton(
- style: IconButton.styleFrom(
- backgroundColor: context.colorScheme.containerNormal,
- minimumSize: const Size(28, 28),
- ),
- onPressed: onClose,
- icon: Icon(
- Icons.close_rounded,
- color: context.colorScheme.textSecondary,
- size: 18,
- ),
- ),
- ],
- ),
- Padding(
- padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16),
- child: Text(
- hint,
- style: AppTextStyles.body2.copyWith(
- color: context.colorScheme.textSecondary,
- ),
- ),
- ),
- if (onActionTap != null)
- FilledButton(
- onPressed: onActionTap,
- style: FilledButton.styleFrom(
- backgroundColor: context.colorScheme.containerNormal,
- foregroundColor: context.colorScheme.textPrimary,
- tapTargetSize: MaterialTapTargetSize.shrinkWrap,
- minimumSize: const Size(double.maxFinite, 40),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- ),
- ),
- child: Text(
- actionTitle,
- style: AppTextStyles.button,
- ),
- )
- ],
- ),
- );
- }
-}
diff --git a/app/lib/ui/flow/home/components/multi_selection_done_button.dart b/app/lib/ui/flow/home/components/multi_selection_done_button.dart
index 90aab74..f806095 100644
--- a/app/lib/ui/flow/home/components/multi_selection_done_button.dart
+++ b/app/lib/ui/flow/home/components/multi_selection_done_button.dart
@@ -1,5 +1,7 @@
+import 'package:cloud_gallery/components/app_dialog.dart';
import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
import 'package:cloud_gallery/ui/flow/home/home_screen_view_model.dart';
+import 'package:data/models/media/media.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -16,6 +18,15 @@ class MultiSelectionDoneButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.read(homeViewStateNotifier.notifier);
+ final selectedMedias = ref
+ .watch(homeViewStateNotifier.select((state) => state.selectedMedias));
+
+ final bool showDeleteFromDriveButton = selectedMedias
+ .any((element) => element.sources.contains(AppMediaSource.googleDrive));
+ final bool showDeleteFromDeviceButton = selectedMedias
+ .any((element) => element.sources.contains(AppMediaSource.local));
+ final bool showUploadToDriveButton = selectedMedias.any(
+ (element) => !element.sources.contains(AppMediaSource.googleDrive));
return FloatingActionButton(
elevation: 3,
backgroundColor: context.colorScheme.primary,
@@ -25,18 +36,77 @@ class MultiSelectionDoneButton extends ConsumerWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
- AppSheetAction(
- icon: SvgPicture.asset(
- Assets.images.icons.googlePhotos,
- height: 24,
- width: 24,
+ if (showUploadToDriveButton)
+ AppSheetAction(
+ icon: SvgPicture.asset(
+ Assets.images.icons.googlePhotos,
+ height: 24,
+ width: 24,
+ ),
+ title: context.l10n.back_up_on_google_drive_text,
+ onPressed: () {
+ notifier.backUpMediaOnGoogleDrive();
+ context.pop();
+ },
+ ),
+ if (showDeleteFromDeviceButton)
+ AppSheetAction(
+ icon: const Icon(CupertinoIcons.delete),
+ title: context.l10n.common_delete_from_device,
+ onPressed: () {
+ showAppAlertDialog(
+ context: context,
+ title: context.l10n.common_delete_from_device,
+ message: context
+ .l10n.delete_media_from_device_confirmation_message,
+ actions: [
+ AppAlertAction(
+ title: context.l10n.common_cancel,
+ onPressed: () {
+ context.pop();
+ },
+ ),
+ AppAlertAction(
+ isDestructiveAction: true,
+ title: context.l10n.common_delete,
+ onPressed: () {
+ notifier.deleteMediasFromLocal();
+ context.pop();
+ },
+ ),
+ ],
+ );
+ },
+ ),
+ if (showDeleteFromDriveButton)
+ AppSheetAction(
+ icon: const Icon(CupertinoIcons.delete),
+ title: context.l10n.common_delete_from_google_drive,
+ onPressed: () {
+ showAppAlertDialog(
+ context: context,
+ title: context.l10n.common_delete_from_google_drive,
+ message: context.l10n
+ .delete_media_from_google_drive_confirmation_message,
+ actions: [
+ AppAlertAction(
+ title: context.l10n.common_cancel,
+ onPressed: () {
+ context.pop();
+ },
+ ),
+ AppAlertAction(
+ isDestructiveAction: true,
+ title: context.l10n.common_delete,
+ onPressed: () {
+ notifier.deleteMediasFromGoogleDrive();
+ context.pop();
+ },
+ ),
+ ],
+ );
+ },
),
- title: context.l10n.back_up_on_google_drive_text,
- onPressed: () {
- notifier.uploadMediaOnGoogleDrive();
- context.pop();
- },
- ),
],
),
);
diff --git a/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart b/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart
index 10b9ebe..4f7fd8b 100644
--- a/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart
+++ b/app/lib/ui/flow/home/components/no_local_medias_access_screen.dart
@@ -1,11 +1,10 @@
+import 'package:cloud_gallery/components/error_view.dart';
import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
import 'package:cloud_gallery/ui/flow/home/home_screen_view_model.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:style/extensions/context_extensions.dart';
-import 'package:style/text/app_text_style.dart';
-import 'package:style/buttons/primary_button.dart';
class NoLocalMediasAccessScreen extends ConsumerWidget {
const NoLocalMediasAccessScreen({super.key});
@@ -13,41 +12,20 @@ class NoLocalMediasAccessScreen extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.read(homeViewStateNotifier.notifier);
- return SafeArea(
- child: Padding(
- padding: const EdgeInsets.all(30),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Icon(
- CupertinoIcons.photo,
- color: context.colorScheme.containerHighOnSurface,
- size: 100,
- ),
- const SizedBox(height: 20),
- Text(context.l10n.cant_find_media_title,
- style: AppTextStyles.subtitle2.copyWith(
- color: context.colorScheme.textPrimary,
- )),
- const SizedBox(height: 20),
- Text(
- context.l10n.ask_for_media_permission_message,
- style: AppTextStyles.body2.copyWith(
- color: context.colorScheme.textSecondary,
- ),
- textAlign: TextAlign.center,
- ),
- const SizedBox(height: 20),
- PrimaryButton(
- onPressed: () async {
- await openAppSettings();
- await notifier.loadLocalMedia();
- },
- text: context.l10n.load_local_media_button_text,
- ),
- ],
- ),
+ return ErrorView(
+ title: context.l10n.cant_find_media_title,
+ icon: Icon(
+ CupertinoIcons.photo,
+ color: context.colorScheme.containerHighOnSurface,
+ size: 100,
+ ),
+ message: context.l10n.ask_for_media_permission_message,
+ action: ErrorViewAction(
+ onPressed: () async {
+ await openAppSettings();
+ await notifier.loadLocalMedia();
+ },
+ title: context.l10n.load_local_media_button_text,
),
);
}
diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart
index 7752fef..fdf9d6b 100644
--- a/app/lib/ui/flow/home/home_screen.dart
+++ b/app/lib/ui/flow/home/home_screen.dart
@@ -1,13 +1,12 @@
import 'dart:io';
-
import 'package:cloud_gallery/components/app_page.dart';
import 'package:cloud_gallery/domain/extensions/widget_extensions.dart';
import 'package:cloud_gallery/domain/formatter/date_formatter.dart';
-import 'package:cloud_gallery/ui/flow/media_preview/media_preview.dart';
import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
import 'package:cloud_gallery/ui/flow/home/components/no_local_medias_access_screen.dart';
import 'package:cloud_gallery/ui/flow/home/home_screen_view_model.dart';
import 'package:collection/collection.dart';
+import 'package:data/models/app_process/app_process.dart';
import 'package:data/models/media/media.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@@ -59,10 +58,35 @@ class _HomeScreenState extends ConsumerState {
Widget build(BuildContext context) {
_errorObserver();
return AppPage(
- //barBackgroundColor: context.colorScheme.surface,
titleWidget: _titleWidget(context: context),
actions: [
+ Consumer(
+ builder: (context, ref, child) {
+ final showTransferButton = ref.watch(homeViewStateNotifier.select(
+ (value) => value.showTransfer));
+ return Visibility(
+ visible: showTransferButton,
+ child: Padding(
+ padding: const EdgeInsets.only(right: 8.0),
+ child: ActionButton(
+ size: 36,
+ backgroundColor: context.colorScheme.containerNormal,
+ onPressed: () {
+ AppRouter.mediaTransfer.push(context);
+ },
+ icon: Icon(
+ CupertinoIcons.arrow_up_arrow_down,
+ color: context.colorScheme.textSecondary,
+ size: 18,
+ ),
+ ),
+ ),
+ );
+ }
+ ),
ActionButton(
+ size: 36,
+ backgroundColor: context.colorScheme.containerNormal,
onPressed: () {
AppRouter.accounts.push(context);
},
@@ -72,6 +96,7 @@ class _HomeScreenState extends ConsumerState {
size: 18,
),
),
+ if (!Platform.isIOS && !Platform.isMacOS) const SizedBox(width: 16),
],
body: _body(context: context),
);
@@ -81,14 +106,14 @@ class _HomeScreenState extends ConsumerState {
//View State
final ({
Map> medias,
- List uploadingMedias,
+ List mediaProcesses,
List selectedMedias,
bool isLoading,
bool hasLocalMediaAccess,
String? lastLocalMediaId
}) state = ref.watch(homeViewStateNotifier.select((value) => (
medias: value.medias,
- uploadingMedias: value.uploadingMedias,
+ mediaProcesses: value.mediaProcesses,
selectedMedias: value.selectedMedias,
isLoading: value.loading,
hasLocalMediaAccess: value.hasLocalMediaAccess,
@@ -107,7 +132,7 @@ class _HomeScreenState extends ConsumerState {
_buildMediaList(
context: context,
medias: state.medias,
- uploadingMedias: state.uploadingMedias,
+ mediaProcesses: state.mediaProcesses,
selectedMedias: state.selectedMedias,
lastLocalMediaId: state.lastLocalMediaId,
),
@@ -123,7 +148,7 @@ class _HomeScreenState extends ConsumerState {
Widget _buildMediaList(
{required BuildContext context,
required Map> medias,
- required List uploadingMedias,
+ required List mediaProcesses,
required String? lastLocalMediaId,
required List selectedMedias}) {
return Scrollbar(
@@ -179,20 +204,20 @@ class _HomeScreenState extends ConsumerState {
if (selectedMedias.isNotEmpty) {
notifier.toggleMediaSelection(media);
} else {
- AppMediaView.showPreview(
- context: context,
- media: media,
- );
+ AppRouter.preview(
+ medias: medias.values
+ .expand((element) => element)
+ .toList(),
+ startFrom: media.id)
+ .push(context);
}
},
onLongTap: () {
notifier.toggleMediaSelection(media);
},
isSelected: selectedMedias.contains(media),
- status: uploadingMedias
- .firstWhereOrNull(
- (element) => element.mediaId == media.id)
- ?.status,
+ process: mediaProcesses.firstWhereOrNull(
+ (process) => process.id == media.id),
media: media,
);
},
@@ -208,8 +233,7 @@ class _HomeScreenState extends ConsumerState {
Widget _titleWidget({required BuildContext context}) {
return Row(
children: [
- if(Platform.isIOS)
- const SizedBox(width: 10),
+ if (Platform.isIOS) const SizedBox(width: 10),
Image.asset(
Assets.images.appIcon,
width: 28,
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 85d0222..2994fb4 100644
--- a/app/lib/ui/flow/home/home_screen_view_model.dart
+++ b/app/lib/ui/flow/home/home_screen_view_model.dart
@@ -1,9 +1,10 @@
import 'dart:async';
import 'package:cloud_gallery/domain/extensions/map_extensions.dart';
-import 'package:cloud_gallery/domain/formatter/date_formatter.dart';
-import 'package:collection/collection.dart';
-import 'package:data/errors/app_error.dart';
+import 'package:cloud_gallery/domain/extensions/media_list_extension.dart';
+import 'package:data/models/app_process/app_process.dart';
import 'package:data/models/media/media.dart';
+import 'package:data/models/media/media_extension.dart';
+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';
@@ -11,6 +12,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_sign_in/google_sign_in.dart';
import 'package:style/extensions/list_extensions.dart';
+import 'home_view_model_helper_mixin.dart';
part 'home_screen_view_model.freezed.dart';
@@ -21,13 +23,17 @@ final homeViewStateNotifier =
ref.read(localMediaServiceProvider),
ref.read(googleDriveServiceProvider),
ref.read(authServiceProvider),
+ ref.read(googleDriveProcessRepoProvider),
);
});
-class HomeViewStateNotifier extends StateNotifier {
- final GoogleDriveService _googleDriveService;
+class HomeViewStateNotifier extends StateNotifier
+ with HomeViewModelHelperMixin {
final AuthService _authService;
+ final GoogleDriveService _googleDriveService;
+ final GoogleDriveProcessRepo _googleDriveProcessRepo;
final LocalMediaService _localMediaService;
+
StreamSubscription? _googleAccountSubscription;
List _uploadedMedia = [];
@@ -36,22 +42,77 @@ class HomeViewStateNotifier extends StateNotifier {
bool _isLocalMediaLoading = false;
bool _isMaxLocalMediaLoaded = false;
- HomeViewStateNotifier(
- this._localMediaService, this._googleDriveService, this._authService)
+ HomeViewStateNotifier(this._localMediaService, this._googleDriveService,
+ this._authService, this._googleDriveProcessRepo)
: super(const HomeViewState()) {
+ _listenUserGoogleAccount();
+ _googleDriveProcessRepo.setBackUpFolderId(_backUpFolderId);
+ _googleDriveProcessRepo.addListener(_listenGoogleDriveProcess);
+ _loadInitialMedia();
+ }
+
+ void _listenUserGoogleAccount() {
_googleAccountSubscription =
_authService.onGoogleAccountChange.listen((event) async {
state = state.copyWith(googleAccount: event);
- await loadGoogleDriveMedia();
- if (event == null) {
+ _googleDriveProcessRepo.clearAllQueue();
+ if (event != null) {
+ _backUpFolderId = await _googleDriveService.getBackupFolderId();
+ _googleDriveProcessRepo.setBackUpFolderId(_backUpFolderId);
+ await loadGoogleDriveMedia();
+ } else {
+ _backUpFolderId = null;
_uploadedMedia.clear();
state = state.copyWith(
- medias: _sortMedias(
- medias: _removeGoogleDriveRefFromMedias(state.medias)),
+ medias: removeGoogleDriveRefFromMediaMap(medias: state.medias),
);
}
});
- _loadInitialMedia();
+ }
+
+ void _listenGoogleDriveProcess() {
+ final successUploads = _googleDriveProcessRepo.uploadQueue
+ .where((element) => element.status.isSuccess);
+
+ final successDeletes = _googleDriveProcessRepo.deleteQueue
+ .where((element) => element.status.isSuccess)
+ .map((e) => e.id);
+
+ final successDownloads = _googleDriveProcessRepo.downloadQueue
+ .where((element) => element.status.isSuccess);
+
+ if (successUploads.isNotEmpty) {
+ state = state.copyWith(
+ medias: addGoogleDriveRefInMediaMap(
+ medias: state.medias,
+ process: successUploads.toList(),
+ ));
+ }
+
+ if (successDeletes.isNotEmpty) {
+ state = state.copyWith(
+ medias: removeGoogleDriveRefFromMediaMap(
+ medias: state.medias,
+ removeFromIds: successDeletes.toList(),
+ ));
+ }
+
+ if (successDownloads.isNotEmpty) {
+ state = state.copyWith(
+ medias: replaceMediaRefInMediaMap(
+ medias: state.medias,
+ process: successDownloads.toList(),
+ ));
+ }
+
+ state = state.copyWith(
+ mediaProcesses: [
+ ..._googleDriveProcessRepo.uploadQueue,
+ ..._googleDriveProcessRepo.deleteQueue,
+ ..._googleDriveProcessRepo.downloadQueue,
+ ],
+ showTransfer: _googleDriveProcessRepo.uploadQueue.isNotEmpty ||
+ _googleDriveProcessRepo.downloadQueue.isNotEmpty);
}
void _loadInitialMedia() async {
@@ -100,19 +161,28 @@ class HomeViewStateNotifier extends StateNotifier {
_isMaxLocalMediaLoaded = true;
}
- final mergedMedia = _mergeCommonMedia(
+ final mergedMedia = mergeCommonMedia(
localMedias: localMedia,
googleDriveMedias: _uploadedMedia,
);
+ List googleDriveMedia = [];
+
+ if (!append) {
+ googleDriveMedia = state.medias.values
+ .expand((element) => element.where((element) =>
+ element.sources.contains(AppMediaSource.googleDrive) &&
+ element.sources.length == 1))
+ .toList();
+ }
state = state.copyWith(
- medias: _sortMedias(
+ medias: sortMedias(
medias: append
? [
...state.medias.values.expand((element) => element).toList(),
...mergedMedia
]
- : mergedMedia,
+ : [...mergedMedia, ...googleDriveMedia],
),
loading: false,
lastLocalMediaId: mergedMedia.length > 10
@@ -138,7 +208,7 @@ class HomeViewStateNotifier extends StateNotifier {
);
// Separate media by its local existence
- List googleDriveMedia = [];
+ List googleDriveMedia = [];
List uploadedMedia = [];
for (var media in driveMedias) {
if (await media.isExist) {
@@ -151,9 +221,11 @@ class HomeViewStateNotifier extends StateNotifier {
//override google drive media if exist.
state = state.copyWith(
- medias: _sortMedias(medias: [
- ..._mergeCommonMedia(
- localMedias: _removeGoogleDriveRefFromMedias(state.medias),
+ medias: sortMedias(medias: [
+ ...mergeCommonMedia(
+ localMedias:
+ state.medias.values.expand((element) => element).toList()
+ ..removeGoogleDriveRefFromMedias(),
googleDriveMedias: uploadedMedia,
),
...googleDriveMedia
@@ -173,131 +245,60 @@ class HomeViewStateNotifier extends StateNotifier {
state = state.copyWith(selectedMedias: selectedMedias);
}
- Future uploadMediaOnGoogleDrive() async {
+ Future deleteMediasFromLocal() async {
+ try {
+ final medias = state.selectedMedias
+ .where((element) => element.sources.contains(AppMediaSource.local))
+ .map((e) => e.id)
+ .toList();
+ await _localMediaService.deleteMedias(medias);
+ state = state.copyWith(selectedMedias: []);
+ await loadLocalMedia();
+ } catch (e) {
+ state = state.copyWith(error: e);
+ }
+ }
+
+ Future deleteMediasFromGoogleDrive() async {
+ try {
+ final medias = state.selectedMedias.where(
+ (element) =>
+ element.sources.contains(AppMediaSource.googleDrive) &&
+ element.driveMediaRefId != null,
+ );
+
+ _googleDriveProcessRepo.deleteMediasFromGoogleDrive(
+ medias: medias.toList());
+ state = state.copyWith(selectedMedias: []);
+ } catch (e) {
+ state = state.copyWith(error: e);
+ }
+ }
+
+ Future backUpMediaOnGoogleDrive() async {
try {
if (!_authService.signedInWithGoogle) {
await _authService.signInWithGoogle();
await loadGoogleDriveMedia();
}
-
- List uploadingMedias = state.selectedMedias
+ List medias = state.selectedMedias
.where((element) =>
!element.sources.contains(AppMediaSource.googleDrive))
.toList();
- state = state.copyWith(
- uploadingMedias: uploadingMedias
- .map((e) =>
- UploadProgress(mediaId: e.id, status: UploadStatus.waiting))
- .toList(),
- error: null,
+ _googleDriveProcessRepo.uploadMediasInGoogleDrive(
+ medias: medias,
);
-
- _backUpFolderId ??= await _googleDriveService.getBackupFolderId();
-
- for (final media in uploadingMedias) {
- state = state.copyWith(
- uploadingMedias: state.uploadingMedias.toList()
- ..updateElement(
- newElement: UploadProgress(
- mediaId: media.id, status: UploadStatus.uploading),
- oldElement: UploadProgress(
- mediaId: media.id, status: UploadStatus.waiting)),
- );
-
- await _googleDriveService.uploadInGoogleDrive(
- media: media,
- folderID: _backUpFolderId!,
- );
-
- state = state.copyWith(
- medias: state.medias.map((key, value) {
- value.updateElement(
- newElement: media.copyWith(
- sources: media.sources.toList()
- ..add(AppMediaSource.googleDrive)),
- oldElement: media);
- return MapEntry(key, value);
- }),
- uploadingMedias: state.uploadingMedias.toList()
- ..removeWhere((element) => element.mediaId == media.id),
- );
- }
-
- state = state.copyWith(uploadingMedias: [], selectedMedias: []);
+ state = state.copyWith(selectedMedias: []);
} catch (error) {
- if (error is BackUpFolderNotFound) {
- _backUpFolderId = await _googleDriveService.getBackupFolderId();
- uploadMediaOnGoogleDrive();
- return;
- }
- state = state.copyWith(error: error, uploadingMedias: []);
- }
- }
-
- //Helper functions
- List _mergeCommonMedia({
- required List localMedias,
- required List googleDriveMedias,
- }) {
- // If one of the lists is empty, return the other list.
- if (googleDriveMedias.isEmpty) return localMedias;
- if (localMedias.isEmpty) return [];
-
- // Convert the lists to mutable lists.
- localMedias = localMedias.toList();
- googleDriveMedias = googleDriveMedias.toList();
-
- final mergedMedias = [];
-
- // Add common media to mergedMedias and remove them from the lists.
- for (AppMedia localMedia in localMedias.toList()) {
- googleDriveMedias
- .toList()
- .where((googleDriveMedia) => googleDriveMedia.path == localMedia.path)
- .forEach((googleDriveMedia) {
- localMedias.removeWhere((media) => media.id == localMedia.id);
-
- mergedMedias.add(localMedia.copyWith(
- sources: [AppMediaSource.local, AppMediaSource.googleDrive],
- thumbnailLink: googleDriveMedia.thumbnailLink,
- ));
- });
- }
-
- return [...mergedMedias, ...localMedias];
- }
-
- Map> _sortMedias({required List medias}) {
- medias.sort((a, b) => (b.createdTime ?? DateTime.now())
- .compareTo(a.createdTime ?? DateTime.now()));
- return groupBy(
- medias,
- (AppMedia media) =>
- media.createdTime?.dateOnly ?? DateTime.now().dateOnly,
- );
- }
-
- List _removeGoogleDriveRefFromMedias(
- Map> medias) {
- final allMedias = medias.values.expand((element) => element).toList();
- for (int index = 0; index < allMedias.length; index++) {
- if (allMedias[index].sources.length > 1) {
- allMedias[index] = allMedias[index].copyWith(
- sources: allMedias[index].sources.toList()
- ..remove(AppMediaSource.googleDrive),
- thumbnailLink: null,
- );
- } else if (allMedias.contains(AppMediaSource.googleDrive)) {
- allMedias.removeAt(index);
- }
+ state = state.copyWith(error: error);
}
- return allMedias;
}
@override
Future dispose() async {
await _googleAccountSubscription?.cancel();
+ _googleDriveProcessRepo.removeListener(_listenGoogleDriveProcess);
super.dispose();
}
}
@@ -309,9 +310,10 @@ class HomeViewState with _$HomeViewState {
@Default(false) bool hasLocalMediaAccess,
@Default(false) bool loading,
GoogleSignInAccount? googleAccount,
+ @Default(false) bool showTransfer,
String? lastLocalMediaId,
@Default({}) Map> medias,
@Default([]) List selectedMedias,
- @Default([]) List uploadingMedias,
+ @Default([]) List mediaProcesses,
}) = _HomeViewState;
}
diff --git a/app/lib/ui/flow/home/home_screen_view_model.freezed.dart b/app/lib/ui/flow/home/home_screen_view_model.freezed.dart
index 74356fb..c03a474 100644
--- a/app/lib/ui/flow/home/home_screen_view_model.freezed.dart
+++ b/app/lib/ui/flow/home/home_screen_view_model.freezed.dart
@@ -20,12 +20,12 @@ mixin _$HomeViewState {
bool get hasLocalMediaAccess => throw _privateConstructorUsedError;
bool get loading => throw _privateConstructorUsedError;
GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError;
+ bool get showTransfer => throw _privateConstructorUsedError;
String? get lastLocalMediaId => throw _privateConstructorUsedError;
Map> get medias =>
throw _privateConstructorUsedError;
List get selectedMedias => throw _privateConstructorUsedError;
- List get uploadingMedias =>
- throw _privateConstructorUsedError;
+ List get mediaProcesses => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomeViewStateCopyWith get copyWith =>
@@ -43,10 +43,11 @@ abstract class $HomeViewStateCopyWith<$Res> {
bool hasLocalMediaAccess,
bool loading,
GoogleSignInAccount? googleAccount,
+ bool showTransfer,
String? lastLocalMediaId,
Map> medias,
List selectedMedias,
- List uploadingMedias});
+ List mediaProcesses});
}
/// @nodoc
@@ -66,10 +67,11 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState>
Object? hasLocalMediaAccess = null,
Object? loading = null,
Object? googleAccount = freezed,
+ Object? showTransfer = null,
Object? lastLocalMediaId = freezed,
Object? medias = null,
Object? selectedMedias = null,
- Object? uploadingMedias = null,
+ Object? mediaProcesses = null,
}) {
return _then(_value.copyWith(
error: freezed == error ? _value.error : error,
@@ -85,6 +87,10 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState>
? _value.googleAccount
: googleAccount // ignore: cast_nullable_to_non_nullable
as GoogleSignInAccount?,
+ showTransfer: null == showTransfer
+ ? _value.showTransfer
+ : showTransfer // ignore: cast_nullable_to_non_nullable
+ as bool,
lastLocalMediaId: freezed == lastLocalMediaId
? _value.lastLocalMediaId
: lastLocalMediaId // ignore: cast_nullable_to_non_nullable
@@ -97,10 +103,10 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState>
? _value.selectedMedias
: selectedMedias // ignore: cast_nullable_to_non_nullable
as List,
- uploadingMedias: null == uploadingMedias
- ? _value.uploadingMedias
- : uploadingMedias // ignore: cast_nullable_to_non_nullable
- as List,
+ mediaProcesses: null == mediaProcesses
+ ? _value.mediaProcesses
+ : mediaProcesses // ignore: cast_nullable_to_non_nullable
+ as List,
) as $Val);
}
}
@@ -118,10 +124,11 @@ abstract class _$$HomeViewStateImplCopyWith<$Res>
bool hasLocalMediaAccess,
bool loading,
GoogleSignInAccount? googleAccount,
+ bool showTransfer,
String? lastLocalMediaId,
Map> medias,
List selectedMedias,
- List uploadingMedias});
+ List mediaProcesses});
}
/// @nodoc
@@ -139,10 +146,11 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
Object? hasLocalMediaAccess = null,
Object? loading = null,
Object? googleAccount = freezed,
+ Object? showTransfer = null,
Object? lastLocalMediaId = freezed,
Object? medias = null,
Object? selectedMedias = null,
- Object? uploadingMedias = null,
+ Object? mediaProcesses = null,
}) {
return _then(_$HomeViewStateImpl(
error: freezed == error ? _value.error : error,
@@ -158,6 +166,10 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
? _value.googleAccount
: googleAccount // ignore: cast_nullable_to_non_nullable
as GoogleSignInAccount?,
+ showTransfer: null == showTransfer
+ ? _value.showTransfer
+ : showTransfer // ignore: cast_nullable_to_non_nullable
+ as bool,
lastLocalMediaId: freezed == lastLocalMediaId
? _value.lastLocalMediaId
: lastLocalMediaId // ignore: cast_nullable_to_non_nullable
@@ -170,10 +182,10 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
? _value._selectedMedias
: selectedMedias // ignore: cast_nullable_to_non_nullable
as List,
- uploadingMedias: null == uploadingMedias
- ? _value._uploadingMedias
- : uploadingMedias // ignore: cast_nullable_to_non_nullable
- as List,
+ mediaProcesses: null == mediaProcesses
+ ? _value._mediaProcesses
+ : mediaProcesses // ignore: cast_nullable_to_non_nullable
+ as List,
));
}
}
@@ -186,13 +198,14 @@ class _$HomeViewStateImpl implements _HomeViewState {
this.hasLocalMediaAccess = false,
this.loading = false,
this.googleAccount,
+ this.showTransfer = false,
this.lastLocalMediaId,
final Map> medias = const {},
final List selectedMedias = const [],
- final List uploadingMedias = const []})
+ final List mediaProcesses = const []})
: _medias = medias,
_selectedMedias = selectedMedias,
- _uploadingMedias = uploadingMedias;
+ _mediaProcesses = mediaProcesses;
@override
final Object? error;
@@ -205,6 +218,9 @@ class _$HomeViewStateImpl implements _HomeViewState {
@override
final GoogleSignInAccount? googleAccount;
@override
+ @JsonKey()
+ final bool showTransfer;
+ @override
final String? lastLocalMediaId;
final Map> _medias;
@override
@@ -224,18 +240,18 @@ class _$HomeViewStateImpl implements _HomeViewState {
return EqualUnmodifiableListView(_selectedMedias);
}
- final List _uploadingMedias;
+ final List _mediaProcesses;
@override
@JsonKey()
- List get uploadingMedias {
- if (_uploadingMedias is EqualUnmodifiableListView) return _uploadingMedias;
+ List get mediaProcesses {
+ if (_mediaProcesses is EqualUnmodifiableListView) return _mediaProcesses;
// ignore: implicit_dynamic_type
- return EqualUnmodifiableListView(_uploadingMedias);
+ return EqualUnmodifiableListView(_mediaProcesses);
}
@override
String toString() {
- return 'HomeViewState(error: $error, hasLocalMediaAccess: $hasLocalMediaAccess, loading: $loading, googleAccount: $googleAccount, lastLocalMediaId: $lastLocalMediaId, medias: $medias, selectedMedias: $selectedMedias, uploadingMedias: $uploadingMedias)';
+ return 'HomeViewState(error: $error, hasLocalMediaAccess: $hasLocalMediaAccess, loading: $loading, googleAccount: $googleAccount, showTransfer: $showTransfer, lastLocalMediaId: $lastLocalMediaId, medias: $medias, selectedMedias: $selectedMedias, mediaProcesses: $mediaProcesses)';
}
@override
@@ -249,13 +265,15 @@ class _$HomeViewStateImpl implements _HomeViewState {
(identical(other.loading, loading) || other.loading == loading) &&
(identical(other.googleAccount, googleAccount) ||
other.googleAccount == googleAccount) &&
+ (identical(other.showTransfer, showTransfer) ||
+ other.showTransfer == showTransfer) &&
(identical(other.lastLocalMediaId, lastLocalMediaId) ||
other.lastLocalMediaId == lastLocalMediaId) &&
const DeepCollectionEquality().equals(other._medias, _medias) &&
const DeepCollectionEquality()
.equals(other._selectedMedias, _selectedMedias) &&
const DeepCollectionEquality()
- .equals(other._uploadingMedias, _uploadingMedias));
+ .equals(other._mediaProcesses, _mediaProcesses));
}
@override
@@ -265,10 +283,11 @@ class _$HomeViewStateImpl implements _HomeViewState {
hasLocalMediaAccess,
loading,
googleAccount,
+ showTransfer,
lastLocalMediaId,
const DeepCollectionEquality().hash(_medias),
const DeepCollectionEquality().hash(_selectedMedias),
- const DeepCollectionEquality().hash(_uploadingMedias));
+ const DeepCollectionEquality().hash(_mediaProcesses));
@JsonKey(ignore: true)
@override
@@ -283,10 +302,11 @@ abstract class _HomeViewState implements HomeViewState {
final bool hasLocalMediaAccess,
final bool loading,
final GoogleSignInAccount? googleAccount,
+ final bool showTransfer,
final String? lastLocalMediaId,
final Map> medias,
final List selectedMedias,
- final List uploadingMedias}) = _$HomeViewStateImpl;
+ final List mediaProcesses}) = _$HomeViewStateImpl;
@override
Object? get error;
@@ -297,13 +317,15 @@ abstract class _HomeViewState implements HomeViewState {
@override
GoogleSignInAccount? get googleAccount;
@override
+ bool get showTransfer;
+ @override
String? get lastLocalMediaId;
@override
Map> get medias;
@override
List get selectedMedias;
@override
- List get uploadingMedias;
+ List get mediaProcesses;
@override
@JsonKey(ignore: true)
_$$HomeViewStateImplCopyWith<_$HomeViewStateImpl> get copyWith =>
diff --git a/app/lib/ui/flow/home/home_view_model_helper_mixin.dart b/app/lib/ui/flow/home/home_view_model_helper_mixin.dart
new file mode 100644
index 0000000..b258dea
--- /dev/null
+++ b/app/lib/ui/flow/home/home_view_model_helper_mixin.dart
@@ -0,0 +1,80 @@
+import 'package:cloud_gallery/domain/extensions/media_list_extension.dart';
+import 'package:cloud_gallery/domain/formatter/date_formatter.dart';
+import 'package:collection/collection.dart';
+import 'package:data/models/app_process/app_process.dart';
+import 'package:data/models/media/media.dart';
+import 'package:data/models/media/media_extension.dart';
+
+mixin HomeViewModelHelperMixin {
+ List mergeCommonMedia({
+ required List localMedias,
+ required List googleDriveMedias,
+ }) {
+ // If one of the lists is empty, return the other list.
+ if (googleDriveMedias.isEmpty) return localMedias;
+ if (localMedias.isEmpty) return [];
+
+ // Convert the lists to mutable lists.
+ localMedias = localMedias.toList();
+ googleDriveMedias = googleDriveMedias.toList();
+
+ final mergedMedias = [];
+
+ // Add common media to mergedMedias and remove them from the lists.
+ for (AppMedia localMedia in localMedias.toList()) {
+ googleDriveMedias
+ .where((googleDriveMedia) => googleDriveMedia.path == localMedia.path)
+ .forEach((googleDriveMedia) {
+ localMedias.removeWhere((media) => media.id == localMedia.id);
+
+ mergedMedias.add(localMedia.mergeGoogleDriveMedia(googleDriveMedia));
+ });
+ }
+
+ return [...mergedMedias, ...localMedias];
+ }
+
+ Map> sortMedias({required List medias}) {
+ medias.sort((a, b) => (b.createdTime ?? DateTime.now())
+ .compareTo(a.createdTime ?? DateTime.now()));
+ return groupBy(
+ medias,
+ (AppMedia media) =>
+ media.createdTime?.dateOnly ?? DateTime.now().dateOnly,
+ );
+ }
+
+ Map> removeGoogleDriveRefFromMediaMap(
+ {required Map> medias,
+ List? removeFromIds}) {
+ return medias.map((key, value) {
+ return MapEntry(key,
+ value..removeGoogleDriveRefFromMedias(removeFromIds: removeFromIds));
+ });
+ }
+
+ Map> addGoogleDriveRefInMediaMap({
+ required Map> medias,
+ required List process,
+ }) {
+ final processIds = process.map((e) => e.id).toList();
+ return medias.map((key, value) {
+ return MapEntry(
+ key,
+ value
+ ..addGoogleDriveRefInMedias(
+ process: process, processIds: processIds));
+ });
+ }
+
+ Map> replaceMediaRefInMediaMap({
+ required Map> medias,
+ required List process,
+ }) {
+ final processIds = process.map((e) => e.id).toList();
+ return medias.map((key, value) {
+ return MapEntry(key,
+ value..replaceMediaRefInMedias(process: process, processIds: processIds));
+ });
+ }
+}
diff --git a/app/lib/ui/flow/media_preview/components/download_require_view.dart b/app/lib/ui/flow/media_preview/components/download_require_view.dart
new file mode 100644
index 0000000..9dafe1e
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/components/download_require_view.dart
@@ -0,0 +1,95 @@
+import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:cloud_gallery/domain/formatter/byte_formatter.dart';
+import 'package:data/models/app_process/app_process.dart';
+import 'package:data/models/media/media.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/indicators/circular_progress_indicator.dart';
+import '../../../../components/error_view.dart';
+
+class DownloadRequireView extends StatelessWidget {
+ final AppMedia media;
+ final AppProcess? downloadProcess;
+ final void Function() onDownload;
+
+ const DownloadRequireView(
+ {super.key,
+ required this.media,
+ this.downloadProcess,
+ required this.onDownload});
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ Hero(
+ tag: media,
+ child: Image.network(
+ height: double.infinity,
+ width: double.infinity,
+ media.thumbnailLink!,
+ fit: BoxFit.cover,
+ ),
+ ),
+ Container(
+ height: double.infinity,
+ width: double.infinity,
+ color: Colors.black38,
+ ),
+ if (downloadProcess?.progress != null &&
+ downloadProcess!.status.isProcessing) ...[
+ ErrorView(
+ foregroundColor: context.colorScheme.onPrimary,
+ icon: Stack(
+ alignment: Alignment.center,
+ children: [
+ AppCircularProgressIndicator(
+ backgroundColor: context.colorScheme.outline,
+ color: context.colorScheme.onPrimary,
+ strokeWidth: 6,
+ size: context.mediaQuerySize.width * 0.15,
+ value: downloadProcess?.progress?.percentageInPoint,
+ ),
+ Icon(
+ CupertinoIcons.cloud_download,
+ color: context.colorScheme.onPrimary,
+ size: context.mediaQuerySize.width * 0.08,
+ ),
+ ],
+ ),
+ title:
+ "${downloadProcess?.progress?.chunk.formatBytes ?? "0.0 B"} - ${downloadProcess?.progress?.total.formatBytes ?? "0.0 B"} ${downloadProcess?.progress?.percentage.toStringAsFixed(0) ?? "0.0"}%",
+ message: context.l10n.download_in_progress_text),
+ ],
+ if (downloadProcess?.status.isWaiting ?? false) ...[
+ ErrorView(
+ foregroundColor: context.colorScheme.onPrimary,
+ icon: Icon(
+ CupertinoIcons.time,
+ size: context.mediaQuerySize.width * 0.15,
+ color: context.colorScheme.onPrimary,
+ ),
+ title: context.l10n.waiting_in_queue_text,
+ message: context.l10n.waiting_in_download_queue_message,
+ ),
+ ],
+ if (downloadProcess?.progress == null)
+ ErrorView(
+ foregroundColor: context.colorScheme.onPrimary,
+ icon: Icon(CupertinoIcons.cloud_download,
+ size: 68, color: context.colorScheme.onPrimary),
+ title: context.l10n.download_require_text,
+ message: context.l10n.download_require_message,
+ action: ErrorViewAction(
+ title: context.l10n.common_download,
+ onPressed: onDownload,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
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
new file mode 100644
index 0000000..c66b552
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/components/image_preview_screen.dart
@@ -0,0 +1,68 @@
+import 'dart:io';
+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:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../../../../domain/extensions/widget_extensions.dart';
+import 'network_image_preview/network_image_preview.dart';
+import 'network_image_preview/network_image_preview_view_model.dart';
+
+class ImagePreview extends ConsumerStatefulWidget {
+ final AppMedia media;
+
+ const ImagePreview({
+ super.key,
+ required this.media,
+ });
+
+ @override
+ ConsumerState createState() => _ImagePreviewScreenState();
+}
+
+class _ImagePreviewScreenState extends ConsumerState {
+ late NetworkImagePreviewStateNotifier notifier;
+
+ @override
+ void initState() {
+ if (!widget.media.sources.contains(AppMediaSource.local)) {
+ notifier = ref.read(networkImagePreviewStateNotifierProvider.notifier);
+ runPostFrame(() {
+ notifier.loadImage(widget.media.id);
+ });
+ }
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return InteractiveViewer(
+ maxScale: 100,
+ child: Center(
+ child: SizedBox(
+ width: double.infinity,
+ child: widget.media.sources.contains(AppMediaSource.local)
+ ? _displayLocalImage(context: context)
+ : NetworkImagePreview(
+ media: widget.media,
+ ),
+ ),
+ ),
+ );
+ }
+
+ Widget _displayLocalImage({required BuildContext context}) {
+ return Hero(
+ tag: widget.media,
+ child: Image.file(File(widget.media.path), fit: BoxFit.contain,
+ errorBuilder: (context, error, stackTrace) {
+ return AppPage(
+ body: ErrorView(
+ title: context.l10n.unable_to_load_media_error,
+ message: context.l10n.unable_to_load_media_message,
+ ));
+ }),
+ );
+ }
+}
diff --git a/app/lib/ui/flow/media_preview/image_preview/components/network_image_preview/network_image_preview.dart b/app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart
similarity index 70%
rename from app/lib/ui/flow/media_preview/image_preview/components/network_image_preview/network_image_preview.dart
rename to app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview.dart
index c949a09..b794206 100644
--- a/app/lib/ui/flow/media_preview/image_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,8 +1,10 @@
import 'dart:typed_data';
+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/error_view.dart';
import 'network_image_preview_view_model.dart';
class NetworkImagePreview extends ConsumerWidget {
@@ -15,7 +17,7 @@ class NetworkImagePreview extends ConsumerWidget {
final state = ref.watch(networkImagePreviewStateNotifierProvider);
if (state.loading) {
- return const Center(child: AppCircularProgressIndicator());
+ return Center(child: AppCircularProgressIndicator(value: state.progress));
} else if (state.mediaBytes != null) {
return Hero(
tag: media,
@@ -23,7 +25,10 @@ class NetworkImagePreview extends ConsumerWidget {
fit: BoxFit.fitWidth),
);
} else if (state.error != null) {
- return const Center(child: Text('Error'));
+ return ErrorView(
+ title: context.l10n.unable_to_load_media_error,
+ message: context.l10n.unable_to_load_media_message,
+ );
}
return const Placeholder();
}
diff --git a/app/lib/ui/flow/media_preview/image_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
similarity index 59%
rename from app/lib/ui/flow/media_preview/image_preview/components/network_image_preview/network_image_preview_view_model.dart
rename to app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.dart
index 571b17f..dbff95e 100644
--- a/app/lib/ui/flow/media_preview/image_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,3 +1,5 @@
+import 'dart:async';
+
import 'package:data/models/media_content/media_content.dart';
import 'package:data/services/google_drive_service.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@@ -13,6 +15,7 @@ final networkImagePreviewStateNotifierProvider = StateNotifierProvider<
class NetworkImagePreviewStateNotifier
extends StateNotifier {
final GoogleDriveService _googleDriveServices;
+ late StreamSubscription _subscription;
NetworkImagePreviewStateNotifier(this._googleDriveServices)
: super(const NetworkImagePreviewState());
@@ -22,15 +25,43 @@ class NetworkImagePreviewStateNotifier
state = state.copyWith(loading: true, error: null);
final mediaContent = await _googleDriveServices.fetchMediaBytes(mediaId);
final mediaByte = [];
- await for (var mediaBytes in mediaContent.stream) {
- mediaByte.addAll(mediaBytes);
- }
- state = state.copyWith(
- mediaContent: mediaContent, mediaBytes: mediaByte, loading: false);
+ 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();
+ },
+ );
} catch (error) {
- state = state.copyWith(error: error, loading: false);
+ state = state.copyWith(
+ error: error,
+ loading: false,
+ );
}
}
+
+ @override
+ void dispose() {
+ _subscription.cancel();
+ super.dispose();
+ }
}
@freezed
@@ -39,6 +70,7 @@ class NetworkImagePreviewState with _$NetworkImagePreviewState {
@Default(false) bool loading,
AppMediaContent? mediaContent,
List? mediaBytes,
+ @Default(0.0) double progress,
Object? error,
}) = _NetworkImagePreviewState;
}
diff --git a/app/lib/ui/flow/media_preview/image_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
similarity index 90%
rename from app/lib/ui/flow/media_preview/image_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart
rename to app/lib/ui/flow/media_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart
index b953966..a1a47a9 100644
--- a/app/lib/ui/flow/media_preview/image_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
@@ -19,6 +19,7 @@ mixin _$NetworkImagePreviewState {
bool get loading => throw _privateConstructorUsedError;
AppMediaContent? get mediaContent => throw _privateConstructorUsedError;
List? get mediaBytes => throw _privateConstructorUsedError;
+ double get progress => throw _privateConstructorUsedError;
Object? get error => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@@ -36,6 +37,7 @@ abstract class $NetworkImagePreviewStateCopyWith<$Res> {
{bool loading,
AppMediaContent? mediaContent,
List? mediaBytes,
+ double progress,
Object? error});
$AppMediaContentCopyWith<$Res>? get mediaContent;
@@ -58,6 +60,7 @@ class _$NetworkImagePreviewStateCopyWithImpl<$Res,
Object? loading = null,
Object? mediaContent = freezed,
Object? mediaBytes = freezed,
+ Object? progress = null,
Object? error = freezed,
}) {
return _then(_value.copyWith(
@@ -73,6 +76,10 @@ class _$NetworkImagePreviewStateCopyWithImpl<$Res,
? _value.mediaBytes
: mediaBytes // ignore: cast_nullable_to_non_nullable
as List?,
+ progress: null == progress
+ ? _value.progress
+ : progress // ignore: cast_nullable_to_non_nullable
+ as double,
error: freezed == error ? _value.error : error,
) as $Val);
}
@@ -103,6 +110,7 @@ abstract class _$$NetworkImagePreviewStateImplCopyWith<$Res>
{bool loading,
AppMediaContent? mediaContent,
List? mediaBytes,
+ double progress,
Object? error});
@override
@@ -125,6 +133,7 @@ class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res>
Object? loading = null,
Object? mediaContent = freezed,
Object? mediaBytes = freezed,
+ Object? progress = null,
Object? error = freezed,
}) {
return _then(_$NetworkImagePreviewStateImpl(
@@ -140,6 +149,10 @@ class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res>
? _value._mediaBytes
: mediaBytes // ignore: cast_nullable_to_non_nullable
as List?,
+ progress: null == progress
+ ? _value.progress
+ : progress // ignore: cast_nullable_to_non_nullable
+ as double,
error: freezed == error ? _value.error : error,
));
}
@@ -152,6 +165,7 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState {
{this.loading = false,
this.mediaContent,
final List? mediaBytes,
+ this.progress = 0.0,
this.error})
: _mediaBytes = mediaBytes;
@@ -170,12 +184,15 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState {
return EqualUnmodifiableListView(value);
}
+ @override
+ @JsonKey()
+ final double progress;
@override
final Object? error;
@override
String toString() {
- return 'NetworkImagePreviewState(loading: $loading, mediaContent: $mediaContent, mediaBytes: $mediaBytes, error: $error)';
+ return 'NetworkImagePreviewState(loading: $loading, mediaContent: $mediaContent, mediaBytes: $mediaBytes, progress: $progress, error: $error)';
}
@override
@@ -188,6 +205,8 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState {
other.mediaContent == mediaContent) &&
const DeepCollectionEquality()
.equals(other._mediaBytes, _mediaBytes) &&
+ (identical(other.progress, progress) ||
+ other.progress == progress) &&
const DeepCollectionEquality().equals(other.error, error));
}
@@ -197,6 +216,7 @@ class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState {
loading,
mediaContent,
const DeepCollectionEquality().hash(_mediaBytes),
+ progress,
const DeepCollectionEquality().hash(error));
@JsonKey(ignore: true)
@@ -212,6 +232,7 @@ abstract class _NetworkImagePreviewState implements NetworkImagePreviewState {
{final bool loading,
final AppMediaContent? mediaContent,
final List? mediaBytes,
+ final double progress,
final Object? error}) = _$NetworkImagePreviewStateImpl;
@override
@@ -221,6 +242,8 @@ abstract class _NetworkImagePreviewState implements NetworkImagePreviewState {
@override
List? get mediaBytes;
@override
+ double get progress;
+ @override
Object? get error;
@override
@JsonKey(ignore: true)
diff --git a/app/lib/ui/flow/media_preview/components/top_bar.dart b/app/lib/ui/flow/media_preview/components/top_bar.dart
new file mode 100644
index 0000000..2c5cc58
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/components/top_bar.dart
@@ -0,0 +1,196 @@
+import 'dart:io';
+import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:data/models/media/media_extension.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:go_router/go_router.dart';
+import 'package:style/animations/cross_fade_animation.dart';
+import 'package:style/buttons/action_button.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/text/app_text_style.dart';
+import '../../../../components/app_dialog.dart';
+import '../../../../components/app_page.dart';
+import '../../../../domain/assets/assets_paths.dart';
+import '../../../../domain/formatter/date_formatter.dart';
+import '../media_preview_view_model.dart';
+
+class PreviewTopBar extends StatelessWidget {
+ final AutoDisposeStateNotifierProvider provider;
+
+ const PreviewTopBar({super.key, required this.provider});
+
+ @override
+ Widget build(BuildContext context) {
+ return Consumer(builder: (context, ref, child) {
+ final notifier = ref.read(provider.notifier);
+ final media = ref
+ .watch(provider.select((state) => state.medias[state.currentIndex]));
+ final showManu = ref.watch(provider.select((state) => state.showActions));
+
+ return CrossFadeAnimation(
+ showChild: showManu,
+ child: AdaptiveAppBar(
+ iosTransitionBetweenRoutes: false,
+ text:
+ media.createdTime?.format(context, DateFormatType.relative) ?? '',
+ actions: [
+ ActionButton(
+ onPressed: () {
+ ///TODO: media details
+ },
+ icon: Icon(
+ CupertinoIcons.info,
+ color: context.colorScheme.textSecondary,
+ size: 22,
+ ),
+ ),
+ ActionButton(
+ onPressed: () async {
+ if (media.isCommonStored && media.driveMediaRefId != null) {
+ showMenu(
+ context: context,
+ position: RelativeRect.fromLTRB(
+ double.infinity,
+ kToolbarHeight + MediaQuery.of(context).padding.top,
+ 0,
+ 0),
+ surfaceTintColor: context.colorScheme.surface,
+ color: context.colorScheme.surface,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(8),
+ ),
+ items: [
+ PopupMenuItem(
+ onTap: () async {
+ _showDeleteFromDriveDialog(
+ context: context,
+ onDelete: () {
+ notifier.deleteMediaFromGoogleDrive(
+ media.driveMediaRefId);
+ context.pop();
+ });
+ },
+ child: Row(
+ children: [
+ SvgPicture.asset(
+ Assets.images.icons.googlePhotos,
+ width: 20,
+ height: 20,
+ ),
+ const SizedBox(width: 16),
+ Text(context.l10n.common_delete_from_google_drive,
+ style: AppTextStyles.body2.copyWith(
+ color: context.colorScheme.textPrimary,
+ )),
+ ],
+ ),
+ ),
+ PopupMenuItem(
+ onTap: () async {
+ _showDeleteFromDeviceDialog(
+ context: context,
+ onDelete: () {
+ notifier.deleteMediaFromLocal(media.id);
+ context.pop();
+ });
+ },
+ child: Row(
+ children: [
+ Icon(CupertinoIcons.trash,
+ color: context.colorScheme.textSecondary,
+ size: 22),
+ const SizedBox(width: 16),
+ Text(
+ context.l10n.common_delete_from_device,
+ style: AppTextStyles.body2.copyWith(
+ color: context.colorScheme.textPrimary,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+ } else if (media.isGoogleDriveStored &&
+ media.driveMediaRefId != null) {
+ _showDeleteFromDriveDialog(
+ context: context,
+ onDelete: () {
+ notifier
+ .deleteMediaFromGoogleDrive(media.driveMediaRefId);
+ context.pop();
+ });
+ } else if (media.isLocalStored) {
+ _showDeleteFromDeviceDialog(
+ context: context,
+ onDelete: () {
+ notifier.deleteMediaFromLocal(media.id);
+ context.pop();
+ });
+ }
+ },
+ icon: Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: Icon(
+ CupertinoIcons.delete,
+ color: context.colorScheme.textSecondary,
+ size: 22,
+ ),
+ ),
+ ),
+ if (!Platform.isIOS && !Platform.isMacOS) const SizedBox(width: 8),
+ ],
+ ),
+ );
+ });
+ }
+
+ Future _showDeleteFromDriveDialog(
+ {required BuildContext context,
+ required void Function() onDelete}) async {
+ await showAppAlertDialog(
+ context: context,
+ title: context.l10n.common_delete_from_google_drive,
+ message: context.l10n.delete_media_from_google_drive_confirmation_message,
+ actions: [
+ AppAlertAction(
+ title: context.l10n.common_cancel,
+ onPressed: () {
+ context.pop();
+ },
+ ),
+ AppAlertAction(
+ isDestructiveAction: true,
+ title: context.l10n.common_delete,
+ onPressed: onDelete,
+ ),
+ ],
+ );
+ }
+
+ Future _showDeleteFromDeviceDialog(
+ {required BuildContext context,
+ required void Function() onDelete}) async {
+ await showAppAlertDialog(
+ context: context,
+ title: context.l10n.common_delete_from_device,
+ message: context.l10n.delete_media_from_device_confirmation_message,
+ actions: [
+ AppAlertAction(
+ title: context.l10n.common_cancel,
+ onPressed: () {
+ context.pop();
+ },
+ ),
+ AppAlertAction(
+ isDestructiveAction: true,
+ title: context.l10n.common_delete,
+ onPressed: onDelete,
+ ),
+ ],
+ );
+ }
+}
diff --git a/app/lib/ui/flow/media_preview/components/video_player_components/video_actions.dart b/app/lib/ui/flow/media_preview/components/video_player_components/video_actions.dart
new file mode 100644
index 0000000..8cb19e2
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/components/video_player_components/video_actions.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/material.dart';
+import 'package:style/animations/cross_fade_animation.dart';
+import 'package:style/animations/on_tap_scale.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/animations/animated_icon.dart';
+
+class VideoActions extends StatelessWidget {
+ final bool showActions;
+ final void Function()? onBackward;
+ final void Function()? onForward;
+ final void Function()? onPlayPause;
+ final bool isPlaying;
+
+ const VideoActions(
+ {super.key,
+ required this.showActions,
+ this.onBackward,
+ this.onForward,
+ this.onPlayPause,
+ this.isPlaying = false});
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.center,
+ child: CrossFadeAnimation(
+ alignment: Alignment.center,
+ replacement: const SizedBox(),
+ showChild: showActions,
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ OnTapScale(
+ onTap: onBackward,
+ child: Padding(
+ padding: const EdgeInsets.only(
+ left: 22, right: 18, top: 18, bottom: 18),
+ child: Icon(
+ CupertinoIcons.gobackward_10,
+ color: context.colorScheme.onPrimary,
+ size: 32,
+ ),
+ ),
+ ),
+ OnTapScale(
+ onTap: onPlayPause,
+ child: AnimatedIconAnimation(
+ value: isPlaying, icon: AnimatedIcons.play_pause, size: 64),
+ ),
+ OnTapScale(
+ onTap: onForward,
+ child: const Padding(
+ padding:
+ EdgeInsets.only(left: 18, right: 22, top: 18, bottom: 18),
+ child: Icon(
+ CupertinoIcons.goforward_10,
+ color: Colors.white,
+ size: 32,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart b/app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart
new file mode 100644
index 0000000..205b8cd
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/components/video_player_components/video_duration_slider.dart
@@ -0,0 +1,80 @@
+import 'dart:ui';
+import 'package:cloud_gallery/domain/formatter/duration_formatter.dart';
+import 'package:flutter/material.dart';
+import 'package:style/animations/cross_fade_animation.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/text/app_text_style.dart';
+
+class VideoDurationSlider extends StatelessWidget {
+ final bool showSlider;
+ final Duration duration;
+ final void Function(Duration duration) onChanged;
+ final void Function(Duration duration) onChangeEnd;
+ final Duration position;
+
+ const VideoDurationSlider(
+ {super.key,
+ required this.showSlider,
+ required this.duration,
+ required this.position,
+ required this.onChangeEnd,
+ required this.onChanged});
+
+ @override
+ Widget build(BuildContext context) {
+ return Align(
+ alignment: Alignment.bottomCenter,
+ child: CrossFadeAnimation(
+ showChild: showSlider,
+ child: Container(
+ padding: EdgeInsets.only(
+ bottom: context.systemPadding.bottom + 8,
+ top: 8,
+ left: 16,
+ right: 16),
+ color: context.colorScheme.barColor,
+ child: BackdropFilter(
+ filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(position.format,
+ style: AppTextStyles.caption
+ .copyWith(color: context.colorScheme.textPrimary)),
+ Expanded(
+ child: SizedBox(
+ height: 30,
+ child: Material(
+ color: Colors.transparent,
+ child: SliderTheme(
+ data: SliderTheme.of(context).copyWith(
+ trackHeight: 4,
+ trackShape: const RoundedRectSliderTrackShape(),
+ rangeTrackShape:
+ const RoundedRectRangeSliderTrackShape(),
+ thumbShape: SliderComponentShape.noThumb,
+ ),
+ child: Slider(
+ value: position.inSeconds.toDouble(),
+ max: duration.inSeconds.toDouble(),
+ min: 0,
+ activeColor: context.colorScheme.primary,
+ inactiveColor: context.colorScheme.outline,
+ onChangeEnd: (value) => onChangeEnd.call(Duration(seconds: value.toInt())),
+ onChanged: (double value) => onChanged.call(Duration(seconds: value.toInt())),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Text(duration.format,
+ style: AppTextStyles.caption
+ .copyWith(color: context.colorScheme.textPrimary)),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/app/lib/ui/flow/media_preview/image_preview/image_preview_screen.dart b/app/lib/ui/flow/media_preview/image_preview/image_preview_screen.dart
deleted file mode 100644
index b075768..0000000
--- a/app/lib/ui/flow/media_preview/image_preview/image_preview_screen.dart
+++ /dev/null
@@ -1,128 +0,0 @@
-import 'dart:io';
-import 'dart:math';
-import 'package:data/models/media/media.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:style/extensions/context_extensions.dart';
-import '../../../../components/app_page.dart';
-import '../../../../domain/extensions/widget_extensions.dart';
-import 'components/network_image_preview/network_image_preview.dart';
-import 'components/network_image_preview/network_image_preview_view_model.dart';
-
-class ImagePreviewScreen extends ConsumerStatefulWidget {
- final AppMedia media;
-
- const ImagePreviewScreen({
- super.key,
- required this.media,
- });
-
- @override
- ConsumerState createState() => _ImagePreviewScreenState();
-}
-
-class _ImagePreviewScreenState extends ConsumerState {
- final _transformationController = TransformationController();
- double _translateY = 0;
- double _scale = 1;
-
- late NetworkImagePreviewStateNotifier notifier;
-
- @override
- void initState() {
- if (!widget.media.sources.contains(AppMediaSource.local)) {
- notifier = ref.read(networkImagePreviewStateNotifierProvider.notifier);
- runPostFrame(() {
- notifier.loadImage(widget.media.id);
- });
- }
- super.initState();
- }
-
- @override
- Widget build(BuildContext context) {
- return GestureDetector(
- onVerticalDragStart: (details) {
- _translateY = 0;
- _scale = 1;
- },
- onVerticalDragUpdate: (details) {
- if (_transformationController.value.getMaxScaleOnAxis() > 1) {
- return;
- }
-
- setState(() {
- _translateY = max(0, _translateY + (details.primaryDelta ?? 0));
- _scale = 1 - (_translateY / 1000);
- });
- },
- onVerticalDragEnd: (details) {
- if (_transformationController.value.getMaxScaleOnAxis() > 1 ||
- _translateY == 0) {
- return;
- }
-
- final velocity = details.primaryVelocity ?? 0;
- if (velocity > 1000) {
- Navigator.of(context).pop();
- } else if (_translateY > 100) {
- Navigator.of(context).pop();
- } else {
- setState(() {
- _translateY = 0;
- _scale = 1;
- });
- }
- },
- child: AppPage(
- backgroundColor: _scale == 1
- ? context.colorScheme.surface
- : context.colorScheme.surface.withOpacity(_scale / 2),
- body: Stack(
- children: [
- Transform.translate(
- offset: Offset(0, _translateY),
- child: Transform.scale(
- scale: _scale,
- child: InteractiveViewer(
- transformationController: _transformationController,
- maxScale: 100,
- child: Center(
- child: SizedBox(
- width: double.infinity,
- child: widget.media.sources.contains(AppMediaSource.local)
- ? _displayLocalImage(context: context)
- : NetworkImagePreview(
- media: widget.media,
- ),
- ),
- ),
- ),
- ),
- ),
- if (_scale == 1)
- Column(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- AdaptiveAppBar(
- iosTransitionBetweenRoutes: false,
- text: widget.media.name ?? '',
- ),
- ],
- ),
- ],
- ),
- ),
- );
- }
-
- Widget _displayLocalImage({required BuildContext context}) {
- return Hero(
- tag: widget.media,
- child: Image.file(
- File(widget.media.path),
- fit: BoxFit.contain,
- ),
- );
- }
-}
diff --git a/app/lib/ui/flow/media_preview/media_preview.dart b/app/lib/ui/flow/media_preview/media_preview.dart
deleted file mode 100644
index 2513eb1..0000000
--- a/app/lib/ui/flow/media_preview/media_preview.dart
+++ /dev/null
@@ -1,22 +0,0 @@
-import 'package:cloud_gallery/components/snack_bar.dart';
-import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
-import 'package:cloud_gallery/ui/navigation/app_router.dart';
-import 'package:data/models/media/media.dart';
-import 'package:flutter/cupertino.dart';
-
-class AppMediaView {
- static void showPreview(
- {required BuildContext context,
- required AppMedia media}) {
- if (media.type.isImage) {
- AppRouter.imagePreview(media: media).push(context);
- } else if (media.type.isVideo) {
- AppRouter.videoPreview(
- path: media.path,
- isLocal: media.sources.contains(AppMediaSource.local))
- .push(context);
- } else {
- showErrorSnackBar(context: context, error: context.l10n.unable_to_open_attachment_error);
- }
- }
-}
diff --git a/app/lib/ui/flow/media_preview/media_preview_screen.dart b/app/lib/ui/flow/media_preview/media_preview_screen.dart
new file mode 100644
index 0000000..5e70c34
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/media_preview_screen.dart
@@ -0,0 +1,283 @@
+import 'dart:io';
+import 'package:cloud_gallery/components/app_page.dart';
+import 'package:cloud_gallery/components/error_view.dart';
+import 'package:cloud_gallery/components/snack_bar.dart';
+import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:cloud_gallery/domain/extensions/widget_extensions.dart';
+import 'package:cloud_gallery/ui/flow/media_preview/components/download_require_view.dart';
+import 'package:cloud_gallery/ui/flow/media_preview/components/image_preview_screen.dart';
+import 'package:cloud_gallery/ui/flow/media_preview/components/top_bar.dart';
+import 'package:cloud_gallery/ui/flow/media_preview/components/video_player_components/video_actions.dart';
+import 'package:cloud_gallery/ui/flow/media_preview/media_preview_view_model.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 'package:go_router/go_router.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/indicators/circular_progress_indicator.dart';
+import 'package:video_player/video_player.dart';
+import 'components/video_player_components/video_duration_slider.dart';
+import 'package:style/animations/dismissible_page.dart';
+
+class MediaPreview extends ConsumerStatefulWidget {
+ final List medias;
+ final String startFrom;
+
+ const MediaPreview(
+ {super.key, required this.medias, required this.startFrom});
+
+ @override
+ ConsumerState createState() => _MediaPreviewState();
+}
+
+class _MediaPreviewState extends ConsumerState {
+ late AutoDisposeStateNotifierProvider _provider;
+ late PageController _pageController;
+ late MediaPreviewStateNotifier notifier;
+
+ VideoPlayerController? _videoPlayerController;
+
+ @override
+ void initState() {
+ final currentIndex =
+ widget.medias.indexWhere((element) => element.id == widget.startFrom);
+
+ //initialize view notifier with initial state
+ _provider = mediaPreviewStateNotifierProvider(
+ MediaPreviewState(currentIndex: currentIndex, medias: widget.medias));
+ notifier = ref.read(_provider.notifier);
+
+ _pageController = PageController(initialPage: currentIndex, keepPage: true);
+
+ if (widget.medias[currentIndex].type.isVideo &&
+ widget.medias[currentIndex].sources.contains(AppMediaSource.local)) {
+ runPostFrame(() => _initializeVideoControllerWithListener(
+ path: widget.medias[currentIndex].path));
+ } else if (widget.medias[currentIndex].type.isVideo &&
+ widget.medias[currentIndex].isGoogleDriveStored) {}
+ super.initState();
+ }
+
+ Future _initializeVideoControllerWithListener(
+ {required String path}) async {
+ _videoPlayerController = VideoPlayerController.file(File(path));
+ _videoPlayerController?.addListener(_observeVideoController);
+ await _videoPlayerController?.initialize();
+ notifier.updateVideoInitialized(
+ _videoPlayerController?.value.isInitialized ?? false);
+ await _videoPlayerController?.play();
+ }
+
+ _observeVideoController() {
+ notifier.updateVideoInitialized(
+ _videoPlayerController?.value.isInitialized ?? false);
+ notifier.updateVideoBuffering(
+ _videoPlayerController?.value.isBuffering ?? false);
+ notifier.updateVideoPosition(
+ _videoPlayerController?.value.position ?? Duration.zero);
+ notifier.updateVideoMaxDuration(
+ _videoPlayerController?.value.duration ?? Duration.zero);
+ notifier
+ .updateVideoPlaying(_videoPlayerController?.value.isPlaying ?? false);
+ }
+
+ void _observeError() {
+ ref.listen(
+ _provider.select((value) => value.error),
+ (previous, next) {
+ if (next != null) {
+ showErrorSnackBar(context: context, error: next);
+ }
+ },
+ );
+ }
+
+ void _updateVideoControllerOnMediaChange() {
+ ref.listen(_provider.select((value) => value.medias[value.currentIndex]),
+ (previous, next) {
+ if (_videoPlayerController != null) {
+ _videoPlayerController?.removeListener(_observeVideoController);
+ notifier.updateVideoInitialized(false);
+ _videoPlayerController?.dispose();
+ _videoPlayerController = null;
+ }
+ if (next.type.isVideo && next.sources.contains(AppMediaSource.local)) {
+ _initializeVideoControllerWithListener(path: next.path);
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _videoPlayerController?.removeListener(_observeVideoController);
+ _videoPlayerController?.dispose();
+ _pageController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ _observeError();
+ _updateVideoControllerOnMediaChange();
+ final medias = ref.watch(_provider.select((state) => state.medias));
+ final showActions =
+ ref.watch(_provider.select((state) => state.showActions));
+
+ return DismissiblePage(
+ backgroundColor: context.colorScheme.surface,
+ onProgress: (progress) {
+ if (progress > 0 && showActions) {
+ notifier.toggleActionVisibility();
+ }
+ },
+ onDismiss: () {
+ context.pop();
+ },
+ child: (progress) => AppPage(
+ backgroundColor:
+ progress == 0 ? context.colorScheme.surface : Colors.transparent,
+ body: Stack(
+ children: [
+ GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onTap: notifier.toggleActionVisibility,
+ child: PageView.builder(
+ onPageChanged: notifier.changeVisibleMediaIndex,
+ controller: _pageController,
+ itemCount: medias.length,
+ itemBuilder: (context, index) =>
+ _preview(context: context, media: medias[index]),
+ ),
+ ),
+ PreviewTopBar(provider: _provider),
+ _videoActions(context),
+ _videoDurationSlider(context),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _preview({required BuildContext context, required AppMedia media}) {
+ if (media.type.isVideo && media.sources.contains(AppMediaSource.local)) {
+ return Center(
+ child: Consumer(builder: (context, ref, child) {
+ ({bool initialized, bool buffring}) state = ref.watch(
+ _provider.select((state) => (
+ initialized: state.isVideoInitialized,
+ buffring: state.isVideoBuffering
+ )));
+
+ if (!state.initialized || state.buffring) {
+ return AppCircularProgressIndicator(
+ color: context.colorScheme.onPrimary,
+ );
+ } else {
+ return Hero(
+ tag: media,
+ child: AspectRatio(
+ aspectRatio: _videoPlayerController!.value.aspectRatio,
+ child: VideoPlayer(_videoPlayerController!),
+ ),
+ );
+ }
+ }),
+ );
+ } else if (media.type.isVideo && media.isGoogleDriveStored) {
+ return _googleDriveVideoView(context: context, media: media);
+ } else if (media.type.isImage) {
+ return ImagePreview(media: media);
+ } else {
+ return ErrorView(
+ title: context.l10n.unable_to_load_media_error,
+ message: context.l10n.unable_to_load_media_message,
+ );
+ }
+ }
+
+ Widget _googleDriveVideoView(
+ {required BuildContext context, required AppMedia media}) {
+ return Consumer(
+ builder: (context, ref, child) {
+ final process = ref.watch(_provider.select((value) => value
+ .downloadProcess
+ .where((element) => element.id == media.id)
+ .firstOrNull));
+ return DownloadRequireView(
+ media: media,
+ downloadProcess: process,
+ onDownload: () {
+ notifier.downloadMediaFromGoogleDrive(media: media);
+ });
+ },
+ );
+ }
+
+ Widget _videoActions(BuildContext context) => Consumer(
+ builder: (context, ref, child) {
+ final ({
+ bool showActions,
+ bool isPlaying,
+ Duration position,
+ }) state = ref.watch(_provider.select((state) => (
+ showActions: state.showActions &&
+ state.medias[state.currentIndex].type.isVideo &&
+ state.isVideoInitialized,
+ isPlaying: state.isVideoPlaying,
+ position: state.videoPosition,
+ )));
+
+ return VideoActions(
+ showActions: state.showActions,
+ isPlaying: state.isPlaying,
+ onBackward: () {
+ notifier.updateVideoPosition(
+ state.position - const Duration(seconds: 10));
+ _videoPlayerController
+ ?.seekTo(state.position - const Duration(seconds: 10));
+ },
+ onForward: () {
+ notifier.updateVideoPosition(
+ state.position + const Duration(seconds: 10));
+ _videoPlayerController
+ ?.seekTo(state.position + const Duration(seconds: 10));
+ },
+ onPlayPause: () {
+ if (state.isPlaying) {
+ _videoPlayerController?.pause();
+ } else {
+ _videoPlayerController?.play();
+ }
+ },
+ );
+ },
+ );
+
+ Widget _videoDurationSlider(BuildContext context) =>
+ Consumer(builder: (context, ref, child) {
+ final ({
+ bool showDurationSlider,
+ Duration duration,
+ Duration position
+ }) state = ref.watch(_provider.select((state) => (
+ showDurationSlider: state.showActions &&
+ state.medias[state.currentIndex].type.isVideo &&
+ state.isVideoInitialized,
+ duration: state.videoMaxDuration,
+ position: state.videoPosition
+ )));
+ return VideoDurationSlider(
+ showSlider: state.showDurationSlider,
+ duration: state.duration,
+ position: state.position,
+ onChangeEnd: (duration) {
+ _videoPlayerController?.seekTo(duration);
+ },
+ onChanged: (duration) {
+ notifier.updateVideoPosition(duration);
+ },
+ );
+ });
+}
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
new file mode 100644
index 0000000..efef08f
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/media_preview_view_model.dart
@@ -0,0 +1,154 @@
+import 'package:cloud_gallery/domain/extensions/media_list_extension.dart';
+import 'package:data/models/app_process/app_process.dart';
+import 'package:data/models/media/media.dart';
+import 'package:data/repositories/google_drive_process_repo.dart';
+import 'package:data/services/google_drive_service.dart';
+import 'package:data/services/local_media_service.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'media_preview_view_model.freezed.dart';
+
+final mediaPreviewStateNotifierProvider = StateNotifierProvider.family
+ .autoDispose(
+ (ref, initial) => MediaPreviewStateNotifier(
+ ref.read(localMediaServiceProvider),
+ ref.read(googleDriveServiceProvider),
+ ref.read(googleDriveProcessRepoProvider),
+ initial,
+ ),
+);
+
+class MediaPreviewStateNotifier extends StateNotifier {
+ final LocalMediaService _localMediaService;
+ final GoogleDriveService _googleDriveService;
+ final GoogleDriveProcessRepo _googleDriveProcessRepo;
+
+ MediaPreviewStateNotifier(this._localMediaService, this._googleDriveService,
+ this._googleDriveProcessRepo, MediaPreviewState initialState)
+ : super(initialState) {
+ _googleDriveProcessRepo.addListener(_listenGoogleDriveProcessUpdates);
+ }
+
+ void _listenGoogleDriveProcessUpdates() {
+ final successUploads = _googleDriveProcessRepo.uploadQueue
+ .where((element) => element.status.isSuccess);
+
+ final successDeletes = _googleDriveProcessRepo.deleteQueue
+ .where((element) => element.status.isSuccess)
+ .map((e) => e.id);
+
+ final successDownloads = _googleDriveProcessRepo.downloadQueue
+ .where((element) => element.status.isSuccess);
+
+ if (successUploads.isNotEmpty) {
+ state = state.copyWith(
+ medias: state.medias.toList()
+ ..addGoogleDriveRefInMedias(
+ process: successUploads.toList(),
+ ),
+ );
+ }
+
+ if (successDeletes.isNotEmpty) {
+ state = state.copyWith(
+ medias: state.medias.toList()
+ ..removeGoogleDriveRefFromMedias(
+ removeFromIds: successDeletes.toList(),
+ ),
+ );
+ }
+ if (successDownloads.isNotEmpty) {
+ state = state.copyWith(
+ medias: state.medias.toList()
+ ..replaceMediaRefInMedias(
+ process: successDownloads.toList(),
+ ),
+ );
+ }
+
+ state = state.copyWith(
+ uploadProcess: _googleDriveProcessRepo.uploadQueue,
+ downloadProcess: _googleDriveProcessRepo.downloadQueue,
+ deleteProcess: _googleDriveProcessRepo.deleteQueue,
+ );
+ }
+
+ void changeVisibleMediaIndex(int index) {
+ state = state.copyWith(currentIndex: index);
+ }
+
+ void toggleActionVisibility() {
+ state = state.copyWith(showActions: !state.showActions);
+ }
+
+ Future deleteMediaFromLocal(String id) async {
+ try {
+ await _localMediaService.deleteMedias([id]);
+ } catch (error) {
+ state = state.copyWith(error: error);
+ }
+ }
+
+ Future deleteMediaFromGoogleDrive(String? id) async {
+ try {
+ await _googleDriveService.deleteMedia(id!);
+ } catch (error) {
+ state = state.copyWith(error: error);
+ }
+ }
+
+ Future downloadMediaFromGoogleDrive({required AppMedia media})async {
+ _googleDriveProcessRepo.downloadMediasFromGoogleDrive(medias: [media]);
+ }
+
+ void updateVideoPosition(Duration position) {
+ if (state.videoPosition == position) return;
+ state = state.copyWith(videoPosition: position);
+ }
+
+ void updateVideoPlaying(bool isPlaying) {
+ if (state.isVideoPlaying == isPlaying) return;
+ state = state.copyWith(isVideoPlaying: isPlaying);
+ }
+
+ void updateVideoBuffering(bool isBuffering) {
+ if (state.isVideoBuffering == isBuffering) return;
+ state = state.copyWith(isVideoBuffering: isBuffering);
+ }
+
+ void updateVideoInitialized(bool isInitialized) {
+ if (state.isVideoInitialized == isInitialized) return;
+ state = state.copyWith(isVideoInitialized: isInitialized);
+ }
+
+ void updateVideoMaxDuration(Duration maxDuration) {
+ if (state.videoMaxDuration == maxDuration) return;
+ state = state.copyWith(videoMaxDuration: maxDuration);
+ }
+
+ @override
+ void dispose() {
+ _googleDriveProcessRepo.removeListener(_listenGoogleDriveProcessUpdates);
+ super.dispose();
+ }
+}
+
+@freezed
+class MediaPreviewState with _$MediaPreviewState {
+ const factory MediaPreviewState({
+ Object? error,
+ @Default([]) List medias,
+ @Default(0) int currentIndex,
+ @Default(true) bool showActions,
+ @Default(false) bool isVideoInitialized,
+ @Default(false) bool isVideoBuffering,
+ @Default(Duration.zero) Duration videoPosition,
+ @Default(Duration.zero) Duration videoMaxDuration,
+ @Default(false) bool isVideoPlaying,
+ @Default([]) List uploadProcess,
+ @Default([]) List downloadProcess,
+ @Default([]) List deleteProcess,
+ }) = _MediaPreviewState;
+}
diff --git a/app/lib/ui/flow/media_preview/media_preview_view_model.freezed.dart b/app/lib/ui/flow/media_preview/media_preview_view_model.freezed.dart
new file mode 100644
index 0000000..d55aae7
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/media_preview_view_model.freezed.dart
@@ -0,0 +1,412 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'media_preview_view_model.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+/// @nodoc
+mixin _$MediaPreviewState {
+ Object? get error => throw _privateConstructorUsedError;
+ List get medias => throw _privateConstructorUsedError;
+ int get currentIndex => throw _privateConstructorUsedError;
+ bool get showActions => throw _privateConstructorUsedError;
+ bool get isVideoInitialized => throw _privateConstructorUsedError;
+ bool get isVideoBuffering => throw _privateConstructorUsedError;
+ Duration get videoPosition => throw _privateConstructorUsedError;
+ Duration get videoMaxDuration => throw _privateConstructorUsedError;
+ bool get isVideoPlaying => throw _privateConstructorUsedError;
+ List get uploadProcess => throw _privateConstructorUsedError;
+ List get downloadProcess => throw _privateConstructorUsedError;
+ List get deleteProcess => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $MediaPreviewStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $MediaPreviewStateCopyWith<$Res> {
+ factory $MediaPreviewStateCopyWith(
+ MediaPreviewState value, $Res Function(MediaPreviewState) then) =
+ _$MediaPreviewStateCopyWithImpl<$Res, MediaPreviewState>;
+ @useResult
+ $Res call(
+ {Object? error,
+ List medias,
+ int currentIndex,
+ bool showActions,
+ bool isVideoInitialized,
+ bool isVideoBuffering,
+ Duration videoPosition,
+ Duration videoMaxDuration,
+ bool isVideoPlaying,
+ List uploadProcess,
+ List downloadProcess,
+ List deleteProcess});
+}
+
+/// @nodoc
+class _$MediaPreviewStateCopyWithImpl<$Res, $Val extends MediaPreviewState>
+ implements $MediaPreviewStateCopyWith<$Res> {
+ _$MediaPreviewStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? error = freezed,
+ Object? medias = null,
+ Object? currentIndex = null,
+ Object? showActions = null,
+ Object? isVideoInitialized = null,
+ Object? isVideoBuffering = null,
+ Object? videoPosition = null,
+ Object? videoMaxDuration = null,
+ Object? isVideoPlaying = null,
+ Object? uploadProcess = null,
+ Object? downloadProcess = null,
+ Object? deleteProcess = null,
+ }) {
+ return _then(_value.copyWith(
+ error: freezed == error ? _value.error : error,
+ medias: null == medias
+ ? _value.medias
+ : medias // ignore: cast_nullable_to_non_nullable
+ as List,
+ currentIndex: null == currentIndex
+ ? _value.currentIndex
+ : currentIndex // ignore: cast_nullable_to_non_nullable
+ as int,
+ showActions: null == showActions
+ ? _value.showActions
+ : showActions // ignore: cast_nullable_to_non_nullable
+ as bool,
+ isVideoInitialized: null == isVideoInitialized
+ ? _value.isVideoInitialized
+ : isVideoInitialized // ignore: cast_nullable_to_non_nullable
+ as bool,
+ isVideoBuffering: null == isVideoBuffering
+ ? _value.isVideoBuffering
+ : isVideoBuffering // ignore: cast_nullable_to_non_nullable
+ as bool,
+ videoPosition: null == videoPosition
+ ? _value.videoPosition
+ : videoPosition // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ videoMaxDuration: null == videoMaxDuration
+ ? _value.videoMaxDuration
+ : videoMaxDuration // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ isVideoPlaying: null == isVideoPlaying
+ ? _value.isVideoPlaying
+ : isVideoPlaying // ignore: cast_nullable_to_non_nullable
+ as bool,
+ uploadProcess: null == uploadProcess
+ ? _value.uploadProcess
+ : uploadProcess // ignore: cast_nullable_to_non_nullable
+ as List,
+ downloadProcess: null == downloadProcess
+ ? _value.downloadProcess
+ : downloadProcess // ignore: cast_nullable_to_non_nullable
+ as List,
+ deleteProcess: null == deleteProcess
+ ? _value.deleteProcess
+ : deleteProcess // ignore: cast_nullable_to_non_nullable
+ as List,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$MediaPreviewStateImplCopyWith<$Res>
+ implements $MediaPreviewStateCopyWith<$Res> {
+ factory _$$MediaPreviewStateImplCopyWith(_$MediaPreviewStateImpl value,
+ $Res Function(_$MediaPreviewStateImpl) then) =
+ __$$MediaPreviewStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {Object? error,
+ List medias,
+ int currentIndex,
+ bool showActions,
+ bool isVideoInitialized,
+ bool isVideoBuffering,
+ Duration videoPosition,
+ Duration videoMaxDuration,
+ bool isVideoPlaying,
+ List uploadProcess,
+ List downloadProcess,
+ List deleteProcess});
+}
+
+/// @nodoc
+class __$$MediaPreviewStateImplCopyWithImpl<$Res>
+ extends _$MediaPreviewStateCopyWithImpl<$Res, _$MediaPreviewStateImpl>
+ implements _$$MediaPreviewStateImplCopyWith<$Res> {
+ __$$MediaPreviewStateImplCopyWithImpl(_$MediaPreviewStateImpl _value,
+ $Res Function(_$MediaPreviewStateImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? error = freezed,
+ Object? medias = null,
+ Object? currentIndex = null,
+ Object? showActions = null,
+ Object? isVideoInitialized = null,
+ Object? isVideoBuffering = null,
+ Object? videoPosition = null,
+ Object? videoMaxDuration = null,
+ Object? isVideoPlaying = null,
+ Object? uploadProcess = null,
+ Object? downloadProcess = null,
+ Object? deleteProcess = null,
+ }) {
+ return _then(_$MediaPreviewStateImpl(
+ error: freezed == error ? _value.error : error,
+ medias: null == medias
+ ? _value._medias
+ : medias // ignore: cast_nullable_to_non_nullable
+ as List,
+ currentIndex: null == currentIndex
+ ? _value.currentIndex
+ : currentIndex // ignore: cast_nullable_to_non_nullable
+ as int,
+ showActions: null == showActions
+ ? _value.showActions
+ : showActions // ignore: cast_nullable_to_non_nullable
+ as bool,
+ isVideoInitialized: null == isVideoInitialized
+ ? _value.isVideoInitialized
+ : isVideoInitialized // ignore: cast_nullable_to_non_nullable
+ as bool,
+ isVideoBuffering: null == isVideoBuffering
+ ? _value.isVideoBuffering
+ : isVideoBuffering // ignore: cast_nullable_to_non_nullable
+ as bool,
+ videoPosition: null == videoPosition
+ ? _value.videoPosition
+ : videoPosition // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ videoMaxDuration: null == videoMaxDuration
+ ? _value.videoMaxDuration
+ : videoMaxDuration // ignore: cast_nullable_to_non_nullable
+ as Duration,
+ isVideoPlaying: null == isVideoPlaying
+ ? _value.isVideoPlaying
+ : isVideoPlaying // ignore: cast_nullable_to_non_nullable
+ as bool,
+ uploadProcess: null == uploadProcess
+ ? _value._uploadProcess
+ : uploadProcess // ignore: cast_nullable_to_non_nullable
+ as List,
+ downloadProcess: null == downloadProcess
+ ? _value._downloadProcess
+ : downloadProcess // ignore: cast_nullable_to_non_nullable
+ as List,
+ deleteProcess: null == deleteProcess
+ ? _value._deleteProcess
+ : deleteProcess // ignore: cast_nullable_to_non_nullable
+ as List,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$MediaPreviewStateImpl implements _MediaPreviewState {
+ const _$MediaPreviewStateImpl(
+ {this.error,
+ final List medias = const [],
+ this.currentIndex = 0,
+ this.showActions = true,
+ this.isVideoInitialized = false,
+ this.isVideoBuffering = false,
+ this.videoPosition = Duration.zero,
+ this.videoMaxDuration = Duration.zero,
+ this.isVideoPlaying = false,
+ final List uploadProcess = const [],
+ final List downloadProcess = const [],
+ final List deleteProcess = const []})
+ : _medias = medias,
+ _uploadProcess = uploadProcess,
+ _downloadProcess = downloadProcess,
+ _deleteProcess = deleteProcess;
+
+ @override
+ final Object? error;
+ final List _medias;
+ @override
+ @JsonKey()
+ List get medias {
+ if (_medias is EqualUnmodifiableListView) return _medias;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_medias);
+ }
+
+ @override
+ @JsonKey()
+ final int currentIndex;
+ @override
+ @JsonKey()
+ final bool showActions;
+ @override
+ @JsonKey()
+ final bool isVideoInitialized;
+ @override
+ @JsonKey()
+ final bool isVideoBuffering;
+ @override
+ @JsonKey()
+ final Duration videoPosition;
+ @override
+ @JsonKey()
+ final Duration videoMaxDuration;
+ @override
+ @JsonKey()
+ final bool isVideoPlaying;
+ final List _uploadProcess;
+ @override
+ @JsonKey()
+ List get uploadProcess {
+ if (_uploadProcess is EqualUnmodifiableListView) return _uploadProcess;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_uploadProcess);
+ }
+
+ final List _downloadProcess;
+ @override
+ @JsonKey()
+ List get downloadProcess {
+ if (_downloadProcess is EqualUnmodifiableListView) return _downloadProcess;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_downloadProcess);
+ }
+
+ final List _deleteProcess;
+ @override
+ @JsonKey()
+ List get deleteProcess {
+ if (_deleteProcess is EqualUnmodifiableListView) return _deleteProcess;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_deleteProcess);
+ }
+
+ @override
+ String toString() {
+ return 'MediaPreviewState(error: $error, medias: $medias, currentIndex: $currentIndex, showActions: $showActions, isVideoInitialized: $isVideoInitialized, isVideoBuffering: $isVideoBuffering, videoPosition: $videoPosition, videoMaxDuration: $videoMaxDuration, isVideoPlaying: $isVideoPlaying, uploadProcess: $uploadProcess, downloadProcess: $downloadProcess, deleteProcess: $deleteProcess)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$MediaPreviewStateImpl &&
+ const DeepCollectionEquality().equals(other.error, error) &&
+ const DeepCollectionEquality().equals(other._medias, _medias) &&
+ (identical(other.currentIndex, currentIndex) ||
+ other.currentIndex == currentIndex) &&
+ (identical(other.showActions, showActions) ||
+ other.showActions == showActions) &&
+ (identical(other.isVideoInitialized, isVideoInitialized) ||
+ other.isVideoInitialized == isVideoInitialized) &&
+ (identical(other.isVideoBuffering, isVideoBuffering) ||
+ other.isVideoBuffering == isVideoBuffering) &&
+ (identical(other.videoPosition, videoPosition) ||
+ other.videoPosition == videoPosition) &&
+ (identical(other.videoMaxDuration, videoMaxDuration) ||
+ other.videoMaxDuration == videoMaxDuration) &&
+ (identical(other.isVideoPlaying, isVideoPlaying) ||
+ other.isVideoPlaying == isVideoPlaying) &&
+ const DeepCollectionEquality()
+ .equals(other._uploadProcess, _uploadProcess) &&
+ const DeepCollectionEquality()
+ .equals(other._downloadProcess, _downloadProcess) &&
+ const DeepCollectionEquality()
+ .equals(other._deleteProcess, _deleteProcess));
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ const DeepCollectionEquality().hash(error),
+ const DeepCollectionEquality().hash(_medias),
+ currentIndex,
+ showActions,
+ isVideoInitialized,
+ isVideoBuffering,
+ videoPosition,
+ videoMaxDuration,
+ isVideoPlaying,
+ const DeepCollectionEquality().hash(_uploadProcess),
+ const DeepCollectionEquality().hash(_downloadProcess),
+ const DeepCollectionEquality().hash(_deleteProcess));
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$MediaPreviewStateImplCopyWith<_$MediaPreviewStateImpl> get copyWith =>
+ __$$MediaPreviewStateImplCopyWithImpl<_$MediaPreviewStateImpl>(
+ this, _$identity);
+}
+
+abstract class _MediaPreviewState implements MediaPreviewState {
+ const factory _MediaPreviewState(
+ {final Object? error,
+ final List medias,
+ final int currentIndex,
+ final bool showActions,
+ final bool isVideoInitialized,
+ final bool isVideoBuffering,
+ final Duration videoPosition,
+ final Duration videoMaxDuration,
+ final bool isVideoPlaying,
+ final List uploadProcess,
+ final List downloadProcess,
+ final List deleteProcess}) = _$MediaPreviewStateImpl;
+
+ @override
+ Object? get error;
+ @override
+ List get medias;
+ @override
+ int get currentIndex;
+ @override
+ bool get showActions;
+ @override
+ bool get isVideoInitialized;
+ @override
+ bool get isVideoBuffering;
+ @override
+ Duration get videoPosition;
+ @override
+ Duration get videoMaxDuration;
+ @override
+ bool get isVideoPlaying;
+ @override
+ List get uploadProcess;
+ @override
+ List get downloadProcess;
+ @override
+ List get deleteProcess;
+ @override
+ @JsonKey(ignore: true)
+ _$$MediaPreviewStateImplCopyWith<_$MediaPreviewStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/app/lib/ui/flow/media_preview/video_preview_screen.dart b/app/lib/ui/flow/media_preview/video_preview_screen.dart
deleted file mode 100644
index fa98229..0000000
--- a/app/lib/ui/flow/media_preview/video_preview_screen.dart
+++ /dev/null
@@ -1,21 +0,0 @@
-import 'package:cloud_gallery/components/app_page.dart';
-import 'package:flutter/cupertino.dart';
-
-class VideoPreviewScreen extends StatefulWidget {
- const VideoPreviewScreen({super.key});
-
- @override
- State createState() => _VideoPreviewScreenState();
-}
-
-class _VideoPreviewScreenState extends State {
- @override
- Widget build(BuildContext context) {
- return const AppPage(
- title: '',
- body: Center(
- child: Text('Video Preview Screen'),
- ),
- );
- }
-}
diff --git a/app/lib/ui/flow/media_transfer/components/transfer_item.dart b/app/lib/ui/flow/media_transfer/components/transfer_item.dart
new file mode 100644
index 0000000..aff7a6a
--- /dev/null
+++ b/app/lib/ui/flow/media_transfer/components/transfer_item.dart
@@ -0,0 +1,195 @@
+import 'dart:typed_data';
+import 'package:cached_network_image/cached_network_image.dart';
+import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:cloud_gallery/domain/formatter/byte_formatter.dart';
+import 'package:data/models/app_process/app_process.dart';
+import 'package:data/models/media/media.dart';
+import 'package:data/models/media/media_extension.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:style/buttons/action_button.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/indicators/circular_progress_indicator.dart';
+import 'package:style/text/app_text_style.dart';
+
+class ProcessItem extends StatefulWidget {
+ final AppProcess process;
+ final void Function() onCancelTap;
+
+ const ProcessItem(
+ {super.key, required this.process, required this.onCancelTap});
+
+ @override
+ State createState() => _ProcessItemState();
+}
+
+class _ProcessItemState extends State {
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ children: [
+ _buildThumbnailView(context: context),
+ const SizedBox(width: 16),
+ Expanded(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.process.media.name != null &&
+ widget.process.media.name!.trim().isNotEmpty
+ ? widget.process.media.name!
+ : widget.process.media.path,
+ style: AppTextStyles.body.copyWith(
+ color: context.colorScheme.textPrimary,
+ ),
+ overflow: TextOverflow.ellipsis,
+ maxLines: 1,
+ ),
+ const SizedBox(height: 8),
+ if (widget.process.status.isWaiting)
+ Text(
+ context.l10n.waiting_in_queue_text,
+ style: AppTextStyles.body2.copyWith(
+ color: context.colorScheme.textSecondary,
+ ),
+ ),
+ if (widget.process.progress != null && widget.process.status.isProcessing) ...[
+ LinearProgressIndicator(
+ value: widget.process.progress?.percentageInPoint,
+ backgroundColor: context.colorScheme.outline,
+ borderRadius: BorderRadius.circular(4),
+ valueColor: AlwaysStoppedAnimation(
+ context.colorScheme.primary),
+ ),
+ const SizedBox(height: 8),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ '${widget.process.progress?.chunk.formatBytes} ${widget.process.progress?.percentage.toStringAsFixed(0)}%',
+ style: AppTextStyles.body2.copyWith(
+ color: context.colorScheme.textSecondary,
+ ),
+ ),
+ Text(
+ '${widget.process.progress?.total.formatBytes}',
+ style: AppTextStyles.body2.copyWith(
+ color: context.colorScheme.textSecondary,
+ ),
+ ),
+ ],
+ )
+ ]
+ ],
+ ),
+ ),
+ if (widget.process.status.isWaiting)
+ ActionButton(
+ onPressed: widget.onCancelTap,
+ icon: const Icon(CupertinoIcons.xmark),
+ )
+ ],
+ );
+ }
+
+ Widget _buildThumbnailView({required BuildContext context}) {
+ if (widget.process.media.sources.contains(AppMediaSource.local)) {
+ return FutureByteLoader(
+ bytes: widget.process.media.loadThumbnail(size: const Size(100, 100)),
+ builder: (context, bytes) => Container(
+ width: 60,
+ height: 60,
+ decoration: BoxDecoration(
+ color: context.colorScheme.containerHighOnSurface,
+ borderRadius: BorderRadius.circular(4),
+ image: DecorationImage(
+ image: MemoryImage(bytes!),
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ errorWidget: (context, error) => _buildErrorWidget(context),
+ placeholder: (context) => _buildPlaceholder(context: context),
+ );
+ } else {
+ return CachedNetworkImage(
+ imageUrl: widget.process.media.thumbnailLink!,
+ width: 80,
+ height: 80,
+ fit: BoxFit.cover,
+ errorWidget: (context, url, error) => _buildErrorWidget(context),
+ progressIndicatorBuilder: (context, url, progress) =>
+ _buildPlaceholder(
+ context: context,
+ value: progress.progress,
+ ));
+ }
+ }
+
+ Widget _buildPlaceholder(
+ {required BuildContext context,
+ double? value,
+ bool showLoader = true}) =>
+ Container(
+ color: context.colorScheme.containerHighOnSurface,
+ alignment: Alignment.center,
+ child: showLoader ? AppCircularProgressIndicator(value: value) : null,
+ );
+
+ Widget _buildErrorWidget(BuildContext context) => Container(
+ color: context.colorScheme.containerNormalOnSurface,
+ alignment: Alignment.center,
+ child: Icon(
+ CupertinoIcons.exclamationmark_circle,
+ color: context.colorScheme.onPrimary,
+ size: 32,
+ ),
+ );
+}
+
+class FutureByteLoader extends StatefulWidget {
+ final Future bytes;
+ final Widget Function(BuildContext context, Uint8List? bytes) builder;
+ final Widget Function(BuildContext context) placeholder;
+ final Widget Function(BuildContext context, Object? error) errorWidget;
+
+ const FutureByteLoader(
+ {super.key,
+ required this.bytes,
+ required this.builder,
+ required this.placeholder,
+ required this.errorWidget});
+
+ @override
+ State createState() => _FutureByteLoaderState();
+}
+
+class _FutureByteLoaderState extends State {
+ late Future bytes;
+
+ @override
+ void initState() {
+ bytes = widget.bytes;
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return FutureBuilder(
+ future: bytes,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done &&
+ snapshot.hasData) {
+ return widget.builder(context, snapshot.data);
+ } else if (snapshot.hasError) {
+ return widget.errorWidget(context, snapshot.error);
+ } else {
+ return widget.placeholder(context);
+ }
+ },
+ );
+ }
+}
diff --git a/app/lib/ui/flow/media_transfer/media_transfer_screen.dart b/app/lib/ui/flow/media_transfer/media_transfer_screen.dart
new file mode 100644
index 0000000..77a9597
--- /dev/null
+++ b/app/lib/ui/flow/media_transfer/media_transfer_screen.dart
@@ -0,0 +1,149 @@
+import 'package:cloud_gallery/components/error_view.dart';
+import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:cloud_gallery/ui/flow/media_transfer/components/transfer_item.dart';
+import 'package:cloud_gallery/ui/flow/media_transfer/media_transfer_view_model.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:style/buttons/segmented_button.dart';
+import 'package:style/extensions/context_extensions.dart';
+import 'package:style/text/app_text_style.dart';
+import '../../../components/app_page.dart';
+
+class MediaTransferScreen extends ConsumerStatefulWidget {
+ const MediaTransferScreen({super.key});
+
+ @override
+ ConsumerState createState() => _MediaTransferScreenState();
+}
+
+class _MediaTransferScreenState extends ConsumerState {
+ late MediaTransferStateNotifier notifier;
+ late PageController pageController;
+
+ @override
+ void initState() {
+ notifier = ref.read(mediaTransferStateNotifierProvider.notifier);
+ pageController = PageController();
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ pageController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final page = ref.watch(
+ mediaTransferStateNotifierProvider.select((value) => value.page));
+ return AppPage(
+ title: context.l10n.transfer_screen_title,
+ body: SafeArea(
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: SizedBox(
+ width: context.mediaQuerySize.width,
+ child: AppSegmentedButton(
+ segmentTextStyle: AppTextStyles.body,
+ tapTargetSize: MaterialTapTargetSize.padded,
+ segments: [
+ AppButtonSegment(
+ value: 0, label: context.l10n.common_upload),
+ AppButtonSegment(
+ value: 1, label: context.l10n.common_download),
+ ],
+ selected: page,
+ onSelectionChanged: (value) {
+ notifier.onPageChange(value);
+ pageController.animateToPage(
+ value,
+ duration: const Duration(milliseconds: 300),
+ curve: Curves.easeInOut,
+ );
+ },
+ ),
+ ),
+ ),
+ Divider(color: context.colorScheme.outline, height: 0.8),
+ Expanded(
+ child: PageView(
+ controller: pageController,
+ onPageChanged: (value) {
+ notifier.onPageChange(value);
+ },
+ children: [
+ _uploadList(),
+ _downloadList(),
+ ],
+ )),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _uploadList() {
+ return Consumer(builder: (context, ref, child) {
+ final upload = ref.watch(
+ mediaTransferStateNotifierProvider.select((value) => value.upload));
+
+ if (upload.isEmpty) {
+ return ErrorView(
+ title: context.l10n.empty_upload_title,
+ message: context.l10n.empty_upload_message,
+ icon: Icon(Icons.cloud_upload_outlined,
+ size: 100, color: context.colorScheme.containerNormal),
+ );
+ }
+
+ return ListView.separated(
+ padding: const EdgeInsets.all(16),
+ itemCount: upload.length,
+ separatorBuilder: (context, index) => const SizedBox(
+ height: 8,
+ ),
+ itemBuilder: (context, index) => ProcessItem(
+ key: ValueKey(upload[index].id),
+ process: upload[index],
+ onCancelTap: () {
+ notifier.onTerminateProcess(upload[index].id, true);
+ }),
+ );
+ });
+ }
+
+ Widget _downloadList() {
+ return Consumer(builder: (context, ref, child) {
+ final download = ref.watch(
+ mediaTransferStateNotifierProvider.select((value) => value.download),
+ );
+
+ if (download.isEmpty) {
+ return ErrorView(
+ title: context.l10n.empty_download_title,
+ message: context.l10n.empty_download_message,
+ icon: Icon(Icons.cloud_download_outlined,
+ size: 100, color: context.colorScheme.containerNormal),
+ );
+ }
+
+ return ListView.separated(
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ itemCount: download.length,
+ separatorBuilder: (context, index) => const SizedBox(
+ height: 8,
+ ),
+ itemBuilder: (context, index) => ProcessItem(
+ key: ValueKey(download[index].id),
+ process: download[index],
+ onCancelTap: () {
+ notifier.onTerminateProcess(download[index].id, false);
+ },
+ ),
+ );
+ });
+ }
+}
diff --git a/app/lib/ui/flow/media_transfer/media_transfer_view_model.dart b/app/lib/ui/flow/media_transfer/media_transfer_view_model.dart
new file mode 100644
index 0000000..c8885c1
--- /dev/null
+++ b/app/lib/ui/flow/media_transfer/media_transfer_view_model.dart
@@ -0,0 +1,55 @@
+import 'package:data/models/app_process/app_process.dart';
+import 'package:data/repositories/google_drive_process_repo.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'media_transfer_view_model.freezed.dart';
+
+final mediaTransferStateNotifierProvider = StateNotifierProvider.autoDispose<
+ MediaTransferStateNotifier, MediaTransferState>(
+ (ref) => MediaTransferStateNotifier(ref.read(googleDriveProcessRepoProvider)),
+);
+
+class MediaTransferStateNotifier extends StateNotifier {
+ final GoogleDriveProcessRepo _googleDriveProcessRepo;
+
+ MediaTransferStateNotifier(this._googleDriveProcessRepo)
+ : super(const MediaTransferState()) {
+ _googleDriveProcessRepo.addListener(_listenGoogleDriveProcess);
+ }
+
+ void _listenGoogleDriveProcess() {
+ state = state.copyWith(
+ download: _googleDriveProcessRepo.downloadQueue.toList(),
+ upload: _googleDriveProcessRepo.uploadQueue.toList(),
+ );
+ }
+
+ void onPageChange(int value) {
+ state = state.copyWith(page: value);
+ }
+
+ void onTerminateProcess(String id, bool isUploading) {
+ if (isUploading) {
+ _googleDriveProcessRepo.terminateUploadProcess(id);
+ } else {
+ _googleDriveProcessRepo.terminateDownloadProcess(id);
+ }
+ }
+
+ @override
+ void dispose() {
+ _googleDriveProcessRepo.removeListener(_listenGoogleDriveProcess);
+ super.dispose();
+ }
+}
+
+@freezed
+class MediaTransferState with _$MediaTransferState {
+ const factory MediaTransferState({
+ Object? error,
+ @Default([]) List upload,
+ @Default([]) List download,
+ @Default(0) int page,
+ }) = _MediaTransferState;
+}
diff --git a/app/lib/ui/flow/media_transfer/media_transfer_view_model.freezed.dart b/app/lib/ui/flow/media_transfer/media_transfer_view_model.freezed.dart
new file mode 100644
index 0000000..39a57d2
--- /dev/null
+++ b/app/lib/ui/flow/media_transfer/media_transfer_view_model.freezed.dart
@@ -0,0 +1,213 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'media_transfer_view_model.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+/// @nodoc
+mixin _$MediaTransferState {
+ Object? get error => throw _privateConstructorUsedError;
+ List get upload => throw _privateConstructorUsedError;
+ List get download => throw _privateConstructorUsedError;
+ int get page => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $MediaTransferStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $MediaTransferStateCopyWith<$Res> {
+ factory $MediaTransferStateCopyWith(
+ MediaTransferState value, $Res Function(MediaTransferState) then) =
+ _$MediaTransferStateCopyWithImpl<$Res, MediaTransferState>;
+ @useResult
+ $Res call(
+ {Object? error,
+ List upload,
+ List download,
+ int page});
+}
+
+/// @nodoc
+class _$MediaTransferStateCopyWithImpl<$Res, $Val extends MediaTransferState>
+ implements $MediaTransferStateCopyWith<$Res> {
+ _$MediaTransferStateCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? error = freezed,
+ Object? upload = null,
+ Object? download = null,
+ Object? page = null,
+ }) {
+ return _then(_value.copyWith(
+ error: freezed == error ? _value.error : error,
+ upload: null == upload
+ ? _value.upload
+ : upload // ignore: cast_nullable_to_non_nullable
+ as List,
+ download: null == download
+ ? _value.download
+ : download // ignore: cast_nullable_to_non_nullable
+ as List,
+ page: null == page
+ ? _value.page
+ : page // ignore: cast_nullable_to_non_nullable
+ as int,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$MediaTransferStateImplCopyWith<$Res>
+ implements $MediaTransferStateCopyWith<$Res> {
+ factory _$$MediaTransferStateImplCopyWith(_$MediaTransferStateImpl value,
+ $Res Function(_$MediaTransferStateImpl) then) =
+ __$$MediaTransferStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {Object? error,
+ List upload,
+ List download,
+ int page});
+}
+
+/// @nodoc
+class __$$MediaTransferStateImplCopyWithImpl<$Res>
+ extends _$MediaTransferStateCopyWithImpl<$Res, _$MediaTransferStateImpl>
+ implements _$$MediaTransferStateImplCopyWith<$Res> {
+ __$$MediaTransferStateImplCopyWithImpl(_$MediaTransferStateImpl _value,
+ $Res Function(_$MediaTransferStateImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? error = freezed,
+ Object? upload = null,
+ Object? download = null,
+ Object? page = null,
+ }) {
+ return _then(_$MediaTransferStateImpl(
+ error: freezed == error ? _value.error : error,
+ upload: null == upload
+ ? _value._upload
+ : upload // ignore: cast_nullable_to_non_nullable
+ as List,
+ download: null == download
+ ? _value._download
+ : download // ignore: cast_nullable_to_non_nullable
+ as List,
+ page: null == page
+ ? _value.page
+ : page // ignore: cast_nullable_to_non_nullable
+ as int,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$MediaTransferStateImpl implements _MediaTransferState {
+ const _$MediaTransferStateImpl(
+ {this.error,
+ final List upload = const [],
+ final List download = const [],
+ this.page = 0})
+ : _upload = upload,
+ _download = download;
+
+ @override
+ final Object? error;
+ final List _upload;
+ @override
+ @JsonKey()
+ List get upload {
+ if (_upload is EqualUnmodifiableListView) return _upload;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_upload);
+ }
+
+ final List _download;
+ @override
+ @JsonKey()
+ List get download {
+ if (_download is EqualUnmodifiableListView) return _download;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(_download);
+ }
+
+ @override
+ @JsonKey()
+ final int page;
+
+ @override
+ String toString() {
+ return 'MediaTransferState(error: $error, upload: $upload, download: $download, page: $page)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$MediaTransferStateImpl &&
+ const DeepCollectionEquality().equals(other.error, error) &&
+ const DeepCollectionEquality().equals(other._upload, _upload) &&
+ const DeepCollectionEquality().equals(other._download, _download) &&
+ (identical(other.page, page) || other.page == page));
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ const DeepCollectionEquality().hash(error),
+ const DeepCollectionEquality().hash(_upload),
+ const DeepCollectionEquality().hash(_download),
+ page);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$MediaTransferStateImplCopyWith<_$MediaTransferStateImpl> get copyWith =>
+ __$$MediaTransferStateImplCopyWithImpl<_$MediaTransferStateImpl>(
+ this, _$identity);
+}
+
+abstract class _MediaTransferState implements MediaTransferState {
+ const factory _MediaTransferState(
+ {final Object? error,
+ final List upload,
+ final List download,
+ final int page}) = _$MediaTransferStateImpl;
+
+ @override
+ Object? get error;
+ @override
+ List get upload;
+ @override
+ List get download;
+ @override
+ int get page;
+ @override
+ @JsonKey(ignore: true)
+ _$$MediaTransferStateImplCopyWith<_$MediaTransferStateImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/app/lib/ui/navigation/app_router.dart b/app/lib/ui/navigation/app_router.dart
index dcc76b7..4fc3f77 100644
--- a/app/lib/ui/navigation/app_router.dart
+++ b/app/lib/ui/navigation/app_router.dart
@@ -1,11 +1,11 @@
import 'package:cloud_gallery/ui/flow/accounts/accounts_screen.dart';
-import 'package:cloud_gallery/ui/flow/media_preview/image_preview/image_preview_screen.dart';
-import 'package:cloud_gallery/ui/flow/media_preview/video_preview_screen.dart';
+import 'package:cloud_gallery/ui/flow/media_transfer/media_transfer_screen.dart';
import 'package:cloud_gallery/ui/flow/onboard/onboard_screen.dart';
import 'package:data/models/media/media.dart';
import 'package:flutter/cupertino.dart';
import 'package:go_router/go_router.dart';
import '../flow/home/home_screen.dart';
+import '../flow/media_preview/media_preview_screen.dart';
import 'app_route.dart';
class AppRouter {
@@ -24,23 +24,28 @@ class AppRouter {
builder: (context) => const AccountsScreen(),
);
- static AppRoute imagePreview({required AppMedia media}) => AppRoute(
- AppRoutePath.imagePreview,
- builder: (context) => ImagePreviewScreen(media: media),
- );
+ static AppRoute get mediaTransfer => AppRoute(
+ AppRoutePath.transfer,
+ builder: (context) => const MediaTransferScreen(),
+ );
- static AppRoute videoPreview({required String path, required bool isLocal}) =>
+ static AppRoute preview(
+ {required List medias, required String startFrom}) =>
AppRoute(
- AppRoutePath.videoPreview,
- builder: (context) => const VideoPreviewScreen(),
+ AppRoutePath.preview,
+ builder: (context) => MediaPreview(
+ medias: medias,
+ startFrom: startFrom,
+ ),
);
static final routes = [
home.goRoute,
onBoard.goRoute,
accounts.goRoute,
+ mediaTransfer.goRoute,
GoRoute(
- path: AppRoutePath.imagePreview,
+ path: AppRoutePath.preview,
pageBuilder: (context, state) {
return CustomTransitionPage(
opaque: false,
@@ -52,10 +57,6 @@ class AppRouter {
);
},
),
- GoRoute(
- path: AppRoutePath.videoPreview,
- builder: (context, state) => state.widget(context),
- ),
];
}
@@ -63,6 +64,6 @@ class AppRoutePath {
static const home = '/';
static const onBoard = '/on-board';
static const accounts = '/accounts';
- static const imagePreview = '/image_preview';
- static const videoPreview = '/video_preview';
+ static const preview = '/preview';
+ static const transfer = '/transfer';
}
diff --git a/data/.flutter-plugins b/data/.flutter-plugins
index 5cde211..58d16bc 100644
--- a/data/.flutter-plugins
+++ b/data/.flutter-plugins
@@ -4,6 +4,9 @@ google_sign_in_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sig
google_sign_in_ios=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.4/
google_sign_in_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.3+3/
package_info_plus=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/package_info_plus-5.0.1/
+path_provider=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider-2.1.2/
+path_provider_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_android-2.2.2/
+path_provider_foundation=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_foundation-2.3.2/
path_provider_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/
path_provider_windows=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/
photo_manager=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0/
diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies
index 1ebfe74..52c68b3 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":"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":"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":"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_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-03-26 16:38:34.855593","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-04 11:45:07.647168","version":"3.19.3"}
\ No newline at end of file
diff --git a/data/lib/extensions/iterable_extension.dart b/data/lib/extensions/iterable_extension.dart
new file mode 100644
index 0000000..9b91d16
--- /dev/null
+++ b/data/lib/extensions/iterable_extension.dart
@@ -0,0 +1,11 @@
+extension ListExtension on List {
+ void updateWhere(
+ {required bool Function(E element) where,
+ required E Function(E element) update}) {
+ for (var i = 0; i < length; i++) {
+ if (where(elementAt(i))) {
+ this[i] = update(elementAt(i));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/lib/models/app_process/app_process.dart b/data/lib/models/app_process/app_process.dart
new file mode 100644
index 0000000..8ae5c4f
--- /dev/null
+++ b/data/lib/models/app_process/app_process.dart
@@ -0,0 +1,51 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+import '../media/media.dart';
+
+part 'app_process.freezed.dart';
+
+enum AppProcessStatus {
+ waiting,
+ uploading,
+ deleting,
+ downloading,
+ success,
+ failed;
+
+ bool get isProcessing =>
+ this == AppProcessStatus.uploading ||
+ this == AppProcessStatus.deleting ||
+ this == AppProcessStatus.downloading;
+
+ bool get isWaiting => this == AppProcessStatus.waiting;
+
+ bool get isSuccess => this == AppProcessStatus.success;
+
+ bool get isFailed => this == AppProcessStatus.failed;
+}
+
+@freezed
+class AppProcess with _$AppProcess {
+ const factory AppProcess({
+ required String id,
+ required AppMedia media,
+ required AppProcessStatus status,
+ Object? response,
+ @Default(null)
+ AppProcessProgress? progress,
+ }) = _AppProcess;
+}
+
+@freezed
+class AppProcessProgress with _$AppProcessProgress {
+ const factory AppProcessProgress({required int total, required int chunk}) =
+ _AppProcessProgress;
+}
+
+extension AppProcessProgressExtension on AppProcessProgress {
+ /// Get the percentage of the progress 0.0 - 1.0
+ double get percentageInPoint => total == 0 ? 0 : chunk / total;
+
+ /// Get the percentage of the progress 0 - 100
+ double get percentage => percentageInPoint*100;
+}
\ No newline at end of file
diff --git a/data/lib/models/app_process/app_process.freezed.dart b/data/lib/models/app_process/app_process.freezed.dart
new file mode 100644
index 0000000..d7055ed
--- /dev/null
+++ b/data/lib/models/app_process/app_process.freezed.dart
@@ -0,0 +1,377 @@
+// coverage:ignore-file
+// GENERATED CODE - DO NOT MODIFY BY HAND
+// ignore_for_file: type=lint
+// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
+
+part of 'app_process.dart';
+
+// **************************************************************************
+// FreezedGenerator
+// **************************************************************************
+
+T _$identity(T value) => value;
+
+final _privateConstructorUsedError = UnsupportedError(
+ 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models');
+
+/// @nodoc
+mixin _$AppProcess {
+ String get id => throw _privateConstructorUsedError;
+ AppMedia get media => throw _privateConstructorUsedError;
+ AppProcessStatus get status => throw _privateConstructorUsedError;
+ Object? get response => throw _privateConstructorUsedError;
+ AppProcessProgress? get progress => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $AppProcessCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $AppProcessCopyWith<$Res> {
+ factory $AppProcessCopyWith(
+ AppProcess value, $Res Function(AppProcess) then) =
+ _$AppProcessCopyWithImpl<$Res, AppProcess>;
+ @useResult
+ $Res call(
+ {String id,
+ AppMedia media,
+ AppProcessStatus status,
+ Object? response,
+ AppProcessProgress? progress});
+
+ $AppMediaCopyWith<$Res> get media;
+ $AppProcessProgressCopyWith<$Res>? get progress;
+}
+
+/// @nodoc
+class _$AppProcessCopyWithImpl<$Res, $Val extends AppProcess>
+ implements $AppProcessCopyWith<$Res> {
+ _$AppProcessCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? id = null,
+ Object? media = null,
+ Object? status = null,
+ Object? response = freezed,
+ Object? progress = freezed,
+ }) {
+ return _then(_value.copyWith(
+ id: null == id
+ ? _value.id
+ : id // ignore: cast_nullable_to_non_nullable
+ as String,
+ media: null == media
+ ? _value.media
+ : media // ignore: cast_nullable_to_non_nullable
+ as AppMedia,
+ status: null == status
+ ? _value.status
+ : status // ignore: cast_nullable_to_non_nullable
+ as AppProcessStatus,
+ response: freezed == response ? _value.response : response,
+ progress: freezed == progress
+ ? _value.progress
+ : progress // ignore: cast_nullable_to_non_nullable
+ as AppProcessProgress?,
+ ) as $Val);
+ }
+
+ @override
+ @pragma('vm:prefer-inline')
+ $AppMediaCopyWith<$Res> get media {
+ return $AppMediaCopyWith<$Res>(_value.media, (value) {
+ return _then(_value.copyWith(media: value) as $Val);
+ });
+ }
+
+ @override
+ @pragma('vm:prefer-inline')
+ $AppProcessProgressCopyWith<$Res>? get progress {
+ if (_value.progress == null) {
+ return null;
+ }
+
+ return $AppProcessProgressCopyWith<$Res>(_value.progress!, (value) {
+ return _then(_value.copyWith(progress: value) as $Val);
+ });
+ }
+}
+
+/// @nodoc
+abstract class _$$AppProcessImplCopyWith<$Res>
+ implements $AppProcessCopyWith<$Res> {
+ factory _$$AppProcessImplCopyWith(
+ _$AppProcessImpl value, $Res Function(_$AppProcessImpl) then) =
+ __$$AppProcessImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {String id,
+ AppMedia media,
+ AppProcessStatus status,
+ Object? response,
+ AppProcessProgress? progress});
+
+ @override
+ $AppMediaCopyWith<$Res> get media;
+ @override
+ $AppProcessProgressCopyWith<$Res>? get progress;
+}
+
+/// @nodoc
+class __$$AppProcessImplCopyWithImpl<$Res>
+ extends _$AppProcessCopyWithImpl<$Res, _$AppProcessImpl>
+ implements _$$AppProcessImplCopyWith<$Res> {
+ __$$AppProcessImplCopyWithImpl(
+ _$AppProcessImpl _value, $Res Function(_$AppProcessImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? id = null,
+ Object? media = null,
+ Object? status = null,
+ Object? response = freezed,
+ Object? progress = freezed,
+ }) {
+ return _then(_$AppProcessImpl(
+ id: null == id
+ ? _value.id
+ : id // ignore: cast_nullable_to_non_nullable
+ as String,
+ media: null == media
+ ? _value.media
+ : media // ignore: cast_nullable_to_non_nullable
+ as AppMedia,
+ status: null == status
+ ? _value.status
+ : status // ignore: cast_nullable_to_non_nullable
+ as AppProcessStatus,
+ response: freezed == response ? _value.response : response,
+ progress: freezed == progress
+ ? _value.progress
+ : progress // ignore: cast_nullable_to_non_nullable
+ as AppProcessProgress?,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$AppProcessImpl implements _AppProcess {
+ const _$AppProcessImpl(
+ {required this.id,
+ required this.media,
+ required this.status,
+ this.response,
+ this.progress = null});
+
+ @override
+ final String id;
+ @override
+ final AppMedia media;
+ @override
+ final AppProcessStatus status;
+ @override
+ final Object? response;
+ @override
+ @JsonKey()
+ final AppProcessProgress? progress;
+
+ @override
+ String toString() {
+ return 'AppProcess(id: $id, media: $media, status: $status, response: $response, progress: $progress)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$AppProcessImpl &&
+ (identical(other.id, id) || other.id == id) &&
+ (identical(other.media, media) || other.media == media) &&
+ (identical(other.status, status) || other.status == status) &&
+ const DeepCollectionEquality().equals(other.response, response) &&
+ (identical(other.progress, progress) ||
+ other.progress == progress));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, id, media, status,
+ const DeepCollectionEquality().hash(response), progress);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$AppProcessImplCopyWith<_$AppProcessImpl> get copyWith =>
+ __$$AppProcessImplCopyWithImpl<_$AppProcessImpl>(this, _$identity);
+}
+
+abstract class _AppProcess implements AppProcess {
+ const factory _AppProcess(
+ {required final String id,
+ required final AppMedia media,
+ required final AppProcessStatus status,
+ final Object? response,
+ final AppProcessProgress? progress}) = _$AppProcessImpl;
+
+ @override
+ String get id;
+ @override
+ AppMedia get media;
+ @override
+ AppProcessStatus get status;
+ @override
+ Object? get response;
+ @override
+ AppProcessProgress? get progress;
+ @override
+ @JsonKey(ignore: true)
+ _$$AppProcessImplCopyWith<_$AppProcessImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+mixin _$AppProcessProgress {
+ int get total => throw _privateConstructorUsedError;
+ int get chunk => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $AppProcessProgressCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $AppProcessProgressCopyWith<$Res> {
+ factory $AppProcessProgressCopyWith(
+ AppProcessProgress value, $Res Function(AppProcessProgress) then) =
+ _$AppProcessProgressCopyWithImpl<$Res, AppProcessProgress>;
+ @useResult
+ $Res call({int total, int chunk});
+}
+
+/// @nodoc
+class _$AppProcessProgressCopyWithImpl<$Res, $Val extends AppProcessProgress>
+ implements $AppProcessProgressCopyWith<$Res> {
+ _$AppProcessProgressCopyWithImpl(this._value, this._then);
+
+ // ignore: unused_field
+ final $Val _value;
+ // ignore: unused_field
+ final $Res Function($Val) _then;
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? total = null,
+ Object? chunk = null,
+ }) {
+ return _then(_value.copyWith(
+ total: null == total
+ ? _value.total
+ : total // ignore: cast_nullable_to_non_nullable
+ as int,
+ chunk: null == chunk
+ ? _value.chunk
+ : chunk // ignore: cast_nullable_to_non_nullable
+ as int,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$AppProcessProgressImplCopyWith<$Res>
+ implements $AppProcessProgressCopyWith<$Res> {
+ factory _$$AppProcessProgressImplCopyWith(_$AppProcessProgressImpl value,
+ $Res Function(_$AppProcessProgressImpl) then) =
+ __$$AppProcessProgressImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({int total, int chunk});
+}
+
+/// @nodoc
+class __$$AppProcessProgressImplCopyWithImpl<$Res>
+ extends _$AppProcessProgressCopyWithImpl<$Res, _$AppProcessProgressImpl>
+ implements _$$AppProcessProgressImplCopyWith<$Res> {
+ __$$AppProcessProgressImplCopyWithImpl(_$AppProcessProgressImpl _value,
+ $Res Function(_$AppProcessProgressImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? total = null,
+ Object? chunk = null,
+ }) {
+ return _then(_$AppProcessProgressImpl(
+ total: null == total
+ ? _value.total
+ : total // ignore: cast_nullable_to_non_nullable
+ as int,
+ chunk: null == chunk
+ ? _value.chunk
+ : chunk // ignore: cast_nullable_to_non_nullable
+ as int,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$AppProcessProgressImpl implements _AppProcessProgress {
+ const _$AppProcessProgressImpl({required this.total, required this.chunk});
+
+ @override
+ final int total;
+ @override
+ final int chunk;
+
+ @override
+ String toString() {
+ return 'AppProcessProgress(total: $total, chunk: $chunk)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$AppProcessProgressImpl &&
+ (identical(other.total, total) || other.total == total) &&
+ (identical(other.chunk, chunk) || other.chunk == chunk));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, total, chunk);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$AppProcessProgressImplCopyWith<_$AppProcessProgressImpl> get copyWith =>
+ __$$AppProcessProgressImplCopyWithImpl<_$AppProcessProgressImpl>(
+ this, _$identity);
+}
+
+abstract class _AppProcessProgress implements AppProcessProgress {
+ const factory _AppProcessProgress(
+ {required final int total,
+ required final int chunk}) = _$AppProcessProgressImpl;
+
+ @override
+ int get total;
+ @override
+ int get chunk;
+ @override
+ @JsonKey(ignore: true)
+ _$$AppProcessProgressImplCopyWith<_$AppProcessProgressImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/data/lib/models/media/media.dart b/data/lib/models/media/media.dart
index 48b7175..f7d629c 100644
--- a/data/lib/models/media/media.dart
+++ b/data/lib/models/media/media.dart
@@ -1,34 +1,13 @@
-import 'dart:io';
-import 'dart:typed_data';
-import 'dart:ui' show Size;
+import 'dart:async';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:googleapis/drive/v3.dart' as drive show File;
import 'package:photo_manager/photo_manager.dart'
- show AssetEntity, ThumbnailFormat, ThumbnailSize;
+ show AssetEntity;
part 'media.freezed.dart';
part 'media.g.dart';
-enum UploadStatus { uploading, waiting, none, failed, success }
-
-class UploadProgress {
- final String mediaId;
- final UploadStatus status;
-
- UploadProgress({required this.mediaId, required this.status});
-
- @override
- bool operator ==(Object other) {
- return other is UploadProgress &&
- other.mediaId == mediaId &&
- other.status == status;
- }
-
- @override
- int get hashCode => mediaId.hashCode ^ status.hashCode;
-}
-
enum AppMediaType {
other,
image,
@@ -98,6 +77,7 @@ enum AppMediaSource {
class AppMedia with _$AppMedia {
const factory AppMedia({
required String id,
+ String? driveMediaRefId,
String? name,
required String path,
String? thumbnailLink,
@@ -147,6 +127,7 @@ class AppMedia with _$AppMedia {
path: file.description ?? file.thumbnailLink ?? '',
thumbnailLink: file.thumbnailLink,
name: file.name,
+ driveMediaRefId: file.id,
createdTime: file.createdTime,
modifiedTime: file.modifiedTime,
mimeType: file.mimeType,
@@ -191,17 +172,3 @@ class AppMedia with _$AppMedia {
}
}
-extension AppMediaExtension on AppMedia {
- Future get isExist async {
- return await File(path).exists();
- }
-
- Future thumbnailDataWithSize(Size size) async {
- return await AssetEntity(id: id, typeInt: type.index, width: 0, height: 0)
- .thumbnailDataWithSize(
- ThumbnailSize(size.width.toInt(), size.height.toInt()),
- format: ThumbnailFormat.jpeg,
- quality: 70,
- );
- }
-}
diff --git a/data/lib/models/media/media.freezed.dart b/data/lib/models/media/media.freezed.dart
index a5ee01b..15b6e14 100644
--- a/data/lib/models/media/media.freezed.dart
+++ b/data/lib/models/media/media.freezed.dart
@@ -21,6 +21,7 @@ AppMedia _$AppMediaFromJson(Map json) {
/// @nodoc
mixin _$AppMedia {
String get id => throw _privateConstructorUsedError;
+ String? get driveMediaRefId => throw _privateConstructorUsedError;
String? get name => throw _privateConstructorUsedError;
String get path => throw _privateConstructorUsedError;
String? get thumbnailLink => throw _privateConstructorUsedError;
@@ -50,6 +51,7 @@ abstract class $AppMediaCopyWith<$Res> {
@useResult
$Res call(
{String id,
+ String? driveMediaRefId,
String? name,
String path,
String? thumbnailLink,
@@ -81,6 +83,7 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia>
@override
$Res call({
Object? id = null,
+ Object? driveMediaRefId = freezed,
Object? name = freezed,
Object? path = null,
Object? thumbnailLink = freezed,
@@ -102,6 +105,10 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia>
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
+ driveMediaRefId: freezed == driveMediaRefId
+ ? _value.driveMediaRefId
+ : driveMediaRefId // ignore: cast_nullable_to_non_nullable
+ as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
@@ -176,6 +183,7 @@ abstract class _$$AppMediaImplCopyWith<$Res>
@useResult
$Res call(
{String id,
+ String? driveMediaRefId,
String? name,
String path,
String? thumbnailLink,
@@ -205,6 +213,7 @@ class __$$AppMediaImplCopyWithImpl<$Res>
@override
$Res call({
Object? id = null,
+ Object? driveMediaRefId = freezed,
Object? name = freezed,
Object? path = null,
Object? thumbnailLink = freezed,
@@ -226,6 +235,10 @@ class __$$AppMediaImplCopyWithImpl<$Res>
? _value.id
: id // ignore: cast_nullable_to_non_nullable
as String,
+ driveMediaRefId: freezed == driveMediaRefId
+ ? _value.driveMediaRefId
+ : driveMediaRefId // ignore: cast_nullable_to_non_nullable
+ as String?,
name: freezed == name
? _value.name
: name // ignore: cast_nullable_to_non_nullable
@@ -295,6 +308,7 @@ class __$$AppMediaImplCopyWithImpl<$Res>
class _$AppMediaImpl implements _AppMedia {
const _$AppMediaImpl(
{required this.id,
+ this.driveMediaRefId,
this.name,
required this.path,
this.thumbnailLink,
@@ -318,6 +332,8 @@ class _$AppMediaImpl implements _AppMedia {
@override
final String id;
@override
+ final String? driveMediaRefId;
+ @override
final String? name;
@override
final String path;
@@ -356,7 +372,7 @@ class _$AppMediaImpl implements _AppMedia {
@override
String toString() {
- return 'AppMedia(id: $id, name: $name, path: $path, thumbnailLink: $thumbnailLink, displayHeight: $displayHeight, displayWidth: $displayWidth, type: $type, mimeType: $mimeType, createdTime: $createdTime, modifiedTime: $modifiedTime, orientation: $orientation, size: $size, videoDuration: $videoDuration, latitude: $latitude, longitude: $longitude, sources: $sources)';
+ return 'AppMedia(id: $id, driveMediaRefId: $driveMediaRefId, name: $name, path: $path, thumbnailLink: $thumbnailLink, displayHeight: $displayHeight, displayWidth: $displayWidth, type: $type, mimeType: $mimeType, createdTime: $createdTime, modifiedTime: $modifiedTime, orientation: $orientation, size: $size, videoDuration: $videoDuration, latitude: $latitude, longitude: $longitude, sources: $sources)';
}
@override
@@ -365,6 +381,8 @@ class _$AppMediaImpl implements _AppMedia {
(other.runtimeType == runtimeType &&
other is _$AppMediaImpl &&
(identical(other.id, id) || other.id == id) &&
+ (identical(other.driveMediaRefId, driveMediaRefId) ||
+ other.driveMediaRefId == driveMediaRefId) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.path, path) || other.path == path) &&
(identical(other.thumbnailLink, thumbnailLink) ||
@@ -397,6 +415,7 @@ class _$AppMediaImpl implements _AppMedia {
int get hashCode => Object.hash(
runtimeType,
id,
+ driveMediaRefId,
name,
path,
thumbnailLink,
@@ -430,6 +449,7 @@ class _$AppMediaImpl implements _AppMedia {
abstract class _AppMedia implements AppMedia {
const factory _AppMedia(
{required final String id,
+ final String? driveMediaRefId,
final String? name,
required final String path,
final String? thumbnailLink,
@@ -452,6 +472,8 @@ abstract class _AppMedia implements AppMedia {
@override
String get id;
@override
+ String? get driveMediaRefId;
+ @override
String? get name;
@override
String get path;
diff --git a/data/lib/models/media/media.g.dart b/data/lib/models/media/media.g.dart
index ef59dee..5d4b1dc 100644
--- a/data/lib/models/media/media.g.dart
+++ b/data/lib/models/media/media.g.dart
@@ -9,6 +9,7 @@ part of 'media.dart';
_$AppMediaImpl _$$AppMediaImplFromJson(Map json) =>
_$AppMediaImpl(
id: json['id'] as String,
+ driveMediaRefId: json['driveMediaRefId'] as String?,
name: json['name'] as String?,
path: json['path'] as String,
thumbnailLink: json['thumbnailLink'] as String?,
@@ -39,6 +40,7 @@ _$AppMediaImpl _$$AppMediaImplFromJson(Map json) =>
Map _$$AppMediaImplToJson(_$AppMediaImpl instance) =>
{
'id': instance.id,
+ 'driveMediaRefId': instance.driveMediaRefId,
'name': instance.name,
'path': instance.path,
'thumbnailLink': instance.thumbnailLink,
diff --git a/data/lib/models/media/media_extension.dart b/data/lib/models/media/media_extension.dart
new file mode 100644
index 0000000..be7857a
--- /dev/null
+++ b/data/lib/models/media/media_extension.dart
@@ -0,0 +1,63 @@
+import 'dart:async';
+import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+import 'package:photo_manager/photo_manager.dart';
+import 'media.dart';
+
+extension AppMediaExtension on AppMedia {
+ Future get isExist async {
+ return await File(path).exists();
+ }
+
+ Future loadThumbnail({Size size = const Size(300, 300)}) async {
+ var rootToken = RootIsolateToken.instance!;
+ final ThumbNailParameter thumbNailParameter =
+ ThumbNailParameter(rootToken, size, id, type);
+ final bytes = await compute(_loadThumbnailInBackground, thumbNailParameter);
+ return bytes;
+ }
+
+ FutureOr _loadThumbnailInBackground(
+ ThumbNailParameter parameters) async {
+ BackgroundIsolateBinaryMessenger.ensureInitialized(parameters.token);
+ return await AssetEntity(
+ id: parameters.id,
+ typeInt: parameters.type.index,
+ width: 0,
+ height: 0,
+ ).thumbnailDataWithSize(
+ ThumbnailSize(
+ parameters.size.width.toInt(),
+ parameters.size.height.toInt(),
+ ),
+ format: ThumbnailFormat.png,
+ quality: 70,
+ );
+ }
+
+ AppMedia mergeGoogleDriveMedia(AppMedia media){
+ return copyWith(
+ thumbnailLink: media.thumbnailLink,
+ driveMediaRefId: media.driveMediaRefId,
+ sources: sources.toList()..add(AppMediaSource.googleDrive),
+ );
+ }
+
+ bool get isGoogleDriveStored =>
+ sources.contains(AppMediaSource.googleDrive) && sources.length == 1;
+
+ bool get isLocalStored =>
+ sources.contains(AppMediaSource.local) && sources.length == 1;
+
+ bool get isCommonStored => sources.length > 1;
+}
+
+class ThumbNailParameter {
+ final RootIsolateToken token;
+ final Size size;
+ final String id;
+ final AppMediaType type;
+
+ ThumbNailParameter(this.token, this.size, this.id, this.type);
+}
diff --git a/data/lib/repositories/google_drive_process_repo.dart b/data/lib/repositories/google_drive_process_repo.dart
new file mode 100644
index 0000000..3431d77
--- /dev/null
+++ b/data/lib/repositories/google_drive_process_repo.dart
@@ -0,0 +1,231 @@
+import 'dart:async';
+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:flutter/cupertino.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../errors/app_error.dart';
+import '../models/media/media.dart';
+
+final googleDriveProcessRepoProvider = Provider((ref) {
+ return GoogleDriveProcessRepo(
+ ref.read(googleDriveServiceProvider),
+ ref.read(localMediaServiceProvider),
+ );
+});
+
+class GoogleDriveProcessRepo extends ChangeNotifier {
+ final GoogleDriveService _googleDriveService;
+ final LocalMediaService _localMediaService;
+
+ final List _uploadQueue = [];
+ final List _deleteQueue = [];
+ final List _downloadQueue = [];
+
+ bool _uploadQueueRunning = false;
+ bool _deleteQueueRunning = false;
+ bool _downloadQueueRunning = false;
+
+ List get uploadQueue => _uploadQueue;
+
+ List get deleteQueue => _deleteQueue;
+
+ List get downloadQueue => _downloadQueue;
+
+ String? _backUpFolderID;
+
+ GoogleDriveProcessRepo(this._googleDriveService, this._localMediaService);
+
+ void setBackUpFolderId(String? backUpFolderId) async {
+ _backUpFolderID = backUpFolderId;
+ }
+
+ void uploadMediasInGoogleDrive({required List medias}) {
+ _uploadQueue.addAll(medias.map((media) => AppProcess(
+ id: media.id, media: media, status: AppProcessStatus.waiting)));
+ notifyListeners();
+ if (!_uploadQueueRunning) _startUploadQueueLoop();
+ }
+
+ Future _startUploadQueueLoop() async {
+ _uploadQueueRunning = true;
+ while (_uploadQueue.firstOrNull != null) {
+ await _uploadInGoogleDrive(_uploadQueue[0]);
+ _uploadQueue.removeAt(0);
+ notifyListeners();
+ }
+ _uploadQueueRunning = false;
+ }
+
+ Future _uploadInGoogleDrive(AppProcess process) async {
+ try {
+ _uploadQueue.updateWhere(
+ where: (element) => element.id == process.id,
+ update: (element) =>
+ element.copyWith(status: AppProcessStatus.uploading),
+ );
+ notifyListeners();
+
+ _backUpFolderID ??= await _googleDriveService.getBackupFolderId();
+
+ final res = await _googleDriveService.uploadInGoogleDrive(
+ folderID: _backUpFolderID!,
+ media: process.media,
+ onProgress: (total, chunk) {
+ _uploadQueue.updateWhere(
+ where: (element) => element.id == process.id,
+ update: (element) => element.copyWith(
+ progress: AppProcessProgress(total: total, chunk: chunk)),
+ );
+ notifyListeners();
+ },
+ );
+ _uploadQueue.updateWhere(
+ where: (element) => element.id == process.id,
+ update: (element) => element.copyWith(
+ status: AppProcessStatus.success,
+ response: res,
+ ),
+ );
+ } catch (error) {
+ if (error is BackUpFolderNotFound) {
+ _backUpFolderID = await _googleDriveService.getBackupFolderId();
+ _uploadInGoogleDrive(process);
+ return;
+ }
+ _uploadQueue.updateWhere(
+ where: (element) => element.id == process.id,
+ update: (element) => element.copyWith(status: AppProcessStatus.failed),
+ );
+ } finally {
+ notifyListeners();
+ }
+ }
+
+ void deleteMediasFromGoogleDrive({required List medias}) {
+ _deleteQueue.addAll(medias.map((media) => AppProcess(
+ id: media.id, media: media, status: AppProcessStatus.waiting)));
+ notifyListeners();
+ if (!_deleteQueueRunning) _startDeleteQueueLoop();
+ }
+
+ Future _startDeleteQueueLoop() async {
+ _deleteQueueRunning = true;
+ while (_deleteQueue.firstOrNull != null) {
+ await _deleteFromGoogleDrive(_deleteQueue[0]);
+ _deleteQueue.removeAt(0);
+ notifyListeners();
+ }
+ _deleteQueueRunning = false;
+ }
+
+ Future _deleteFromGoogleDrive(AppProcess process) async {
+ try {
+ _deleteQueue.updateWhere(
+ where: (element) => element.id == process.id,
+ update: (element) =>
+ element.copyWith(status: AppProcessStatus.deleting),
+ );
+ notifyListeners();
+ await _googleDriveService.deleteMedia(process.media.driveMediaRefId!);
+ _deleteQueue.updateWhere(
+ where: (element) => element.id == process.id,
+ update: (element) => element.copyWith(status: AppProcessStatus.success),
+ );
+ } catch (error) {
+ _deleteQueue.updateWhere(
+ where: (element) => element.id == process.id,
+ update: (element) => element.copyWith(status: AppProcessStatus.failed),
+ );
+ } finally {
+ notifyListeners();
+ }
+ }
+
+ void downloadMediasFromGoogleDrive({required List medias}) {
+ _downloadQueue.addAll(medias.map((media) => AppProcess(
+ id: media.id, media: media, status: AppProcessStatus.waiting)));
+ notifyListeners();
+ if (!_downloadQueueRunning) _startDownloadQueueLoop();
+ }
+
+ Future