diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index a799a2a..0df894e 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -219,6 +219,13 @@ + + + + + + @@ -282,6 +289,13 @@ + + + + + + @@ -296,6 +310,13 @@ + + + + + + @@ -334,7 +355,7 @@ - @@ -369,7 +390,7 @@ - @@ -383,7 +404,7 @@ - @@ -405,8 +426,7 @@ - @@ -459,6 +479,27 @@ + + + + + + + + + + + + + + + + + + @@ -476,21 +517,21 @@ - - - @@ -511,7 +552,84 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -522,6 +640,13 @@ + + + + + + @@ -578,6 +703,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -697,6 +871,27 @@ + + + + + + + + + + + + + + + + + + @@ -721,7 +916,7 @@ - @@ -732,6 +927,27 @@ + + + + + + + + + + + + + + + + + + @@ -773,6 +989,7 @@ + @@ -780,24 +997,25 @@ + + - + - + - + - - + @@ -805,15 +1023,30 @@ + + + - - - + + + - + + + + + + + + + + + + + @@ -822,6 +1055,13 @@ + + + + + + + @@ -838,11 +1078,17 @@ + + + - + + + + diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index 9f849ae..28165f8 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -2,12 +2,23 @@ - + - + + + + + + + + + + + + diff --git a/app/android/app/build.gradle b/app/android/app/build.gradle index 5aeb50e..b5280ff 100644 --- a/app/android/app/build.gradle +++ b/app/android/app/build.gradle @@ -46,7 +46,7 @@ android { applicationId "com.canopas.cloud_gallery" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. - minSdkVersion flutter.minSdkVersion + minSdkVersion 21 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/app/assets/images/app_logo.png b/app/assets/images/app_logo.png index 540044f..1e006a6 100644 Binary files a/app/assets/images/app_logo.png and b/app/assets/images/app_logo.png differ diff --git a/app/assets/images/icons/google_photos.svg b/app/assets/images/icons/google_photos.svg new file mode 100644 index 0000000..44b3fb3 --- /dev/null +++ b/app/assets/images/icons/google_photos.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/assets/locales/app_en.arb b/app/assets/locales/app_en.arb index bf8f169..b52cf92 100644 --- a/app/assets/locales/app_en.arb +++ b/app/assets/locales/app_en.arb @@ -1,10 +1,15 @@ { "app_name": "Cloud Gallery", - - "common_get_started":"Get Started", - + "common_get_started": "Get Started", + "common_done": "Done", + "common_local": "Local", + "google_drive_title": "Google Drive", "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.", - - "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! 🚀" + "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.", + "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! 🚀", + "back_up_on_google_drive_text": "Back up on Google Drive", + "cant_find_media_title": "Can't find your photos or videos", + "ask_for_media_permission_message": "Please give us permission to access your local media, so you can load and enjoy all your favorite photos and videos effortlessly.", + "load_local_media_button_text": "Load local media" } \ No newline at end of file diff --git a/app/ios/Podfile.lock b/app/ios/Podfile.lock index 4c18d2f..9ef5402 100644 --- a/app/ios/Podfile.lock +++ b/app/ios/Podfile.lock @@ -14,9 +14,12 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreInternal (10.19.0): + - FirebaseCoreInternal (10.20.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - Flutter (1.0.0) + - fluttertoast (0.0.2): + - Flutter + - Toast - google_sign_in_ios (0.0.1): - Flutter - FlutterMacOS @@ -33,7 +36,9 @@ PODS: - GTMAppAuth (2.0.0): - AppAuth/Core (~> 1.6) - GTMSessionFetcher/Core (< 4.0, >= 1.5) - - GTMSessionFetcher/Core (3.2.0) + - GTMSessionFetcher/Core (3.3.1) + - permission_handler_apple (9.3.0): + - Flutter - photo_manager (2.0.0): - Flutter - FlutterMacOS @@ -41,11 +46,14 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - Toast (4.1.0) DEPENDENCIES: - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - Flutter (from `Flutter`) + - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) @@ -60,14 +68,19 @@ SPEC REPOS: - GTMAppAuth - GTMSessionFetcher - PromisesObjC + - Toast EXTERNAL SOURCES: firebase_core: :path: ".symlinks/plugins/firebase_core/ios" Flutter: :path: Flutter + fluttertoast: + :path: ".symlinks/plugins/fluttertoast/ios" google_sign_in_ios: :path: ".symlinks/plugins/google_sign_in_ios/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" photo_manager: :path: ".symlinks/plugins/photo_manager/ios" shared_preferences_foundation: @@ -78,16 +91,19 @@ SPEC CHECKSUMS: Firebase: 414ad272f8d02dfbf12662a9d43f4bba9bec2a06 firebase_core: 0af4a2b24f62071f9bf283691c0ee41556dcb3f5 FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f - FirebaseCoreInternal: b444828ea7cfd594fca83046b95db98a2be4f290 + FirebaseCoreInternal: efeeb171ac02d623bdaefe121539939821e10811 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 google_sign_in_ios: 1bfaf6607b44cd1b24c4d4bc39719870440f9ce1 GoogleSignIn: b232380cf495a429b8095d3178a8d5855b42e842 GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34 GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae - GTMSessionFetcher: 41b9ef0b4c08a6db4b7eb51a21ae5183ec99a2c8 + GTMSessionFetcher: 8a1b34ad97ebe6f909fb8b9b77fba99943007556 + permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78 photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4 shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 + Toast: ec33c32b8688982cecc6348adeae667c1b9938da PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 196edbc..60917a9 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -220,7 +220,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1430; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 331C8080294A63A400263BE5 = { diff --git a/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 87131a0..8e3ca5d 100644 --- a/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ showAppSheet( + {required BuildContext context, required Widget child}) { + return showModalBottomSheet( + backgroundColor: context.colorScheme.containerNormalOnSurface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(22), + ), + context: context, + builder: (context) { + return Container( + width: context.mediaQuerySize.width, + decoration: BoxDecoration( + color: context.colorScheme.containerNormalOnSurface, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ), + ), + padding: const EdgeInsets.all(16), + child: SafeArea( + child: child, + ), + ); + }, + ); +} diff --git a/app/lib/components/snack_bar.dart b/app/lib/components/snack_bar.dart new file mode 100644 index 0000000..175146a --- /dev/null +++ b/app/lib/components/snack_bar.dart @@ -0,0 +1,86 @@ +import 'dart:io'; +import 'package:cloud_gallery/domain/extensions/app_error_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; + +void showErrorSnackBar({required BuildContext context, required Object error}) { + final message = error.l10nMessage(context); + showSnackBar( + context: context, + text: message, + icon: Icon( + Icons.warning_amber_rounded, + color: context.colorScheme.alert, + )); +} + +final _toast = FToast(); + +void showSnackBar({ + required BuildContext context, + required String text, + Widget? icon, + Duration duration = const Duration(seconds: 4), +}) { + if (Platform.isIOS || Platform.isMacOS) { + _toast.init(context); + _toast.removeCustomToast(); + _toast.showToast( + fadeDuration: const Duration(milliseconds: 100), + toastDuration: duration, + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: context.colorScheme.containerNormalOnSurface, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + if (icon != null) icon, + if (icon != null) const SizedBox(width: 10), + Flexible( + child: Text( + text, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + overflow: TextOverflow.visible, + ), + ), + ], + ), + ), + gravity: ToastGravity.TOP, + ); + } else { + final snackBar = SnackBar( + elevation: 0, + margin: const EdgeInsets.all(16), + content: Row( + children: [ + if (icon != null) icon, + if (icon != null) const SizedBox(width: 10), + Flexible( + child: Text( + text, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textPrimary, + ), + overflow: TextOverflow.visible, + ), + ), + ], + ), + backgroundColor: context.colorScheme.containerNormalOnSurface, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + behavior: SnackBarBehavior.floating, + duration: duration, + ); + ScaffoldMessenger.of(context).removeCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar(snackBar); + } +} diff --git a/app/lib/utils/assets/assets_paths.dart b/app/lib/domain/assets/assets_paths.dart similarity index 51% rename from app/lib/utils/assets/assets_paths.dart rename to app/lib/domain/assets/assets_paths.dart index 66c3c5f..395413a 100644 --- a/app/lib/utils/assets/assets_paths.dart +++ b/app/lib/domain/assets/assets_paths.dart @@ -4,4 +4,10 @@ class Assets { class Images { String get appIcon => 'assets/images/app_logo.png'; + + Icons get icons => Icons(); +} + +class Icons { + String get googlePhotos => 'assets/images/icons/google_photos.svg'; } diff --git a/app/lib/domain/extensions/app_error_extensions.dart b/app/lib/domain/extensions/app_error_extensions.dart new file mode 100644 index 0000000..45b5ef8 --- /dev/null +++ b/app/lib/domain/extensions/app_error_extensions.dart @@ -0,0 +1,26 @@ +import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; +import 'package:data/errors/app_error.dart'; +import 'package:data/errors/l10n_error_codes.dart'; +import 'package:flutter/cupertino.dart'; + +extension AppErrorExtensions on Object { + String l10nMessage(BuildContext context) { + if (this is AppError) { + switch ((this as AppError).l10nCode) { + case AppErrorL10nCodes.noInternetConnection: + return context.l10n.no_internet_connection_error; + case AppErrorL10nCodes.somethingWentWrongError: + return context.l10n.something_went_wrong_error; + case AppErrorL10nCodes.googleSignInUserNotFoundError: + return context.l10n.user_google_sign_in_account_not_found_error; + default: + return (this as AppError).message ?? + context.l10n.something_went_wrong_error; + } + } else if (this is String) { + return this as String; + } else { + return context.l10n.something_went_wrong_error; + } + } +} diff --git a/app/lib/domain/extensions/context_extensions.dart b/app/lib/domain/extensions/context_extensions.dart new file mode 100644 index 0000000..e8349c1 --- /dev/null +++ b/app/lib/domain/extensions/context_extensions.dart @@ -0,0 +1,6 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +extension BuildContextExtensions on BuildContext { + AppLocalizations get l10n => AppLocalizations.of(this); +} diff --git a/app/lib/domain/extensions/widget_extensions.dart b/app/lib/domain/extensions/widget_extensions.dart new file mode 100644 index 0000000..1aaf65b --- /dev/null +++ b/app/lib/domain/extensions/widget_extensions.dart @@ -0,0 +1,7 @@ +import 'package:flutter/widgets.dart'; + +void runPostFrame(Function() block) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + block(); + }); +} diff --git a/app/lib/extensions/app_error_extensions.dart b/app/lib/extensions/app_error_extensions.dart deleted file mode 100644 index 6f27def..0000000 --- a/app/lib/extensions/app_error_extensions.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:cloud_gallery/utils/extensions/context_extensions.dart'; -import 'package:data/errors/app_error.dart'; -import 'package:data/errors/l10n_error_codes.dart'; -import 'package:flutter/cupertino.dart'; - -extension AppErrorExtensions on AppError { - String l10nMessage(BuildContext context) { - switch (l10nCode) { - case AppErrorL10nCodes.noInternetConnection: - return context.l10n.no_internet_connection_error; - case AppErrorL10nCodes.somethingWentWrongError: - return context.l10n.something_went_wrong_error; - default: - return message ?? context.l10n.something_went_wrong_error; - } - } -} diff --git a/app/lib/ui/app.dart b/app/lib/ui/app.dart index a2047db..bcbca76 100644 --- a/app/lib/ui/app.dart +++ b/app/lib/ui/app.dart @@ -1,11 +1,12 @@ import 'package:cloud_gallery/ui/navigation/app_router.dart'; -import 'package:cloud_gallery/utils/extensions/context_extensions.dart'; +import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:style/extensions/context_extensions.dart'; import 'package:style/theme/theme.dart'; import 'package:style/theme/app_theme_builder.dart'; import 'package:data/storage/app_preferences.dart'; diff --git a/app/lib/ui/flow/accounts/accounts_screen.dart b/app/lib/ui/flow/accounts/accounts_screen.dart new file mode 100644 index 0000000..9f5b40f --- /dev/null +++ b/app/lib/ui/flow/accounts/accounts_screen.dart @@ -0,0 +1,21 @@ +import 'package:cloud_gallery/components/app_page.dart'; +import 'package:flutter/cupertino.dart'; + +class AccountsScreen extends StatefulWidget { + const AccountsScreen({super.key}); + + @override + State createState() => _AccountsScreenState(); +} + +class _AccountsScreenState extends State { + @override + Widget build(BuildContext context) { + return const AppPage( + title: '', + body: Center( + child: Text("Accounts"), + ), + ); + } +} diff --git a/app/lib/ui/flow/home/components/image_item.dart b/app/lib/ui/flow/home/components/image_item.dart new file mode 100644 index 0000000..d5de8ed --- /dev/null +++ b/app/lib/ui/flow/home/components/image_item.dart @@ -0,0 +1,111 @@ +import 'package:flutter/cupertino.dart'; +import 'package:style/animations/on_tap_scale.dart'; +import 'package:style/animations/parallex_effect.dart'; +import 'package:style/extensions/context_extensions.dart'; + +class ImageItem extends StatefulWidget { + final VoidCallback? onTap; + final VoidCallback? onLongTap; + final ImageProvider imageProvider; + final bool isSelected; + + const ImageItem({ + super.key, + required this.imageProvider, + this.onTap, + this.onLongTap, + this.isSelected = false, + }); + + @override + State createState() => _ImageItemState(); +} + +class _ImageItemState extends State { + final _backgroundImageKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ItemSelector( + onTap: widget.onTap, + onLongTap: widget.onLongTap, + isSelected: widget.isSelected, + child: LayoutBuilder( + builder: (context, constraints) { + return ClipRRect( + borderRadius: BorderRadius.circular(12), + child: Flow( + delegate: ParallaxFlowDelegate( + scrollable: Scrollable.of(context), + listItemContext: context, + backgroundImageKey: _backgroundImageKey, + ), + children: [ + Image( + key: _backgroundImageKey, + image: widget.imageProvider, + fit: BoxFit.cover, + width: constraints.maxWidth, + height: constraints.maxHeight * 1.5, + ), + ], + ), + ); + }, + ), + ); + } +} + +class ItemSelector extends StatelessWidget { + final void Function()? onTap; + final void Function()? onLongTap; + final bool isSelected; + final Widget child; + + const ItemSelector( + {super.key, + this.onTap, + this.onLongTap, + required this.isSelected, + required this.child}); + + @override + Widget build(BuildContext context) { + return OnTapScale( + onTap: onTap, + onLongTap: onLongTap, + child: Stack( + children: [ + AnimatedScale( + scale: isSelected ? 0.9 : 1, + duration: const Duration(milliseconds: 100), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 100), + opacity: isSelected ? 0.7 : 1, + child: child), + ), + if (isSelected) + Align( + alignment: Alignment.bottomRight, + child: Container( + padding: const EdgeInsets.all(2), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: context.colorScheme.surface, + border: Border.all( + color: const Color(0xff808080), + ), + ), + child: const Icon( + CupertinoIcons.checkmark_alt, + color: Color(0xff808080), + size: 16, + ), + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/ui/flow/home/components/screen_source_segment_control.dart b/app/lib/ui/flow/home/components/screen_source_segment_control.dart new file mode 100644 index 0000000..9a91741 --- /dev/null +++ b/app/lib/ui/flow/home/components/screen_source_segment_control.dart @@ -0,0 +1,58 @@ +import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; +import '../home_screen_view_model.dart'; + +class ScreenSourceSegmentControl extends ConsumerWidget { + const ScreenSourceSegmentControl({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final sources = + ref.watch(homeViewStateNotifier.select((state) => state.sourcePage)); + final notifier = ref.read(homeViewStateNotifier.notifier); + return Padding( + padding: const EdgeInsets.only(top: 10, left: 16, right: 16, bottom: 10), + child: SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: MediaSource.local, + label: + Text(context.l10n.common_local, style: AppTextStyles.body2), + ), + ButtonSegment( + value: MediaSource.googleDrive, + label: Text( + context.l10n.google_drive_title, + style: AppTextStyles.body2, + ), + ) + ], + selected: {sources.sourcePage}, + multiSelectionEnabled: false, + onSelectionChanged: (source) { + notifier.updateMediaSource( + source: source.first, isChangedByScroll: false); + }, + showSelectedIcon: false, + style: SegmentedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide.none, + ), + side: BorderSide.none, + foregroundColor: context.colorScheme.textPrimary, + selectedForegroundColor: context.colorScheme.onPrimary, + selectedBackgroundColor: context.colorScheme.primary, + backgroundColor: context.colorScheme.containerNormalOnSurface, + visualDensity: VisualDensity.compact, + ), + ), + ), + ); + } +} diff --git a/app/lib/ui/flow/home/google_drive/google_drive_medias_screen.dart b/app/lib/ui/flow/home/google_drive/google_drive_medias_screen.dart new file mode 100644 index 0000000..13a8b2d --- /dev/null +++ b/app/lib/ui/flow/home/google_drive/google_drive_medias_screen.dart @@ -0,0 +1,18 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class GoogleDriveMediasScreen extends ConsumerStatefulWidget { + const GoogleDriveMediasScreen({super.key}); + + @override + ConsumerState createState() => _GoogleDriveViewState(); +} + +class _GoogleDriveViewState extends ConsumerState { + @override + Widget build(BuildContext context) { + return const Center( + child: Text('Google Drive'), + ); + } +} diff --git a/app/lib/ui/flow/home/google_drive/google_drive_medias_screen_view_model.dart b/app/lib/ui/flow/home/google_drive/google_drive_medias_screen_view_model.dart new file mode 100644 index 0000000..e69de29 diff --git a/app/lib/ui/flow/home/home_screen.dart b/app/lib/ui/flow/home/home_screen.dart new file mode 100644 index 0000000..8285a35 --- /dev/null +++ b/app/lib/ui/flow/home/home_screen.dart @@ -0,0 +1,100 @@ +import 'package:cloud_gallery/components/app_page.dart'; +import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; +import 'package:cloud_gallery/ui/flow/home/components/screen_source_segment_control.dart'; +import 'package:cloud_gallery/ui/flow/home/home_screen_view_model.dart'; +import 'package:cloud_gallery/ui/flow/home/local/local_medias_screen.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:style/extensions/context_extensions.dart'; +import '../../../domain/assets/assets_paths.dart'; +import '../../navigation/app_router.dart'; +import 'google_drive/google_drive_medias_screen.dart'; + +class HomeScreen extends ConsumerStatefulWidget { + const HomeScreen({super.key}); + + @override + ConsumerState createState() => _HomeScreenState(); +} + +class _HomeScreenState extends ConsumerState { + late HomeViewStateNotifier notifier; + final _pageController = PageController(); + + @override + void initState() { + notifier = ref.read(homeViewStateNotifier.notifier); + super.initState(); + } + + @override + void dispose() { + _pageController.dispose(); + super.dispose(); + } + + void _updatePageOnChangeSource() { + ref.listen(homeViewStateNotifier.select((value) => value.sourcePage), + (previous, next) { + if (!next.viewChangedByScroll) { + _pageController.animateToPage(next.sourcePage.index, + duration: const Duration(milliseconds: 300), curve: Curves.easeInOut); + } + }); + } + + @override + Widget build(BuildContext context) { + _updatePageOnChangeSource(); + return AppPage( + titleWidget: _titleWidget(context: context), + actions: [ + IconButton( + style: IconButton.styleFrom( + backgroundColor: context.colorScheme.containerNormalOnSurface, + minimumSize: const Size(28, 28), + ), + onPressed: () { + AppRouter.accounts.push(context); + }, + icon: Icon( + CupertinoIcons.person, + color: context.colorScheme.textSecondary, + size: 18, + ), + ), + ], + body: Column( + children: [ + const ScreenSourceSegmentControl(), + Expanded( + child: PageView( + onPageChanged: (value) { + notifier.updateMediaSource( + isChangedByScroll: true, source: MediaSource.values[value]); + }, + controller: _pageController, + children: const [ + LocalMediasScreen(), + GoogleDriveMediasScreen(), + ], + ), + ), + ], + ), + ); + } + + Widget _titleWidget({required BuildContext context}) => Row( + children: [ + const SizedBox(width: 16), + Image.asset( + Assets.images.appIcon, + width: 28, + ), + const SizedBox(width: 10), + Text(context.l10n.app_name) + ], + ); +} diff --git a/app/lib/ui/flow/home/home_screen_view_model.dart b/app/lib/ui/flow/home/home_screen_view_model.dart new file mode 100644 index 0000000..c564e33 --- /dev/null +++ b/app/lib/ui/flow/home/home_screen_view_model.dart @@ -0,0 +1,40 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'home_screen_view_model.freezed.dart'; + +final homeViewStateNotifier = + StateNotifierProvider.autoDispose( + (ref) => HomeViewStateNotifier(), +); + +enum MediaSource { local, googleDrive } + +class HomeViewStateNotifier extends StateNotifier { + HomeViewStateNotifier() : super(const HomeViewState()); + + Future updateMediaSource( + {required MediaSource source, required bool isChangedByScroll}) async { + state = state.copyWith( + sourcePage: SourcePage( + sourcePage: source, viewChangedByScroll: isChangedByScroll)); + } +} + +@freezed +class HomeViewState with _$HomeViewState { + const factory HomeViewState({ + @Default(SourcePage()) SourcePage sourcePage, + @Default(false) bool isLastViewChangedByScroll, + }) = _HomeViewState; +} + +class SourcePage { + final MediaSource sourcePage; + final bool viewChangedByScroll; + + const SourcePage({ + this.sourcePage = MediaSource.local, + this.viewChangedByScroll = false, + }); +} 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 new file mode 100644 index 0000000..3076101 --- /dev/null +++ b/app/lib/ui/flow/home/home_screen_view_model.freezed.dart @@ -0,0 +1,158 @@ +// 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 'home_screen_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#custom-getters-and-methods'); + +/// @nodoc +mixin _$HomeViewState { + SourcePage get sourcePage => throw _privateConstructorUsedError; + bool get isLastViewChangedByScroll => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HomeViewStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HomeViewStateCopyWith<$Res> { + factory $HomeViewStateCopyWith( + HomeViewState value, $Res Function(HomeViewState) then) = + _$HomeViewStateCopyWithImpl<$Res, HomeViewState>; + @useResult + $Res call({SourcePage sourcePage, bool isLastViewChangedByScroll}); +} + +/// @nodoc +class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState> + implements $HomeViewStateCopyWith<$Res> { + _$HomeViewStateCopyWithImpl(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? sourcePage = null, + Object? isLastViewChangedByScroll = null, + }) { + return _then(_value.copyWith( + sourcePage: null == sourcePage + ? _value.sourcePage + : sourcePage // ignore: cast_nullable_to_non_nullable + as SourcePage, + isLastViewChangedByScroll: null == isLastViewChangedByScroll + ? _value.isLastViewChangedByScroll + : isLastViewChangedByScroll // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HomeViewStateImplCopyWith<$Res> + implements $HomeViewStateCopyWith<$Res> { + factory _$$HomeViewStateImplCopyWith( + _$HomeViewStateImpl value, $Res Function(_$HomeViewStateImpl) then) = + __$$HomeViewStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({SourcePage sourcePage, bool isLastViewChangedByScroll}); +} + +/// @nodoc +class __$$HomeViewStateImplCopyWithImpl<$Res> + extends _$HomeViewStateCopyWithImpl<$Res, _$HomeViewStateImpl> + implements _$$HomeViewStateImplCopyWith<$Res> { + __$$HomeViewStateImplCopyWithImpl( + _$HomeViewStateImpl _value, $Res Function(_$HomeViewStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? sourcePage = null, + Object? isLastViewChangedByScroll = null, + }) { + return _then(_$HomeViewStateImpl( + sourcePage: null == sourcePage + ? _value.sourcePage + : sourcePage // ignore: cast_nullable_to_non_nullable + as SourcePage, + isLastViewChangedByScroll: null == isLastViewChangedByScroll + ? _value.isLastViewChangedByScroll + : isLastViewChangedByScroll // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$HomeViewStateImpl implements _HomeViewState { + const _$HomeViewStateImpl( + {this.sourcePage = const SourcePage(), + this.isLastViewChangedByScroll = false}); + + @override + @JsonKey() + final SourcePage sourcePage; + @override + @JsonKey() + final bool isLastViewChangedByScroll; + + @override + String toString() { + return 'HomeViewState(sourcePage: $sourcePage, isLastViewChangedByScroll: $isLastViewChangedByScroll)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HomeViewStateImpl && + (identical(other.sourcePage, sourcePage) || + other.sourcePage == sourcePage) && + (identical(other.isLastViewChangedByScroll, + isLastViewChangedByScroll) || + other.isLastViewChangedByScroll == isLastViewChangedByScroll)); + } + + @override + int get hashCode => + Object.hash(runtimeType, sourcePage, isLastViewChangedByScroll); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HomeViewStateImplCopyWith<_$HomeViewStateImpl> get copyWith => + __$$HomeViewStateImplCopyWithImpl<_$HomeViewStateImpl>(this, _$identity); +} + +abstract class _HomeViewState implements HomeViewState { + const factory _HomeViewState( + {final SourcePage sourcePage, + final bool isLastViewChangedByScroll}) = _$HomeViewStateImpl; + + @override + SourcePage get sourcePage; + @override + bool get isLastViewChangedByScroll; + @override + @JsonKey(ignore: true) + _$$HomeViewStateImplCopyWith<_$HomeViewStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/app/lib/ui/flow/home/local/components/multi_selection_done_button.dart b/app/lib/ui/flow/home/local/components/multi_selection_done_button.dart new file mode 100644 index 0000000..a7ad362 --- /dev/null +++ b/app/lib/ui/flow/home/local/components/multi_selection_done_button.dart @@ -0,0 +1,46 @@ +import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; +import 'package:cloud_gallery/ui/flow/home/local/local_media_screen_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:style/extensions/context_extensions.dart'; +import '../../../../../components/action_sheet.dart'; +import '../../../../../components/app_sheet.dart'; +import '../../../../../domain/assets/assets_paths.dart'; + +class MultiSelectionDoneButton extends ConsumerWidget { + const MultiSelectionDoneButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final notifier = ref.read(localMediasViewStateNotifier.notifier); + return FloatingActionButton( + elevation: 3, + backgroundColor: context.colorScheme.primary, + onPressed: () { + showAppSheet( + context: context, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + AppSheetAction( + icon: SvgPicture.asset( + Assets.images.icons.googlePhotos, + height: 24, + width: 24, + ), + title: context.l10n.back_up_on_google_drive_text, + onPressed: notifier.uploadMediaOnGoogleDrive, + ), + ], + ), + ); + }, + child: Icon( + CupertinoIcons.checkmark_alt, + color: context.colorScheme.onPrimary, + ), + ); + } +} diff --git a/app/lib/ui/flow/home/local/components/no_local_medias_access_screen.dart b/app/lib/ui/flow/home/local/components/no_local_medias_access_screen.dart new file mode 100644 index 0000000..42ae9f5 --- /dev/null +++ b/app/lib/ui/flow/home/local/components/no_local_medias_access_screen.dart @@ -0,0 +1,55 @@ +import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; +import 'package:cloud_gallery/ui/flow/home/local/local_media_screen_view_model.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:permission_handler/permission_handler.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; +import 'package:style/buttons/primary_button.dart'; + +class NoLocalMediasAccessScreen extends ConsumerWidget { + const NoLocalMediasAccessScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final notifier = ref.read(localMediasViewStateNotifier.notifier); + return SafeArea( + child: Padding( + padding: const EdgeInsets.all(30), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + CupertinoIcons.photo, + color: context.colorScheme.containerHighOnSurface, + size: 100, + ), + const SizedBox(height: 20), + Text(context.l10n.cant_find_media_title, + style: AppTextStyles.subtitle2.copyWith( + color: context.colorScheme.textPrimary, + )), + const SizedBox(height: 20), + Text( + context.l10n.ask_for_media_permission_message, + style: AppTextStyles.body2.copyWith( + color: context.colorScheme.textSecondary, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + PrimaryButton( + onPressed: () async { + await openAppSettings(); + await notifier.loadMediaCount(); + await notifier.loadMedia(); + }, + text: context.l10n.load_local_media_button_text, + ), + ], + ), + ), + ); + } +} diff --git a/app/lib/ui/flow/home/local/local_media_screen_view_model.dart b/app/lib/ui/flow/home/local/local_media_screen_view_model.dart new file mode 100644 index 0000000..537fe59 --- /dev/null +++ b/app/lib/ui/flow/home/local/local_media_screen_view_model.dart @@ -0,0 +1,117 @@ +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:flutter_riverpod/flutter_riverpod.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'local_media_screen_view_model.freezed.dart'; + +final localMediasViewStateNotifier = StateNotifierProvider.autoDispose< + LocalMediasViewStateNotifier, LocalMediasViewState>( + (ref) => LocalMediasViewStateNotifier( + ref.read(localMediaServiceProvider), + ref.read(googleDriveServiceProvider), + ref.read(authServiceProvider), + ), +); + +class LocalMediasViewStateNotifier extends StateNotifier { + final LocalMediaService _localMediaService; + final GoogleDriveService _googleDriveService; + final AuthService _authService; + + bool _loading = false; + + LocalMediasViewStateNotifier( + this._localMediaService, this._googleDriveService, this._authService) + : super(const LocalMediasViewState()); + + Future loadMediaCount() async { + try { + state = state.copyWith(error: null); + final hasAccess = await _localMediaService.requestPermission(); + if (hasAccess) { + final count = await _localMediaService.getMediaCount(); + state = state.copyWith( + mediaCount: count, + hasLocalMediaAccess: hasAccess, + ); + } else { + state = state.copyWith(hasLocalMediaAccess: hasAccess); + } + } catch (error) { + state = state.copyWith(error: error); + } + } + + Future loadMedia({bool append = false}) async { + if (_loading == true) return; + _loading = true; + try { + state = state.copyWith(loading: state.medias.isEmpty, error: null); + final medias = await _localMediaService.getMedia( + start: append ? state.medias.length : 0, + end: append + ? state.medias.length + 20 + : state.medias.length < 20 + ? 20 + : state.medias.length, + ); + state = state.copyWith( + medias: [...state.medias, ...medias], + loading: false, + ); + } catch (error) { + state = state.copyWith(error: error, loading: false); + } + _loading = false; + } + + void mediaSelection(AppMedia media) { + final selectedMedias = state.selectedMedias; + if (selectedMedias.contains(media)) { + state = state.copyWith( + selectedMedias: selectedMedias.toList()..remove(media), + error: null, + ); + } else { + state = state.copyWith( + selectedMedias: [...selectedMedias, media], + error: null, + ); + } + } + + Future uploadMediaOnGoogleDrive() async { + try { + if (_authService.getUser == null) { + await _authService.signInWithGoogle(); + } + state = + state.copyWith(uploadingMedias: state.selectedMedias, error: null); + final folderId = await _googleDriveService.getBackupFolderId(); + for (final media in state.selectedMedias) { + await _googleDriveService.uploadInGoogleDrive( + media: media, folderID: folderId!); + } + state = state.copyWith(uploadingMedias: [], selectedMedias: []); + } catch (error) { + state = state.copyWith(error: error, uploadingMedias: []); + } + } +} + +@freezed +class LocalMediasViewState with _$LocalMediasViewState { + const factory LocalMediasViewState({ + @Default(false) bool loading, + @Default([]) List uploadingMedias, + @Default([]) List medias, + @Default([]) List selectedMedias, + @Default(0) int mediaCount, + @Default(false) hasLocalMediaAccess, + Object? error, + }) = _LocalMediasViewState; +} diff --git a/app/lib/ui/flow/home/local/local_media_screen_view_model.freezed.dart b/app/lib/ui/flow/home/local/local_media_screen_view_model.freezed.dart new file mode 100644 index 0000000..e139c63 --- /dev/null +++ b/app/lib/ui/flow/home/local/local_media_screen_view_model.freezed.dart @@ -0,0 +1,291 @@ +// 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 'local_media_screen_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#custom-getters-and-methods'); + +/// @nodoc +mixin _$LocalMediasViewState { + bool get loading => throw _privateConstructorUsedError; + List get uploadingMedias => throw _privateConstructorUsedError; + List get medias => throw _privateConstructorUsedError; + List get selectedMedias => throw _privateConstructorUsedError; + int get mediaCount => throw _privateConstructorUsedError; + dynamic get hasLocalMediaAccess => throw _privateConstructorUsedError; + Object? get error => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LocalMediasViewStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LocalMediasViewStateCopyWith<$Res> { + factory $LocalMediasViewStateCopyWith(LocalMediasViewState value, + $Res Function(LocalMediasViewState) then) = + _$LocalMediasViewStateCopyWithImpl<$Res, LocalMediasViewState>; + @useResult + $Res call( + {bool loading, + List uploadingMedias, + List medias, + List selectedMedias, + int mediaCount, + dynamic hasLocalMediaAccess, + Object? error}); +} + +/// @nodoc +class _$LocalMediasViewStateCopyWithImpl<$Res, + $Val extends LocalMediasViewState> + implements $LocalMediasViewStateCopyWith<$Res> { + _$LocalMediasViewStateCopyWithImpl(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? uploadingMedias = null, + Object? medias = null, + Object? selectedMedias = null, + Object? mediaCount = null, + Object? hasLocalMediaAccess = freezed, + Object? error = freezed, + }) { + return _then(_value.copyWith( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + uploadingMedias: null == uploadingMedias + ? _value.uploadingMedias + : uploadingMedias // ignore: cast_nullable_to_non_nullable + as List, + medias: null == medias + ? _value.medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + selectedMedias: null == selectedMedias + ? _value.selectedMedias + : selectedMedias // ignore: cast_nullable_to_non_nullable + as List, + mediaCount: null == mediaCount + ? _value.mediaCount + : mediaCount // ignore: cast_nullable_to_non_nullable + as int, + hasLocalMediaAccess: freezed == hasLocalMediaAccess + ? _value.hasLocalMediaAccess + : hasLocalMediaAccess // ignore: cast_nullable_to_non_nullable + as dynamic, + error: freezed == error ? _value.error : error, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LocalMediasViewStateImplCopyWith<$Res> + implements $LocalMediasViewStateCopyWith<$Res> { + factory _$$LocalMediasViewStateImplCopyWith(_$LocalMediasViewStateImpl value, + $Res Function(_$LocalMediasViewStateImpl) then) = + __$$LocalMediasViewStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool loading, + List uploadingMedias, + List medias, + List selectedMedias, + int mediaCount, + dynamic hasLocalMediaAccess, + Object? error}); +} + +/// @nodoc +class __$$LocalMediasViewStateImplCopyWithImpl<$Res> + extends _$LocalMediasViewStateCopyWithImpl<$Res, _$LocalMediasViewStateImpl> + implements _$$LocalMediasViewStateImplCopyWith<$Res> { + __$$LocalMediasViewStateImplCopyWithImpl(_$LocalMediasViewStateImpl _value, + $Res Function(_$LocalMediasViewStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? loading = null, + Object? uploadingMedias = null, + Object? medias = null, + Object? selectedMedias = null, + Object? mediaCount = null, + Object? hasLocalMediaAccess = freezed, + Object? error = freezed, + }) { + return _then(_$LocalMediasViewStateImpl( + loading: null == loading + ? _value.loading + : loading // ignore: cast_nullable_to_non_nullable + as bool, + uploadingMedias: null == uploadingMedias + ? _value._uploadingMedias + : uploadingMedias // ignore: cast_nullable_to_non_nullable + as List, + medias: null == medias + ? _value._medias + : medias // ignore: cast_nullable_to_non_nullable + as List, + selectedMedias: null == selectedMedias + ? _value._selectedMedias + : selectedMedias // ignore: cast_nullable_to_non_nullable + as List, + mediaCount: null == mediaCount + ? _value.mediaCount + : mediaCount // ignore: cast_nullable_to_non_nullable + as int, + hasLocalMediaAccess: freezed == hasLocalMediaAccess + ? _value.hasLocalMediaAccess! + : hasLocalMediaAccess, + error: freezed == error ? _value.error : error, + )); + } +} + +/// @nodoc + +class _$LocalMediasViewStateImpl implements _LocalMediasViewState { + const _$LocalMediasViewStateImpl( + {this.loading = false, + final List uploadingMedias = const [], + final List medias = const [], + final List selectedMedias = const [], + this.mediaCount = 0, + this.hasLocalMediaAccess = false, + this.error}) + : _uploadingMedias = uploadingMedias, + _medias = medias, + _selectedMedias = selectedMedias; + + @override + @JsonKey() + final bool loading; + final List _uploadingMedias; + @override + @JsonKey() + List get uploadingMedias { + if (_uploadingMedias is EqualUnmodifiableListView) return _uploadingMedias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_uploadingMedias); + } + + final List _medias; + @override + @JsonKey() + List get medias { + if (_medias is EqualUnmodifiableListView) return _medias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_medias); + } + + final List _selectedMedias; + @override + @JsonKey() + List get selectedMedias { + if (_selectedMedias is EqualUnmodifiableListView) return _selectedMedias; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_selectedMedias); + } + + @override + @JsonKey() + final int mediaCount; + @override + @JsonKey() + final dynamic hasLocalMediaAccess; + @override + final Object? error; + + @override + String toString() { + return 'LocalMediasViewState(loading: $loading, uploadingMedias: $uploadingMedias, medias: $medias, selectedMedias: $selectedMedias, mediaCount: $mediaCount, hasLocalMediaAccess: $hasLocalMediaAccess, error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LocalMediasViewStateImpl && + (identical(other.loading, loading) || other.loading == loading) && + const DeepCollectionEquality() + .equals(other._uploadingMedias, _uploadingMedias) && + const DeepCollectionEquality().equals(other._medias, _medias) && + const DeepCollectionEquality() + .equals(other._selectedMedias, _selectedMedias) && + (identical(other.mediaCount, mediaCount) || + other.mediaCount == mediaCount) && + const DeepCollectionEquality() + .equals(other.hasLocalMediaAccess, hasLocalMediaAccess) && + const DeepCollectionEquality().equals(other.error, error)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + loading, + const DeepCollectionEquality().hash(_uploadingMedias), + const DeepCollectionEquality().hash(_medias), + const DeepCollectionEquality().hash(_selectedMedias), + mediaCount, + const DeepCollectionEquality().hash(hasLocalMediaAccess), + const DeepCollectionEquality().hash(error)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LocalMediasViewStateImplCopyWith<_$LocalMediasViewStateImpl> + get copyWith => + __$$LocalMediasViewStateImplCopyWithImpl<_$LocalMediasViewStateImpl>( + this, _$identity); +} + +abstract class _LocalMediasViewState implements LocalMediasViewState { + const factory _LocalMediasViewState( + {final bool loading, + final List uploadingMedias, + final List medias, + final List selectedMedias, + final int mediaCount, + final dynamic hasLocalMediaAccess, + final Object? error}) = _$LocalMediasViewStateImpl; + + @override + bool get loading; + @override + List get uploadingMedias; + @override + List get medias; + @override + List get selectedMedias; + @override + int get mediaCount; + @override + dynamic get hasLocalMediaAccess; + @override + Object? get error; + @override + @JsonKey(ignore: true) + _$$LocalMediasViewStateImplCopyWith<_$LocalMediasViewStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/app/lib/ui/flow/home/local/local_medias_screen.dart b/app/lib/ui/flow/home/local/local_medias_screen.dart new file mode 100644 index 0000000..9638564 --- /dev/null +++ b/app/lib/ui/flow/home/local/local_medias_screen.dart @@ -0,0 +1,136 @@ +import 'dart:io'; +import 'package:cloud_gallery/ui/flow/home/local/components/multi_selection_done_button.dart'; +import 'package:cloud_gallery/ui/flow/home/local/components/no_local_medias_access_screen.dart'; +import 'package:cloud_gallery/ui/flow/home/local/local_media_screen_view_model.dart'; +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/snack_bar.dart'; +import '../../../../domain/extensions/widget_extensions.dart'; +import '../components/image_item.dart'; + +class LocalMediasScreen extends ConsumerStatefulWidget { + const LocalMediasScreen({super.key}); + + @override + ConsumerState createState() => _LocalSourceViewState(); +} + +class _LocalSourceViewState extends ConsumerState { + late LocalMediasViewStateNotifier notifier; + final _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + notifier = ref.read(localMediasViewStateNotifier.notifier); + runPostFrame(() async { + await notifier.loadMediaCount(); + await notifier.loadMedia(); + }); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + void _observeError() { + ref.listen(localMediasViewStateNotifier.select((value) => value.error), + (previous, next) { + if (next != null) { + showErrorSnackBar(context: context, error: next); + } + }); + } + + @override + Widget build(BuildContext context) { + //Listeners + _observeError(); + + //States + final medias = + ref.watch(localMediasViewStateNotifier.select((state) => state.medias)); + + final selectedMedia = ref.watch( + localMediasViewStateNotifier.select((state) => state.selectedMedias)); + + final isLoading = ref + .watch(localMediasViewStateNotifier.select((state) => state.loading)); + + final mediaCounts = ref.watch( + localMediasViewStateNotifier.select((state) => state.mediaCount)); + + final hasAccess = ref.watch(localMediasViewStateNotifier + .select((state) => state.hasLocalMediaAccess)); + + //View + if (!hasAccess) { + return const NoLocalMediasAccessScreen(); + } else if (isLoading && medias.isEmpty) { + return const Center(child: CircularProgressIndicator.adaptive()); + } + return Stack( + alignment: Alignment.bottomRight, + children: [ + Scrollbar( + controller: _scrollController, + child: GridView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + ), + itemCount: mediaCounts, + itemBuilder: (context, index) { + if (index > medias.length - 6) { + runPostFrame(() { + notifier.loadMedia(append: true); + }); + } + if (index < medias.length) { + if (medias[index].type != AppMediaType.image) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: context.colorScheme.outline, + ), + ); + } + return ImageItem( + onTap: () { + if (selectedMedia.isNotEmpty) { + notifier.mediaSelection(medias[index]); + } + }, + onLongTap: () { + notifier.mediaSelection(medias[index]); + }, + isSelected: selectedMedia.contains(medias[index]), + imageProvider: FileImage(File(medias[index].path)), + ); + } else { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + color: context.colorScheme.primary, + ), + ); + } + }, + ), + ), + if (selectedMedia.isNotEmpty) + Padding( + padding: context.systemPadding + const EdgeInsets.all(16), + child: const MultiSelectionDoneButton(), + ), + ], + ); + } +} diff --git a/app/lib/ui/flow/main/main_screen.dart b/app/lib/ui/flow/main/main_screen.dart deleted file mode 100644 index fb6aae6..0000000 --- a/app/lib/ui/flow/main/main_screen.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:io'; -import 'package:cloud_gallery/components/app_page.dart'; -import 'package:data/models/media/media.dart'; -import 'package:data/services/local_media_service.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:style/animations/parallex_effect.dart'; - -class HomeScreen extends ConsumerStatefulWidget { - const HomeScreen({super.key}); - - @override - ConsumerState createState() => _MainScreenState(); -} - -class _MainScreenState extends ConsumerState { - late LocalMediaService localMediaService; - - @override - void initState() { - localMediaService = ref.read(localMediaServiceProvider); - super.initState(); - } - - @override - Widget build(BuildContext context) { - return AppPage( - title: 'Cloud Gallery', - body: FutureBuilder( - future: localMediaService.getAssets(), - builder: (context, snapshot) { - final res = snapshot.data; - if (res is List) { - return GridView.builder( - addAutomaticKeepAlives: true, - padding: const EdgeInsets.all(16), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: 16, - mainAxisSpacing: 16, - ), - itemCount: res.length, - itemBuilder: (context, index) => - res[index].type == AppMediaType.image - ? ImageItem( - imageProvider: FileImage( - File(res[index].path), - )) - : const SizedBox(), - ); - } - return const Center(child: CircularProgressIndicator()); - }, - ), - ); - } -} - -class ImageItem extends StatefulWidget { - final ImageProvider imageProvider; - - const ImageItem({super.key, required this.imageProvider}); - - @override - State createState() => _ImageItemState(); -} - -class _ImageItemState extends State { - final _backgroundImageKey = GlobalKey(); - - @override - Widget build(BuildContext context) { - return LayoutBuilder( - builder: (context, constraints) { - return ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Flow( - delegate: ParallaxFlowDelegate( - scrollable: Scrollable.of(context), - listItemContext: context, - backgroundImageKey: _backgroundImageKey, - ), - children: [ - Image( - key: _backgroundImageKey, - image: widget.imageProvider, - fit: BoxFit.cover, - width: constraints.maxWidth, - height: constraints.maxHeight * 1.5, - ), - ], - ), - ); - }, - ); - } -} diff --git a/app/lib/ui/flow/onboard/onboard_screen.dart b/app/lib/ui/flow/onboard/onboard_screen.dart index afc9856..2fdebd4 100644 --- a/app/lib/ui/flow/onboard/onboard_screen.dart +++ b/app/lib/ui/flow/onboard/onboard_screen.dart @@ -1,13 +1,14 @@ import 'package:cloud_gallery/components/app_page.dart'; +import 'package:cloud_gallery/domain/extensions/context_extensions.dart'; import 'package:cloud_gallery/ui/navigation/app_router.dart'; -import 'package:cloud_gallery/utils/assets/assets_paths.dart'; -import 'package:cloud_gallery/utils/extensions/context_extensions.dart'; import 'package:data/storage/app_preferences.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:style/extensions/context_extensions.dart'; import 'package:style/text/app_text_style.dart'; import 'package:style/animations/on_tap_scale.dart'; +import '../../../domain/assets/assets_paths.dart'; class OnBoardScreen extends ConsumerWidget { const OnBoardScreen({super.key}); diff --git a/app/lib/ui/navigation/app_route.dart b/app/lib/ui/navigation/app_route.dart index 8fec94a..3ce2c63 100644 --- a/app/lib/ui/navigation/app_route.dart +++ b/app/lib/ui/navigation/app_route.dart @@ -58,11 +58,11 @@ class AppRoute { GoRoute get goRoute => GoRoute( path: path, - name: path, + name: name, builder: (context, state) => builder(context), ); } extension GoRouterStateExtensions on GoRouterState { Widget widget(BuildContext context) => (extra as WidgetBuilder)(context); -} \ No newline at end of file +} diff --git a/app/lib/ui/navigation/app_router.dart b/app/lib/ui/navigation/app_router.dart index 909e8ba..e4b55f6 100644 --- a/app/lib/ui/navigation/app_router.dart +++ b/app/lib/ui/navigation/app_router.dart @@ -1,6 +1,7 @@ -import 'package:cloud_gallery/ui/flow/main/main_screen.dart'; +import 'package:cloud_gallery/ui/flow/accounts/accounts_screen.dart'; import 'package:cloud_gallery/ui/flow/onboard/onboard_screen.dart'; import 'package:go_router/go_router.dart'; +import '../flow/home/home_screen.dart'; import 'app_route.dart'; class AppRouter { @@ -14,13 +15,20 @@ class AppRouter { builder: (context) => const OnBoardScreen(), ); + static AppRoute get accounts => AppRoute( + AppRoutePath.accounts, + builder: (context) => const AccountsScreen(), + ); + static final routes = [ home.goRoute, onBoard.goRoute, + accounts.goRoute, ]; } class AppRoutePath { static const home = '/'; static const onBoard = '/on-board'; + static const accounts = '/accounts'; } diff --git a/app/pubspec.lock b/app/pubspec.lock index 1160cda..626afe4 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -348,6 +348,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + url: "https://pub.dev" + source: hosted + version: "8.2.4" freezed: dependency: "direct main" description: @@ -392,10 +400,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "0c56c2c5d60d6dfaf9725f5ad4699f04749fb196ee5a70487a46ef184837ccf6" + sha256: "972ff30eebf6a5eab28be3e1e47a45df087ed64d5aefdac0df47758ecdec5385" url: "https://pub.dev" source: hosted - version: "0.3.0+2" + version: "0.3.1" google_sign_in: dependency: transitive description: @@ -432,10 +440,10 @@ packages: dependency: transitive description: name: google_sign_in_web - sha256: a278ea2d01013faf341cbb093da880d0f2a552bbd1cb6ee90b5bebac9ba69d77 + sha256: fc0f14ed45ea616a6cfb4d1c7534c2221b7092cc4f29a709f0c3053cc3e821bd url: "https://pub.dev" source: hosted - version: "0.12.3+2" + version: "0.12.4" googleapis: dependency: transitive description: @@ -448,10 +456,10 @@ packages: dependency: transitive description: name: googleapis_auth - sha256: "127b1bbd32170ab8312f503bd57f1d654d8e4039ddfbc63c027d3f7ade0eff74" + sha256: "772779fe28a8b70939eab9c390a5f8f46cbb59bda9f1f10ea60dd894eff59ff0" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.2" graphs: dependency: transitive description: @@ -472,10 +480,10 @@ packages: dependency: transitive description: name: http - sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "0.13.6" + version: "1.2.1" http_multi_server: dependency: transitive description: @@ -532,6 +540,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" lints: dependency: transitive description: @@ -552,26 +584,26 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" meta: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -592,10 +624,10 @@ packages: dependency: transitive description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_parsing: dependency: transitive description: @@ -628,6 +660,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.1" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44" + url: "https://pub.dev" + source: hosted + version: "11.3.0" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474" + url: "https://pub.dev" + source: hosted + version: "12.0.5" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b + url: "https://pub.dev" + source: hosted + version: "9.4.0" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + url: "https://pub.dev" + source: hosted + version: "0.1.1" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -760,10 +840,10 @@ packages: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.0" shared_preferences_windows: dependency: transitive description: @@ -964,10 +1044,10 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "1d9158c616048c38f712a6646e317a3426da10e884447626167240d45209cbad" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.0" web_socket_channel: dependency: transitive description: @@ -1009,5 +1089,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.3 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.19.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 5d2dc90..6c630b4 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -26,10 +26,14 @@ dependencies: # UI cupertino_icons: ^1.0.2 flutter_svg: ^2.0.9 + fluttertoast: ^8.2.4 # state management flutter_riverpod: ^2.4.9 + # permission + permission_handler: ^11.3.0 + # navigation go_router: ^13.0.1 @@ -60,4 +64,5 @@ flutter: generate: true assets: - - assets/images/ \ No newline at end of file + - assets/images/ + - assets/images/icons/ \ No newline at end of file diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc index 1a82e7d..5893c2f 100644 --- a/app/windows/flutter/generated_plugin_registrant.cc +++ b/app/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); } diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake index fa8a39b..873f19d 100644 --- a/app/windows/flutter/generated_plugins.cmake +++ b/app/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST firebase_core + permission_handler_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/data/.flutter-plugins b/data/.flutter-plugins index 872b031..aef9544 100644 --- a/data/.flutter-plugins +++ b/data/.flutter-plugins @@ -2,7 +2,7 @@ google_sign_in=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in-6.2.1/ google_sign_in_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_android-6.1.21/ google_sign_in_ios=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_ios-5.7.3/ -google_sign_in_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.3+2/ +google_sign_in_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/google_sign_in_web-0.12.4/ path_provider_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/ path_provider_windows=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/path_provider_windows-2.2.1/ photo_manager=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0-dev.5/ @@ -10,5 +10,5 @@ shared_preferences=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_prefere shared_preferences_android=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_android-2.2.1/ shared_preferences_foundation=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.3.5/ shared_preferences_linux=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.3.2/ -shared_preferences_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.2.2/ +shared_preferences_web=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.3.0/ shared_preferences_windows=/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.3.2/ diff --git a/data/.flutter-plugins-dependencies b/data/.flutter-plugins-dependencies index e0d85c3..785553b 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.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0-dev.5/","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":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0-dev.5/","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.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0-dev.5/","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":"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":"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+2/","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":"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-01-31 12:16:18.127044","version":"3.16.7"} \ 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.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0-dev.5/","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":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0-dev.5/","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.3/","shared_darwin_source":true,"native_build":true,"dependencies":[]},{"name":"photo_manager","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/photo_manager-3.0.0-dev.5/","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":"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":"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.4/","dependencies":[]},{"name":"shared_preferences_web","path":"/Users/pratikcanopas/.pub-cache/hosted/pub.dev/shared_preferences_web-2.3.0/","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":"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-02-27 12:34:36.609081","version":"3.19.1"} \ No newline at end of file diff --git a/data/lib/errors/app_error.dart b/data/lib/errors/app_error.dart index 711b037..b7f638d 100644 --- a/data/lib/errors/app_error.dart +++ b/data/lib/errors/app_error.dart @@ -15,7 +15,9 @@ class AppError implements Exception { } factory AppError.fromError(Object error) { - if (error is SocketException) { + if (error is AppError) { + return error; + } else if (error is SocketException) { return const NoConnectionError(); } else { return const SomethingWentWrongError(); @@ -31,6 +33,14 @@ class NoConnectionError extends AppError { "No internet connection. Please check your network and try again."); } +class UserGoogleSignInAccountNotFound extends AppError { + const UserGoogleSignInAccountNotFound() + : super( + l10nCode: AppErrorL10nCodes.googleSignInUserNotFoundError, + message: + "User google signed in account not found. Please sign in again"); +} + 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 288e379..4b6136d 100644 --- a/data/lib/errors/l10n_error_codes.dart +++ b/data/lib/errors/l10n_error_codes.dart @@ -1,4 +1,5 @@ class AppErrorL10nCodes { static const noInternetConnection = 'no-internet-connection'; static const somethingWentWrongError = 'something-went-wrong'; + static const googleSignInUserNotFoundError = 'google-sing-in-user-not-found'; } diff --git a/data/lib/extensions/sucesser_function.dart b/data/lib/extensions/sucesser_function.dart deleted file mode 100644 index 8b13789..0000000 --- a/data/lib/extensions/sucesser_function.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/data/lib/models/media/media.dart b/data/lib/models/media/media.dart index e20e4bc..277ad7c 100644 --- a/data/lib/models/media/media.dart +++ b/data/lib/models/media/media.dart @@ -18,7 +18,7 @@ class AppMedia with _$AppMedia { double? displayWidth, required AppMediaType type, String? mimeType, - required DateTime createdTime, + DateTime? createdTime, DateTime? modifiedTime, AppMediaOrientation? orientation, required double? latitude, @@ -29,3 +29,4 @@ class AppMedia with _$AppMedia { factory AppMedia.fromJson(Map json) => _$AppMediaFromJson(json); } + diff --git a/data/lib/models/media/media.freezed.dart b/data/lib/models/media/media.freezed.dart index d143f82..d8d547c 100644 --- a/data/lib/models/media/media.freezed.dart +++ b/data/lib/models/media/media.freezed.dart @@ -27,7 +27,7 @@ mixin _$AppMedia { double? get displayWidth => throw _privateConstructorUsedError; AppMediaType get type => throw _privateConstructorUsedError; String? get mimeType => throw _privateConstructorUsedError; - DateTime get createdTime => throw _privateConstructorUsedError; + DateTime? get createdTime => throw _privateConstructorUsedError; DateTime? get modifiedTime => throw _privateConstructorUsedError; AppMediaOrientation? get orientation => throw _privateConstructorUsedError; double? get latitude => throw _privateConstructorUsedError; @@ -53,7 +53,7 @@ abstract class $AppMediaCopyWith<$Res> { double? displayWidth, AppMediaType type, String? mimeType, - DateTime createdTime, + DateTime? createdTime, DateTime? modifiedTime, AppMediaOrientation? orientation, double? latitude, @@ -81,7 +81,7 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia> Object? displayWidth = freezed, Object? type = null, Object? mimeType = freezed, - Object? createdTime = null, + Object? createdTime = freezed, Object? modifiedTime = freezed, Object? orientation = freezed, Object? latitude = freezed, @@ -117,10 +117,10 @@ class _$AppMediaCopyWithImpl<$Res, $Val extends AppMedia> ? _value.mimeType : mimeType // ignore: cast_nullable_to_non_nullable as String?, - createdTime: null == createdTime + createdTime: freezed == createdTime ? _value.createdTime : createdTime // ignore: cast_nullable_to_non_nullable - as DateTime, + as DateTime?, modifiedTime: freezed == modifiedTime ? _value.modifiedTime : modifiedTime // ignore: cast_nullable_to_non_nullable @@ -161,7 +161,7 @@ abstract class _$$AppMediaImplCopyWith<$Res> double? displayWidth, AppMediaType type, String? mimeType, - DateTime createdTime, + DateTime? createdTime, DateTime? modifiedTime, AppMediaOrientation? orientation, double? latitude, @@ -187,7 +187,7 @@ class __$$AppMediaImplCopyWithImpl<$Res> Object? displayWidth = freezed, Object? type = null, Object? mimeType = freezed, - Object? createdTime = null, + Object? createdTime = freezed, Object? modifiedTime = freezed, Object? orientation = freezed, Object? latitude = freezed, @@ -223,10 +223,10 @@ class __$$AppMediaImplCopyWithImpl<$Res> ? _value.mimeType : mimeType // ignore: cast_nullable_to_non_nullable as String?, - createdTime: null == createdTime + createdTime: freezed == createdTime ? _value.createdTime : createdTime // ignore: cast_nullable_to_non_nullable - as DateTime, + as DateTime?, modifiedTime: freezed == modifiedTime ? _value.modifiedTime : modifiedTime // ignore: cast_nullable_to_non_nullable @@ -262,7 +262,7 @@ class _$AppMediaImpl implements _AppMedia { this.displayWidth, required this.type, this.mimeType, - required this.createdTime, + this.createdTime, this.modifiedTime, this.orientation, required this.latitude, @@ -287,7 +287,7 @@ class _$AppMediaImpl implements _AppMedia { @override final String? mimeType; @override - final DateTime createdTime; + final DateTime? createdTime; @override final DateTime? modifiedTime; @override @@ -374,7 +374,7 @@ abstract class _AppMedia implements AppMedia { final double? displayWidth, required final AppMediaType type, final String? mimeType, - required final DateTime createdTime, + final DateTime? createdTime, final DateTime? modifiedTime, final AppMediaOrientation? orientation, required final double? latitude, @@ -399,7 +399,7 @@ abstract class _AppMedia implements AppMedia { @override String? get mimeType; @override - DateTime get createdTime; + DateTime? get createdTime; @override DateTime? get modifiedTime; @override diff --git a/data/lib/models/media/media.g.dart b/data/lib/models/media/media.g.dart index 8d07a5b..e2d0648 100644 --- a/data/lib/models/media/media.g.dart +++ b/data/lib/models/media/media.g.dart @@ -15,7 +15,9 @@ _$AppMediaImpl _$$AppMediaImplFromJson(Map json) => displayWidth: (json['displayWidth'] as num?)?.toDouble(), type: $enumDecode(_$AppMediaTypeEnumMap, json['type']), mimeType: json['mimeType'] as String?, - createdTime: DateTime.parse(json['createdTime'] as String), + createdTime: json['createdTime'] == null + ? null + : DateTime.parse(json['createdTime'] as String), modifiedTime: json['modifiedTime'] == null ? null : DateTime.parse(json['modifiedTime'] as String), @@ -35,7 +37,7 @@ Map _$$AppMediaImplToJson(_$AppMediaImpl instance) => 'displayWidth': instance.displayWidth, 'type': _$AppMediaTypeEnumMap[instance.type]!, 'mimeType': instance.mimeType, - 'createdTime': instance.createdTime.toIso8601String(), + 'createdTime': instance.createdTime?.toIso8601String(), 'modifiedTime': instance.modifiedTime?.toIso8601String(), 'orientation': _$AppMediaOrientationEnumMap[instance.orientation], 'latitude': instance.latitude, diff --git a/data/lib/services/auth_service.dart b/data/lib/services/auth_service.dart index 1dade3c..d663e76 100644 --- a/data/lib/services/auth_service.dart +++ b/data/lib/services/auth_service.dart @@ -42,9 +42,7 @@ class AuthService { } } - Future isSignedIn() async { - return _googleSignIn.isSignedIn(); - } + Future get isSignedIn async => await _googleSignIn.isSignedIn(); GoogleSignInAccount? get getUser => _googleSignIn.currentUser; } diff --git a/data/lib/services/google_drive_service.dart b/data/lib/services/google_drive_service.dart index 8e26ae9..f13fdcc 100644 --- a/data/lib/services/google_drive_service.dart +++ b/data/lib/services/google_drive_service.dart @@ -1,7 +1,10 @@ +import 'dart:io'; +import 'package:data/models/media/media.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'; import 'package:googleapis/drive/v3.dart' as drive; +import '../errors/app_error.dart'; import 'auth_service.dart'; final googleDriveServiceProvider = Provider( @@ -9,17 +12,63 @@ 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; const GoogleDriveService(this._googleSignIn); - Future getDriveFiles() async { - if (_googleSignIn.currentUser != null) { - final client = await _googleSignIn.authenticatedClient(); - final driveApi = drive.DriveApi(client!); - await driveApi.files.list(); + Future _getGoogleDriveAPI() async { + if (_googleSignIn.currentUser == null) { + throw const UserGoogleSignInAccountNotFound(); + } + final client = await _googleSignIn.authenticatedClient(); + return drive.DriveApi(client!); + } + + Future getBackupFolderId() async { + try { + final driveApi = await _getGoogleDriveAPI(); + + final response = await driveApi.files.list( + q: "name='$_backUpFolderName' and description='$_backUpFolderDescription' and mimeType='application/vnd.google-apps.folder'", + ); + + 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); + return googleDriveFolder.id; + } + } catch (e) { + throw AppError.fromError(e); + } + } + + Future uploadInGoogleDrive( + {required String folderID, required AppMedia media}) async { + final localFile = File(media.path); + try { + final driveApi = await _getGoogleDriveAPI(); - ///TODO: Convert File to custom object + final file = drive.File( + name: media.name ?? localFile.path.split('/').last, + id: media.id, + parents: [folderID], + ); + await driveApi.files.create( + file, + uploadMedia: drive.Media(localFile.openRead(), localFile.lengthSync()), + ); + } catch (error) { + throw AppError.fromError(error); } } } diff --git a/data/lib/services/google_photos_service.dart b/data/lib/services/google_photos_service.dart index da2b861..636fc8c 100644 --- a/data/lib/services/google_photos_service.dart +++ b/data/lib/services/google_photos_service.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_sign_in/google_sign_in.dart'; import 'package:googleapis/photoslibrary/v1.dart' as google_photos; +import '../errors/app_error.dart'; import 'auth_service.dart'; final googlePhotosServiceProvider = Provider( @@ -16,18 +17,11 @@ class GooglePhotosService { const GooglePhotosService(this._googleSignIn); - Future createAlbum() async { - if (_googleSignIn.currentUser == null) return; + Future getGooglePhotosApi() async { + if (_googleSignIn.currentUser == null) { + throw const UserGoogleSignInAccountNotFound(); + } final client = await _googleSignIn.authenticatedClient(); - final photosLibraryApi = google_photos.PhotosLibraryApi(client!); - final album = google_photos.Album( - title: "Cloud Gallery", - isWriteable: true, - ); - await photosLibraryApi.albums - .create(google_photos.CreateAlbumRequest(album: album)); - - ///TODO: Steps after create albums + return google_photos.PhotosLibraryApi(client!); } - } diff --git a/data/lib/services/local_media_service.dart b/data/lib/services/local_media_service.dart index 203eec0..2ae278c 100644 --- a/data/lib/services/local_media_service.dart +++ b/data/lib/services/local_media_service.dart @@ -10,12 +10,18 @@ final localMediaServiceProvider = Provider( class LocalMediaService { const LocalMediaService(); - Future> getAssets() async { - await PhotoManager.requestPermissionExtend(); - final imageCount = await PhotoManager.getAssetCount(); - final assets = - await PhotoManager.getAssetListRange(start: 0, end: imageCount); + Future requestPermission() async { + final state = await PhotoManager.requestPermissionExtend(); + return state.hasAccess; + } + + Future getMediaCount() async { + return await PhotoManager.getAssetCount(); + } + Future> getMedia( + {required int start, required int end}) async { + final assets = await PhotoManager.getAssetListRange(start: start, end: end); final files = await Future.wait( assets.map( (asset) async { @@ -41,7 +47,6 @@ class LocalMediaService { }, ), ); - return files.whereNotNull().toList(); } } diff --git a/data/pubspec.yaml b/data/pubspec.yaml index 3db22e0..cd47fa5 100644 --- a/data/pubspec.yaml +++ b/data/pubspec.yaml @@ -12,6 +12,7 @@ dependencies: # services googleapis: ^12.0.0 + http: ^1.2.1 photo_manager: ^3.0.0-dev.5 # authentication diff --git a/style/lib/buttons/primary_button.dart b/style/lib/buttons/primary_button.dart new file mode 100644 index 0000000..8888f89 --- /dev/null +++ b/style/lib/buttons/primary_button.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:style/extensions/context_extensions.dart'; +import 'package:style/text/app_text_style.dart'; + +class PrimaryButton extends StatelessWidget { + final String text; + final Widget? child; + final VoidCallback onPressed; + + const PrimaryButton( + {super.key, this.text = '', this.child, required this.onPressed}); + + @override + Widget build(BuildContext context) { + return FilledButton( + onPressed: onPressed, + style: FilledButton.styleFrom( + backgroundColor: context.colorScheme.primary, + foregroundColor: context.colorScheme.onPrimary, + ), + child: child ?? + Text( + text, + style: AppTextStyles.button + .copyWith(color: context.colorScheme.onPrimary), + ), + ); + } +} diff --git a/app/lib/utils/extensions/context_extensions.dart b/style/lib/extensions/context_extensions.dart similarity index 82% rename from app/lib/utils/extensions/context_extensions.dart rename to style/lib/extensions/context_extensions.dart index da67f3a..1e0bd18 100644 --- a/app/lib/utils/extensions/context_extensions.dart +++ b/style/lib/extensions/context_extensions.dart @@ -1,10 +1,7 @@ import 'package:flutter/cupertino.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:style/theme/theme.dart'; extension BuildContextExtensions on BuildContext { - AppLocalizations get l10n => AppLocalizations.of(this); - EdgeInsets get systemPadding => MediaQuery.of(this).padding; Size get mediaQuerySize => MediaQuery.of(this).size; diff --git a/style/lib/theme/theme.dart b/style/lib/theme/theme.dart index 2637e51..81d5b62 100644 --- a/style/lib/theme/theme.dart +++ b/style/lib/theme/theme.dart @@ -28,6 +28,7 @@ class AppColorScheme { final Color tertiary; final Color tertiaryInverse; final Color surface; + final Color surfaceInverse; final Color outline; final Color textPrimary; final Color textSecondary; @@ -57,6 +58,7 @@ class AppColorScheme { required this.tertiary, required this.tertiaryInverse, required this.surface, + required this.surfaceInverse, required this.outline, required this.textPrimary, required this.textSecondary, @@ -97,6 +99,7 @@ final appColorSchemeLight = AppColorScheme( tertiary: AppColors.tertiaryLightColor, tertiaryInverse: AppColors.tertiaryDarkColor, surface: AppColors.surfaceLightColor, + surfaceInverse: AppColors.surfaceDarkColor, outline: AppColors.outlineLightColor, outlineInverse: AppColors.outlineDarkColor, textPrimary: AppColors.textPrimaryLightColor, @@ -127,6 +130,7 @@ final appColorSchemeDark = AppColorScheme( tertiary: AppColors.tertiaryDarkColor, tertiaryInverse: AppColors.tertiaryLightColor, surface: AppColors.surfaceDarkColor, + surfaceInverse: AppColors.surfaceLightColor, outline: AppColors.outlineDarkColor, outlineInverse: AppColors.outlineLightColor, textPrimary: AppColors.textPrimaryDarkColor,