diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml
index f48099f..6bccb1c 100644
--- a/.idea/libraries/Dart_Packages.xml
+++ b/.idea/libraries/Dart_Packages.xml
@@ -1014,7 +1014,7 @@
-
+
@@ -1252,7 +1252,7 @@
-
+
diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml
index 8f6f0be..5371787 100644
--- a/.idea/libraries/Flutter_Plugins.xml
+++ b/.idea/libraries/Flutter_Plugins.xml
@@ -22,13 +22,13 @@
-
+
diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb
index e55366b..28f1489 100644
--- a/app/assets/locales/app_en.arb
+++ b/app/assets/locales/app_en.arb
@@ -10,10 +10,15 @@
"common_google_drive": "Google Drive",
"common_term_and_condition": "Terms and Conditions",
"common_privacy_policy": "Privacy Policy",
+ "common_today": "Today",
+ "common_yesterday": "Yesterday",
+ "common_tomorrow": "Tomorrow",
"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!",
"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! 🚀",
diff --git a/app/lib/components/app_page.dart b/app/lib/components/app_page.dart
index 4839f8f..e91d72f 100644
--- a/app/lib/components/app_page.dart
+++ b/app/lib/components/app_page.dart
@@ -9,8 +9,11 @@ class AppPage extends StatelessWidget {
final Widget? leading;
final Widget? floatingActionButton;
final Widget? body;
+ final Widget Function(BuildContext context)? bodyBuilder;
final bool automaticallyImplyLeading;
final bool? resizeToAvoidBottomInset;
+ final Color? backgroundColor;
+ final Color? barBackgroundColor;
const AppPage({
super.key,
@@ -21,7 +24,10 @@ class AppPage extends StatelessWidget {
this.body,
this.floatingActionButton,
this.resizeToAvoidBottomInset,
+ this.bodyBuilder,
this.automaticallyImplyLeading = true,
+ this.barBackgroundColor,
+ this.backgroundColor,
});
@override
@@ -39,6 +45,7 @@ class AppPage extends StatelessWidget {
leading == null
? null
: CupertinoNavigationBar(
+ backgroundColor: barBackgroundColor,
leading: leading,
middle: titleWidget ?? _title(context),
border: null,
@@ -56,10 +63,15 @@ class AppPage extends StatelessWidget {
: null,
),
resizeToAvoidBottomInset: resizeToAvoidBottomInset ?? true,
+ backgroundColor: backgroundColor,
child: Stack(
alignment: Alignment.bottomRight,
children: [
- body ?? const SizedBox(),
+ body ??
+ Builder(
+ builder: (context) =>
+ bodyBuilder?.call(context) ?? const SizedBox(),
+ ),
SafeArea(
child: Padding(
padding: const EdgeInsets.only(right: 16, bottom: 16),
@@ -76,12 +88,18 @@ class AppPage extends StatelessWidget {
leading == null
? null
: AppBar(
+ backgroundColor: barBackgroundColor,
title: titleWidget ?? _title(context),
actions: actions,
leading: leading,
automaticallyImplyLeading: automaticallyImplyLeading,
),
- body: body,
+ body: body ??
+ Builder(
+ builder: (context) =>
+ bodyBuilder?.call(context) ?? const SizedBox(),
+ ),
+ backgroundColor: backgroundColor,
floatingActionButton: floatingActionButton,
resizeToAvoidBottomInset: resizeToAvoidBottomInset,
);
@@ -92,3 +110,46 @@ class AppPage extends StatelessWidget {
overflow: TextOverflow.ellipsis,
);
}
+
+class AdaptiveAppBar extends StatelessWidget {
+ final String text;
+ final Widget? leading;
+ final List? actions;
+ final bool iosTransitionBetweenRoutes;
+ final bool automaticallyImplyLeading;
+
+ const AdaptiveAppBar(
+ {super.key,
+ required this.text,
+ this.leading,
+ this.actions,
+ this.iosTransitionBetweenRoutes = true,
+ this.automaticallyImplyLeading = true});
+
+ @override
+ Widget build(BuildContext context) {
+ return Platform.isIOS || Platform.isMacOS
+ ? CupertinoNavigationBar(
+ transitionBetweenRoutes: iosTransitionBetweenRoutes,
+ middle: Text(text),
+ previousPageTitle:
+ MaterialLocalizations.of(context).backButtonTooltip,
+ automaticallyImplyLeading: automaticallyImplyLeading,
+ leading: leading,
+ trailing: actions == null
+ ? null
+ : actions!.length == 1
+ ? actions!.first
+ : Row(
+ mainAxisSize: MainAxisSize.min,
+ children: actions!,
+ ),
+ )
+ : AppBar(
+ leading: leading,
+ actions: actions,
+ automaticallyImplyLeading: automaticallyImplyLeading,
+ title: Text(text),
+ );
+ }
+}
diff --git a/app/lib/domain/extensions/app_error_extensions.dart b/app/lib/domain/extensions/app_error_extensions.dart
index 45b5ef8..a1e751c 100644
--- a/app/lib/domain/extensions/app_error_extensions.dart
+++ b/app/lib/domain/extensions/app_error_extensions.dart
@@ -13,6 +13,8 @@ extension AppErrorExtensions on Object {
return context.l10n.something_went_wrong_error;
case AppErrorL10nCodes.googleSignInUserNotFoundError:
return context.l10n.user_google_sign_in_account_not_found_error;
+ case AppErrorL10nCodes.backUpFolderNotFound:
+ return context.l10n.back_up_folder_not_found_error;
default:
return (this as AppError).message ??
context.l10n.something_went_wrong_error;
diff --git a/app/lib/domain/extensions/date_extensions.dart b/app/lib/domain/extensions/date_extensions.dart
deleted file mode 100644
index 016df91..0000000
--- a/app/lib/domain/extensions/date_extensions.dart
+++ /dev/null
@@ -1,3 +0,0 @@
-extension DateTimeExtensions on DateTime {
- DateTime get dateOnly => DateTime(year, month, day);
-}
\ No newline at end of file
diff --git a/app/lib/domain/extensions/map_extensions.dart b/app/lib/domain/extensions/map_extensions.dart
new file mode 100644
index 0000000..017dff4
--- /dev/null
+++ b/app/lib/domain/extensions/map_extensions.dart
@@ -0,0 +1,4 @@
+extension MapExtension on Map> {
+ List valuesWhere(bool Function(E element) test) =>
+ values.expand((element) => element.where(test)).toList();
+}
diff --git a/app/lib/domain/formatter/date_formatter.dart b/app/lib/domain/formatter/date_formatter.dart
new file mode 100644
index 0000000..ad8d842
--- /dev/null
+++ b/app/lib/domain/formatter/date_formatter.dart
@@ -0,0 +1,54 @@
+import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:intl/intl.dart';
+
+enum DateFormatType {
+ dayMonthYear("d MMMM, y"),
+ dayMonthYearShort("d MMM, y"),
+ monthYear("MMMM y"),
+ monthYearShort("MMM y"),
+ dayMonth("d MMMM"),
+ dayMonthShort("d MMM"),
+ year("y"),
+ month("MMMM"),
+ monthShort("MMM"),
+ week("EEEE"),
+ time("HH:mm:ss a"),
+ timeShort("HH:mm a"),
+ relative("relative");
+
+ final String formatPattern;
+
+ const DateFormatType(this.formatPattern);
+
+ bool get isRelative => this == DateFormatType.relative;
+}
+
+extension DateFormatter on DateTime {
+ DateTime get dateOnly => DateTime(year, month, day);
+
+ String format(BuildContext context, DateFormatType type) {
+ if (isUtc) return toLocal().format(context, type);
+ if (type.isRelative) return relativeFormat(context);
+ return DateFormat(type.formatPattern).format(this);
+ }
+
+ String relativeFormat(BuildContext context) {
+ if (isUtc) return toLocal().relativeFormat(context);
+ final now = DateTime.now().toLocal();
+ final diff = now.difference(this);
+ if (diff.inDays == 0) {
+ return context.l10n.common_today;
+ } else if (diff.inDays == 1) {
+ return context.l10n.common_yesterday;
+ } else if (diff.inDays == -1) {
+ return context.l10n.common_tomorrow;
+ } else if (diff.inDays < 7) {
+ return DateFormat(DateFormatType.week.formatPattern).format(this);
+ } else if (now.year == year) {
+ return DateFormat(DateFormatType.dayMonth.formatPattern).format(this);
+ } else {
+ return DateFormat(DateFormatType.dayMonthYear.formatPattern).format(this);
+ }
+ }
+}
diff --git a/app/lib/domain/formatter/duration_formatter.dart b/app/lib/domain/formatter/duration_formatter.dart
new file mode 100644
index 0000000..b66f2d6
--- /dev/null
+++ b/app/lib/domain/formatter/duration_formatter.dart
@@ -0,0 +1,18 @@
+extension DurationFormatter on Duration {
+ String get format {
+ String twoDigits(int n) {
+ if (n >= 10) return "$n";
+ return "0$n";
+ }
+
+ String twoDigitHours = twoDigits(inHours.remainder(24));
+ String twoDigitMinutes = twoDigits(inMinutes.remainder(60));
+ String twoDigitSeconds = twoDigits(inSeconds.remainder(60));
+
+ if (inHours > 0) {
+ return "$twoDigitHours:$twoDigitMinutes:$twoDigitSeconds";
+ } else {
+ return "$twoDigitMinutes:$twoDigitSeconds";
+ }
+ }
+}
diff --git a/app/lib/ui/flow/accounts/accounts_screen.dart b/app/lib/ui/flow/accounts/accounts_screen.dart
index 5cc6cb4..3fdf06c 100644
--- a/app/lib/ui/flow/accounts/accounts_screen.dart
+++ b/app/lib/ui/flow/accounts/accounts_screen.dart
@@ -39,56 +39,58 @@ class _AccountsScreenState extends ConsumerState {
return AppPage(
title: context.l10n.common_accounts,
- body: ListView(
- padding: const EdgeInsets.all(16),
- children: [
- if (googleAccount != null)
- AccountsTab(
- name: googleAccount.displayName ?? googleAccount.email,
- serviceDescription: context.l10n.common_google_drive,
- profileImage: googleAccount.photoUrl,
- actionList: ActionList(buttons: [
- ActionListButton(
- title: context.l10n.common_auto_back_up,
- trailing: Consumer(
- builder: (context, ref, child) {
- final googleDriveAutoBackUp = ref
- .watch(AppPreferences.canTakeAutoBackUpInGoogleDrive);
- return AppSwitch(
- value: googleDriveAutoBackUp,
- onChanged: (bool value) {
- ref
- .read(AppPreferences
- .canTakeAutoBackUpInGoogleDrive.notifier)
- .state = value;
- },
- );
- },
+ bodyBuilder: (context) {
+ return ListView(
+ padding: context.systemPadding + const EdgeInsets.all(16),
+ children: [
+ if (googleAccount != null)
+ AccountsTab(
+ name: googleAccount.displayName ?? googleAccount.email,
+ serviceDescription: context.l10n.common_google_drive,
+ profileImage: googleAccount.photoUrl,
+ actionList: ActionList(buttons: [
+ ActionListButton(
+ title: context.l10n.common_auto_back_up,
+ trailing: Consumer(
+ builder: (context, ref, child) {
+ final googleDriveAutoBackUp = ref.watch(
+ AppPreferences.canTakeAutoBackUpInGoogleDrive);
+ return AppSwitch(
+ value: googleDriveAutoBackUp,
+ onChanged: (bool value) {
+ ref
+ .read(AppPreferences
+ .canTakeAutoBackUpInGoogleDrive.notifier)
+ .state = value;
+ },
+ );
+ },
+ ),
),
+ ActionListButton(
+ title: context.l10n.common_sign_out,
+ onPressed: notifier.signOutWithGoogle,
+ ),
+ ]),
+ backgroundColor: AppColors.googleDriveColor.withAlpha(50),
+ ),
+ if (googleAccount == null)
+ OnTapScale(
+ onTap: () {
+ notifier.signInWithGoogle();
+ },
+ child: AccountsTab(
+ name: context.l10n.add_account_title,
+ backgroundColor: context.colorScheme.containerNormal,
),
- ActionListButton(
- title: context.l10n.common_sign_out,
- onPressed: notifier.signOutWithGoogle,
- ),
- ]),
- backgroundColor: AppColors.googleDriveColor.withAlpha(50),
- ),
- if (googleAccount == null)
- OnTapScale(
- onTap: () {
- notifier.signInWithGoogle();
- },
- child: AccountsTab(
- name: context.l10n.add_account_title,
- backgroundColor: context.colorScheme.containerNormal,
),
- ),
- const SizedBox(height: 16),
- const SettingsActionList(),
- const SizedBox(height: 16),
- _buildVersion(context: context),
- ],
- ),
+ const SizedBox(height: 16),
+ const SettingsActionList(),
+ const SizedBox(height: 16),
+ _buildVersion(context: 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 f1ba4c7..c7e3b03 100644
--- a/app/lib/ui/flow/home/components/app_media_item.dart
+++ b/app/lib/ui/flow/home/components/app_media_item.dart
@@ -1,10 +1,14 @@
-import 'dart:io';
+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/media/media.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';
+import 'package:style/text/app_text_style.dart';
import '../../../../domain/assets/assets_paths.dart';
import 'package:style/animations/item_selector.dart';
@@ -13,7 +17,7 @@ class AppMediaItem extends StatefulWidget {
final void Function()? onTap;
final void Function()? onLongTap;
final bool isSelected;
- final bool isUploading;
+ final UploadStatus? status;
const AppMediaItem({
super.key,
@@ -21,7 +25,7 @@ class AppMediaItem extends StatefulWidget {
this.onTap,
this.onLongTap,
this.isSelected = false,
- this.isUploading = false,
+ this.status,
});
@override
@@ -30,145 +34,158 @@ class AppMediaItem extends StatefulWidget {
class _AppMediaItemState extends State
with AutomaticKeepAliveClientMixin {
- //VideoPlayerController? _videoPlayerController;
+ late Future thumbnailByte;
@override
void initState() {
- if (widget.media.type.isVideo &&
- widget.media.sources.contains(AppMediaSource.local)) {
- // if (widget.media.sources.contains(AppMediaSource.local)) {
- // _videoPlayerController =
- // VideoPlayerController.file(File(widget.media.path))
- // ..initialize().then((_) {
- // setState(() {});
- // });
- // }
+ if (widget.media.sources.contains(AppMediaSource.local)) {
+ _loadImage();
}
super.initState();
}
- @override
- void dispose() {
- // _videoPlayerController?.dispose();
- super.dispose();
+ _loadImage() async {
+ thumbnailByte = widget.media.thumbnailDataWithSize(const Size(300, 300));
}
@override
Widget build(BuildContext context) {
super.build(context);
- return ItemSelector(
- onTap: widget.onTap,
- onLongTap: widget.onLongTap,
- isSelected: widget.isSelected,
- child: ClipRRect(
- borderRadius: BorderRadius.circular(4),
- child: Stack(
- alignment: Alignment.bottomLeft,
- children: [
- widget.media.type.isVideo &&
- widget.media.sources.contains(AppMediaSource.local)
- ? _buildVideoView(context: context)
- : _buildImageView(context: context),
- if (widget.media.sources.contains(AppMediaSource.googleDrive) ||
- widget.isUploading)
+ return LayoutBuilder(
+ builder: (context, constraints) => ItemSelector(
+ onTap: widget.onTap,
+ onLongTap: widget.onLongTap,
+ isSelected: widget.isSelected,
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(4),
+ child: Stack(
+ alignment: Alignment.bottomLeft,
+ children: [
+ _buildMediaView(context: context, constraints: constraints),
+ if (widget.media.type.isVideo) _videoDuration(context),
_sourceIndicators(context: context),
- ],
+ ],
+ ),
),
),
);
}
- Widget _sourceIndicators({required BuildContext context}) {
- return Container(
- margin: const EdgeInsets.all(4),
- padding: const EdgeInsets.all(4),
- decoration: BoxDecoration(
- color: context.colorScheme.surface,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (widget.media.sources.contains(AppMediaSource.googleDrive))
- SvgPicture.asset(
- Assets.images.icons.googlePhotos,
- height: 12,
- width: 12,
- ),
- if (widget.isUploading)
- const Padding(
- padding: EdgeInsets.all(2),
- child: AppCircularProgressIndicator(size: 12),
- ),
- ],
- ),
- );
- }
-
- Widget _buildImageView({required BuildContext context}) {
- return LayoutBuilder(builder: (context, constraints) {
- return Image(
- image: widget.media.sources.contains(AppMediaSource.local)
- ? ResizeImage(
- FileImage(File(widget.media.path)),
- height: constraints.maxHeight.toInt(),
- width: constraints.maxWidth.toInt(),
- policy: ResizeImagePolicy.fit,
- )
- : CachedNetworkImageProvider(widget.media.thumbnailPath!)
- as ImageProvider,
- fit: BoxFit.cover,
- loadingBuilder: (context, child, loadingProgress) {
- if (loadingProgress == null) {
- return child;
- }
- return Container(
- width: double.maxFinite,
- height: double.maxFinite,
- color: context.colorScheme.containerNormalOnSurface,
- child: Center(
- child: AppCircularProgressIndicator(
- value: (loadingProgress.expectedTotalBytes != null &&
- (loadingProgress.expectedTotalBytes ?? 0) > 0)
- ? loadingProgress.cumulativeBytesLoaded /
- loadingProgress.expectedTotalBytes!
- : null,
+ Widget _videoDuration(BuildContext context) => Align(
+ alignment: Alignment.bottomRight,
+ child: _BackgroundContainer(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(
+ CupertinoIcons.play_fill,
+ color: context.colorScheme.surfaceInverse,
+ size: 14,
),
- ),
- );
- },
- errorBuilder: (context, error, stackTrace) {
- return Container(
- width: double.maxFinite,
- height: double.maxFinite,
- color: context.colorScheme.containerNormalOnSurface,
- child: Center(
- child: Icon(
- CupertinoIcons.photo,
- color: context.colorScheme.onPrimary,
- size: 32,
+ const SizedBox(width: 2),
+ Text(
+ (widget.media.videoDuration ?? Duration.zero).format,
+ style: AppTextStyles.caption.copyWith(
+ color: context.colorScheme.surfaceInverse,
+ ),
),
- ),
- );
+ ],
+ ),
+ ),
+ );
+
+ Widget _buildMediaView(
+ {required BuildContext context, required BoxConstraints constraints}) {
+ if (widget.media.sources.contains(AppMediaSource.local)) {
+ return FutureBuilder(
+ future: thumbnailByte,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState == ConnectionState.done &&
+ snapshot.hasData) {
+ return Hero(
+ tag: widget.media,
+ child: Image.memory(
+ snapshot.data!,
+ width: constraints.maxWidth,
+ height: constraints.maxHeight,
+ fit: BoxFit.cover,
+ ),
+ );
+ } else {
+ return _buildPlaceholder(context: context, showLoader: false);
+ }
},
- width: double.maxFinite,
- height: double.maxFinite,
);
- });
+ } else {
+ return Hero(
+ tag: widget.media,
+ child: CachedNetworkImage(
+ imageUrl: widget.media.thumbnailLink!,
+ width: constraints.maxWidth,
+ height: constraints.maxHeight,
+ fit: BoxFit.cover,
+ errorWidget: (context, url, error) => _buildErrorWidget(context),
+ progressIndicatorBuilder: (context, url, progress) =>
+ _buildPlaceholder(
+ context: context,
+ value: progress.progress,
+ )),
+ );
+ }
}
- Widget _buildVideoView({required BuildContext context}) {
- return Stack(
- alignment: Alignment.center,
+ 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,
+ ),
+ );
+
+ Widget _sourceIndicators({required BuildContext context}) {
+ return Row(
children: [
- Container(
- decoration: BoxDecoration(
- color: context.colorScheme.containerNormalOnSurface,
+ if (widget.media.sources.contains(AppMediaSource.googleDrive))
+ _BackgroundContainer(
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (widget.media.sources.contains(AppMediaSource.googleDrive))
+ SvgPicture.asset(
+ Assets.images.icons.googlePhotos,
+ height: 14,
+ width: 14,
+ ),
+ ],
+ ),
+ ),
+ if (widget.status == UploadStatus.uploading)
+ _BackgroundContainer(
+ child: AppCircularProgressIndicator(
+ size: 16,
+ color: context.colorScheme.surfaceInverse,
+ ),
+ ),
+ if (widget.status == UploadStatus.waiting)
+ _BackgroundContainer(
+ child: Icon(
+ CupertinoIcons.time,
+ size: 16,
+ color: context.colorScheme.surfaceInverse,
+ ),
),
- // child: VideoPlayer(_videoPlayerController!),
- ),
- Icon(CupertinoIcons.play_arrow_solid,
- color: context.colorScheme.onPrimary),
],
);
}
@@ -176,3 +193,22 @@ class _AppMediaItemState extends State
@override
bool get wantKeepAlive => true;
}
+
+class _BackgroundContainer extends StatelessWidget {
+ final Widget child;
+
+ const _BackgroundContainer({required this.child});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: const EdgeInsets.all(4),
+ padding: const EdgeInsets.all(4),
+ decoration: BoxDecoration(
+ color: context.colorScheme.surface.withOpacity(0.6),
+ borderRadius: const BorderRadius.all(Radius.circular(4)),
+ ),
+ child: child,
+ );
+ }
+}
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 d3ebd2f..10b9ebe 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
@@ -42,7 +42,7 @@ class NoLocalMediasAccessScreen extends ConsumerWidget {
PrimaryButton(
onPressed: () async {
await openAppSettings();
- await notifier.loadMedias();
+ await notifier.loadLocalMedia();
},
text: 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 2a21319..7752fef 100644
--- a/app/lib/ui/flow/home/home_screen.dart
+++ b/app/lib/ui/flow/home/home_screen.dart
@@ -1,12 +1,17 @@
+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/media/media.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:intl/intl.dart';
import 'package:style/extensions/context_extensions.dart';
import 'package:style/indicators/circular_progress_indicator.dart';
import 'package:style/text/app_text_style.dart';
@@ -16,7 +21,7 @@ import '../../navigation/app_router.dart';
import 'components/app_media_item.dart';
import 'components/hints.dart';
import 'components/multi_selection_done_button.dart';
-import 'package:style/slivers/sticky_header_delegate.dart';
+import 'package:style/buttons/action_button.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@@ -53,15 +58,11 @@ class _HomeScreenState extends ConsumerState {
@override
Widget build(BuildContext context) {
_errorObserver();
-
return AppPage(
+ //barBackgroundColor: context.colorScheme.surface,
titleWidget: _titleWidget(context: context),
actions: [
- IconButton(
- style: IconButton.styleFrom(
- backgroundColor: context.colorScheme.containerNormalOnSurface,
- minimumSize: const Size(28, 28),
- ),
+ ActionButton(
onPressed: () {
AppRouter.accounts.push(context);
},
@@ -77,126 +78,138 @@ class _HomeScreenState extends ConsumerState {
}
Widget _body({required BuildContext context}) {
- //States
- final medias =
- ref.watch(homeViewStateNotifier.select((state) => state.medias));
- final isLoading =
- ref.watch(homeViewStateNotifier.select((state) => state.loading));
-
- final selectedMedias = ref
- .watch(homeViewStateNotifier.select((state) => state.selectedMedias));
-
- final uploadingMedias = ref
- .watch(homeViewStateNotifier.select((state) => state.uploadingMedias));
-
- final hasLocalMediaAccess = ref.watch(
- homeViewStateNotifier.select((state) => state.hasLocalMediaAccess));
+ //View State
+ final ({
+ Map> medias,
+ List uploadingMedias,
+ List selectedMedias,
+ bool isLoading,
+ bool hasLocalMediaAccess,
+ String? lastLocalMediaId
+ }) state = ref.watch(homeViewStateNotifier.select((value) => (
+ medias: value.medias,
+ uploadingMedias: value.uploadingMedias,
+ selectedMedias: value.selectedMedias,
+ isLoading: value.loading,
+ hasLocalMediaAccess: value.hasLocalMediaAccess,
+ lastLocalMediaId: value.lastLocalMediaId,
+ )));
//View
- if (isLoading) {
+ if (state.isLoading) {
return const Center(child: AppCircularProgressIndicator());
- } else if (medias.isEmpty && !hasLocalMediaAccess) {
+ } else if (state.medias.isEmpty && !state.hasLocalMediaAccess) {
return const NoLocalMediasAccessScreen();
}
- return RefreshIndicator.adaptive(
- onRefresh: () async {
- await notifier.loadMedias();
- },
- child: Stack(
- alignment: Alignment.bottomRight,
- children: [
- _buildMediaList(
- context: context,
- medias: medias,
- uploadingMedias: uploadingMedias,
- selectedMedias: selectedMedias,
+ return Stack(
+ alignment: Alignment.bottomRight,
+ children: [
+ _buildMediaList(
+ context: context,
+ medias: state.medias,
+ uploadingMedias: state.uploadingMedias,
+ selectedMedias: state.selectedMedias,
+ lastLocalMediaId: state.lastLocalMediaId,
+ ),
+ if (state.selectedMedias.isNotEmpty)
+ Padding(
+ padding: context.systemPadding + const EdgeInsets.all(16),
+ child: const MultiSelectionDoneButton(),
),
- if (selectedMedias.isNotEmpty)
- Padding(
- padding: context.systemPadding + const EdgeInsets.all(16),
- child: const MultiSelectionDoneButton(),
- ),
- ],
- ),
+ ],
);
}
Widget _buildMediaList(
{required BuildContext context,
required Map> medias,
- required List uploadingMedias,
+ required List uploadingMedias,
+ required String? lastLocalMediaId,
required List selectedMedias}) {
return Scrollbar(
controller: _scrollController,
interactive: true,
- child: CustomScrollView(controller: _scrollController, slivers: [
- const SliverToBoxAdapter(
- child: HomeScreenHints(),
- ),
- ...medias.entries
- .map(
- (e) => SliverMainAxisGroup(
- slivers: [
- SliverPersistentHeader(
- delegate: SliverStickyHeaderDelegate(
- header: Container(
- padding: const EdgeInsets.only(left: 16),
- alignment: Alignment.centerLeft,
- decoration: BoxDecoration(
- color: context.colorScheme.surface,
- ),
- child: Text(
- DateFormat("d MMMM, y").format(e.key),
- style: AppTextStyles.subtitle1.copyWith(
- color: context.colorScheme.textPrimary,
- ),
- ),
- ),
- ),
- pinned: true,
+ child: ListView.builder(
+ controller: _scrollController,
+ itemCount: medias.length + 1,
+ itemBuilder: (context, index) {
+ if (index == 0) {
+ return const HomeScreenHints();
+ } else {
+ final gridEntry = medias.entries.elementAt(index - 1);
+ return Column(
+ children: [
+ Container(
+ height: 45,
+ padding: const EdgeInsets.only(left: 16, top: 5),
+ margin: EdgeInsets.zero,
+ alignment: Alignment.centerLeft,
+ decoration: BoxDecoration(
+ color: context.colorScheme.surface,
),
- SliverPadding(
- padding:
- const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
- sliver: SliverGrid.builder(
- itemBuilder: (context, index) {
- final media = e.value[index];
- return AppMediaItem(
- key: ValueKey(media.id),
- onTap: () {
- if (selectedMedias.isNotEmpty) {
- notifier.toggleMediaSelection(media);
- }
- },
- onLongTap: () {
- notifier.toggleMediaSelection(media);
- },
- isSelected: selectedMedias.contains(media),
- isUploading: uploadingMedias.contains(media.id),
- media: media,
- );
- },
- itemCount: e.value.length,
- gridDelegate:
- const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 3,
- crossAxisSpacing: 4,
- mainAxisSpacing: 4,
- ),
+ child: Text(
+ gridEntry.key.format(context, DateFormatType.relative),
+ style: AppTextStyles.subtitle1.copyWith(
+ color: context.colorScheme.textPrimary,
),
),
- ],
- ),
- )
- .toList(),
- ]),
+ ),
+ GridView.builder(
+ padding: const EdgeInsets.all(4),
+ physics: const NeverScrollableScrollPhysics(),
+ shrinkWrap: true,
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: context.mediaQuerySize.width > 600
+ ? context.mediaQuerySize.width ~/ 180
+ : context.mediaQuerySize.width ~/ 100,
+ crossAxisSpacing: 4,
+ mainAxisSpacing: 4,
+ ),
+ itemCount: gridEntry.value.length,
+ itemBuilder: (context, index) {
+ final media = gridEntry.value[index];
+ if (media.id == lastLocalMediaId) {
+ runPostFrame(() {
+ notifier.loadLocalMedia(append: true);
+ });
+ }
+ return AppMediaItem(
+ key: ValueKey(media.id),
+ onTap: () {
+ if (selectedMedias.isNotEmpty) {
+ notifier.toggleMediaSelection(media);
+ } else {
+ AppMediaView.showPreview(
+ context: context,
+ media: media,
+ );
+ }
+ },
+ onLongTap: () {
+ notifier.toggleMediaSelection(media);
+ },
+ isSelected: selectedMedias.contains(media),
+ status: uploadingMedias
+ .firstWhereOrNull(
+ (element) => element.mediaId == media.id)
+ ?.status,
+ media: media,
+ );
+ },
+ ),
+ ],
+ );
+ }
+ },
+ ),
);
}
Widget _titleWidget({required BuildContext context}) {
return Row(
children: [
- const SizedBox(width: 16),
+ 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 2fe72c7..85d0222 100644
--- a/app/lib/ui/flow/home/home_screen_view_model.dart
+++ b/app/lib/ui/flow/home/home_screen_view_model.dart
@@ -1,11 +1,12 @@
import 'dart:async';
-import 'package:cloud_gallery/domain/extensions/date_extensions.dart';
+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:data/models/media/media.dart';
import 'package:data/services/auth_service.dart';
import 'package:data/services/google_drive_service.dart';
import 'package:data/services/local_media_service.dart';
-import 'package:data/storage/app_preferences.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:google_sign_in/google_sign_in.dart';
@@ -16,20 +17,11 @@ part 'home_screen_view_model.freezed.dart';
final homeViewStateNotifier =
StateNotifierProvider.autoDispose(
(ref) {
- final homeViewStateNotifier = HomeViewStateNotifier(
+ return HomeViewStateNotifier(
ref.read(localMediaServiceProvider),
ref.read(googleDriveServiceProvider),
ref.read(authServiceProvider),
- ref.read(AppPreferences.canTakeAutoBackUpInGoogleDrive),
);
- final subscription = ref.listen(AppPreferences.canTakeAutoBackUpInGoogleDrive,
- (previous, next) {
- homeViewStateNotifier.updateAutoBackUpStatus(next);
- });
- ref.onDispose(() {
- subscription.close();
- });
- return homeViewStateNotifier;
});
class HomeViewStateNotifier extends StateNotifier {
@@ -37,98 +29,39 @@ class HomeViewStateNotifier extends StateNotifier {
final AuthService _authService;
final LocalMediaService _localMediaService;
StreamSubscription? _googleAccountSubscription;
- bool _isAutoBackUpEnabled = false;
- bool _isAutoBackUpWorking = false;
+ List _uploadedMedia = [];
String? _backUpFolderId;
- List _localMedias = [];
- int? _localMediaCount;
- bool _loading = false;
- List _googleDriveMedias = [];
+ bool _isGoogleDriveLoading = false;
+ bool _isLocalMediaLoading = false;
+ bool _isMaxLocalMediaLoaded = false;
- HomeViewStateNotifier(this._localMediaService, this._googleDriveService,
- this._authService, this._isAutoBackUpEnabled)
+ HomeViewStateNotifier(
+ this._localMediaService, this._googleDriveService, this._authService)
: super(const HomeViewState()) {
_googleAccountSubscription =
- _authService.onGoogleAccountChange.listen((event) {
+ _authService.onGoogleAccountChange.listen((event) async {
state = state.copyWith(googleAccount: event);
- loadMedias();
- });
- if (_isAutoBackUpEnabled) {
- _autoBackUpMedias();
- }
- loadMedias();
- }
-
- @override
- Future dispose() async {
- await _googleAccountSubscription?.cancel();
- super.dispose();
- }
-
- Future _autoBackUpMedias() async {
- _backUpFolderId ??= await _googleDriveService.getBackupFolderId();
- _isAutoBackUpWorking = true;
- for (final media in state.medias.values.expand((element) => element)) {
- if (!_isAutoBackUpEnabled) {
- _isAutoBackUpWorking = false;
- return;
- }
- if (!media.sources.contains(AppMediaSource.googleDrive)) {
- state = state.copyWith(
- uploadingMedias: state.uploadingMedias.toList()..add(media.id));
- await _googleDriveService.uploadInGoogleDrive(
- media: media,
- folderID: _backUpFolderId!,
- );
+ await loadGoogleDriveMedia();
+ if (event == null) {
+ _uploadedMedia.clear();
state = state.copyWith(
- uploadingMedias: state.uploadingMedias.toList()..remove(media.id),
- medias: state.medias.map((key, value) {
- value.updateElement(
- newElement: media.copyWith(
- sources: media.sources.toList()
- ..add(AppMediaSource.googleDrive)),
- oldElement: media);
- return MapEntry(key, value);
- }),
+ medias: _sortMedias(
+ medias: _removeGoogleDriveRefFromMedias(state.medias)),
);
}
- }
- _isAutoBackUpWorking = false;
- }
-
- Future updateAutoBackUpStatus(bool status) async {
- _isAutoBackUpEnabled = status;
- if (_isAutoBackUpEnabled && !_isAutoBackUpWorking) {
- _autoBackUpMedias();
- }
+ });
+ _loadInitialMedia();
}
- Future loadMedias() async {
- if (_loading == true) return;
- _loading = true;
- _googleDriveMedias = [];
- _localMedias = [];
- state = state.copyWith(loading: state.medias.isEmpty, error: null);
- try {
- _localMediaCount ??= await _getLocalMediaCount();
- if (_localMediaCount != null) {
- await Future.wait([
- _getGoogleDriveMedias(),
- _getLocalMedias(),
- ]);
- } else {
- await _getGoogleDriveMedias();
- }
- state = state.copyWith(
- loading: false,
- medias: _sortMedias(medias: _getAllMedias()),
- hasLocalMediaAccess: _localMediaCount != null,
- );
- } catch (error) {
- state = state.copyWith(loading: false, error: error);
- } finally {
- _loading = false;
+ void _loadInitialMedia() async {
+ state = state.copyWith(loading: true, error: null);
+ final hasAccess = await _localMediaService.requestPermission();
+ state = state.copyWith(hasLocalMediaAccess: hasAccess, loading: false);
+ if (hasAccess) {
+ await Future.wait([loadLocalMedia(), loadGoogleDriveMedia()]);
+ } else {
+ await loadGoogleDriveMedia();
}
}
@@ -141,58 +74,96 @@ class HomeViewStateNotifier extends StateNotifier {
}
}
- List _getAllMedias() {
- final commonMedias = [];
-
- for (AppMedia localMedia in _localMedias.toList()) {
- _googleDriveMedias
- .toList()
- .where((element) => element.path == localMedia.path)
- .forEach((googleDriveMedia) {
- _googleDriveMedias
- .removeWhere((media) => media.id == googleDriveMedia.id);
- _localMedias.removeWhere((media) => media.id == localMedia.id);
- commonMedias.add(localMedia.copyWith(
- sources: [AppMediaSource.local, AppMediaSource.googleDrive],
- thumbnailPath: googleDriveMedia.thumbnailPath));
- });
+ Future loadLocalMedia({bool append = false}) async {
+ if (_isLocalMediaLoading || (_isMaxLocalMediaLoaded && append)) return;
+ if (_isMaxLocalMediaLoaded && !append) {
+ _isMaxLocalMediaLoaded = false;
}
+ _isLocalMediaLoading = true;
+ try {
+ state = state.copyWith(loading: state.medias.isEmpty, error: null);
- return [..._localMedias, ..._googleDriveMedias, ...commonMedias];
- }
+ final loadedLocalMediaCount = state.medias
+ .valuesWhere((e) => e.sources.contains(AppMediaSource.local))
+ .length;
- 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,
- );
- }
+ final localMedia = await _localMediaService.getLocalMedia(
+ start: append ? loadedLocalMediaCount : 0,
+ end: append
+ ? loadedLocalMediaCount + 30
+ : loadedLocalMediaCount < 30
+ ? 30
+ : loadedLocalMediaCount,
+ );
- Future _getGoogleDriveMedias() async {
- if (_authService.signedInWithGoogle) {
- _backUpFolderId ??= await _googleDriveService.getBackupFolderId();
- _googleDriveMedias = await _googleDriveService.getDriveMedias(
- backUpFolderId: _backUpFolderId!);
- }
- }
+ if (localMedia.length < 30) {
+ _isMaxLocalMediaLoaded = true;
+ }
- Future _getLocalMediaCount() async {
- final hasAccess = await _localMediaService.requestPermission();
- if (hasAccess) {
- return await _localMediaService.getMediaCount();
+ final mergedMedia = _mergeCommonMedia(
+ localMedias: localMedia,
+ googleDriveMedias: _uploadedMedia,
+ );
+
+ state = state.copyWith(
+ medias: _sortMedias(
+ medias: append
+ ? [
+ ...state.medias.values.expand((element) => element).toList(),
+ ...mergedMedia
+ ]
+ : mergedMedia,
+ ),
+ loading: false,
+ lastLocalMediaId: mergedMedia.length > 10
+ ? mergedMedia.elementAt(mergedMedia.length - 10).id
+ : state.lastLocalMediaId,
+ );
+ } catch (e) {
+ state = state.copyWith(loading: false, error: e);
+ } finally {
+ _isLocalMediaLoading = false;
}
- return null;
}
- Future _getLocalMedias() async {
- if (_localMediaCount != null) {
- _localMedias = await _localMediaService.getLocalMedia(
- start: 0,
- end: _localMediaCount!,
+ Future loadGoogleDriveMedia() async {
+ if (state.googleAccount == null || _isGoogleDriveLoading) return;
+ _isGoogleDriveLoading = true;
+ try {
+ _backUpFolderId ??= await _googleDriveService.getBackupFolderId();
+
+ state = state.copyWith(loading: state.medias.isEmpty, error: null);
+ final driveMedias = await _googleDriveService.getDriveMedias(
+ backUpFolderId: _backUpFolderId!,
+ );
+
+ // Separate media by its local existence
+ List googleDriveMedia = [];
+ List uploadedMedia = [];
+ for (var media in driveMedias) {
+ if (await media.isExist) {
+ uploadedMedia.add(media);
+ } else {
+ googleDriveMedia.add(media);
+ }
+ }
+ _uploadedMedia = uploadedMedia;
+
+ //override google drive media if exist.
+ state = state.copyWith(
+ medias: _sortMedias(medias: [
+ ..._mergeCommonMedia(
+ localMedias: _removeGoogleDriveRefFromMedias(state.medias),
+ googleDriveMedias: uploadedMedia,
+ ),
+ ...googleDriveMedia
+ ]),
+ loading: false,
);
+ } catch (e) {
+ state = state.copyWith(loading: false, error: e);
+ } finally {
+ _isGoogleDriveLoading = false;
}
}
@@ -206,20 +177,34 @@ class HomeViewStateNotifier extends StateNotifier {
try {
if (!_authService.signedInWithGoogle) {
await _authService.signInWithGoogle();
- loadMedias();
+ await loadGoogleDriveMedia();
}
+
List uploadingMedias = state.selectedMedias
.where((element) =>
!element.sources.contains(AppMediaSource.googleDrive))
.toList();
state = state.copyWith(
- uploadingMedias: uploadingMedias.map((e) => e.id).toList(),
+ uploadingMedias: uploadingMedias
+ .map((e) =>
+ UploadProgress(mediaId: e.id, status: UploadStatus.waiting))
+ .toList(),
error: null,
);
+
_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!,
@@ -234,15 +219,87 @@ class HomeViewStateNotifier extends StateNotifier {
oldElement: media);
return MapEntry(key, value);
}),
- uploadingMedias: state.uploadingMedias.toList()..remove(media.id),
+ uploadingMedias: state.uploadingMedias.toList()
+ ..removeWhere((element) => element.mediaId == media.id),
);
}
state = state.copyWith(uploadingMedias: [], 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);
+ }
+ }
+ return allMedias;
+ }
+
+ @override
+ Future dispose() async {
+ await _googleAccountSubscription?.cancel();
+ super.dispose();
+ }
}
@freezed
@@ -252,8 +309,9 @@ class HomeViewState with _$HomeViewState {
@Default(false) bool hasLocalMediaAccess,
@Default(false) bool loading,
GoogleSignInAccount? googleAccount,
+ String? lastLocalMediaId,
@Default({}) Map> medias,
@Default([]) List selectedMedias,
- @Default([]) List uploadingMedias,
+ @Default([]) List uploadingMedias,
}) = _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 7f07220..74356fb 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,10 +20,12 @@ mixin _$HomeViewState {
bool get hasLocalMediaAccess => throw _privateConstructorUsedError;
bool get loading => throw _privateConstructorUsedError;
GoogleSignInAccount? get googleAccount => throw _privateConstructorUsedError;
+ String? get lastLocalMediaId => throw _privateConstructorUsedError;
Map> get medias =>
throw _privateConstructorUsedError;
List get selectedMedias => throw _privateConstructorUsedError;
- List get uploadingMedias => throw _privateConstructorUsedError;
+ List get uploadingMedias =>
+ throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomeViewStateCopyWith get copyWith =>
@@ -41,9 +43,10 @@ abstract class $HomeViewStateCopyWith<$Res> {
bool hasLocalMediaAccess,
bool loading,
GoogleSignInAccount? googleAccount,
+ String? lastLocalMediaId,
Map> medias,
List selectedMedias,
- List uploadingMedias});
+ List uploadingMedias});
}
/// @nodoc
@@ -63,6 +66,7 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState>
Object? hasLocalMediaAccess = null,
Object? loading = null,
Object? googleAccount = freezed,
+ Object? lastLocalMediaId = freezed,
Object? medias = null,
Object? selectedMedias = null,
Object? uploadingMedias = null,
@@ -81,6 +85,10 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState>
? _value.googleAccount
: googleAccount // ignore: cast_nullable_to_non_nullable
as GoogleSignInAccount?,
+ lastLocalMediaId: freezed == lastLocalMediaId
+ ? _value.lastLocalMediaId
+ : lastLocalMediaId // ignore: cast_nullable_to_non_nullable
+ as String?,
medias: null == medias
? _value.medias
: medias // ignore: cast_nullable_to_non_nullable
@@ -92,7 +100,7 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState>
uploadingMedias: null == uploadingMedias
? _value.uploadingMedias
: uploadingMedias // ignore: cast_nullable_to_non_nullable
- as List,
+ as List,
) as $Val);
}
}
@@ -110,9 +118,10 @@ abstract class _$$HomeViewStateImplCopyWith<$Res>
bool hasLocalMediaAccess,
bool loading,
GoogleSignInAccount? googleAccount,
+ String? lastLocalMediaId,
Map> medias,
List selectedMedias,
- List uploadingMedias});
+ List uploadingMedias});
}
/// @nodoc
@@ -130,6 +139,7 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
Object? hasLocalMediaAccess = null,
Object? loading = null,
Object? googleAccount = freezed,
+ Object? lastLocalMediaId = freezed,
Object? medias = null,
Object? selectedMedias = null,
Object? uploadingMedias = null,
@@ -148,6 +158,10 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
? _value.googleAccount
: googleAccount // ignore: cast_nullable_to_non_nullable
as GoogleSignInAccount?,
+ lastLocalMediaId: freezed == lastLocalMediaId
+ ? _value.lastLocalMediaId
+ : lastLocalMediaId // ignore: cast_nullable_to_non_nullable
+ as String?,
medias: null == medias
? _value._medias
: medias // ignore: cast_nullable_to_non_nullable
@@ -159,7 +173,7 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
uploadingMedias: null == uploadingMedias
? _value._uploadingMedias
: uploadingMedias // ignore: cast_nullable_to_non_nullable
- as List,
+ as List,
));
}
}
@@ -172,9 +186,10 @@ class _$HomeViewStateImpl implements _HomeViewState {
this.hasLocalMediaAccess = false,
this.loading = false,
this.googleAccount,
+ this.lastLocalMediaId,
final Map> medias = const {},
final List selectedMedias = const [],
- final List uploadingMedias = const []})
+ final List uploadingMedias = const []})
: _medias = medias,
_selectedMedias = selectedMedias,
_uploadingMedias = uploadingMedias;
@@ -189,6 +204,8 @@ class _$HomeViewStateImpl implements _HomeViewState {
final bool loading;
@override
final GoogleSignInAccount? googleAccount;
+ @override
+ final String? lastLocalMediaId;
final Map> _medias;
@override
@JsonKey()
@@ -207,10 +224,10 @@ class _$HomeViewStateImpl implements _HomeViewState {
return EqualUnmodifiableListView(_selectedMedias);
}
- final List _uploadingMedias;
+ final List _uploadingMedias;
@override
@JsonKey()
- List get uploadingMedias {
+ List get uploadingMedias {
if (_uploadingMedias is EqualUnmodifiableListView) return _uploadingMedias;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_uploadingMedias);
@@ -218,7 +235,7 @@ class _$HomeViewStateImpl implements _HomeViewState {
@override
String toString() {
- return 'HomeViewState(error: $error, hasLocalMediaAccess: $hasLocalMediaAccess, loading: $loading, googleAccount: $googleAccount, medias: $medias, selectedMedias: $selectedMedias, uploadingMedias: $uploadingMedias)';
+ return 'HomeViewState(error: $error, hasLocalMediaAccess: $hasLocalMediaAccess, loading: $loading, googleAccount: $googleAccount, lastLocalMediaId: $lastLocalMediaId, medias: $medias, selectedMedias: $selectedMedias, uploadingMedias: $uploadingMedias)';
}
@override
@@ -232,6 +249,8 @@ class _$HomeViewStateImpl implements _HomeViewState {
(identical(other.loading, loading) || other.loading == loading) &&
(identical(other.googleAccount, googleAccount) ||
other.googleAccount == googleAccount) &&
+ (identical(other.lastLocalMediaId, lastLocalMediaId) ||
+ other.lastLocalMediaId == lastLocalMediaId) &&
const DeepCollectionEquality().equals(other._medias, _medias) &&
const DeepCollectionEquality()
.equals(other._selectedMedias, _selectedMedias) &&
@@ -246,6 +265,7 @@ class _$HomeViewStateImpl implements _HomeViewState {
hasLocalMediaAccess,
loading,
googleAccount,
+ lastLocalMediaId,
const DeepCollectionEquality().hash(_medias),
const DeepCollectionEquality().hash(_selectedMedias),
const DeepCollectionEquality().hash(_uploadingMedias));
@@ -263,9 +283,10 @@ abstract class _HomeViewState implements HomeViewState {
final bool hasLocalMediaAccess,
final bool loading,
final GoogleSignInAccount? googleAccount,
+ final String? lastLocalMediaId,
final Map> medias,
final List selectedMedias,
- final List uploadingMedias}) = _$HomeViewStateImpl;
+ final List uploadingMedias}) = _$HomeViewStateImpl;
@override
Object? get error;
@@ -276,11 +297,13 @@ abstract class _HomeViewState implements HomeViewState {
@override
GoogleSignInAccount? get googleAccount;
@override
+ String? get lastLocalMediaId;
+ @override
Map> get medias;
@override
List get selectedMedias;
@override
- List get uploadingMedias;
+ List get uploadingMedias;
@override
@JsonKey(ignore: true)
_$$HomeViewStateImplCopyWith<_$HomeViewStateImpl> get copyWith =>
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/image_preview/components/network_image_preview/network_image_preview.dart
new file mode 100644
index 0000000..c949a09
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/image_preview/components/network_image_preview/network_image_preview.dart
@@ -0,0 +1,30 @@
+import 'dart:typed_data';
+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 'network_image_preview_view_model.dart';
+
+class NetworkImagePreview extends ConsumerWidget {
+ final AppMedia media;
+
+ const NetworkImagePreview({super.key, required this.media});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final state = ref.watch(networkImagePreviewStateNotifierProvider);
+
+ if (state.loading) {
+ return const Center(child: AppCircularProgressIndicator());
+ } else if (state.mediaBytes != null) {
+ return Hero(
+ tag: media,
+ child: Image.memory(Uint8List.fromList(state.mediaBytes!),
+ fit: BoxFit.fitWidth),
+ );
+ } else if (state.error != null) {
+ return const Center(child: Text('Error'));
+ }
+ 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/image_preview/components/network_image_preview/network_image_preview_view_model.dart
new file mode 100644
index 0000000..571b17f
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/image_preview/components/network_image_preview/network_image_preview_view_model.dart
@@ -0,0 +1,44 @@
+import 'package:data/models/media_content/media_content.dart';
+import 'package:data/services/google_drive_service.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+
+part 'network_image_preview_view_model.freezed.dart';
+
+final networkImagePreviewStateNotifierProvider = StateNotifierProvider<
+ NetworkImagePreviewStateNotifier, NetworkImagePreviewState>((ref) {
+ return NetworkImagePreviewStateNotifier(ref.read(googleDriveServiceProvider));
+});
+
+class NetworkImagePreviewStateNotifier
+ extends StateNotifier {
+ final GoogleDriveService _googleDriveServices;
+
+ NetworkImagePreviewStateNotifier(this._googleDriveServices)
+ : super(const NetworkImagePreviewState());
+
+ Future loadImage(String mediaId) async {
+ try {
+ 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);
+ } catch (error) {
+ state = state.copyWith(error: error, loading: false);
+ }
+ }
+}
+
+@freezed
+class NetworkImagePreviewState with _$NetworkImagePreviewState {
+ const factory NetworkImagePreviewState({
+ @Default(false) bool loading,
+ AppMediaContent? mediaContent,
+ List? mediaBytes,
+ 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/image_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart
new file mode 100644
index 0000000..b953966
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/image_preview/components/network_image_preview/network_image_preview_view_model.freezed.dart
@@ -0,0 +1,229 @@
+// 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 'network_image_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 _$NetworkImagePreviewState {
+ bool get loading => throw _privateConstructorUsedError;
+ AppMediaContent? get mediaContent => throw _privateConstructorUsedError;
+ List? get mediaBytes => throw _privateConstructorUsedError;
+ Object? get error => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $NetworkImagePreviewStateCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $NetworkImagePreviewStateCopyWith<$Res> {
+ factory $NetworkImagePreviewStateCopyWith(NetworkImagePreviewState value,
+ $Res Function(NetworkImagePreviewState) then) =
+ _$NetworkImagePreviewStateCopyWithImpl<$Res, NetworkImagePreviewState>;
+ @useResult
+ $Res call(
+ {bool loading,
+ AppMediaContent? mediaContent,
+ List? mediaBytes,
+ Object? error});
+
+ $AppMediaContentCopyWith<$Res>? get mediaContent;
+}
+
+/// @nodoc
+class _$NetworkImagePreviewStateCopyWithImpl<$Res,
+ $Val extends NetworkImagePreviewState>
+ implements $NetworkImagePreviewStateCopyWith<$Res> {
+ _$NetworkImagePreviewStateCopyWithImpl(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? loading = null,
+ Object? mediaContent = freezed,
+ Object? mediaBytes = freezed,
+ Object? error = freezed,
+ }) {
+ return _then(_value.copyWith(
+ loading: null == loading
+ ? _value.loading
+ : loading // ignore: cast_nullable_to_non_nullable
+ as bool,
+ mediaContent: freezed == mediaContent
+ ? _value.mediaContent
+ : mediaContent // ignore: cast_nullable_to_non_nullable
+ as AppMediaContent?,
+ mediaBytes: freezed == mediaBytes
+ ? _value.mediaBytes
+ : mediaBytes // ignore: cast_nullable_to_non_nullable
+ as List?,
+ error: freezed == error ? _value.error : error,
+ ) as $Val);
+ }
+
+ @override
+ @pragma('vm:prefer-inline')
+ $AppMediaContentCopyWith<$Res>? get mediaContent {
+ if (_value.mediaContent == null) {
+ return null;
+ }
+
+ return $AppMediaContentCopyWith<$Res>(_value.mediaContent!, (value) {
+ return _then(_value.copyWith(mediaContent: value) as $Val);
+ });
+ }
+}
+
+/// @nodoc
+abstract class _$$NetworkImagePreviewStateImplCopyWith<$Res>
+ implements $NetworkImagePreviewStateCopyWith<$Res> {
+ factory _$$NetworkImagePreviewStateImplCopyWith(
+ _$NetworkImagePreviewStateImpl value,
+ $Res Function(_$NetworkImagePreviewStateImpl) then) =
+ __$$NetworkImagePreviewStateImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call(
+ {bool loading,
+ AppMediaContent? mediaContent,
+ List? mediaBytes,
+ Object? error});
+
+ @override
+ $AppMediaContentCopyWith<$Res>? get mediaContent;
+}
+
+/// @nodoc
+class __$$NetworkImagePreviewStateImplCopyWithImpl<$Res>
+ extends _$NetworkImagePreviewStateCopyWithImpl<$Res,
+ _$NetworkImagePreviewStateImpl>
+ implements _$$NetworkImagePreviewStateImplCopyWith<$Res> {
+ __$$NetworkImagePreviewStateImplCopyWithImpl(
+ _$NetworkImagePreviewStateImpl _value,
+ $Res Function(_$NetworkImagePreviewStateImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? loading = null,
+ Object? mediaContent = freezed,
+ Object? mediaBytes = freezed,
+ Object? error = freezed,
+ }) {
+ return _then(_$NetworkImagePreviewStateImpl(
+ loading: null == loading
+ ? _value.loading
+ : loading // ignore: cast_nullable_to_non_nullable
+ as bool,
+ mediaContent: freezed == mediaContent
+ ? _value.mediaContent
+ : mediaContent // ignore: cast_nullable_to_non_nullable
+ as AppMediaContent?,
+ mediaBytes: freezed == mediaBytes
+ ? _value._mediaBytes
+ : mediaBytes // ignore: cast_nullable_to_non_nullable
+ as List?,
+ error: freezed == error ? _value.error : error,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$NetworkImagePreviewStateImpl implements _NetworkImagePreviewState {
+ const _$NetworkImagePreviewStateImpl(
+ {this.loading = false,
+ this.mediaContent,
+ final List? mediaBytes,
+ this.error})
+ : _mediaBytes = mediaBytes;
+
+ @override
+ @JsonKey()
+ final bool loading;
+ @override
+ final AppMediaContent? mediaContent;
+ final List? _mediaBytes;
+ @override
+ List? get mediaBytes {
+ final value = _mediaBytes;
+ if (value == null) return null;
+ if (_mediaBytes is EqualUnmodifiableListView) return _mediaBytes;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableListView(value);
+ }
+
+ @override
+ final Object? error;
+
+ @override
+ String toString() {
+ return 'NetworkImagePreviewState(loading: $loading, mediaContent: $mediaContent, mediaBytes: $mediaBytes, error: $error)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$NetworkImagePreviewStateImpl &&
+ (identical(other.loading, loading) || other.loading == loading) &&
+ (identical(other.mediaContent, mediaContent) ||
+ other.mediaContent == mediaContent) &&
+ const DeepCollectionEquality()
+ .equals(other._mediaBytes, _mediaBytes) &&
+ const DeepCollectionEquality().equals(other.error, error));
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ runtimeType,
+ loading,
+ mediaContent,
+ const DeepCollectionEquality().hash(_mediaBytes),
+ const DeepCollectionEquality().hash(error));
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$NetworkImagePreviewStateImplCopyWith<_$NetworkImagePreviewStateImpl>
+ get copyWith => __$$NetworkImagePreviewStateImplCopyWithImpl<
+ _$NetworkImagePreviewStateImpl>(this, _$identity);
+}
+
+abstract class _NetworkImagePreviewState implements NetworkImagePreviewState {
+ const factory _NetworkImagePreviewState(
+ {final bool loading,
+ final AppMediaContent? mediaContent,
+ final List? mediaBytes,
+ final Object? error}) = _$NetworkImagePreviewStateImpl;
+
+ @override
+ bool get loading;
+ @override
+ AppMediaContent? get mediaContent;
+ @override
+ List? get mediaBytes;
+ @override
+ Object? get error;
+ @override
+ @JsonKey(ignore: true)
+ _$$NetworkImagePreviewStateImplCopyWith<_$NetworkImagePreviewStateImpl>
+ get copyWith => throw _privateConstructorUsedError;
+}
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
new file mode 100644
index 0000000..b075768
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/image_preview/image_preview_screen.dart
@@ -0,0 +1,128 @@
+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
new file mode 100644
index 0000000..2513eb1
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/media_preview.dart
@@ -0,0 +1,22 @@
+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/video_preview_screen.dart b/app/lib/ui/flow/media_preview/video_preview_screen.dart
new file mode 100644
index 0000000..fa98229
--- /dev/null
+++ b/app/lib/ui/flow/media_preview/video_preview_screen.dart
@@ -0,0 +1,21 @@
+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/navigation/app_router.dart b/app/lib/ui/navigation/app_router.dart
index e4b55f6..dcc76b7 100644
--- a/app/lib/ui/navigation/app_router.dart
+++ b/app/lib/ui/navigation/app_router.dart
@@ -1,5 +1,9 @@
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/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 'app_route.dart';
@@ -20,10 +24,38 @@ class AppRouter {
builder: (context) => const AccountsScreen(),
);
+ static AppRoute imagePreview({required AppMedia media}) => AppRoute(
+ AppRoutePath.imagePreview,
+ builder: (context) => ImagePreviewScreen(media: media),
+ );
+
+ static AppRoute videoPreview({required String path, required bool isLocal}) =>
+ AppRoute(
+ AppRoutePath.videoPreview,
+ builder: (context) => const VideoPreviewScreen(),
+ );
+
static final routes = [
home.goRoute,
onBoard.goRoute,
accounts.goRoute,
+ GoRoute(
+ path: AppRoutePath.imagePreview,
+ pageBuilder: (context, state) {
+ return CustomTransitionPage(
+ opaque: false,
+ key: state.pageKey,
+ child: state.widget(context),
+ transitionsBuilder: (context, animation, secondaryAnimation, child) {
+ return FadeTransition(opacity: animation, child: child);
+ },
+ );
+ },
+ ),
+ GoRoute(
+ path: AppRoutePath.videoPreview,
+ builder: (context, state) => state.widget(context),
+ ),
];
}
@@ -31,4 +63,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';
}
diff --git a/app/pubspec.lock b/app/pubspec.lock
index f256aad..6ddf9e0 100644
--- a/app/pubspec.lock
+++ b/app/pubspec.lock
@@ -1156,10 +1156,10 @@ packages:
dependency: "direct main"
description:
name: video_player
- sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2
+ sha256: afc65f4b8bcb2c188f64a591f84fb471f4f2e19fc607c65fd8d2f8fedb3dec23
url: "https://pub.dev"
source: hosted
- version: "2.8.2"
+ version: "2.8.3"
video_player_android:
dependency: transitive
description:
diff --git a/app/pubspec.yaml b/app/pubspec.yaml
index dd9198a..3989ba8 100644
--- a/app/pubspec.yaml
+++ b/app/pubspec.yaml
@@ -28,7 +28,7 @@ dependencies:
flutter_svg: ^2.0.9
fluttertoast: ^8.2.4
cached_network_image: ^3.3.1
- video_player: ^2.8.2
+ video_player: ^2.8.3
visibility_detector: ^0.4.0+2
flutter_displaymode: ^0.6.0
diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies
index 24d471e..1ebfe74 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-05 17:32:12.442322","version":"3.19.1"}
\ 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":"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
diff --git a/data/lib/errors/app_error.dart b/data/lib/errors/app_error.dart
index b7f638d..46f196e 100644
--- a/data/lib/errors/app_error.dart
+++ b/data/lib/errors/app_error.dart
@@ -41,6 +41,13 @@ class UserGoogleSignInAccountNotFound extends AppError {
"User google signed in account not found. Please sign in again");
}
+class BackUpFolderNotFound extends AppError {
+ const BackUpFolderNotFound()
+ : super(
+ l10nCode: AppErrorL10nCodes.backUpFolderNotFound,
+ message: "Back up folder not found");
+}
+
class SomethingWentWrongError extends AppError {
const SomethingWentWrongError({String? message, String? statusCode})
: super(
diff --git a/data/lib/errors/l10n_error_codes.dart b/data/lib/errors/l10n_error_codes.dart
index 4b6136d..fcad6a8 100644
--- a/data/lib/errors/l10n_error_codes.dart
+++ b/data/lib/errors/l10n_error_codes.dart
@@ -2,4 +2,5 @@ class AppErrorL10nCodes {
static const noInternetConnection = 'no-internet-connection';
static const somethingWentWrongError = 'something-went-wrong';
static const googleSignInUserNotFoundError = 'google-sing-in-user-not-found';
+ static const backUpFolderNotFound = 'back-up-folder-not-found';
}
diff --git a/data/lib/models/media/media.dart b/data/lib/models/media/media.dart
index 865f80d..48b7175 100644
--- a/data/lib/models/media/media.dart
+++ b/data/lib/models/media/media.dart
@@ -1,15 +1,38 @@
+import 'dart:io';
+import 'dart:typed_data';
+import 'dart:ui' show Size;
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;
+import 'package:photo_manager/photo_manager.dart'
+ show AssetEntity, ThumbnailFormat, ThumbnailSize;
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,
- video,
- other;
+ video;
bool get isImage => this == AppMediaType.image;
@@ -77,7 +100,7 @@ class AppMedia with _$AppMedia {
required String id,
String? name,
required String path,
- String? thumbnailPath,
+ String? thumbnailLink,
double? displayHeight,
double? displayWidth,
required AppMediaType type,
@@ -97,8 +120,7 @@ class AppMedia with _$AppMedia {
factory AppMedia.fromGoogleDriveFile(drive.File file) {
final type = AppMediaType.getType(
- mimeType: file.mimeType,
- location: file.thumbnailLink ?? file.description ?? '');
+ mimeType: file.mimeType, location: file.description ?? '');
final height = type.isImage
? file.imageMediaMetadata?.height?.toDouble()
@@ -120,11 +142,10 @@ class AppMedia with _$AppMedia {
milliseconds:
int.parse(file.videoMediaMetadata?.durationMillis ?? '0'))
: null;
-
return AppMedia(
id: file.id!,
path: file.description ?? file.thumbnailLink ?? '',
- thumbnailPath: file.thumbnailLink,
+ thumbnailLink: file.thumbnailLink,
name: file.name,
createdTime: file.createdTime,
modifiedTime: file.modifiedTime,
@@ -135,6 +156,8 @@ class AppMedia with _$AppMedia {
displayWidth: width,
videoDuration: videoDuration,
orientation: orientation,
+ latitude: file.imageMediaMetadata?.location?.latitude,
+ longitude: file.imageMediaMetadata?.location?.longitude,
sources: [AppMediaSource.googleDrive],
);
}
@@ -149,6 +172,7 @@ class AppMedia with _$AppMedia {
return AppMedia(
id: asset.id,
path: file.path,
+ name: asset.title,
mimeType: asset.mimeType,
size: length.toString(),
type: type,
@@ -166,3 +190,18 @@ 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 7c6a696..a5ee01b 100644
--- a/data/lib/models/media/media.freezed.dart
+++ b/data/lib/models/media/media.freezed.dart
@@ -23,7 +23,7 @@ mixin _$AppMedia {
String get id => throw _privateConstructorUsedError;
String? get name => throw _privateConstructorUsedError;
String get path => throw _privateConstructorUsedError;
- String? get thumbnailPath => throw _privateConstructorUsedError;
+ String? get thumbnailLink => throw _privateConstructorUsedError;
double? get displayHeight => throw _privateConstructorUsedError;
double? get displayWidth => throw _privateConstructorUsedError;
AppMediaType get type => throw _privateConstructorUsedError;
@@ -52,7 +52,7 @@ abstract class $AppMediaCopyWith<$Res> {
{String id,
String? name,
String path,
- String? thumbnailPath,
+ String? thumbnailLink,
double? displayHeight,
double? displayWidth,
AppMediaType type,
@@ -83,7 +83,7 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia>
Object? id = null,
Object? name = freezed,
Object? path = null,
- Object? thumbnailPath = freezed,
+ Object? thumbnailLink = freezed,
Object? displayHeight = freezed,
Object? displayWidth = freezed,
Object? type = null,
@@ -110,9 +110,9 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia>
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String,
- thumbnailPath: freezed == thumbnailPath
- ? _value.thumbnailPath
- : thumbnailPath // ignore: cast_nullable_to_non_nullable
+ thumbnailLink: freezed == thumbnailLink
+ ? _value.thumbnailLink
+ : thumbnailLink // ignore: cast_nullable_to_non_nullable
as String?,
displayHeight: freezed == displayHeight
? _value.displayHeight
@@ -178,7 +178,7 @@ abstract class _$$AppMediaImplCopyWith<$Res>
{String id,
String? name,
String path,
- String? thumbnailPath,
+ String? thumbnailLink,
double? displayHeight,
double? displayWidth,
AppMediaType type,
@@ -207,7 +207,7 @@ class __$$AppMediaImplCopyWithImpl<$Res>
Object? id = null,
Object? name = freezed,
Object? path = null,
- Object? thumbnailPath = freezed,
+ Object? thumbnailLink = freezed,
Object? displayHeight = freezed,
Object? displayWidth = freezed,
Object? type = null,
@@ -234,9 +234,9 @@ class __$$AppMediaImplCopyWithImpl<$Res>
? _value.path
: path // ignore: cast_nullable_to_non_nullable
as String,
- thumbnailPath: freezed == thumbnailPath
- ? _value.thumbnailPath
- : thumbnailPath // ignore: cast_nullable_to_non_nullable
+ thumbnailLink: freezed == thumbnailLink
+ ? _value.thumbnailLink
+ : thumbnailLink // ignore: cast_nullable_to_non_nullable
as String?,
displayHeight: freezed == displayHeight
? _value.displayHeight
@@ -297,7 +297,7 @@ class _$AppMediaImpl implements _AppMedia {
{required this.id,
this.name,
required this.path,
- this.thumbnailPath,
+ this.thumbnailLink,
this.displayHeight,
this.displayWidth,
required this.type,
@@ -322,7 +322,7 @@ class _$AppMediaImpl implements _AppMedia {
@override
final String path;
@override
- final String? thumbnailPath;
+ final String? thumbnailLink;
@override
final double? displayHeight;
@override
@@ -356,7 +356,7 @@ class _$AppMediaImpl implements _AppMedia {
@override
String toString() {
- return 'AppMedia(id: $id, name: $name, path: $path, thumbnailPath: $thumbnailPath, 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, 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
@@ -367,8 +367,8 @@ class _$AppMediaImpl implements _AppMedia {
(identical(other.id, id) || other.id == id) &&
(identical(other.name, name) || other.name == name) &&
(identical(other.path, path) || other.path == path) &&
- (identical(other.thumbnailPath, thumbnailPath) ||
- other.thumbnailPath == thumbnailPath) &&
+ (identical(other.thumbnailLink, thumbnailLink) ||
+ other.thumbnailLink == thumbnailLink) &&
(identical(other.displayHeight, displayHeight) ||
other.displayHeight == displayHeight) &&
(identical(other.displayWidth, displayWidth) ||
@@ -399,7 +399,7 @@ class _$AppMediaImpl implements _AppMedia {
id,
name,
path,
- thumbnailPath,
+ thumbnailLink,
displayHeight,
displayWidth,
type,
@@ -432,7 +432,7 @@ abstract class _AppMedia implements AppMedia {
{required final String id,
final String? name,
required final String path,
- final String? thumbnailPath,
+ final String? thumbnailLink,
final double? displayHeight,
final double? displayWidth,
required final AppMediaType type,
@@ -456,7 +456,7 @@ abstract class _AppMedia implements AppMedia {
@override
String get path;
@override
- String? get thumbnailPath;
+ String? get thumbnailLink;
@override
double? get displayHeight;
@override
diff --git a/data/lib/models/media/media.g.dart b/data/lib/models/media/media.g.dart
index 8f0c93e..ef59dee 100644
--- a/data/lib/models/media/media.g.dart
+++ b/data/lib/models/media/media.g.dart
@@ -11,7 +11,7 @@ _$AppMediaImpl _$$AppMediaImplFromJson(Map json) =>
id: json['id'] as String,
name: json['name'] as String?,
path: json['path'] as String,
- thumbnailPath: json['thumbnailPath'] as String?,
+ thumbnailLink: json['thumbnailLink'] as String?,
displayHeight: (json['displayHeight'] as num?)?.toDouble(),
displayWidth: (json['displayWidth'] as num?)?.toDouble(),
type: $enumDecode(_$AppMediaTypeEnumMap, json['type']),
@@ -41,7 +41,7 @@ Map _$$AppMediaImplToJson(_$AppMediaImpl instance) =>
'id': instance.id,
'name': instance.name,
'path': instance.path,
- 'thumbnailPath': instance.thumbnailPath,
+ 'thumbnailLink': instance.thumbnailLink,
'displayHeight': instance.displayHeight,
'displayWidth': instance.displayWidth,
'type': _$AppMediaTypeEnumMap[instance.type]!,
@@ -58,9 +58,9 @@ Map _$$AppMediaImplToJson(_$AppMediaImpl instance) =>
};
const _$AppMediaTypeEnumMap = {
+ AppMediaType.other: 'other',
AppMediaType.image: 'image',
AppMediaType.video: 'video',
- AppMediaType.other: 'other',
};
const _$AppMediaOrientationEnumMap = {
diff --git a/data/lib/models/media_content/media_content.dart b/data/lib/models/media_content/media_content.dart
new file mode 100644
index 0000000..882786d
--- /dev/null
+++ b/data/lib/models/media_content/media_content.dart
@@ -0,0 +1,18 @@
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:googleapis/drive/v3.dart' as drive show Media;
+
+part 'media_content.freezed.dart';
+
+@freezed
+class AppMediaContent with _$AppMediaContent {
+ const factory AppMediaContent(
+ {required Stream> stream,
+ required int? length,
+ required String contentType}) = _AppMediaContent;
+
+ factory AppMediaContent.fromGoogleDrive(drive.Media media) => AppMediaContent(
+ stream: media.stream,
+ length: media.length,
+ contentType: media.contentType,
+ );
+}
diff --git a/data/lib/models/media_content/media_content.freezed.dart b/data/lib/models/media_content/media_content.freezed.dart
new file mode 100644
index 0000000..7bd4880
--- /dev/null
+++ b/data/lib/models/media_content/media_content.freezed.dart
@@ -0,0 +1,170 @@
+// 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_content.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 _$AppMediaContent {
+ Stream> get stream => throw _privateConstructorUsedError;
+ int? get length => throw _privateConstructorUsedError;
+ String get contentType => throw _privateConstructorUsedError;
+
+ @JsonKey(ignore: true)
+ $AppMediaContentCopyWith get copyWith =>
+ throw _privateConstructorUsedError;
+}
+
+/// @nodoc
+abstract class $AppMediaContentCopyWith<$Res> {
+ factory $AppMediaContentCopyWith(
+ AppMediaContent value, $Res Function(AppMediaContent) then) =
+ _$AppMediaContentCopyWithImpl<$Res, AppMediaContent>;
+ @useResult
+ $Res call({Stream> stream, int? length, String contentType});
+}
+
+/// @nodoc
+class _$AppMediaContentCopyWithImpl<$Res, $Val extends AppMediaContent>
+ implements $AppMediaContentCopyWith<$Res> {
+ _$AppMediaContentCopyWithImpl(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? stream = null,
+ Object? length = freezed,
+ Object? contentType = null,
+ }) {
+ return _then(_value.copyWith(
+ stream: null == stream
+ ? _value.stream
+ : stream // ignore: cast_nullable_to_non_nullable
+ as Stream>,
+ length: freezed == length
+ ? _value.length
+ : length // ignore: cast_nullable_to_non_nullable
+ as int?,
+ contentType: null == contentType
+ ? _value.contentType
+ : contentType // ignore: cast_nullable_to_non_nullable
+ as String,
+ ) as $Val);
+ }
+}
+
+/// @nodoc
+abstract class _$$AppMediaContentImplCopyWith<$Res>
+ implements $AppMediaContentCopyWith<$Res> {
+ factory _$$AppMediaContentImplCopyWith(_$AppMediaContentImpl value,
+ $Res Function(_$AppMediaContentImpl) then) =
+ __$$AppMediaContentImplCopyWithImpl<$Res>;
+ @override
+ @useResult
+ $Res call({Stream> stream, int? length, String contentType});
+}
+
+/// @nodoc
+class __$$AppMediaContentImplCopyWithImpl<$Res>
+ extends _$AppMediaContentCopyWithImpl<$Res, _$AppMediaContentImpl>
+ implements _$$AppMediaContentImplCopyWith<$Res> {
+ __$$AppMediaContentImplCopyWithImpl(
+ _$AppMediaContentImpl _value, $Res Function(_$AppMediaContentImpl) _then)
+ : super(_value, _then);
+
+ @pragma('vm:prefer-inline')
+ @override
+ $Res call({
+ Object? stream = null,
+ Object? length = freezed,
+ Object? contentType = null,
+ }) {
+ return _then(_$AppMediaContentImpl(
+ stream: null == stream
+ ? _value.stream
+ : stream // ignore: cast_nullable_to_non_nullable
+ as Stream>,
+ length: freezed == length
+ ? _value.length
+ : length // ignore: cast_nullable_to_non_nullable
+ as int?,
+ contentType: null == contentType
+ ? _value.contentType
+ : contentType // ignore: cast_nullable_to_non_nullable
+ as String,
+ ));
+ }
+}
+
+/// @nodoc
+
+class _$AppMediaContentImpl implements _AppMediaContent {
+ const _$AppMediaContentImpl(
+ {required this.stream, required this.length, required this.contentType});
+
+ @override
+ final Stream> stream;
+ @override
+ final int? length;
+ @override
+ final String contentType;
+
+ @override
+ String toString() {
+ return 'AppMediaContent(stream: $stream, length: $length, contentType: $contentType)';
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ (other.runtimeType == runtimeType &&
+ other is _$AppMediaContentImpl &&
+ (identical(other.stream, stream) || other.stream == stream) &&
+ (identical(other.length, length) || other.length == length) &&
+ (identical(other.contentType, contentType) ||
+ other.contentType == contentType));
+ }
+
+ @override
+ int get hashCode => Object.hash(runtimeType, stream, length, contentType);
+
+ @JsonKey(ignore: true)
+ @override
+ @pragma('vm:prefer-inline')
+ _$$AppMediaContentImplCopyWith<_$AppMediaContentImpl> get copyWith =>
+ __$$AppMediaContentImplCopyWithImpl<_$AppMediaContentImpl>(
+ this, _$identity);
+}
+
+abstract class _AppMediaContent implements AppMediaContent {
+ const factory _AppMediaContent(
+ {required final Stream> stream,
+ required final int? length,
+ required final String contentType}) = _$AppMediaContentImpl;
+
+ @override
+ Stream> get stream;
+ @override
+ int? get length;
+ @override
+ String get contentType;
+ @override
+ @JsonKey(ignore: true)
+ _$$AppMediaContentImplCopyWith<_$AppMediaContentImpl> get copyWith =>
+ throw _privateConstructorUsedError;
+}
diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart
index 98893f5..db8ebcb 100644
--- a/data/lib/services/google_drive_service.dart
+++ b/data/lib/services/google_drive_service.dart
@@ -1,5 +1,6 @@
import 'dart:io';
import 'package:data/models/media/media.dart';
+import 'package:data/models/media_content/media_content.dart';
import 'package:extension_google_sign_in_as_googleapis_auth/extension_google_sign_in_as_googleapis_auth.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_sign_in/google_sign_in.dart';
@@ -13,8 +14,6 @@ final googleDriveServiceProvider = Provider(
class GoogleDriveService {
final String _backUpFolderName = "Cloud Gallery Backup";
- final String _backUpFolderDescription =
- "This folder is used to backup media from Cloud Gallery";
final GoogleSignIn _googleSignIn;
@@ -25,7 +24,9 @@ class GoogleDriveService {
throw const UserGoogleSignInAccountNotFound();
}
final client = await _googleSignIn.authenticatedClient();
- return drive.DriveApi(client!);
+ final api = drive.DriveApi(client!);
+ client.close();
+ return api;
}
Future getBackupFolderId() async {
@@ -33,16 +34,14 @@ class GoogleDriveService {
final driveApi = await _getGoogleDriveAPI();
final response = await driveApi.files.list(
- q: "name='$_backUpFolderName' and mimeType='application/vnd.google-apps.folder'",
+ q: "name='$_backUpFolderName' and trashed=false and mimeType='application/vnd.google-apps.folder'",
);
- if (response.files?.isNotEmpty ??
- false || response.files?.first.trashed == false) {
+ if (response.files?.isNotEmpty ?? false) {
return response.files?.first.id;
} else {
final folder = drive.File(
name: _backUpFolderName,
- description: _backUpFolderDescription,
mimeType: 'application/vnd.google-apps.folder',
);
final googleDriveFolder = await driveApi.files.create(folder);
@@ -61,7 +60,7 @@ class GoogleDriveService {
final response = await driveApi.files.list(
q: "'$backUpFolderId' in parents and trashed=false",
$fields:
- "files(id, name, description, mimeType, thumbnailLink, webContentLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata)",
+ "files(id, name, description, mimeType, thumbnailLink, createdTime, modifiedTime, size, imageMediaMetadata, videoMediaMetadata)",
);
return (response.files ?? [])
@@ -90,7 +89,17 @@ class GoogleDriveService {
uploadMedia: drive.Media(localFile.openRead(), localFile.lengthSync()),
);
} catch (error) {
+ if (error is drive.DetailedApiRequestError && error.status == 404) {
+ throw const BackUpFolderNotFound();
+ }
throw AppError.fromError(error);
}
}
+
+ Future fetchMediaBytes(String mediaId) async {
+ final api = await _getGoogleDriveAPI();
+ final media = await api.files.get(mediaId,
+ downloadOptions: drive.DownloadOptions.fullMedia) as drive.Media;
+ return AppMediaContent.fromGoogleDrive(media);
+ }
}
diff --git a/data/lib/services/local_media_service.dart b/data/lib/services/local_media_service.dart
index 5b02374..982d3e5 100644
--- a/data/lib/services/local_media_service.dart
+++ b/data/lib/services/local_media_service.dart
@@ -3,6 +3,8 @@ import 'package:data/models/media/media.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:photo_manager/photo_manager.dart';
+import '../errors/app_error.dart';
+
final localMediaServiceProvider = Provider(
(ref) => const LocalMediaService(),
);
@@ -25,18 +27,22 @@ class LocalMediaService {
Future> getLocalMedia(
{required int start, required int end}) async {
- final assets = await PhotoManager.getAssetListRange(
- start: start,
- end: end,
- filterOption: FilterOptionGroup(
- orders: [const OrderOption(type: OrderOptionType.createDate)],
- ),
- );
- final files = await Future.wait(
- assets.map(
- (asset) => AppMedia.fromAssetEntity(asset),
- ),
- );
- return files.whereNotNull().toList();
+ try {
+ final assets = await PhotoManager.getAssetListRange(
+ start: start,
+ end: end,
+ filterOption: FilterOptionGroup(
+ orders: [const OrderOption(type: OrderOptionType.createDate)],
+ ),
+ );
+ final files = await Future.wait(
+ assets.map(
+ (asset) => AppMedia.fromAssetEntity(asset),
+ ),
+ );
+ return files.whereNotNull().toList();
+ } catch (e) {
+ throw AppError.fromError(e);
+ }
}
}
diff --git a/style/lib/buttons/action_button.dart b/style/lib/buttons/action_button.dart
new file mode 100644
index 0000000..cdef9d5
--- /dev/null
+++ b/style/lib/buttons/action_button.dart
@@ -0,0 +1,48 @@
+import 'dart:io';
+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';
+
+class ActionButton extends StatelessWidget {
+ final void Function() onPressed;
+ final Widget icon;
+ final bool progress;
+ final Color? backgroundColor;
+ final double size;
+ final EdgeInsets padding;
+
+ const ActionButton(
+ {super.key,
+ required this.onPressed,
+ required this.icon,
+ this.size = 30,
+ this.backgroundColor,
+ this.progress = false,
+ this.padding = const EdgeInsets.all(0)});
+
+ @override
+ Widget build(BuildContext context) {
+ if (Platform.isIOS) {
+ return CupertinoButton(
+ minSize: size,
+ borderRadius: BorderRadius.circular(size),
+ color: backgroundColor ?? context.colorScheme.containerNormal,
+ onPressed: onPressed,
+ padding: padding,
+ child: progress ? const AppCircularProgressIndicator() : icon,
+ );
+ } else {
+ return IconButton(
+ style: IconButton.styleFrom(
+ backgroundColor:
+ backgroundColor ?? context.colorScheme.containerNormal,
+ minimumSize: Size(size, size),
+ ),
+ onPressed: onPressed,
+ icon: progress ? const AppCircularProgressIndicator() : icon,
+ padding: padding,
+ );
+ }
+ }
+}
diff --git a/style/lib/indicators/circular_progress_indicator.dart b/style/lib/indicators/circular_progress_indicator.dart
index 54de2b6..756510b 100644
--- a/style/lib/indicators/circular_progress_indicator.dart
+++ b/style/lib/indicators/circular_progress_indicator.dart
@@ -19,6 +19,7 @@ class AppCircularProgressIndicator extends StatelessWidget {
width: size,
height: size,
child: CircularProgressIndicator.adaptive(
+ strokeWidth: size / 8,
value: value,
valueColor: AlwaysStoppedAnimation(
color ?? context.colorScheme.primary,
diff --git a/style/lib/theme/app_theme_builder.dart b/style/lib/theme/app_theme_builder.dart
index 6dd6ded..e4e8ea5 100644
--- a/style/lib/theme/app_theme_builder.dart
+++ b/style/lib/theme/app_theme_builder.dart
@@ -32,7 +32,6 @@ class AppThemeBuilder {
surfaceTintColor: colorScheme.surface,
foregroundColor: colorScheme.textPrimary,
scrolledUnderElevation: 3,
-
),
);
}
@@ -43,7 +42,7 @@ class AppThemeBuilder {
brightness: colorScheme.brightness,
primaryColor: colorScheme.primary,
primaryContrastingColor: colorScheme.onPrimary,
- barBackgroundColor: colorScheme.surface,
+ barBackgroundColor: colorScheme.barColor,
scaffoldBackgroundColor: colorScheme.surface,
textTheme: CupertinoTextThemeData(
primaryColor: colorScheme.textPrimary,
diff --git a/style/lib/theme/colors.dart b/style/lib/theme/colors.dart
index 605718a..867f7f4 100644
--- a/style/lib/theme/colors.dart
+++ b/style/lib/theme/colors.dart
@@ -20,6 +20,9 @@ class AppColors {
static const surfaceLightColor = Color(0xFFFFFFFF);
static const surfaceDarkColor = Color(0xFF000000);
+ static const barLightColor = Color(0xCCFFFFFF);
+ static const barDarkColor = Color(0xCC000000);
+
static const textPrimaryLightColor = Color(0xDE000000);
static const textSecondaryLightColor = Color(0x99000000);
static const textDisabledLightColor = Color(0x66000000);
diff --git a/style/lib/theme/theme.dart b/style/lib/theme/theme.dart
index 81d5b62..71531a2 100644
--- a/style/lib/theme/theme.dart
+++ b/style/lib/theme/theme.dart
@@ -32,6 +32,7 @@ class AppColorScheme {
final Color outline;
final Color textPrimary;
final Color textSecondary;
+ final Color barColor;
final Color textDisabled;
final Color outlineInverse;
final Color textInversePrimary;
@@ -65,6 +66,7 @@ class AppColorScheme {
required this.textDisabled,
required this.outlineInverse,
required this.textInversePrimary,
+ required this.barColor,
required this.textInverseSecondary,
required this.textInverseDisabled,
required this.containerNormalInverse,
@@ -90,8 +92,6 @@ class AppColorScheme {
Color get containerLowOnSurface => Color.alphaBlend(containerLow, surface);
}
-
-
final appColorSchemeLight = AppColorScheme(
primary: AppColors.primaryColor,
secondary: AppColors.secondaryLightColor,
@@ -120,6 +120,7 @@ final appColorSchemeLight = AppColorScheme(
onPrimary: AppColors.textPrimaryDarkColor,
onSecondary: AppColors.textSecondaryDarkColor,
onDisabled: AppColors.textDisabledLightColor,
+ barColor: AppColors.barLightColor,
brightness: Brightness.light,
);
@@ -151,5 +152,6 @@ final appColorSchemeDark = AppColorScheme(
onPrimary: AppColors.textPrimaryDarkColor,
onSecondary: AppColors.textSecondaryDarkColor,
onDisabled: AppColors.textDisabledDarkColor,
+ barColor: AppColors.barDarkColor,
brightness: Brightness.dark,
);