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,