diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml
index 96671da..0df894e 100644
--- a/.idea/libraries/Dart_Packages.xml
+++ b/.idea/libraries/Dart_Packages.xml
@@ -310,6 +310,13 @@
+
+
+
+
+
+
+
@@ -577,6 +584,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -949,6 +998,7 @@
+
@@ -988,6 +1038,12 @@
+
+
+
+
+
+
diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml
index 46bf14e..def2977 100644
--- a/.idea/libraries/Flutter_Plugins.xml
+++ b/.idea/libraries/Flutter_Plugins.xml
@@ -12,10 +12,13 @@
-
-
+
+
+
+
+
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/locales/app_en.arb b/app/assets/locales/app_en.arb
index 6e944a6..be20eb7 100644
--- a/app/assets/locales/app_en.arb
+++ b/app/assets/locales/app_en.arb
@@ -3,10 +3,17 @@
"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! 🚀",
- "back_up_on_google_drive_text":"Back up on Google Drive"
+ "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 4332dfc..9ef5402 100644
--- a/app/ios/Podfile.lock
+++ b/app/ios/Podfile.lock
@@ -17,6 +17,9 @@ PODS:
- 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
@@ -34,6 +37,8 @@ PODS:
- AppAuth/Core (~> 1.6)
- GTMSessionFetcher/Core (< 4.0, >= 1.5)
- 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:
@@ -80,14 +93,17 @@ SPEC CHECKSUMS:
FirebaseCore: 2322423314d92f946219c8791674d2f3345b598f
FirebaseCoreInternal: efeeb171ac02d623bdaefe121539939821e10811
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
google_sign_in_ios: 1bfaf6607b44cd1b24c4d4bc39719870440f9ce1
GoogleSignIn: b232380cf495a429b8095d3178a8d5855b42e842
GoogleUtilities: 0759d1a57ebb953965c2dfe0ba4c82e95ccc2e34
GTMAppAuth: 99fb010047ba3973b7026e45393f51f27ab965ae
GTMSessionFetcher: 8a1b34ad97ebe6f909fb8b9b77fba99943007556
+ permission_handler_apple: 036b856153a2b1f61f21030ff725f3e6fece2b78
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
+ Toast: ec33c32b8688982cecc6348adeae667c1b9938da
PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796
diff --git a/app/lib/components/action_sheet.dart b/app/lib/components/action_sheet.dart
index 0400cc2..872f817 100644
--- a/app/lib/components/action_sheet.dart
+++ b/app/lib/components/action_sheet.dart
@@ -1,4 +1,4 @@
-import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
+import 'package:style/extensions/context_extensions.dart';
import 'package:flutter/cupertino.dart';
import 'package:style/animations/on_tap_scale.dart';
import 'package:style/text/app_text_style.dart';
diff --git a/app/lib/components/app_sheet.dart b/app/lib/components/app_sheet.dart
index 64b0c0b..03d93c7 100644
--- a/app/lib/components/app_sheet.dart
+++ b/app/lib/components/app_sheet.dart
@@ -1,5 +1,5 @@
-import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
import 'package:flutter/material.dart';
+import 'package:style/extensions/context_extensions.dart';
Future showAppSheet(
{required BuildContext context, required Widget 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/domain/extensions/app_error_extensions.dart b/app/lib/domain/extensions/app_error_extensions.dart
index b90a6c0..c02ff99 100644
--- a/app/lib/domain/extensions/app_error_extensions.dart
+++ b/app/lib/domain/extensions/app_error_extensions.dart
@@ -3,15 +3,22 @@ import 'package:data/errors/app_error.dart';
import 'package:data/errors/l10n_error_codes.dart';
import 'package:flutter/cupertino.dart';
-extension AppErrorExtensions on AppError {
+extension AppErrorExtensions on Object {
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;
+ 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;
+ 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
index da67f3a..e8349c1 100644
--- a/app/lib/domain/extensions/context_extensions.dart
+++ b/app/lib/domain/extensions/context_extensions.dart
@@ -1,21 +1,6 @@
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;
-
- AppColorScheme get colorScheme => appColorSchemeOf(this);
-
- Brightness get brightness => MediaQuery.of(this).platformBrightness;
-
- bool get isDarkMode => brightness == Brightness.dark;
-
- FocusScopeNode get focusScope => FocusScope.of(this);
-
- bool get use24Hour => MediaQuery.of(this).alwaysUse24HourFormat;
}
diff --git a/app/lib/ui/app.dart b/app/lib/ui/app.dart
index 0ce7dc5..bcbca76 100644
--- a/app/lib/ui/app.dart
+++ b/app/lib/ui/app.dart
@@ -6,6 +6,7 @@ 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/home/components/image_item.dart b/app/lib/ui/flow/home/components/image_item.dart
index f13c043..d5de8ed 100644
--- a/app/lib/ui/flow/home/components/image_item.dart
+++ b/app/lib/ui/flow/home/components/image_item.dart
@@ -1,7 +1,7 @@
-import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
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;
@@ -89,18 +89,18 @@ class ItemSelector extends StatelessWidget {
Align(
alignment: Alignment.bottomRight,
child: Container(
- padding: const EdgeInsets.all(4),
+ padding: const EdgeInsets.all(2),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: context.colorScheme.surface,
border: Border.all(
- color: CupertinoColors.systemGreen,
+ color: const Color(0xff808080),
),
),
- child: const Icon(
+ child: const Icon(
CupertinoIcons.checkmark_alt,
- color: CupertinoColors.systemGreen,
- size: 18,
+ 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
index 59e80cc..ef7bb9f 100644
--- a/app/lib/ui/flow/home/home_screen.dart
+++ b/app/lib/ui/flow/home/home_screen.dart
@@ -1,17 +1,13 @@
-import 'dart:io';
import 'package:cloud_gallery/components/app_page.dart';
-import 'package:cloud_gallery/components/app_sheet.dart';
import 'package:cloud_gallery/domain/extensions/context_extensions.dart';
-import 'package:cloud_gallery/domain/extensions/widget_extensions.dart';
+import 'package:cloud_gallery/ui/flow/home/components/screen_source_segment_control.dart';
import 'package:cloud_gallery/ui/flow/home/home_screen_view_model.dart';
-import 'package:data/models/media/media.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:flutter_svg/flutter_svg.dart';
-import 'package:style/text/app_text_style.dart';
-import '../../../components/action_sheet.dart';
import '../../../domain/assets/assets_paths.dart';
-import 'components/image_item.dart';
+import 'google_drive/google_drive_medias_screen.dart';
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@@ -22,127 +18,64 @@ class HomeScreen extends ConsumerStatefulWidget {
class _HomeScreenState extends ConsumerState {
late HomeViewStateNotifier notifier;
- final ScrollController _controller = ScrollController();
+ final _pageController = PageController();
@override
void initState() {
notifier = ref.read(homeViewStateNotifier.notifier);
- runPostFrame(() async {
- await notifier.loadMediaCount();
- await notifier.loadMedia();
- });
-
super.initState();
}
@override
void dispose() {
- _controller.dispose();
+ _pageController.dispose();
super.dispose();
}
+ void _updatePageOnChangeSource() {
+ ref.listen(homeViewStateNotifier.select((value) => value.sourcePage),
+ (previous, next) {
+ if (!next.viewChangedByScroll) {
+ _pageController.jumpToPage(next.sourcePage.index);
+ }
+ });
+ }
+
@override
Widget build(BuildContext context) {
- final medias = ref.watch(
- homeViewStateNotifier.select((state) => state.medias),
- );
-
- final selectedMedia = ref.watch(
- homeViewStateNotifier.select((state) => state.selectedMedias),
- );
-
- final isLoading = ref.watch(
- homeViewStateNotifier.select((state) => state.loading),
- );
-
- final mediaCounts = ref.watch(
- homeViewStateNotifier.select((state) => state.mediaCount),
- );
-
+ _updatePageOnChangeSource();
return AppPage(
- title: context.l10n.app_name,
- actions: [
- if (selectedMedia.isNotEmpty)
- TextButton(
- 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: Text(
- context.l10n.common_done,
- style: AppTextStyles.button.copyWith(
- color: context.colorScheme.primary,
- ),
- ),
- )
- ],
- body: Visibility(
- visible: !isLoading,
- replacement: const Center(child: CircularProgressIndicator()),
- child: Scrollbar(
- controller: _controller,
- child: GridView.builder(
- controller: _controller,
- padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 16),
- gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: 3,
- crossAxisSpacing: 8,
- mainAxisSpacing: 8,
+ titleWidget: _titleWidget(context: context),
+ body: Column(
+ children: [
+ const ScreenSourceSegmentControl(),
+ Expanded(
+ child: PageView(
+ onPageChanged: (value) {
+ notifier.updateMediaSource(
+ isChangedByScroll: true, source: MediaSource.values[value]);
+ },
+ controller: _pageController,
+ children: const [
+ LocalMediasScreen(),
+ GoogleDriveMediasScreen(),
+ ],
),
- 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,
- ),
- );
- }
- },
),
- ),
+ ],
),
);
}
+
+ 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
index ac24dc1..c564e33 100644
--- a/app/lib/ui/flow/home/home_screen_view_model.dart
+++ b/app/lib/ui/flow/home/home_screen_view_model.dart
@@ -1,8 +1,3 @@
-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';
@@ -10,97 +5,36 @@ part 'home_screen_view_model.freezed.dart';
final homeViewStateNotifier =
StateNotifierProvider.autoDispose(
- (ref) => HomeViewStateNotifier(
- ref.read(localMediaServiceProvider),
- ref.read(googleDriveServiceProvider),
- ref.read(authServiceProvider),
- ),
+ (ref) => HomeViewStateNotifier(),
);
-class HomeViewStateNotifier extends StateNotifier {
- final LocalMediaService _localMediaService;
- final GoogleDriveService _googleDriveService;
- final AuthService _authService;
-
- bool _loading = false;
-
- HomeViewStateNotifier(this._localMediaService, this._googleDriveService, this._authService)
- : super(const HomeViewState());
-
- Future loadMediaCount() async {
- try {
- final count = await _localMediaService.getMediaCount();
- state = state.copyWith(mediaCount: count);
- } 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);
- 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;
- }
+enum MediaSource { local, googleDrive }
- void mediaSelection(AppMedia media) {
- final selectedMedias = state.selectedMedias;
- if (selectedMedias.contains(media)) {
- state = state.copyWith(
- selectedMedias: selectedMedias.toList()..remove(media),
- );
- } else {
- state = state.copyWith(
- selectedMedias: [...selectedMedias, media],
- );
- }
- }
+class HomeViewStateNotifier extends StateNotifier {
+ HomeViewStateNotifier() : super(const HomeViewState());
- Future uploadMediaOnGoogleDrive() async {
- try {
- if(_authService.getUser == null){
- await _authService.signInWithGoogle();
- }
- state = state.copyWith(uploadingMedias: state.selectedMedias);
- 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) {
- if(error is UserGoogleSignInAccountNotFound){
- await _authService.signInWithGoogle();
- await uploadMediaOnGoogleDrive();
- }
- state = state.copyWith(error: error, uploadingMedias: []);
- }
+ 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(false) bool loading,
- @Default([]) List uploadingMedias,
- @Default([]) List medias,
- @Default([]) List selectedMedias,
- @Default(0) int mediaCount,
- Object? error,
+ @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
index 51c8a02..3076101 100644
--- a/app/lib/ui/flow/home/home_screen_view_model.freezed.dart
+++ b/app/lib/ui/flow/home/home_screen_view_model.freezed.dart
@@ -16,12 +16,8 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc
mixin _$HomeViewState {
- 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;
- Object? get error => throw _privateConstructorUsedError;
+ SourcePage get sourcePage => throw _privateConstructorUsedError;
+ bool get isLastViewChangedByScroll => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$HomeViewStateCopyWith get copyWith =>
@@ -34,13 +30,7 @@ abstract class $HomeViewStateCopyWith<$Res> {
HomeViewState value, $Res Function(HomeViewState) then) =
_$HomeViewStateCopyWithImpl<$Res, HomeViewState>;
@useResult
- $Res call(
- {bool loading,
- List uploadingMedias,
- List medias,
- List selectedMedias,
- int mediaCount,
- Object? error});
+ $Res call({SourcePage sourcePage, bool isLastViewChangedByScroll});
}
/// @nodoc
@@ -56,35 +46,18 @@ class _$HomeViewStateCopyWithImpl<$Res, $Val extends HomeViewState>
@pragma('vm:prefer-inline')
@override
$Res call({
- Object? loading = null,
- Object? uploadingMedias = null,
- Object? medias = null,
- Object? selectedMedias = null,
- Object? mediaCount = null,
- Object? error = freezed,
+ Object? sourcePage = null,
+ Object? isLastViewChangedByScroll = null,
}) {
return _then(_value.copyWith(
- loading: null == loading
- ? _value.loading
- : loading // ignore: cast_nullable_to_non_nullable
+ 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,
- 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,
- error: freezed == error ? _value.error : error,
) as $Val);
}
}
@@ -97,13 +70,7 @@ abstract class _$$HomeViewStateImplCopyWith<$Res>
__$$HomeViewStateImplCopyWithImpl<$Res>;
@override
@useResult
- $Res call(
- {bool loading,
- List uploadingMedias,
- List medias,
- List selectedMedias,
- int mediaCount,
- Object? error});
+ $Res call({SourcePage sourcePage, bool isLastViewChangedByScroll});
}
/// @nodoc
@@ -117,35 +84,18 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
@pragma('vm:prefer-inline')
@override
$Res call({
- Object? loading = null,
- Object? uploadingMedias = null,
- Object? medias = null,
- Object? selectedMedias = null,
- Object? mediaCount = null,
- Object? error = freezed,
+ Object? sourcePage = null,
+ Object? isLastViewChangedByScroll = null,
}) {
return _then(_$HomeViewStateImpl(
- loading: null == loading
- ? _value.loading
- : loading // ignore: cast_nullable_to_non_nullable
+ 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,
- 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,
- error: freezed == error ? _value.error : error,
));
}
}
@@ -154,55 +104,19 @@ class __$$HomeViewStateImplCopyWithImpl<$Res>
class _$HomeViewStateImpl implements _HomeViewState {
const _$HomeViewStateImpl(
- {this.loading = false,
- final List uploadingMedias = const [],
- final List medias = const [],
- final List selectedMedias = const [],
- this.mediaCount = 0,
- this.error})
- : _uploadingMedias = uploadingMedias,
- _medias = medias,
- _selectedMedias = selectedMedias;
+ {this.sourcePage = const SourcePage(),
+ this.isLastViewChangedByScroll = false});
@override
@JsonKey()
- final bool loading;
- final List _uploadingMedias;
+ final SourcePage sourcePage;
@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
- final Object? error;
+ final bool isLastViewChangedByScroll;
@override
String toString() {
- return 'HomeViewState(loading: $loading, uploadingMedias: $uploadingMedias, medias: $medias, selectedMedias: $selectedMedias, mediaCount: $mediaCount, error: $error)';
+ return 'HomeViewState(sourcePage: $sourcePage, isLastViewChangedByScroll: $isLastViewChangedByScroll)';
}
@override
@@ -210,26 +124,16 @@ class _$HomeViewStateImpl implements _HomeViewState {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$HomeViewStateImpl &&
- (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.error, error));
+ (identical(other.sourcePage, sourcePage) ||
+ other.sourcePage == sourcePage) &&
+ (identical(other.isLastViewChangedByScroll,
+ isLastViewChangedByScroll) ||
+ other.isLastViewChangedByScroll == isLastViewChangedByScroll));
}
@override
- int get hashCode => Object.hash(
- runtimeType,
- loading,
- const DeepCollectionEquality().hash(_uploadingMedias),
- const DeepCollectionEquality().hash(_medias),
- const DeepCollectionEquality().hash(_selectedMedias),
- mediaCount,
- const DeepCollectionEquality().hash(error));
+ int get hashCode =>
+ Object.hash(runtimeType, sourcePage, isLastViewChangedByScroll);
@JsonKey(ignore: true)
@override
@@ -240,25 +144,13 @@ class _$HomeViewStateImpl implements _HomeViewState {
abstract class _HomeViewState implements HomeViewState {
const factory _HomeViewState(
- {final bool loading,
- final List uploadingMedias,
- final List medias,
- final List selectedMedias,
- final int mediaCount,
- final Object? error}) = _$HomeViewStateImpl;
+ {final SourcePage sourcePage,
+ final bool isLastViewChangedByScroll}) = _$HomeViewStateImpl;
@override
- bool get loading;
- @override
- List get uploadingMedias;
- @override
- List get medias;
- @override
- List get selectedMedias;
- @override
- int get mediaCount;
+ SourcePage get sourcePage;
@override
- Object? get error;
+ bool get isLastViewChangedByScroll;
@override
@JsonKey(ignore: true)
_$$HomeViewStateImplCopyWith<_$HomeViewStateImpl> get copyWith =>
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..9c1b57b
--- /dev/null
+++ b/app/lib/ui/flow/home/local/local_media_screen_view_model.dart
@@ -0,0 +1,118 @@
+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 {
+ 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);
+ 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),
+ );
+ } else {
+ state = state.copyWith(
+ selectedMedias: [...selectedMedias, media],
+ );
+ }
+ }
+
+ Future uploadMediaOnGoogleDrive() async {
+ try {
+ if (_authService.getUser == null) {
+ await _authService.signInWithGoogle();
+ }
+ state = state.copyWith(uploadingMedias: state.selectedMedias);
+ 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) {
+ if (error is UserGoogleSignInAccountNotFound) {
+ await _authService.signInWithGoogle();
+ await uploadMediaOnGoogleDrive();
+ }
+ 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..5c3d18d
--- /dev/null
+++ b/app/lib/ui/flow/home/local/local_medias_screen.dart
@@ -0,0 +1,127 @@
+import 'dart:io';
+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();
+ }
+ return Visibility(
+ visible: !isLoading,
+ replacement: const Center(child: CircularProgressIndicator.adaptive()),
+ child: 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,
+ ),
+ );
+ }
+ },
+ ),
+ ),
+ );
+ }
+}
diff --git a/app/lib/ui/flow/onboard/onboard_screen.dart b/app/lib/ui/flow/onboard/onboard_screen.dart
index 3bdbb93..2fdebd4 100644
--- a/app/lib/ui/flow/onboard/onboard_screen.dart
+++ b/app/lib/ui/flow/onboard/onboard_screen.dart
@@ -5,6 +5,7 @@ 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';
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/pubspec.lock b/app/pubspec.lock
index fdb27fe..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:
@@ -652,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:
diff --git a/app/pubspec.yaml b/app/pubspec.yaml
index c68407a..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
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/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/services/local_media_service.dart b/data/lib/services/local_media_service.dart
index fb80e30..2ae278c 100644
--- a/data/lib/services/local_media_service.dart
+++ b/data/lib/services/local_media_service.dart
@@ -10,14 +10,18 @@ final localMediaServiceProvider = Provider(
class LocalMediaService {
const LocalMediaService();
+ Future requestPermission() async {
+ final state = await PhotoManager.requestPermissionExtend();
+ return state.hasAccess;
+ }
+
Future getMediaCount() async {
- await PhotoManager.requestPermissionExtend();
return await PhotoManager.getAssetCount();
}
- Future> getMedia({required int start, required int end}) async {
- final assets =
- await PhotoManager.getAssetListRange(start: start, end: end);
+ 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 {
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/style/lib/extensions/context_extensions.dart b/style/lib/extensions/context_extensions.dart
new file mode 100644
index 0000000..1e0bd18
--- /dev/null
+++ b/style/lib/extensions/context_extensions.dart
@@ -0,0 +1,18 @@
+import 'package:flutter/cupertino.dart';
+import 'package:style/theme/theme.dart';
+
+extension BuildContextExtensions on BuildContext {
+ EdgeInsets get systemPadding => MediaQuery.of(this).padding;
+
+ Size get mediaQuerySize => MediaQuery.of(this).size;
+
+ AppColorScheme get colorScheme => appColorSchemeOf(this);
+
+ Brightness get brightness => MediaQuery.of(this).platformBrightness;
+
+ bool get isDarkMode => brightness == Brightness.dark;
+
+ FocusScopeNode get focusScope => FocusScope.of(this);
+
+ bool get use24Hour => MediaQuery.of(this).alwaysUse24HourFormat;
+}