diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index 17758ff..81d8549 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -58,6 +58,7 @@ "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", @@ -65,6 +66,11 @@ "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." + "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/error_view.dart b/app/lib/components/error_view.dart index 700dbb2..b6e3609 100644 --- a/app/lib/components/error_view.dart +++ b/app/lib/components/error_view.dart @@ -42,15 +42,15 @@ class ErrorView extends StatelessWidget { color: context.colorScheme.containerHighOnSurface, size: 100, ), - const SizedBox(height: 20), + const SizedBox(height: 40), Text(title, - style: AppTextStyles.subtitle2.copyWith( + style: AppTextStyles.subtitle1.copyWith( color: foregroundColor?? context.colorScheme.textPrimary, )), const SizedBox(height: 20), Text( message, - style: AppTextStyles.body2.copyWith( + style: AppTextStyles.subtitle2.copyWith( color: foregroundColor ??context.colorScheme.textSecondary, ), textAlign: TextAlign.center, diff --git a/app/lib/domain/extensions/media_list_extension.dart b/app/lib/domain/extensions/media_list_extension.dart index 738981e..6b7cbc8 100644 --- a/app/lib/domain/extensions/media_list_extension.dart +++ b/app/lib/domain/extensions/media_list_extension.dart @@ -1,8 +1,9 @@ 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 MediaListHelper on List { +extension MediaListExtension on List { void removeGoogleDriveRefFromMedias({List? removeFromIds}) { for (int index = 0; index < length; index++) { if (this[index].isGoogleDriveStored && @@ -20,28 +21,27 @@ extension MediaListHelper on List { } } - void addGoogleDriveRefInMedia( - {required List process, required List processIds}) { + void addGoogleDriveRefInMedias( + {required List process, List? processIds}) { + processIds ??= process.map((e) => e.id).toList(); updateWhere( - where: (media) => processIds.contains(media.id), + where: (media) => processIds?.contains(media.id) ?? false, update: (media) { final res = process .where((element) => element.id == media.id) .first .response as AppMedia?; - return media.copyWith( - thumbnailLink: res?.thumbnailLink, - driveMediaRefId: res?.id, - sources: media.sources.toList()..add(AppMediaSource.googleDrive), - ); + if (res == null) return media; + return media.margeGoogleDriveMedia(res); }, ); } - void addLocalRefInMedias( - {required List process, required List processIds}) { + void replaceMediaRefInMedias( + {required List process, List? processIds}) { + processIds ??= process.map((e) => e.id).toList(); updateWhere( - where: (media) => processIds.contains(media.id), + where: (media) => processIds?.contains(media.id) ?? false, update: (media) { final res = process .where((element) => element.id == media.id) @@ -49,11 +49,7 @@ extension MediaListHelper on List { .response as AppMedia?; if (res == null) return media; - return res.copyWith( - thumbnailLink: media.thumbnailLink, - driveMediaRefId: media.id, - sources: res.sources.toList()..add(AppMediaSource.googleDrive), - ); + return res; }, ); } 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 b1ed149..12efc36 100644 --- a/app/lib/ui/flow/home/components/app_media_item.dart +++ b/app/lib/ui/flow/home/components/app_media_item.dart @@ -1,42 +1,17 @@ import 'dart:async'; -import 'dart:ui'; +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/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:style/extensions/context_extensions.dart'; import 'package:style/indicators/circular_progress_indicator.dart'; import 'package:style/text/app_text_style.dart'; import '../../../../domain/assets/assets_paths.dart'; import 'package:style/animations/item_selector.dart'; -import 'package:photo_manager/photo_manager.dart' - show AssetEntity, ThumbnailFormat, ThumbnailSize; - -class ThumbNailParameter { - final RootIsolateToken token; - final Size size; - final String id; - final AppMediaType type; - - ThumbNailParameter(this.token,this.size, this.id, this.type); -} - -FutureOr thumbnailDataWithSize(ThumbNailParameter thumbNailParameter) async { - BackgroundIsolateBinaryMessenger.ensureInitialized(thumbNailParameter.token); - - return await AssetEntity(id: thumbNailParameter.id, typeInt: thumbNailParameter.type.index, width: 0, height: 0) - .thumbnailDataWithSize( - ThumbnailSize(thumbNailParameter.size.width.toInt(), thumbNailParameter.size.height.toInt()), - format: ThumbnailFormat.png, - quality: 70, - ); -} - - class AppMediaItem extends StatefulWidget { final AppMedia media; @@ -60,29 +35,16 @@ class AppMediaItem extends StatefulWidget { class _AppMediaItemState extends State with AutomaticKeepAliveClientMixin { - late Future thumbnailByte; + late Future thumbnailByte; @override void initState() { if (widget.media.sources.contains(AppMediaSource.local)) { - // _loadImage(); + thumbnailByte = widget.media.loadThumbnail(); } super.initState(); } - Future _loadImage() async { - var rootToken = RootIsolateToken.instance!; - final ThumbNailParameter thumbNailParameter= ThumbNailParameter(rootToken,const Size(300,300), widget.media.id, widget.media.type); - final bytes=await compute( - thumbnailDataWithSize, thumbNailParameter - ); - - return bytes; - //thumbnailByte = widget.media.thumbnailDataWithSize(const Size(300, 300)); - } - - - @override Widget build(BuildContext context) { super.build(context); @@ -106,8 +68,6 @@ class _AppMediaItemState extends State ); } - - Widget _videoDuration(BuildContext context) => Align( alignment: Alignment.bottomRight, child: _BackgroundContainer( @@ -135,7 +95,7 @@ class _AppMediaItemState extends State {required BuildContext context, required BoxConstraints constraints}) { if (widget.media.sources.contains(AppMediaSource.local)) { return FutureBuilder( - future: _loadImage(), + future: thumbnailByte, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done && snapshot.hasData) { @@ -157,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, 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 c3c2090..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,7 +1,9 @@ import 'dart:async'; import 'package:cloud_gallery/domain/extensions/map_extensions.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'; @@ -62,7 +64,7 @@ class HomeViewStateNotifier extends StateNotifier _backUpFolderId = null; _uploadedMedia.clear(); state = state.copyWith( - medias: removeGoogleDriveRefFromMedias(medias: state.medias), + medias: removeGoogleDriveRefFromMediaMap(medias: state.medias), ); } }); @@ -81,7 +83,7 @@ class HomeViewStateNotifier extends StateNotifier if (successUploads.isNotEmpty) { state = state.copyWith( - medias: addGoogleDriveMediaRef( + medias: addGoogleDriveRefInMediaMap( medias: state.medias, process: successUploads.toList(), )); @@ -89,7 +91,7 @@ class HomeViewStateNotifier extends StateNotifier if (successDeletes.isNotEmpty) { state = state.copyWith( - medias: removeGoogleDriveRefFromMedias( + medias: removeGoogleDriveRefFromMediaMap( medias: state.medias, removeFromIds: successDeletes.toList(), )); @@ -97,7 +99,7 @@ class HomeViewStateNotifier extends StateNotifier if (successDownloads.isNotEmpty) { state = state.copyWith( - medias: addLocalMediaRef( + medias: replaceMediaRefInMediaMap( medias: state.medias, process: successDownloads.toList(), )); @@ -221,10 +223,9 @@ class HomeViewStateNotifier extends StateNotifier state = state.copyWith( medias: sortMedias(medias: [ ...mergeCommonMedia( - localMedias: removeGoogleDriveRefFromMedias(medias: state.medias) - .values - .expand((element) => element) - .toList(), + localMedias: + state.medias.values.expand((element) => element).toList() + ..removeGoogleDriveRefFromMedias(), googleDriveMedias: uploadedMedia, ), ...googleDriveMedia 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 index 534286c..1e3c670 100644 --- a/app/lib/ui/flow/home/home_view_model_helper_mixin.dart +++ b/app/lib/ui/flow/home/home_view_model_helper_mixin.dart @@ -1,8 +1,9 @@ +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/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'; mixin HomeViewModelHelperMixin { List mergeCommonMedia({ @@ -26,11 +27,7 @@ mixin HomeViewModelHelperMixin { .forEach((googleDriveMedia) { localMedias.removeWhere((media) => media.id == localMedia.id); - mergedMedias.add(localMedia.copyWith( - sources: [AppMediaSource.local, AppMediaSource.googleDrive], - thumbnailLink: googleDriveMedia.thumbnailLink, - driveMediaRefId: googleDriveMedia.id, - )); + mergedMedias.add(localMedia.margeGoogleDriveMedia(googleDriveMedia)); }); } @@ -47,29 +44,16 @@ mixin HomeViewModelHelperMixin { ); } - Map> removeGoogleDriveRefFromMedias( + Map> removeGoogleDriveRefFromMediaMap( {required Map> medias, List? removeFromIds}) { - return medias.map((key, mediaList) { - for (int index = 0; index < mediaList.length; index++) { - if (mediaList[index].isGoogleDriveStored && - (removeFromIds?.contains(mediaList[index].id) ?? true)) { - mediaList.removeAt(index); - } else if (mediaList[index].isCommonStored && - (removeFromIds?.contains(mediaList[index].id) ?? true)) { - mediaList[index] = mediaList[index].copyWith( - sources: mediaList[index].sources.toList() - ..remove(AppMediaSource.googleDrive), - thumbnailLink: null, - driveMediaRefId: null, - ); - } - } - return MapEntry(key, mediaList); + return medias.map((key, value) { + return MapEntry(key, + value..removeGoogleDriveRefFromMedias(removeFromIds: removeFromIds)); }); } - Map> addGoogleDriveMediaRef({ + Map> addGoogleDriveRefInMediaMap({ required Map> medias, required List process, }) { @@ -78,50 +62,19 @@ mixin HomeViewModelHelperMixin { return MapEntry( key, value - ..updateWhere( - where: (media) => processIds.contains(media.id), - update: (media) { - final res = process - .where((element) => element.id == media.id) - .first - .response as AppMedia?; - return media.copyWith( - thumbnailLink: res?.thumbnailLink, - driveMediaRefId: res?.id, - sources: media.sources.toList() - ..add(AppMediaSource.googleDrive), - ); - }, - )); + ..addGoogleDriveRefInMedias( + process: process, processIds: processIds)); }); } - Map> addLocalMediaRef({ + 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 - ..updateWhere( - where: (media) => processIds.contains(media.id), - update: (media) { - final res = process - .where((element) => element.id == media.id) - .first - .response as AppMedia?; - - if (res == null) return media; - return res.copyWith( - thumbnailLink: media.thumbnailLink, - driveMediaRefId: media.id, - sources: res.sources.toList() - ..add(AppMediaSource.googleDrive), - ); - }, - )); + 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 index 7fea5f3..48df419 100644 --- a/app/lib/ui/flow/media_preview/components/download_require_view.dart +++ b/app/lib/ui/flow/media_preview/components/download_require_view.dart @@ -1,12 +1,23 @@ +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; - const DownloadRequireView({super.key, required this.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) { @@ -24,17 +35,59 @@ class DownloadRequireView extends StatelessWidget { ), ), Container( + height: double.infinity, + width: double.infinity, color: Colors.black38, - child: ErrorView( + ), + 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 ?? "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: "Download Required", - message: - "To watch the video, simply download it first. Tap the download button to begin.", - action: ErrorViewAction(title: "Download", onPressed: () {}), + 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/top_bar.dart b/app/lib/ui/flow/media_preview/components/top_bar.dart index 311fbe9..2c5cc58 100644 --- a/app/lib/ui/flow/media_preview/components/top_bar.dart +++ b/app/lib/ui/flow/media_preview/components/top_bar.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; -import 'package:data/models/media/media.dart'; +import 'package:data/models/media/media_extension.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; diff --git a/app/lib/ui/flow/media_preview/media_preview_screen.dart b/app/lib/ui/flow/media_preview/media_preview_screen.dart index a69fcfe..5e70c34 100644 --- a/app/lib/ui/flow/media_preview/media_preview_screen.dart +++ b/app/lib/ui/flow/media_preview/media_preview_screen.dart @@ -10,6 +10,7 @@ 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'; @@ -135,9 +136,8 @@ class _MediaPreviewState extends ConsumerState { context.pop(); }, child: (progress) => AppPage( - backgroundColor: progress == 0 - ? context.colorScheme.surface - : Colors.transparent, + backgroundColor: + progress == 0 ? context.colorScheme.surface : Colors.transparent, body: Stack( children: [ GestureDetector( @@ -186,7 +186,7 @@ class _MediaPreviewState extends ConsumerState { }), ); } else if (media.type.isVideo && media.isGoogleDriveStored) { - return DownloadRequireView(media: media); + return _googleDriveVideoView(context: context, media: media); } else if (media.type.isImage) { return ImagePreview(media: media); } else { @@ -197,6 +197,24 @@ class _MediaPreviewState extends ConsumerState { } } + 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 ({ @@ -263,4 +281,3 @@ class _MediaPreviewState extends ConsumerState { ); }); } - diff --git a/app/lib/ui/flow/media_preview/media_preview_view_model.dart b/app/lib/ui/flow/media_preview/media_preview_view_model.dart index 38cf592..efef08f 100644 --- a/app/lib/ui/flow/media_preview/media_preview_view_model.dart +++ b/app/lib/ui/flow/media_preview/media_preview_view_model.dart @@ -1,4 +1,7 @@ +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'; @@ -12,6 +15,7 @@ final mediaPreviewStateNotifierProvider = StateNotifierProvider.family (ref, initial) => MediaPreviewStateNotifier( ref.read(localMediaServiceProvider), ref.read(googleDriveServiceProvider), + ref.read(googleDriveProcessRepoProvider), initial, ), ); @@ -19,10 +23,57 @@ final mediaPreviewStateNotifierProvider = StateNotifierProvider.family class MediaPreviewStateNotifier extends StateNotifier { final LocalMediaService _localMediaService; final GoogleDriveService _googleDriveService; + final GoogleDriveProcessRepo _googleDriveProcessRepo; MediaPreviewStateNotifier(this._localMediaService, this._googleDriveService, - MediaPreviewState initialState) - : super(initialState); + 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); @@ -48,31 +99,40 @@ class MediaPreviewStateNotifier extends StateNotifier { } } + Future downloadMediaFromGoogleDrive({required AppMedia media})async { + _googleDriveProcessRepo.downloadMediasFromGoogleDrive(medias: [media]); + } - void updateVideoPosition(Duration position) { - if(state.videoPosition == position) return; + void updateVideoPosition(Duration position) { + if (state.videoPosition == position) return; state = state.copyWith(videoPosition: position); } void updateVideoPlaying(bool isPlaying) { - if(state.isVideoPlaying == isPlaying) return; + if (state.isVideoPlaying == isPlaying) return; state = state.copyWith(isVideoPlaying: isPlaying); } void updateVideoBuffering(bool isBuffering) { - if(state.isVideoBuffering == isBuffering) return; + if (state.isVideoBuffering == isBuffering) return; state = state.copyWith(isVideoBuffering: isBuffering); } void updateVideoInitialized(bool isInitialized) { - if(state.isVideoInitialized == isInitialized) return; + if (state.isVideoInitialized == isInitialized) return; state = state.copyWith(isVideoInitialized: isInitialized); } void updateVideoMaxDuration(Duration maxDuration) { - if(state.videoMaxDuration == maxDuration) return; + if (state.videoMaxDuration == maxDuration) return; state = state.copyWith(videoMaxDuration: maxDuration); } + + @override + void dispose() { + _googleDriveProcessRepo.removeListener(_listenGoogleDriveProcessUpdates); + super.dispose(); + } } @freezed @@ -87,5 +147,8 @@ class MediaPreviewState with _$MediaPreviewState { @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 index 44c8737..d55aae7 100644 --- 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 @@ -25,6 +25,9 @@ mixin _$MediaPreviewState { 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 => @@ -46,7 +49,10 @@ abstract class $MediaPreviewStateCopyWith<$Res> { bool isVideoBuffering, Duration videoPosition, Duration videoMaxDuration, - bool isVideoPlaying}); + bool isVideoPlaying, + List uploadProcess, + List downloadProcess, + List deleteProcess}); } /// @nodoc @@ -71,6 +77,9 @@ class _$MediaPreviewStateCopyWithImpl<$Res, $Val extends MediaPreviewState> 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, @@ -106,6 +115,18 @@ class _$MediaPreviewStateCopyWithImpl<$Res, $Val extends MediaPreviewState> ? _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); } } @@ -127,7 +148,10 @@ abstract class _$$MediaPreviewStateImplCopyWith<$Res> bool isVideoBuffering, Duration videoPosition, Duration videoMaxDuration, - bool isVideoPlaying}); + bool isVideoPlaying, + List uploadProcess, + List downloadProcess, + List deleteProcess}); } /// @nodoc @@ -150,6 +174,9 @@ class __$$MediaPreviewStateImplCopyWithImpl<$Res> 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, @@ -185,6 +212,18 @@ class __$$MediaPreviewStateImplCopyWithImpl<$Res> ? _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, )); } } @@ -201,8 +240,14 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { this.isVideoBuffering = false, this.videoPosition = Duration.zero, this.videoMaxDuration = Duration.zero, - this.isVideoPlaying = false}) - : _medias = medias; + 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; @@ -236,10 +281,36 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { @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)'; + 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 @@ -262,7 +333,13 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { (identical(other.videoMaxDuration, videoMaxDuration) || other.videoMaxDuration == videoMaxDuration) && (identical(other.isVideoPlaying, isVideoPlaying) || - 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 @@ -276,7 +353,10 @@ class _$MediaPreviewStateImpl implements _MediaPreviewState { isVideoBuffering, videoPosition, videoMaxDuration, - isVideoPlaying); + isVideoPlaying, + const DeepCollectionEquality().hash(_uploadProcess), + const DeepCollectionEquality().hash(_downloadProcess), + const DeepCollectionEquality().hash(_deleteProcess)); @JsonKey(ignore: true) @override @@ -296,7 +376,10 @@ abstract class _MediaPreviewState implements MediaPreviewState { final bool isVideoBuffering, final Duration videoPosition, final Duration videoMaxDuration, - final bool isVideoPlaying}) = _$MediaPreviewStateImpl; + final bool isVideoPlaying, + final List uploadProcess, + final List downloadProcess, + final List deleteProcess}) = _$MediaPreviewStateImpl; @override Object? get error; @@ -317,6 +400,12 @@ abstract class _MediaPreviewState implements MediaPreviewState { @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_transfer/components/transfer_item.dart b/app/lib/ui/flow/media_transfer/components/transfer_item.dart index 4486d05..5e6ddf7 100644 --- a/app/lib/ui/flow/media_transfer/components/transfer_item.dart +++ b/app/lib/ui/flow/media_transfer/components/transfer_item.dart @@ -4,6 +4,7 @@ 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'; @@ -12,13 +13,19 @@ 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 StatelessWidget { +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( @@ -31,10 +38,10 @@ class ProcessItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - process.media.name != null && - process.media.name!.trim().isNotEmpty - ? process.media.name! - : process.media.path, + 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, ), @@ -42,16 +49,16 @@ class ProcessItem extends StatelessWidget { maxLines: 1, ), const SizedBox(height: 8), - if (process.status.isWaiting) + if (widget.process.status.isWaiting) Text( context.l10n.waiting_in_queue_text, style: AppTextStyles.body2.copyWith( color: context.colorScheme.textSecondary, ), ), - if (process.progress != null && process.status.isProcessing) ...[ + if (widget.process.progress != null && widget.process.status.isProcessing) ...[ LinearProgressIndicator( - value: process.progress?.percentageInPoint, + value: widget.process.progress?.percentageInPoint, backgroundColor: context.colorScheme.outline, borderRadius: BorderRadius.circular(4), valueColor: AlwaysStoppedAnimation( @@ -62,13 +69,13 @@ class ProcessItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '${process.progress?.chunk.formatBytes} ${process.progress?.percentage.toStringAsFixed(2)}%', + '${widget.process.progress?.chunk.formatBytes} ${widget.process.progress?.percentage.toStringAsFixed(2)}%', style: AppTextStyles.body2.copyWith( color: context.colorScheme.textSecondary, ), ), Text( - '${process.progress?.total.formatBytes}', + '${widget.process.progress?.total.formatBytes}', style: AppTextStyles.body2.copyWith( color: context.colorScheme.textSecondary, ), @@ -79,9 +86,9 @@ class ProcessItem extends StatelessWidget { ], ), ), - if (process.status.isWaiting) + if (widget.process.status.isWaiting) ActionButton( - onPressed: onCancelTap, + onPressed: widget.onCancelTap, icon: const Icon(CupertinoIcons.xmark), ) ], @@ -89,9 +96,9 @@ class ProcessItem extends StatelessWidget { } Widget _buildThumbnailView({required BuildContext context}) { - if (process.media.sources.contains(AppMediaSource.local)) { + if (widget.process.media.sources.contains(AppMediaSource.local)) { return FutureByteLoader( - bytes: process.media.thumbnailDataWithSize(const Size(100, 100)), + bytes: widget.process.media.loadThumbnail(size: const Size(100, 100)), builder: (context, bytes) => Container( width: 60, height: 60, @@ -109,7 +116,7 @@ class ProcessItem extends StatelessWidget { ); } else { return CachedNetworkImage( - imageUrl: process.media.thumbnailLink!, + imageUrl: widget.process.media.thumbnailLink!, width: 80, height: 80, fit: BoxFit.cover, diff --git a/style/lib/indicators/circular_progress_indicator.dart b/style/lib/indicators/circular_progress_indicator.dart index bb62b68..9891b65 100644 --- a/style/lib/indicators/circular_progress_indicator.dart +++ b/style/lib/indicators/circular_progress_indicator.dart @@ -3,14 +3,18 @@ import 'package:style/extensions/context_extensions.dart'; class AppCircularProgressIndicator extends StatelessWidget { final Color? color; + final Color? backgroundColor; final double? value; + final double? strokeWidth; final double size; const AppCircularProgressIndicator({ super.key, this.color, this.size = 32, + this.backgroundColor, this.value, + this.strokeWidth, }); @override @@ -20,11 +24,12 @@ class AppCircularProgressIndicator extends StatelessWidget { height: size, child: value != null ? CircularProgressIndicator( - strokeWidth: size / 8, + strokeWidth: strokeWidth ?? size / 8, value: value, valueColor: AlwaysStoppedAnimation( color ?? context.colorScheme.primary, ), + backgroundColor: backgroundColor, strokeCap: StrokeCap.round, ) : CircularProgressIndicator.adaptive( @@ -32,6 +37,7 @@ class AppCircularProgressIndicator extends StatelessWidget { valueColor: AlwaysStoppedAnimation( color ?? context.colorScheme.primary, ), + backgroundColor: backgroundColor, strokeCap: StrokeCap.round, ), );