From bab40e00f336e54b6943e5be345211b54f69d7e6 Mon Sep 17 00:00:00 2001 From: remartin Date: Sat, 30 Mar 2024 08:46:12 +0100 Subject: [PATCH] feat: init clean architecture --- analysis_options.yaml | 68 ++ .../app/src/main/res/values-night/styles.xml | 2 + android/app/src/main/res/values/styles.xml | 3 +- lib/app.dart | 404 ++++--- lib/components/cards/album_card.dart | 19 +- .../local/preferences_datasource.dart | 14 + .../local/secure_storage_datasource.dart | 9 + .../data/datasources/remote/api_client.dart | 48 + .../datasources/remote/api_interceptor.dart | 110 ++ .../datasources/remote/remote_datasource.dart | 192 +++ lib/core/data/models/api_failure_model.dart | 20 + lib/core/data/models/api_failure_model.g.dart | 17 + lib/core/enum/request_format_enum.dart | 10 + lib/core/enum/sort_method_enum.dart | 58 + lib/core/errors/failures.dart | 33 + lib/core/errors/failures.freezed.dart | 566 +++++++++ .../album_preferences_extension.dart | 22 + .../api_failure_mapper_extension.dart | 16 + .../extensions/api_preferences_extension.dart | 18 + .../extensions/build_context_extension.dart | 26 + lib/core/extensions/string_extension.dart | 3 + lib/core/extensions/theme_mode_extension.dart | 27 + lib/core/injector/injector.dart | 21 + lib/core/network/network_info.dart | 17 + .../widgets/animated/collapsible_widget.dart | 32 + .../widgets/app_bars/expanded_app_bar.dart | 78 ++ .../widgets/buttons/custom_button.dart | 70 ++ .../buttons/custom_popup_menu_button.dart | 60 + .../widgets/buttons/custom_text_button.dart | 36 + .../widgets/form/custom_text_field.dart | 184 +++ .../widgets/images/custom_network_image.dart | 151 +++ lib/core/router/app_router.dart | 46 + lib/core/router/app_router.freezed.dart | 144 +++ lib/core/router/app_routes.dart | 8 + lib/core/router/page_route_arguments.dart | 6 + lib/core/utils/app_assets.dart | 5 + lib/core/utils/app_colors.dart | 37 + lib/core/utils/app_regexp.dart | 5 + lib/core/utils/app_strings.dart | 11 + lib/core/utils/app_text_styles.dart | 13 + lib/core/utils/constants/api_constants.dart | 182 +++ lib/core/utils/constants/api_errors.dart | 9 + lib/core/utils/constants/hero_tags.dart | 5 + .../utils/constants/local_key_constants.dart | 30 + .../utils/constants/settings_constants.dart | 20 + lib/core/utils/constants/ui_constants.dart | 29 + .../listeners/immersive_scroll_listener.dart | 16 + lib/core/utils/result.dart | 11 + lib/core/utils/result.freezed.dart | 343 ++++++ lib/core/utils/themes/app_themes.dart | 337 ++++++ .../utils/validators/field_validator.dart | 28 + .../_template/data/datasources/.gitkeep | 0 lib/features/_template/data/models/.gitkeep | 0 .../_template/data/repositories/.gitkeep | 0 .../_template/domain/entities/.gitkeep | 0 .../_template/domain/repositories/.gitkeep | 0 .../_template/domain/usecases/.gitkeep | 0 .../_template/presentation/blocs/.gitkeep | 0 .../_template/presentation/pages/.gitkeep | 0 .../_template/presentation/widgets/.gitkeep | 0 .../data/datasources/album_datasource.dart | 13 + .../datasources/album_datasource.impl.dart | 68 ++ .../albums/data/models/album_model.dart | 35 + .../albums/data/models/album_model.g.dart | 54 + .../repositories/album_repository.impl.dart | 21 + .../albums/domain/entities/album_entity.dart | 73 ++ .../domain/enums/album_status_enum.dart | 11 + .../domain/repositories/album_repository.dart | 13 + .../usecases/fetch_album_images_use_case.dart | 29 + .../usecases/fetch_albums_use_case.dart | 24 + .../album_content/album_content_bloc.dart | 38 + .../album_content_bloc.freezed.dart | 866 ++++++++++++++ .../album_content/album_content_event.dart | 6 + .../album_content/album_content_state.dart | 20 + .../blocs/album_images/album_images_bloc.dart | 38 + .../album_images_bloc.freezed.dart | 866 ++++++++++++++ .../album_images/album_images_event.dart | 6 + .../album_images/album_images_state.dart | 20 + .../albums/presentation/pages/album_page.dart | 67 ++ .../albums/presentation/pages/root_page.dart | 134 +++ .../painters/album_card_painter.dart | 67 ++ .../widgets/album_card_widget.dart | 128 ++ .../widgets/album_content_widget.dart | 86 ++ .../authentication_datasource.dart | 15 + .../authentication_datasource.impl.dart | 111 ++ .../data/enums/user_status_enum.dart | 18 + .../data/models/session_status_model.dart | 30 + .../data/models/session_status_model.g.dart | 59 + .../authentication_repository.impl.dart | 24 + .../entities/session_status_entity.dart | 57 + .../authentication_repository.dart | 15 + .../domain/usecases/auto_login_use_case.dart | 11 + .../domain/usecases/login_use_case.dart | 56 + .../domain/usecases/logout_use_case.dart | 11 + .../usecases/session_status_use_case.dart | 12 + .../presentation/blocs/login/login_bloc.dart | 75 ++ .../blocs/login/login_bloc.freezed.dart | 1030 ++++++++++++++++ .../presentation/blocs/login/login_event.dart | 16 + .../presentation/blocs/login/login_state.dart | 16 + .../session_status/session_status_bloc.dart | 63 + .../session_status_bloc.freezed.dart | 1059 +++++++++++++++++ .../session_status/session_status_event.dart | 8 + .../session_status/session_status_state.dart | 29 + .../presentation/pages/login_page.dart | 95 ++ .../presentation/pages/startup_page.dart | 42 + .../widgets/login_form_widget.dart | 234 ++++ .../images/data/enums/image_size_enum.dart | 46 + .../data/models/image_derivative_model.dart | 19 + .../data/models/image_derivative_model.g.dart | 19 + .../images/data/models/image_model.dart | 70 ++ .../images/data/models/image_model.g.dart | 70 ++ .../data/models/image_parent_model.dart | 19 + .../data/models/image_parent_model.g.dart | 19 + .../entities/image_derivative_entity.dart | 20 + .../entities/image_derivative_entity.g.dart | 19 + .../images/domain/entities/image_entity.dart | 148 +++ .../domain/entities/image_entity.g.dart | 70 ++ .../domain/entities/image_parent_entity.dart | 23 + .../entities/image_parent_entity.g.dart | 19 + .../presentation/pages/image_search_page.dart | 78 ++ .../widgets/image_card_widget.dart | 160 +++ .../settings_local_datasource.dart | 7 + .../settings_local_datasource.impl.dart | 12 + .../settings_repository.impl.dart | 13 + .../repositories/settings_repository.dart | 7 + .../get_user_theme_mode_use_case.dart | 11 + .../blocs/theme/current_theme_bloc.dart | 57 + .../theme/current_theme_bloc.freezed.dart | 129 ++ .../presentation/pages/settings_page.dart | 47 + lib/main.dart | 46 +- lib/models/album_model.dart | 12 +- pubspec.yaml | 18 +- 132 files changed, 10286 insertions(+), 260 deletions(-) create mode 100644 analysis_options.yaml create mode 100644 lib/core/data/datasources/local/preferences_datasource.dart create mode 100644 lib/core/data/datasources/local/secure_storage_datasource.dart create mode 100644 lib/core/data/datasources/remote/api_client.dart create mode 100644 lib/core/data/datasources/remote/api_interceptor.dart create mode 100644 lib/core/data/datasources/remote/remote_datasource.dart create mode 100644 lib/core/data/models/api_failure_model.dart create mode 100644 lib/core/data/models/api_failure_model.g.dart create mode 100644 lib/core/enum/request_format_enum.dart create mode 100644 lib/core/enum/sort_method_enum.dart create mode 100644 lib/core/errors/failures.dart create mode 100644 lib/core/errors/failures.freezed.dart create mode 100644 lib/core/extensions/album_preferences_extension.dart create mode 100644 lib/core/extensions/api_failure_mapper_extension.dart create mode 100644 lib/core/extensions/api_preferences_extension.dart create mode 100644 lib/core/extensions/build_context_extension.dart create mode 100644 lib/core/extensions/string_extension.dart create mode 100644 lib/core/extensions/theme_mode_extension.dart create mode 100644 lib/core/injector/injector.dart create mode 100644 lib/core/network/network_info.dart create mode 100644 lib/core/presentation/widgets/animated/collapsible_widget.dart create mode 100644 lib/core/presentation/widgets/app_bars/expanded_app_bar.dart create mode 100644 lib/core/presentation/widgets/buttons/custom_button.dart create mode 100644 lib/core/presentation/widgets/buttons/custom_popup_menu_button.dart create mode 100644 lib/core/presentation/widgets/buttons/custom_text_button.dart create mode 100644 lib/core/presentation/widgets/form/custom_text_field.dart create mode 100644 lib/core/presentation/widgets/images/custom_network_image.dart create mode 100644 lib/core/router/app_router.dart create mode 100644 lib/core/router/app_router.freezed.dart create mode 100644 lib/core/router/app_routes.dart create mode 100644 lib/core/router/page_route_arguments.dart create mode 100644 lib/core/utils/app_assets.dart create mode 100644 lib/core/utils/app_colors.dart create mode 100644 lib/core/utils/app_regexp.dart create mode 100644 lib/core/utils/app_strings.dart create mode 100644 lib/core/utils/app_text_styles.dart create mode 100644 lib/core/utils/constants/api_constants.dart create mode 100644 lib/core/utils/constants/api_errors.dart create mode 100644 lib/core/utils/constants/hero_tags.dart create mode 100644 lib/core/utils/constants/local_key_constants.dart create mode 100644 lib/core/utils/constants/settings_constants.dart create mode 100644 lib/core/utils/constants/ui_constants.dart create mode 100644 lib/core/utils/listeners/immersive_scroll_listener.dart create mode 100644 lib/core/utils/result.dart create mode 100644 lib/core/utils/result.freezed.dart create mode 100644 lib/core/utils/themes/app_themes.dart create mode 100644 lib/core/utils/validators/field_validator.dart create mode 100644 lib/features/_template/data/datasources/.gitkeep create mode 100644 lib/features/_template/data/models/.gitkeep create mode 100644 lib/features/_template/data/repositories/.gitkeep create mode 100644 lib/features/_template/domain/entities/.gitkeep create mode 100644 lib/features/_template/domain/repositories/.gitkeep create mode 100644 lib/features/_template/domain/usecases/.gitkeep create mode 100644 lib/features/_template/presentation/blocs/.gitkeep create mode 100644 lib/features/_template/presentation/pages/.gitkeep create mode 100644 lib/features/_template/presentation/widgets/.gitkeep create mode 100644 lib/features/albums/data/datasources/album_datasource.dart create mode 100644 lib/features/albums/data/datasources/album_datasource.impl.dart create mode 100644 lib/features/albums/data/models/album_model.dart create mode 100644 lib/features/albums/data/models/album_model.g.dart create mode 100644 lib/features/albums/data/repositories/album_repository.impl.dart create mode 100644 lib/features/albums/domain/entities/album_entity.dart create mode 100644 lib/features/albums/domain/enums/album_status_enum.dart create mode 100644 lib/features/albums/domain/repositories/album_repository.dart create mode 100644 lib/features/albums/domain/usecases/fetch_album_images_use_case.dart create mode 100644 lib/features/albums/domain/usecases/fetch_albums_use_case.dart create mode 100644 lib/features/albums/presentation/blocs/album_content/album_content_bloc.dart create mode 100644 lib/features/albums/presentation/blocs/album_content/album_content_bloc.freezed.dart create mode 100644 lib/features/albums/presentation/blocs/album_content/album_content_event.dart create mode 100644 lib/features/albums/presentation/blocs/album_content/album_content_state.dart create mode 100644 lib/features/albums/presentation/blocs/album_images/album_images_bloc.dart create mode 100644 lib/features/albums/presentation/blocs/album_images/album_images_bloc.freezed.dart create mode 100644 lib/features/albums/presentation/blocs/album_images/album_images_event.dart create mode 100644 lib/features/albums/presentation/blocs/album_images/album_images_state.dart create mode 100644 lib/features/albums/presentation/pages/album_page.dart create mode 100644 lib/features/albums/presentation/pages/root_page.dart create mode 100644 lib/features/albums/presentation/painters/album_card_painter.dart create mode 100644 lib/features/albums/presentation/widgets/album_card_widget.dart create mode 100644 lib/features/albums/presentation/widgets/album_content_widget.dart create mode 100644 lib/features/authentication/data/datasources/authentication_datasource.dart create mode 100644 lib/features/authentication/data/datasources/authentication_datasource.impl.dart create mode 100644 lib/features/authentication/data/enums/user_status_enum.dart create mode 100644 lib/features/authentication/data/models/session_status_model.dart create mode 100644 lib/features/authentication/data/models/session_status_model.g.dart create mode 100644 lib/features/authentication/data/repositories/authentication_repository.impl.dart create mode 100644 lib/features/authentication/domain/entities/session_status_entity.dart create mode 100644 lib/features/authentication/domain/repositories/authentication_repository.dart create mode 100644 lib/features/authentication/domain/usecases/auto_login_use_case.dart create mode 100644 lib/features/authentication/domain/usecases/login_use_case.dart create mode 100644 lib/features/authentication/domain/usecases/logout_use_case.dart create mode 100644 lib/features/authentication/domain/usecases/session_status_use_case.dart create mode 100644 lib/features/authentication/presentation/blocs/login/login_bloc.dart create mode 100644 lib/features/authentication/presentation/blocs/login/login_bloc.freezed.dart create mode 100644 lib/features/authentication/presentation/blocs/login/login_event.dart create mode 100644 lib/features/authentication/presentation/blocs/login/login_state.dart create mode 100644 lib/features/authentication/presentation/blocs/session_status/session_status_bloc.dart create mode 100644 lib/features/authentication/presentation/blocs/session_status/session_status_bloc.freezed.dart create mode 100644 lib/features/authentication/presentation/blocs/session_status/session_status_event.dart create mode 100644 lib/features/authentication/presentation/blocs/session_status/session_status_state.dart create mode 100644 lib/features/authentication/presentation/pages/login_page.dart create mode 100644 lib/features/authentication/presentation/pages/startup_page.dart create mode 100644 lib/features/authentication/presentation/widgets/login_form_widget.dart create mode 100644 lib/features/images/data/enums/image_size_enum.dart create mode 100644 lib/features/images/data/models/image_derivative_model.dart create mode 100644 lib/features/images/data/models/image_derivative_model.g.dart create mode 100644 lib/features/images/data/models/image_model.dart create mode 100644 lib/features/images/data/models/image_model.g.dart create mode 100644 lib/features/images/data/models/image_parent_model.dart create mode 100644 lib/features/images/data/models/image_parent_model.g.dart create mode 100644 lib/features/images/domain/entities/image_derivative_entity.dart create mode 100644 lib/features/images/domain/entities/image_derivative_entity.g.dart create mode 100644 lib/features/images/domain/entities/image_entity.dart create mode 100644 lib/features/images/domain/entities/image_entity.g.dart create mode 100644 lib/features/images/domain/entities/image_parent_entity.dart create mode 100644 lib/features/images/domain/entities/image_parent_entity.g.dart create mode 100644 lib/features/images/presentation/pages/image_search_page.dart create mode 100644 lib/features/images/presentation/widgets/image_card_widget.dart create mode 100644 lib/features/settings/data/datasources/settings_local_datasource.dart create mode 100644 lib/features/settings/data/datasources/settings_local_datasource.impl.dart create mode 100644 lib/features/settings/data/repositories/settings_repository.impl.dart create mode 100644 lib/features/settings/domain/repositories/settings_repository.dart create mode 100644 lib/features/settings/domain/usecases/get_user_theme_mode_use_case.dart create mode 100644 lib/features/settings/presentation/blocs/theme/current_theme_bloc.dart create mode 100644 lib/features/settings/presentation/blocs/theme/current_theme_bloc.freezed.dart create mode 100644 lib/features/settings/presentation/pages/settings_page.dart diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..4d60ecb --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,68 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +analyzer: + exclude: + - 'lib/views/**.dart' + - 'lib/components/**.dart' + - 'lib/network/**.dart' + - 'lib/services/**.dart' + - 'lib/utils/**.dart' + - 'lib/models/**.dart' + - '**/*.g.dart' + - '**/*.freezed.dart' + - '**/injector/*.config.dart' + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + avoid_print: true + sort_constructors_first: true + prefer_single_quotes: true + prefer_double_quotes: false + always_specify_types: true + lines_longer_than_80_chars: false + avoid_redundant_argument_values: true + always_use_package_imports: true + require_trailing_commas: true + avoid_bool_literals_in_conditional_expressions: true + annotate_overrides: true + avoid_final_parameters: true + avoid_null_checks_in_equality_operators: true + avoid_return_types_on_setters: true + avoid_returning_null: true + avoid_unnecessary_containers: true + avoid_unused_constructor_parameters: true + await_only_futures: true + cascade_invocations: true + curly_braces_in_flow_control_structures: true + directives_ordering: true + empty_constructor_bodies: true + empty_catches: true + file_names: true + use_build_context_synchronously: false + join_return_with_assignment: true + null_closures: true + prefer_adjacent_string_concatenation: true + prefer_conditional_assignment: true + prefer_const_constructors: true + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 449a9f9..106454c 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -5,6 +5,7 @@ @drawable/launch_background + shortEdges diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index e68b8dc..138d296 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,6 +5,7 @@ @drawable/launch_background + shortEdges diff --git a/lib/app.dart b/lib/app.dart index e7f02d0..30f8bc9 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,221 +1,215 @@ import 'package:flutter/material.dart'; -import 'package:flutter_easyloading/flutter_easyloading.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:image_picker/image_picker.dart'; -import 'package:piwigo_ng/services/app_providers.dart'; -import 'package:piwigo_ng/services/preferences_service.dart'; -import 'package:piwigo_ng/utils/overscroll_behavior.dart'; -import 'package:piwigo_ng/utils/themes.dart'; -import 'package:piwigo_ng/views/album/album_page.dart'; -import 'package:piwigo_ng/views/album/album_privacy_page.dart'; -import 'package:piwigo_ng/views/album/root_album_page.dart'; -import 'package:piwigo_ng/views/authentication/login_page.dart'; -import 'package:piwigo_ng/views/authentication/login_settings_page.dart'; -import 'package:piwigo_ng/views/image/edit_image_page.dart'; -import 'package:piwigo_ng/views/image/image_favorites_page.dart'; -import 'package:piwigo_ng/views/image/image_page.dart'; -import 'package:piwigo_ng/views/image/image_search_page.dart'; -import 'package:piwigo_ng/views/image/video_player_page.dart'; -import 'package:piwigo_ng/views/settings/auto_upload_page.dart'; -import 'package:piwigo_ng/views/settings/privacy_policy_page.dart'; -import 'package:piwigo_ng/views/settings/select_language_page.dart'; -import 'package:piwigo_ng/views/settings/settings_page.dart'; -import 'package:piwigo_ng/views/unknown_route_page.dart'; -import 'package:piwigo_ng/views/upload/upload_page.dart'; -import 'package:piwigo_ng/views/upload/upload_status_page.dart'; - -import 'models/image_model.dart'; +import 'package:piwigo_ng/core/router/app_router.dart'; +import 'package:piwigo_ng/core/router/app_routes.dart'; +import 'package:piwigo_ng/core/utils/themes/app_themes.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; +import 'package:piwigo_ng/features/settings/presentation/blocs/theme/current_theme_bloc.dart'; class App extends StatelessWidget { - const App({Key? key}) : super(key: key); + const App({super.key}); + + static const AppRouter _router = AppRouter(); + static const AppThemes _themes = AppThemes(); - static final GlobalKey scaffoldMessengerKey = - GlobalKey(); - static final GlobalKey navigatorKey = - GlobalKey(); + static final GlobalKey scaffoldMessengerKey = GlobalKey(); + static final GlobalKey navigatorKey = GlobalKey(); static final GlobalKey appKey = GlobalKey(); @override Widget build(BuildContext context) { - return AppProviders( - builder: (localNotifier, themeNotifier) { - return MaterialApp( - title: 'Piwigo NG', - key: appKey, - navigatorKey: navigatorKey, - scaffoldMessengerKey: scaffoldMessengerKey, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: const [ - Locale('en'), - Locale('de'), - Locale('fr'), - Locale('es'), - Locale('lt'), - Locale('sk'), - Locale('zh'), - ], - locale: localNotifier.locale, - themeMode: themeNotifier.isDark ? ThemeMode.dark : ThemeMode.light, - darkTheme: darkTheme, - theme: lightTheme, - builder: (context, child) { - EasyLoading.instance - ..loadingStyle = EasyLoadingStyle.custom - ..backgroundColor = Theme.of(context).scaffoldBackgroundColor - ..indicatorColor = Theme.of(context).textTheme.bodyMedium?.color - ..textColor = Theme.of(context).textTheme.bodyMedium?.color; - return ScrollConfiguration( - behavior: OverscrollBehavior(), - child: EasyLoading.init().call(context, child), - ); - }, - onGenerateRoute: generateRoute, - onGenerateInitialRoutes: (String route) { - return [ - MaterialPageRoute( - builder: (_) => const LoginPage(autoLogin: true), - ), - ]; - }, - initialRoute: '/login', - ); - }, - ); - } -} - -Route generateRoute(RouteSettings settings) { - Map arguments = {}; - if (settings.arguments != null) { - arguments = settings.arguments as Map; - } - - bool isAdmin = appPreferences.getBool(Preferences.isAdminKey) ?? false; - - if (settings.name == null) { - debugPrint("no route name"); - return MaterialPageRoute( - builder: (_) => UnknownRoutePage(route: settings), - settings: settings, - ); - } - - switch (settings.name) { - case LoginPage.routeName: - return MaterialPageRoute( - builder: (_) => const LoginPage(), - settings: settings, - ); - case LoginSettingsPage.routeName: - return MaterialPageRoute( - builder: (_) => LoginSettingsPage(), - settings: settings, - ); - case RootAlbumPage.routeName: - return MaterialPageRoute( - builder: (_) => RootAlbumPage( - albumId: arguments['albumId'] ?? 0, - isAdmin: arguments['isAdmin'] ?? isAdmin, - ), - settings: settings, - ); - case AlbumPage.routeName: - return MaterialPageRoute( - builder: (_) => AlbumPage( - album: arguments['album'], - isAdmin: arguments['isAdmin'] ?? isAdmin, - ), - settings: settings, - ); - case AlbumPrivacyPage.routeName: - return MaterialPageRoute( - builder: (_) => AlbumPrivacyPage( - album: arguments['album']!, - ), - settings: settings, - ); - case ImageSearchPage.routeName: - return MaterialPageRoute( - builder: (_) => ImageSearchPage( - isAdmin: arguments['isAdmin'] ?? isAdmin, + return MultiBlocProvider( + providers: >[ + BlocProvider( + create: (BuildContext context) => CurrentThemeBloc()..add(InitThemeEvent()), ), - settings: settings, - ); - case ImageFavoritesPage.routeName: - return MaterialPageRoute( - builder: (_) => ImageFavoritesPage( - isAdmin: arguments['isAdmin'] ?? isAdmin, + BlocProvider( + create: (BuildContext context) => SessionStatusBloc(), ), - settings: settings, - ); - case UploadPage.routeName: - return MaterialPageRoute( - builder: (_) => UploadPage( - imageList: arguments["images"] ?? [], - albumId: arguments["category"], + ], + child: BlocListener( + listener: (BuildContext context, SessionStatusState state) => state.whenOrNull( + loggedOut: () => navigatorKey.currentState?.pushNamedAndRemoveUntil( + AppRoutes.login, + (Route route) => false, + ), ), - settings: settings, - ); - case UploadStatusPage.routeName: - return MaterialPageRoute( - builder: (_) => UploadStatusPage(), - settings: settings, - ); - case AutoUploadPage.routeName: - return MaterialPageRoute( - builder: (_) => AutoUploadPage(), - settings: settings, - ); - case ImagePage.routeName: - return MaterialPageRoute?>( - builder: (_) => ImagePage( - images: arguments['images'] ?? [], - startId: arguments['startId'], - album: arguments['album'], - isAdmin: arguments['isAdmin'] ?? isAdmin, - ), - settings: settings, - ); - case VideoPlayerPage.routeName: - return MaterialPageRoute( - builder: (_) => VideoPlayerPage( - videoUrl: arguments['videoUrl'], - thumbnailUrl: arguments['thumbnailUrl'], - ), - settings: settings, - ); - case EditImagePage.routeName: - return MaterialPageRoute( - builder: (_) => EditImagePage( - images: arguments['images'] ?? [], + child: BlocBuilder( + builder: (BuildContext context, CurrentThemeState themeState) { + return MaterialApp( + // title: 'Piwigo NG', + debugShowCheckedModeBanner: false, + key: appKey, + navigatorKey: navigatorKey, + scaffoldMessengerKey: scaffoldMessengerKey, + localizationsDelegates: const >[ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + // locale: localNotifier.locale, + themeMode: ThemeMode.light, + darkTheme: _themes.dark, + theme: _themes.light, + // builder: (context, child) { + // EasyLoading.instance + // ..loadingStyle = EasyLoadingStyle.custom + // ..backgroundColor = Theme.of(context).scaffoldBackgroundColor + // ..indicatorColor = Theme.of(context).textTheme.bodyMedium?.color + // ..textColor = Theme.of(context).textTheme.bodyMedium?.color; + // return ScrollConfiguration( + // behavior: OverscrollBehavior(), + // child: EasyLoading.init().call(context, child), + // ); + // }, + onGenerateRoute: (RouteSettings settings) => _router.generateRoute(settings), + initialRoute: AppRoutes.main, + // onGenerateInitialRoutes: (String route) { + // return [ + // MaterialPageRoute( + // builder: (_) => const LoginPage(autoLogin: true), + // ), + // ]; + // }, + // initialRoute: '/login', + ); + }, ), - settings: settings, - ); - case SettingsPage.routeName: - return MaterialPageRoute( - builder: (_) => SettingsPage(), - settings: settings, - ); - case PrivacyPolicyPage.routeName: - return MaterialPageRoute( - builder: (_) => const PrivacyPolicyPage(), - settings: settings, - ); - case SelectLanguagePage.routeName: - return MaterialPageRoute( - builder: (_) => const SelectLanguagePage(), - settings: settings, - ); - default: - return MaterialPageRoute( - builder: (_) => UnknownRoutePage(route: settings), - settings: settings, - ); + ), + ); } } + +// Route generateRoute(RouteSettings settings) { +// Map arguments = {}; +// if (settings.arguments != null) { +// arguments = settings.arguments as Map; +// } +// +// bool isAdmin = appPreferences.getBool(Preferences.isAdminKey) ?? false; +// +// if (settings.name == null) { +// debugPrint("no route name"); +// return MaterialPageRoute( +// builder: (_) => UnknownRoutePage(route: settings), +// settings: settings, +// ); +// } +// +// switch (settings.name) { +// case LoginPage.routeName: +// return MaterialPageRoute( +// builder: (_) => const LoginPage(), +// settings: settings, +// ); +// case LoginSettingsPage.routeName: +// return MaterialPageRoute( +// builder: (_) => LoginSettingsPage(), +// settings: settings, +// ); +// case RootAlbumPage.routeName: +// return MaterialPageRoute( +// builder: (_) => RootAlbumPage( +// albumId: arguments['albumId'] ?? 0, +// isAdmin: arguments['isAdmin'] ?? isAdmin, +// ), +// settings: settings, +// ); +// case AlbumPage.routeName: +// return MaterialPageRoute( +// builder: (_) => AlbumPage( +// album: arguments['album'], +// isAdmin: arguments['isAdmin'] ?? isAdmin, +// ), +// settings: settings, +// ); +// case AlbumPrivacyPage.routeName: +// return MaterialPageRoute( +// builder: (_) => AlbumPrivacyPage( +// album: arguments['album']!, +// ), +// settings: settings, +// ); +// case ImageSearchPage.routeName: +// return MaterialPageRoute( +// builder: (_) => ImageSearchPage( +// isAdmin: arguments['isAdmin'] ?? isAdmin, +// ), +// settings: settings, +// ); +// case ImageFavoritesPage.routeName: +// return MaterialPageRoute( +// builder: (_) => ImageFavoritesPage( +// isAdmin: arguments['isAdmin'] ?? isAdmin, +// ), +// settings: settings, +// ); +// case UploadPage.routeName: +// return MaterialPageRoute( +// builder: (_) => UploadPage( +// imageList: arguments["images"] ?? [], +// albumId: arguments["category"], +// ), +// settings: settings, +// ); +// case UploadStatusPage.routeName: +// return MaterialPageRoute( +// builder: (_) => UploadStatusPage(), +// settings: settings, +// ); +// case AutoUploadPage.routeName: +// return MaterialPageRoute( +// builder: (_) => AutoUploadPage(), +// settings: settings, +// ); +// case ImagePage.routeName: +// return MaterialPageRoute?>( +// builder: (_) => ImagePage( +// images: arguments['images'] ?? [], +// startId: arguments['startId'], +// album: arguments['album'], +// isAdmin: arguments['isAdmin'] ?? isAdmin, +// ), +// settings: settings, +// ); +// case VideoPlayerPage.routeName: +// return MaterialPageRoute( +// builder: (_) => VideoPlayerPage( +// videoUrl: arguments['videoUrl'], +// thumbnailUrl: arguments['thumbnailUrl'], +// ), +// settings: settings, +// ); +// case EditImagePage.routeName: +// return MaterialPageRoute( +// builder: (_) => EditImagePage( +// images: arguments['images'] ?? [], +// ), +// settings: settings, +// ); +// case SettingsPage.routeName: +// return MaterialPageRoute( +// builder: (_) => SettingsPage(), +// settings: settings, +// ); +// case PrivacyPolicyPage.routeName: +// return MaterialPageRoute( +// builder: (_) => const PrivacyPolicyPage(), +// settings: settings, +// ); +// case SelectLanguagePage.routeName: +// return MaterialPageRoute( +// builder: (_) => const SelectLanguagePage(), +// settings: settings, +// ); +// default: +// return MaterialPageRoute( +// builder: (_) => UnknownRoutePage(route: settings), +// settings: settings, +// ); +// } +// } diff --git a/lib/components/cards/album_card.dart b/lib/components/cards/album_card.dart index 0d5aa5b..0623e65 100644 --- a/lib/components/cards/album_card.dart +++ b/lib/components/cards/album_card.dart @@ -68,8 +68,8 @@ class AlbumCard extends StatelessWidget { icon: Icons.edit, ), AlbumCardAction( - backgroundColor: - const Color(0xFF4B4B4B), // Todo: Theme grey action color + backgroundColor: const Color(0xFF4B4B4B), + // Todo: Theme grey action color foregroundColor: Colors.white, autoClose: true, onPressed: onMove, @@ -102,8 +102,7 @@ class AlbumCard extends StatelessWidget { width: ALBUM_ANCHOR_RADIUS * 2.0 + 1.0, decoration: BoxDecoration( color: Theme.of(context).primaryColor, - border: Border.all( - color: Theme.of(context).primaryColor, width: 1.0), + border: Border.all(color: Theme.of(context).primaryColor, width: 1.0), ), ), ), @@ -123,8 +122,7 @@ class AlbumCard extends StatelessWidget { padding: const EdgeInsets.all(8.0).copyWith(right: 0.0), decoration: BoxDecoration( color: Theme.of(context).cardColor, - border: - Border.all(color: Theme.of(context).cardColor, width: 1.0), + border: Border.all(color: Theme.of(context).cardColor, width: 1.0), ), child: AlbumCardContent( album: album, @@ -226,8 +224,7 @@ class AlbumCardContent extends StatelessWidget { album.name, maxLines: 1, textAlign: TextAlign.center, - maxFontSize: - Theme.of(context).textTheme.titleLarge!.fontSize!, + maxFontSize: Theme.of(context).textTheme.titleLarge!.fontSize!, minFontSize: 10.0, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.titleLarge, @@ -240,8 +237,7 @@ class AlbumCardContent extends StatelessWidget { child: Builder(builder: (context) { return AutoSizeText( album.comment ?? '', - maxFontSize: - Theme.of(context).textTheme.bodySmall!.fontSize!, + maxFontSize: Theme.of(context).textTheme.bodySmall!.fontSize!, minFontSize: 10.0, overflow: TextOverflow.fade, style: Theme.of(context).textTheme.bodySmall, @@ -254,8 +250,7 @@ class AlbumCardContent extends StatelessWidget { appStrings.imageCount(album.nbTotalImages), maxLines: 1, textAlign: TextAlign.center, - maxFontSize: - Theme.of(context).textTheme.labelSmall!.fontSize!, + maxFontSize: Theme.of(context).textTheme.labelSmall!.fontSize!, minFontSize: 8.0, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.labelSmall, diff --git a/lib/core/data/datasources/local/preferences_datasource.dart b/lib/core/data/datasources/local/preferences_datasource.dart new file mode 100644 index 0000000..cce993b --- /dev/null +++ b/lib/core/data/datasources/local/preferences_datasource.dart @@ -0,0 +1,14 @@ +import 'package:piwigo_ng/core/injector/injector.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class PreferencesDatasource { + const PreferencesDatasource(); + + static final SharedPreferences _prefs = serviceLocator(); + + SharedPreferences get instance => _prefs; +} + +mixin AppPreferencesMixin { + PreferencesDatasource get prefs => const PreferencesDatasource(); +} diff --git a/lib/core/data/datasources/local/secure_storage_datasource.dart b/lib/core/data/datasources/local/secure_storage_datasource.dart new file mode 100644 index 0000000..a2a53a5 --- /dev/null +++ b/lib/core/data/datasources/local/secure_storage_datasource.dart @@ -0,0 +1,9 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; + +class SecureStorageDatasource { + const SecureStorageDatasource(); + + static const FlutterSecureStorage _secureStorage = FlutterSecureStorage(); + + FlutterSecureStorage get instance => _secureStorage; +} diff --git a/lib/core/data/datasources/remote/api_client.dart b/lib/core/data/datasources/remote/api_client.dart new file mode 100644 index 0000000..3662791 --- /dev/null +++ b/lib/core/data/datasources/remote/api_client.dart @@ -0,0 +1,48 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:dio/dio.dart'; +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:flutter/foundation.dart'; +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/data/datasources/remote/api_interceptor.dart'; +import 'package:piwigo_ng/core/utils/constants/local_key_constants.dart'; + +class ApiClient { + ApiClient() + : dio = Dio(BaseOptions()) + ..interceptors.add(ApiInterceptor()) + ..interceptors.add(CookieManager(CookieJar())); + final Dio dio; +} + +class SSLHttpOverrides extends HttpOverrides with AppPreferencesMixin { + @override + HttpClient createHttpClient(SecurityContext? context) { + return super.createHttpClient(context)..badCertificateCallback = _badCertificateCallback; + } + + bool _badCertificateCallback(X509Certificate cert, String host, int port) { + if (prefs.instance.getBool(LocalKeyConstants.enableSSLKey) ?? false) { + return true; + } + return false; + } +} + +Map tryParseJson(String data) { + try { + return json.decode(data); + } on FormatException catch (_) { + if (kDebugMode) { + print('Invalid json data'); + print(data); + } + int start = data.indexOf('{'); + int end = data.lastIndexOf('}'); + String parsedData = data.substring(start, end + 1); + if (kDebugMode) print("Parsed : $parsedData"); + return json.decode(parsedData); + } +} diff --git a/lib/core/data/datasources/remote/api_interceptor.dart b/lib/core/data/datasources/remote/api_interceptor.dart new file mode 100644 index 0000000..a8014df --- /dev/null +++ b/lib/core/data/datasources/remote/api_interceptor.dart @@ -0,0 +1,110 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/app.dart'; +import 'package:piwigo_ng/components/snackbars.dart'; +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/extensions/api_preferences_extension.dart'; +import 'package:piwigo_ng/utils/localizations.dart'; + +class ApiInterceptor extends Interceptor with AppPreferencesMixin { + @override + void onRequest( + RequestOptions options, + RequestInterceptorHandler handler, + ) async { + debugPrint('[${options.method}] ${options.queryParameters['method']}'); + try { + options = _buildRequest(options); + return super.onRequest(options, handler); + } catch (error) { + return handler.reject( + DioError( + error: error, + requestOptions: options, + type: DioErrorType.response, + ), + ); + } + } + + RequestOptions _buildRequest(RequestOptions options) { + // Get server url + options.baseUrl = prefs.apiBaseUrl!; + + // Server needs Basic authorization + options.headers['authorization'] = prefs.apiBasicHeader; + + return options; + } + + @override + void onResponse( + Response response, + ResponseInterceptorHandler handler, + ) async { + debugPrint("[${response.statusCode}] ${response.requestOptions.queryParameters['method']}"); + return super.onResponse(response, handler); + } + + @override + void onError( + DioError err, + ErrorInterceptorHandler handler, + ) async { + debugPrint("[${err.response?.statusCode}] ${err.requestOptions.queryParameters['method']}"); + debugPrint('${err.error}\n${err.response?.data}\n${err.stackTrace}'); + + switch (err.response?.statusCode) { + case 403: + App.scaffoldMessengerKey.currentState?.showSnackBar( + errorSnackBar( + message: appStrings.sessionStatusError_title, + icon: Icons.block, + ), + ); + break; + case null: + // Handle invalid SSL + if (err.error is HandshakeException) { + HandshakeException handshakeError = err.error as HandshakeException; + String? message = handshakeError.osError?.message; + if (message != null && message.contains('CERTIFICATE_VERIFY_FAILED')) { + App.scaffoldMessengerKey.currentState?.showSnackBar( + errorSnackBar( + message: appStrings.loginCertFailed_title, + icon: Icons.bookmark_outlined, + ), + ); + break; + } + } + + // Handle invalid URL + if (err.error is SocketException) { + SocketException socketError = err.error as SocketException; + int? code = socketError.osError?.errorCode; + if (code == 7) { + App.scaffoldMessengerKey.currentState?.showSnackBar( + errorSnackBar( + message: appStrings.serverURLerror_title, + icon: Icons.public_off, + ), + ); + break; + } + } + + // Unknown server error + App.scaffoldMessengerKey.currentState?.showSnackBar( + errorSnackBar( + message: appStrings.internetErrorGeneral_title, + icon: Icons.signal_wifi_connected_no_internet_4, + ), + ); + break; + } + return super.onError(err, handler); + } +} diff --git a/lib/core/data/datasources/remote/remote_datasource.dart b/lib/core/data/datasources/remote/remote_datasource.dart new file mode 100644 index 0000000..f02a5b7 --- /dev/null +++ b/lib/core/data/datasources/remote/remote_datasource.dart @@ -0,0 +1,192 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:piwigo_ng/core/data/datasources/remote/api_client.dart'; +import 'package:piwigo_ng/core/data/models/api_failure_model.dart'; +import 'package:piwigo_ng/core/enum/request_format_enum.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/extensions/api_failure_mapper_extension.dart'; +import 'package:piwigo_ng/core/injector/injector.dart'; +import 'package:piwigo_ng/core/network/network_info.dart'; +import 'package:piwigo_ng/core/utils/constants/api_constants.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; + +class RemoteDatasource { + const RemoteDatasource(); + + static final ApiClient _apiClient = serviceLocator(); + static const RequestFormatEnum _defaultRequestFormat = RequestFormatEnum.json; + + Map _buildQueries({ + required String method, + RequestFormatEnum? format, + Map? queryParameters, + }) => + { + 'format': (format ?? _defaultRequestFormat).value, + 'method': method, + if (queryParameters != null) ...queryParameters, + }; + + dynamic _buildData( + Map? data, + ) { + if (data == null) return null; + return FormData.fromMap(data); + } + + Future> performRequest( + Future> Function() request, + ) async { + try { + if (await serviceLocator().isConnected) { + Response response = await request(); + + Map data = json.decode(response.data as String); + if (data['stat'] == 'fail') { + return Result.failure( + ApiFailureMapper.fromApiFailure(ApiFailureModel.fromJson(data)), + ); + } else if (data['stat'] == 'ok') { + return Result.success(data['result']); + } + return Result.failure(const Failure.unknown()); + } else { + return Result.failure(const Failure.connectivity()); + } + } on DioError catch (error) { + return Result.failure(Failure.dio(error)); + } + } + + Future> get({ + String path = ApiConstants.webServiceUrl, + required String method, + RequestFormatEnum? format, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + void Function(int, int)? onReceiveProgress, + }) async => + performRequest( + () => _apiClient.dio.get( + path, + queryParameters: _buildQueries( + format: format, + method: method, + queryParameters: queryParameters, + ), + options: options, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ), + ); + + Future> post({ + String path = ApiConstants.webServiceUrl, + required String method, + RequestFormatEnum? format, + Map? data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + void Function(int, int)? onSendProgress, + void Function(int, int)? onReceiveProgress, + }) => + performRequest( + () => _apiClient.dio.post( + path, + data: _buildData(data), + queryParameters: _buildQueries( + format: format, + method: method, + queryParameters: queryParameters, + ), + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ), + ); + + Future> put({ + String path = ApiConstants.webServiceUrl, + required String method, + RequestFormatEnum? format, + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + void Function(int, int)? onSendProgress, + void Function(int, int)? onReceiveProgress, + }) => + performRequest( + () => _apiClient.dio.put( + path, + data: data, + queryParameters: _buildQueries( + format: format, + method: method, + queryParameters: queryParameters, + ), + options: options, + cancelToken: cancelToken, + onSendProgress: onSendProgress, + onReceiveProgress: onReceiveProgress, + ), + ); + + Future> delete({ + String path = ApiConstants.webServiceUrl, + required String method, + RequestFormatEnum? format, + dynamic data, + Map? queryParameters, + Options? options, + CancelToken? cancelToken, + void Function(int, int)? onSendProgress, + void Function(int, int)? onReceiveProgress, + }) => + performRequest( + () => _apiClient.dio.delete( + path, + data: data, + queryParameters: _buildQueries( + format: format, + method: method, + queryParameters: queryParameters, + ), + options: options, + cancelToken: cancelToken, + ), + ); + + Future> download({ + required String path, + required String outputPath, + required String method, + RequestFormatEnum? format, + dynamic data, + Map? queryParameters, + Options? options, + String lengthHeader = Headers.contentLengthHeader, + CancelToken? cancelToken, + void Function(int, int)? onReceiveProgress, + }) => + performRequest( + () => _apiClient.dio.download( + path, + outputPath, + data: data, + queryParameters: _buildQueries( + format: format, + method: method, + queryParameters: queryParameters, + ), + options: options, + lengthHeader: lengthHeader, + cancelToken: cancelToken, + onReceiveProgress: onReceiveProgress, + ), + ); +} diff --git a/lib/core/data/models/api_failure_model.dart b/lib/core/data/models/api_failure_model.dart new file mode 100644 index 0000000..d7dbf39 --- /dev/null +++ b/lib/core/data/models/api_failure_model.dart @@ -0,0 +1,20 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'api_failure_model.g.dart'; + +@JsonSerializable() +class ApiFailureModel { + factory ApiFailureModel.fromJson(Map json) => _$ApiFailureModelFromJson(json); + + const ApiFailureModel({ + required this.code, + this.message, + }); + + @JsonKey(name: 'err') + final int code; + + final String? message; + + Map toJson() => _$ApiFailureModelToJson(this); +} diff --git a/lib/core/data/models/api_failure_model.g.dart b/lib/core/data/models/api_failure_model.g.dart new file mode 100644 index 0000000..50a4e64 --- /dev/null +++ b/lib/core/data/models/api_failure_model.g.dart @@ -0,0 +1,17 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_failure_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ApiFailureModel _$ApiFailureModelFromJson(Map json) => ApiFailureModel( + code: json['err'] as int, + message: json['message'] as String?, + ); + +Map _$ApiFailureModelToJson(ApiFailureModel instance) => { + 'err': instance.code, + 'message': instance.message, + }; diff --git a/lib/core/enum/request_format_enum.dart b/lib/core/enum/request_format_enum.dart new file mode 100644 index 0000000..3c0bd53 --- /dev/null +++ b/lib/core/enum/request_format_enum.dart @@ -0,0 +1,10 @@ +enum RequestFormatEnum { + json('json'), + xml('rest'), + php('php'), + xmlRPC('xmlrpc'); + + const RequestFormatEnum(this.value); + + final String value; +} diff --git a/lib/core/enum/sort_method_enum.dart b/lib/core/enum/sort_method_enum.dart new file mode 100644 index 0000000..76ab32b --- /dev/null +++ b/lib/core/enum/sort_method_enum.dart @@ -0,0 +1,58 @@ +import 'package:flutter/cupertino.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; + +enum SortMethodEnum { + nameAsc('name ASC'), + nameDesc('name DESC'), + fileAsc('file ASC'), + fileDesc('file DESC'), + dateCreatedAsc('date_creation ASC'), + dateCreatedDesc('date_creation DESC'), + dateAvailableAsc('date_available ASC'), + dateAvailableDesc('date_available DESC'), + rateAsc('rating_score ASC'), + rateDesc('rating_score DESC'), + hitAsc('hit ASC'), + hitDesc('hit DESC'), + random('random'), + custom(''); + + const SortMethodEnum(this.value); + + final String value; +} + +extension SortMethodExtension on SortMethodEnum { + String getLabel(BuildContext context) { + switch (this) { + case SortMethodEnum.nameAsc: + return context.localizations.categorySort_nameAscending; + case SortMethodEnum.nameDesc: + return context.localizations.categorySort_nameDescending; + case SortMethodEnum.fileAsc: + return context.localizations.categorySort_fileNameAscending; + case SortMethodEnum.fileDesc: + return context.localizations.categorySort_fileNameDescending; + case SortMethodEnum.dateCreatedAsc: + return context.localizations.categorySort_dateCreatedAscending; + case SortMethodEnum.dateCreatedDesc: + return context.localizations.categorySort_dateCreatedDescending; + case SortMethodEnum.dateAvailableAsc: + return context.localizations.categorySort_datePostedAscending; + case SortMethodEnum.dateAvailableDesc: + return context.localizations.categorySort_datePostedDescending; + case SortMethodEnum.rateAsc: + return context.localizations.categorySort_ratingScoreAscending; + case SortMethodEnum.rateDesc: + return context.localizations.categorySort_ratingScoreDescending; + case SortMethodEnum.hitAsc: + return context.localizations.categorySort_visitsAscending; + case SortMethodEnum.hitDesc: + return context.localizations.categorySort_visitsDescending; + case SortMethodEnum.random: + return context.localizations.categorySort_random; + default: + return context.localizations.categorySort_manual; + } + } +} diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart new file mode 100644 index 0000000..4e4f978 --- /dev/null +++ b/lib/core/errors/failures.dart @@ -0,0 +1,33 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; + +part 'failures.freezed.dart'; + +@freezed +class Failure with _$Failure { + const Failure._(); + + const factory Failure.unknown() = _UnknownFailure; + + const factory Failure.connectivity() = ConnectivityFailure; + + const factory Failure.dio(DioError error) = DioFailure; + + //region Authentication + const factory Failure.invalidCredentials() = InvalidCredentialsFailure; + + // const factory Failure.loginError() = LoginErrorFailure; + //endregion + + String getMessage(BuildContext context) { + switch (runtimeType) { + case DioFailure: + return (this as DioError).message; + case _UnknownFailure: + default: + return context.localizations.serverUnknownError_message; + } + } +} diff --git a/lib/core/errors/failures.freezed.dart b/lib/core/errors/failures.freezed.dart new file mode 100644 index 0000000..293b6cb --- /dev/null +++ b/lib/core/errors/failures.freezed.dart @@ -0,0 +1,566 @@ +// 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 'failures.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 _$Failure { + @optionalTypeArgs + TResult when({ + required TResult Function() unknown, + required TResult Function() connectivity, + required TResult Function(DioError error) dio, + required TResult Function() invalidCredentials, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unknown, + TResult? Function()? connectivity, + TResult? Function(DioError error)? dio, + TResult? Function()? invalidCredentials, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unknown, + TResult Function()? connectivity, + TResult Function(DioError error)? dio, + TResult Function()? invalidCredentials, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult map({ + required TResult Function(_UnknownFailure value) unknown, + required TResult Function(ConnectivityFailure value) connectivity, + required TResult Function(DioFailure value) dio, + required TResult Function(InvalidCredentialsFailure value) invalidCredentials, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_UnknownFailure value)? unknown, + TResult? Function(ConnectivityFailure value)? connectivity, + TResult? Function(DioFailure value)? dio, + TResult? Function(InvalidCredentialsFailure value)? invalidCredentials, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_UnknownFailure value)? unknown, + TResult Function(ConnectivityFailure value)? connectivity, + TResult Function(DioFailure value)? dio, + TResult Function(InvalidCredentialsFailure value)? invalidCredentials, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FailureCopyWith<$Res> { + factory $FailureCopyWith(Failure value, $Res Function(Failure) then) = _$FailureCopyWithImpl<$Res, Failure>; +} + +/// @nodoc +class _$FailureCopyWithImpl<$Res, $Val extends Failure> implements $FailureCopyWith<$Res> { + _$FailureCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$UnknownFailureImplCopyWith<$Res> { + factory _$$UnknownFailureImplCopyWith(_$UnknownFailureImpl value, $Res Function(_$UnknownFailureImpl) then) = + __$$UnknownFailureImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$UnknownFailureImplCopyWithImpl<$Res> extends _$FailureCopyWithImpl<$Res, _$UnknownFailureImpl> + implements _$$UnknownFailureImplCopyWith<$Res> { + __$$UnknownFailureImplCopyWithImpl(_$UnknownFailureImpl _value, $Res Function(_$UnknownFailureImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$UnknownFailureImpl extends _UnknownFailure { + const _$UnknownFailureImpl() : super._(); + + @override + String toString() { + return 'Failure.unknown()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$UnknownFailureImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unknown, + required TResult Function() connectivity, + required TResult Function(DioError error) dio, + required TResult Function() invalidCredentials, + }) { + return unknown(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unknown, + TResult? Function()? connectivity, + TResult? Function(DioError error)? dio, + TResult? Function()? invalidCredentials, + }) { + return unknown?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unknown, + TResult Function()? connectivity, + TResult Function(DioError error)? dio, + TResult Function()? invalidCredentials, + required TResult orElse(), + }) { + if (unknown != null) { + return unknown(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_UnknownFailure value) unknown, + required TResult Function(ConnectivityFailure value) connectivity, + required TResult Function(DioFailure value) dio, + required TResult Function(InvalidCredentialsFailure value) invalidCredentials, + }) { + return unknown(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_UnknownFailure value)? unknown, + TResult? Function(ConnectivityFailure value)? connectivity, + TResult? Function(DioFailure value)? dio, + TResult? Function(InvalidCredentialsFailure value)? invalidCredentials, + }) { + return unknown?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_UnknownFailure value)? unknown, + TResult Function(ConnectivityFailure value)? connectivity, + TResult Function(DioFailure value)? dio, + TResult Function(InvalidCredentialsFailure value)? invalidCredentials, + required TResult orElse(), + }) { + if (unknown != null) { + return unknown(this); + } + return orElse(); + } +} + +abstract class _UnknownFailure extends Failure { + const factory _UnknownFailure() = _$UnknownFailureImpl; + const _UnknownFailure._() : super._(); +} + +/// @nodoc +abstract class _$$ConnectivityFailureImplCopyWith<$Res> { + factory _$$ConnectivityFailureImplCopyWith( + _$ConnectivityFailureImpl value, $Res Function(_$ConnectivityFailureImpl) then) = + __$$ConnectivityFailureImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$ConnectivityFailureImplCopyWithImpl<$Res> extends _$FailureCopyWithImpl<$Res, _$ConnectivityFailureImpl> + implements _$$ConnectivityFailureImplCopyWith<$Res> { + __$$ConnectivityFailureImplCopyWithImpl( + _$ConnectivityFailureImpl _value, $Res Function(_$ConnectivityFailureImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$ConnectivityFailureImpl extends ConnectivityFailure { + const _$ConnectivityFailureImpl() : super._(); + + @override + String toString() { + return 'Failure.connectivity()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$ConnectivityFailureImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unknown, + required TResult Function() connectivity, + required TResult Function(DioError error) dio, + required TResult Function() invalidCredentials, + }) { + return connectivity(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unknown, + TResult? Function()? connectivity, + TResult? Function(DioError error)? dio, + TResult? Function()? invalidCredentials, + }) { + return connectivity?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unknown, + TResult Function()? connectivity, + TResult Function(DioError error)? dio, + TResult Function()? invalidCredentials, + required TResult orElse(), + }) { + if (connectivity != null) { + return connectivity(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_UnknownFailure value) unknown, + required TResult Function(ConnectivityFailure value) connectivity, + required TResult Function(DioFailure value) dio, + required TResult Function(InvalidCredentialsFailure value) invalidCredentials, + }) { + return connectivity(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_UnknownFailure value)? unknown, + TResult? Function(ConnectivityFailure value)? connectivity, + TResult? Function(DioFailure value)? dio, + TResult? Function(InvalidCredentialsFailure value)? invalidCredentials, + }) { + return connectivity?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_UnknownFailure value)? unknown, + TResult Function(ConnectivityFailure value)? connectivity, + TResult Function(DioFailure value)? dio, + TResult Function(InvalidCredentialsFailure value)? invalidCredentials, + required TResult orElse(), + }) { + if (connectivity != null) { + return connectivity(this); + } + return orElse(); + } +} + +abstract class ConnectivityFailure extends Failure { + const factory ConnectivityFailure() = _$ConnectivityFailureImpl; + const ConnectivityFailure._() : super._(); +} + +/// @nodoc +abstract class _$$DioFailureImplCopyWith<$Res> { + factory _$$DioFailureImplCopyWith(_$DioFailureImpl value, $Res Function(_$DioFailureImpl) then) = + __$$DioFailureImplCopyWithImpl<$Res>; + @useResult + $Res call({DioError error}); +} + +/// @nodoc +class __$$DioFailureImplCopyWithImpl<$Res> extends _$FailureCopyWithImpl<$Res, _$DioFailureImpl> + implements _$$DioFailureImplCopyWith<$Res> { + __$$DioFailureImplCopyWithImpl(_$DioFailureImpl _value, $Res Function(_$DioFailureImpl) _then) : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? error = null, + }) { + return _then(_$DioFailureImpl( + null == error + ? _value.error + : error // ignore: cast_nullable_to_non_nullable + as DioError, + )); + } +} + +/// @nodoc + +class _$DioFailureImpl extends DioFailure { + const _$DioFailureImpl(this.error) : super._(); + + @override + final DioError error; + + @override + String toString() { + return 'Failure.dio(error: $error)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DioFailureImpl && + (identical(other.error, error) || other.error == error)); + } + + @override + int get hashCode => Object.hash(runtimeType, error); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$DioFailureImplCopyWith<_$DioFailureImpl> get copyWith => + __$$DioFailureImplCopyWithImpl<_$DioFailureImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unknown, + required TResult Function() connectivity, + required TResult Function(DioError error) dio, + required TResult Function() invalidCredentials, + }) { + return dio(error); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unknown, + TResult? Function()? connectivity, + TResult? Function(DioError error)? dio, + TResult? Function()? invalidCredentials, + }) { + return dio?.call(error); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unknown, + TResult Function()? connectivity, + TResult Function(DioError error)? dio, + TResult Function()? invalidCredentials, + required TResult orElse(), + }) { + if (dio != null) { + return dio(error); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_UnknownFailure value) unknown, + required TResult Function(ConnectivityFailure value) connectivity, + required TResult Function(DioFailure value) dio, + required TResult Function(InvalidCredentialsFailure value) invalidCredentials, + }) { + return dio(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_UnknownFailure value)? unknown, + TResult? Function(ConnectivityFailure value)? connectivity, + TResult? Function(DioFailure value)? dio, + TResult? Function(InvalidCredentialsFailure value)? invalidCredentials, + }) { + return dio?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_UnknownFailure value)? unknown, + TResult Function(ConnectivityFailure value)? connectivity, + TResult Function(DioFailure value)? dio, + TResult Function(InvalidCredentialsFailure value)? invalidCredentials, + required TResult orElse(), + }) { + if (dio != null) { + return dio(this); + } + return orElse(); + } +} + +abstract class DioFailure extends Failure { + const factory DioFailure(final DioError error) = _$DioFailureImpl; + const DioFailure._() : super._(); + + DioError get error; + @JsonKey(ignore: true) + _$$DioFailureImplCopyWith<_$DioFailureImpl> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$InvalidCredentialsFailureImplCopyWith<$Res> { + factory _$$InvalidCredentialsFailureImplCopyWith( + _$InvalidCredentialsFailureImpl value, $Res Function(_$InvalidCredentialsFailureImpl) then) = + __$$InvalidCredentialsFailureImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$InvalidCredentialsFailureImplCopyWithImpl<$Res> + extends _$FailureCopyWithImpl<$Res, _$InvalidCredentialsFailureImpl> + implements _$$InvalidCredentialsFailureImplCopyWith<$Res> { + __$$InvalidCredentialsFailureImplCopyWithImpl( + _$InvalidCredentialsFailureImpl _value, $Res Function(_$InvalidCredentialsFailureImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$InvalidCredentialsFailureImpl extends InvalidCredentialsFailure { + const _$InvalidCredentialsFailureImpl() : super._(); + + @override + String toString() { + return 'Failure.invalidCredentials()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$InvalidCredentialsFailureImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() unknown, + required TResult Function() connectivity, + required TResult Function(DioError error) dio, + required TResult Function() invalidCredentials, + }) { + return invalidCredentials(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? unknown, + TResult? Function()? connectivity, + TResult? Function(DioError error)? dio, + TResult? Function()? invalidCredentials, + }) { + return invalidCredentials?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? unknown, + TResult Function()? connectivity, + TResult Function(DioError error)? dio, + TResult Function()? invalidCredentials, + required TResult orElse(), + }) { + if (invalidCredentials != null) { + return invalidCredentials(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_UnknownFailure value) unknown, + required TResult Function(ConnectivityFailure value) connectivity, + required TResult Function(DioFailure value) dio, + required TResult Function(InvalidCredentialsFailure value) invalidCredentials, + }) { + return invalidCredentials(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_UnknownFailure value)? unknown, + TResult? Function(ConnectivityFailure value)? connectivity, + TResult? Function(DioFailure value)? dio, + TResult? Function(InvalidCredentialsFailure value)? invalidCredentials, + }) { + return invalidCredentials?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_UnknownFailure value)? unknown, + TResult Function(ConnectivityFailure value)? connectivity, + TResult Function(DioFailure value)? dio, + TResult Function(InvalidCredentialsFailure value)? invalidCredentials, + required TResult orElse(), + }) { + if (invalidCredentials != null) { + return invalidCredentials(this); + } + return orElse(); + } +} + +abstract class InvalidCredentialsFailure extends Failure { + const factory InvalidCredentialsFailure() = _$InvalidCredentialsFailureImpl; + const InvalidCredentialsFailure._() : super._(); +} diff --git a/lib/core/extensions/album_preferences_extension.dart b/lib/core/extensions/album_preferences_extension.dart new file mode 100644 index 0000000..ec6c519 --- /dev/null +++ b/lib/core/extensions/album_preferences_extension.dart @@ -0,0 +1,22 @@ +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/injector/injector.dart'; +import 'package:piwigo_ng/core/utils/constants/local_key_constants.dart'; +import 'package:piwigo_ng/core/utils/constants/settings_constants.dart'; +import 'package:piwigo_ng/features/images/data/enums/image_size_enum.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +extension AlbumPreferencesExtension on PreferencesDatasource { + SharedPreferences get _prefs => serviceLocator(); + + ImageSizeEnum get getAlbumThumbnailSize => ImageSizeEnum.fromJson( + _prefs.getString(LocalKeyConstants.albumThumbnailSizeKey) ?? SettingsConstants.defaultAlbumThumbnailSize.value, + ); + + ImageSizeEnum get getImageThumbnailSize => ImageSizeEnum.fromJson( + _prefs.getString(LocalKeyConstants.imageThumbnailSizeKey) ?? SettingsConstants.defaultImageThumbnailSize.value, + ); + + bool get getShowThumbnailTitle { + return _prefs.getBool(LocalKeyConstants.showThumbnailTitleKey) ?? SettingsConstants.defaultShowThumbnailTitle; + } +} diff --git a/lib/core/extensions/api_failure_mapper_extension.dart b/lib/core/extensions/api_failure_mapper_extension.dart new file mode 100644 index 0000000..6f6e319 --- /dev/null +++ b/lib/core/extensions/api_failure_mapper_extension.dart @@ -0,0 +1,16 @@ +import 'package:piwigo_ng/core/data/models/api_failure_model.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/utils/constants/api_errors.dart'; + +extension ApiFailureMapper on Failure { + static Failure fromApiFailure(ApiFailureModel failure) { + switch (failure.code) { + case ApiErrors.invalidCredentialsCode: + return const Failure.invalidCredentials(); + case ApiErrors.missingUsernameCode: + return const Failure.invalidCredentials(); + default: + return const Failure.unknown(); + } + } +} diff --git a/lib/core/extensions/api_preferences_extension.dart b/lib/core/extensions/api_preferences_extension.dart new file mode 100644 index 0000000..02f3005 --- /dev/null +++ b/lib/core/extensions/api_preferences_extension.dart @@ -0,0 +1,18 @@ +import 'dart:convert'; + +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/utils/constants/api_constants.dart'; +import 'package:piwigo_ng/core/utils/constants/local_key_constants.dart'; + +extension ApiPreferencesExtension on PreferencesDatasource { + String? get apiBaseUrl => instance.getString(LocalKeyConstants.serverUrlKey); + + bool get isBasicAuthEnabled => instance.getBool(LocalKeyConstants.enableBasicAuthKey) ?? false; + + String? get apiBasicHeader { + if (!isBasicAuthEnabled) return null; + String username = instance.getString(LocalKeyConstants.basicUsernameKey) ?? ''; + String password = instance.getString(LocalKeyConstants.basicPasswordKey) ?? ''; + return '${ApiConstants.basicPrefix} ${base64.encode(utf8.encode('$username:$password'))}'; + } +} diff --git a/lib/core/extensions/build_context_extension.dart b/lib/core/extensions/build_context_extension.dart new file mode 100644 index 0000000..7216dc0 --- /dev/null +++ b/lib/core/extensions/build_context_extension.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +extension BuildContextExtension on BuildContext { + NavigatorState get navigator => Navigator.of(this); + + AppLocalizations get localizations => AppLocalizations.of(this)!; + + Size screenSize({bool safeArea = false}) { + Size size = MediaQuery.of(this).size; + if (!safeArea) return size; + EdgeInsets safeAreaPadding = screenPadding; + return Size( + size.width - safeAreaPadding.horizontal, + size.height - safeAreaPadding.vertical, + ); + } + + EdgeInsets get screenPadding => MediaQuery.of(this).viewPadding; + + //region Themes + ThemeData get theme => Theme.of(this); + + TextTheme get textStyles => Theme.of(this).textTheme; +//endregion +} diff --git a/lib/core/extensions/string_extension.dart b/lib/core/extensions/string_extension.dart new file mode 100644 index 0000000..49ec465 --- /dev/null +++ b/lib/core/extensions/string_extension.dart @@ -0,0 +1,3 @@ +extension StringExtension on String { + String capitalize() => '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; +} diff --git a/lib/core/extensions/theme_mode_extension.dart b/lib/core/extensions/theme_mode_extension.dart new file mode 100644 index 0000000..c014959 --- /dev/null +++ b/lib/core/extensions/theme_mode_extension.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; + +extension ThemeModeExtension on ThemeMode { + String toJson() { + switch (this) { + case ThemeMode.system: + return 'system'; + case ThemeMode.light: + return 'light'; + case ThemeMode.dark: + return 'dark'; + } + } +} + +ThemeMode themeModeFromJson(String? json) { + switch (json) { + case 'system': + return ThemeMode.system; + case 'light': + return ThemeMode.light; + case 'dark': + return ThemeMode.dark; + default: + return ThemeMode.system; + } +} diff --git a/lib/core/injector/injector.dart b/lib/core/injector/injector.dart new file mode 100644 index 0000000..5cb693c --- /dev/null +++ b/lib/core/injector/injector.dart @@ -0,0 +1,21 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:get_it/get_it.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:piwigo_ng/core/data/datasources/remote/api_client.dart'; +import 'package:piwigo_ng/core/network/network_info.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final GetIt serviceLocator = GetIt.instance; + +Future init() async { + // Core + serviceLocator + ..registerLazySingleton(() => NetworkInfoImpl(serviceLocator())) + ..registerLazySingleton(() => Connectivity()) + ..registerLazySingleton(() => ApiClient()) + ..registerLazySingletonAsync(() => SharedPreferences.getInstance()) + ..registerLazySingletonAsync(() => PackageInfo.fromPlatform()); + + await serviceLocator.isReady(); + await serviceLocator.isReady(); +} diff --git a/lib/core/network/network_info.dart b/lib/core/network/network_info.dart new file mode 100644 index 0000000..37b20f4 --- /dev/null +++ b/lib/core/network/network_info.dart @@ -0,0 +1,17 @@ +import 'package:connectivity_plus/connectivity_plus.dart'; + +abstract class NetworkInfo { + Future get isConnected; +} + +class NetworkInfoImpl implements NetworkInfo { + NetworkInfoImpl(this.connectivity); + + final Connectivity connectivity; + + @override + Future get isConnected async { + final ConnectivityResult connectivityResult = await connectivity.checkConnectivity(); + return connectivityResult != ConnectivityResult.none; + } +} diff --git a/lib/core/presentation/widgets/animated/collapsible_widget.dart b/lib/core/presentation/widgets/animated/collapsible_widget.dart new file mode 100644 index 0000000..e1197be --- /dev/null +++ b/lib/core/presentation/widgets/animated/collapsible_widget.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; + +class CollapsibleWidget extends StatelessWidget { + const CollapsibleWidget({ + super.key, + required this.child, + this.expanded = true, + this.alignment = Alignment.topCenter, + }); + + final bool expanded; + final AlignmentGeometry alignment; + final Widget child; + + @override + Widget build(BuildContext context) { + return AnimatedSize( + duration: UIConstants.animationDurationShort, + curve: Curves.ease, + alignment: Alignment.topCenter, + child: Builder( + builder: (BuildContext context) { + if (expanded) return child; + return SizedBox.fromSize( + size: const Size.fromHeight(0.0), + ); + }, + ), + ); + } +} diff --git a/lib/core/presentation/widgets/app_bars/expanded_app_bar.dart b/lib/core/presentation/widgets/app_bars/expanded_app_bar.dart new file mode 100644 index 0000000..095b11d --- /dev/null +++ b/lib/core/presentation/widgets/app_bars/expanded_app_bar.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; + +class ExpandedAppBar extends AnimatedWidget { + const ExpandedAppBar({ + super.key, + required this.scrollController, + required this.title, + this.leading, + this.actions, + this.child, + }) : super(listenable: scrollController); + + final ScrollController scrollController; + final String title; + final Widget? child; + final Widget? leading; + final List? actions; + + static const double _expandedHeight = kToolbarHeight * 2; + static const double _opacityScale = 0.3; + + double get _titleOpacity { + if (scrollController.hasClients) { + if (scrollController.offset > _expandedHeight * _opacityScale) { + return 0.0; + } + return (_expandedHeight * _opacityScale - scrollController.offset) / (_expandedHeight * _opacityScale); + } + return 1.0; + } + + double get _horizontalTitlePadding { + double basePadding = UIConstants.paddingMedium; + double delta = (kToolbarHeight - basePadding) / basePadding; + + if (scrollController.hasClients) { + if (scrollController.offset > (_expandedHeight - kToolbarHeight)) { + // In case 0% of the expanded height is viewed + return basePadding * delta + basePadding; + } + + // In case 0%-100% of the expanded height is viewed + double scrollDelta = (_expandedHeight - scrollController.offset) / _expandedHeight; + double scrollPercent = (scrollDelta * 2 - 1); + return (1 - scrollPercent) * delta * basePadding + basePadding; + } + + return basePadding; + } + + @override + Widget build(BuildContext context) { + return SliverAppBar( + leading: leading, + pinned: true, + centerTitle: true, + titleSpacing: 0.0, + title: Opacity( + opacity: _titleOpacity, + child: child, + ), + actions: actions, + expandedHeight: _expandedHeight, + flexibleSpace: FlexibleSpaceBar( + titlePadding: EdgeInsets.symmetric( + horizontal: _horizontalTitlePadding, + vertical: UIConstants.paddingMedium, + ), + title: Text( + title, + textScaleFactor: 1, + style: Theme.of(context).appBarTheme.titleTextStyle, + ), + ), + ); + } +} diff --git a/lib/core/presentation/widgets/buttons/custom_button.dart b/lib/core/presentation/widgets/buttons/custom_button.dart new file mode 100644 index 0000000..8155e5f --- /dev/null +++ b/lib/core/presentation/widgets/buttons/custom_button.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/utils/app_text_styles.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; + +class CustomButton extends StatelessWidget { + const CustomButton({ + super.key, + this.onTap, + required this.text, + this.isLoading = false, + this.backgroundColor, + this.foregroundColor, + }); + + final void Function()? onTap; + final Color? backgroundColor; + final Color? foregroundColor; + final bool isLoading; + final String text; + + @override + Widget build(BuildContext context) => ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingMedium, + vertical: UIConstants.paddingXSmall, + ), + minimumSize: const Size.square(UIConstants.buttonHeight), + maximumSize: const Size.fromHeight(UIConstants.buttonHeight), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + ), + backgroundColor: backgroundColor ?? context.theme.colorScheme.secondary, + foregroundColor: foregroundColor ?? context.theme.colorScheme.primary, + ), + child: AnimatedSwitcher( + duration: UIConstants.animationDurationShort, + switchInCurve: Curves.ease, + switchOutCurve: Curves.ease, + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition( + scale: animation, + child: FadeTransition( + opacity: animation, + child: child, + ), + ); + }, + child: Builder( + key: ValueKey(isLoading), + builder: (BuildContext context) { + if (isLoading) { + return AspectRatio( + aspectRatio: 1, + child: CircularProgressIndicator( + color: foregroundColor ?? context.theme.colorScheme.primary, + ), + ); + } + return Text( + text, + style: AppTextStyles.button, + ); + }, + ), + ), + ); +} diff --git a/lib/core/presentation/widgets/buttons/custom_popup_menu_button.dart b/lib/core/presentation/widgets/buttons/custom_popup_menu_button.dart new file mode 100644 index 0000000..313d047 --- /dev/null +++ b/lib/core/presentation/widgets/buttons/custom_popup_menu_button.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; + +class CustomPopupMenuButton extends StatelessWidget { + const CustomPopupMenuButton({ + super.key, + required this.items, + }); + + final List items; + + bool get _hasNotification => items.where((CustomPopupMenuItem item) => item.notification).isNotEmpty; + + @override + Widget build(BuildContext context) => PopupMenuButton( + position: PopupMenuPosition.under, + icon: Badge( + isLabelVisible: _hasNotification, + child: const Icon(Icons.more_vert), + ), + itemBuilder: (BuildContext context) => items + .where((CustomPopupMenuItem items) => items.available) + .map>( + (CustomPopupMenuItem item) => PopupMenuItem( + onTap: item.onTap, + child: Row( + children: [ + Badge( + isLabelVisible: item.notification, + child: Icon(item.icon), + ), + const SizedBox(width: UIConstants.paddingSmall), + Text( + item.label, + style: context.theme.textTheme.bodyMedium, + ), + ], + ), + ), + ) + .toList(), + ); +} + +class CustomPopupMenuItem { + const CustomPopupMenuItem({ + required this.label, + this.icon, + this.onTap, + this.available = true, + this.notification = false, + }); + + final Function()? onTap; + final String label; + final IconData? icon; + final bool available; + final bool notification; // todo: notification +} diff --git a/lib/core/presentation/widgets/buttons/custom_text_button.dart b/lib/core/presentation/widgets/buttons/custom_text_button.dart new file mode 100644 index 0000000..aa31a6a --- /dev/null +++ b/lib/core/presentation/widgets/buttons/custom_text_button.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/utils/app_text_styles.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; + +class CustomTextButton extends StatelessWidget { + const CustomTextButton({ + super.key, + this.onTap, + required this.text, + this.foregroundColor, + }); + + final void Function()? onTap; + final Color? foregroundColor; + final String text; + + @override + Widget build(BuildContext context) => Center( + child: TextButton( + style: TextButton.styleFrom( + foregroundColor: foregroundColor ?? context.theme.colorScheme.secondary, + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingXSmall, + vertical: UIConstants.paddingTiny, + ), + minimumSize: Size.zero, + ), + onPressed: onTap, + child: Text( + text, + style: AppTextStyles.textButton, + ), + ), + ); +} diff --git a/lib/core/presentation/widgets/form/custom_text_field.dart b/lib/core/presentation/widgets/form/custom_text_field.dart new file mode 100644 index 0000000..f696617 --- /dev/null +++ b/lib/core/presentation/widgets/form/custom_text_field.dart @@ -0,0 +1,184 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/core/utils/validators/field_validator.dart'; + +class CustomTextField extends StatefulWidget { + const CustomTextField({ + super.key, + this.controller, + this.prefix, + this.suffix, + this.focusNode, + this.textInputAction, + this.keyboardType, + this.hint, + this.label, + this.minLines, + this.maxLines = 1, + this.padding, + this.obscureText = false, + this.canErase = false, + this.color, + this.onFieldSubmitted, + this.onChanged, + this.autofillHints, + this.validators = const [], + }); + + final Widget? prefix; + final Widget? suffix; + final TextEditingController? controller; + final FocusNode? focusNode; + final TextInputAction? textInputAction; + final TextInputType? keyboardType; + final String? hint; + final String? label; + final int? minLines; + final int? maxLines; + final EdgeInsets? padding; + final bool obscureText; + final bool canErase; + final Color? color; + final Function(String)? onFieldSubmitted; + final Function(String)? onChanged; + final Iterable? autofillHints; + final List validators; + + @override + State createState() => _CustomTextFieldState(); +} + +class _CustomTextFieldState extends State { + @override + void initState() { + widget.controller?.addListener(_listener); + super.initState(); + } + + @override + void dispose() { + widget.controller?.removeListener(_listener); + super.dispose(); + } + + void _listener() => setState(() {}); + + EdgeInsets get _contentPadding => + widget.padding ?? + const EdgeInsets.symmetric( + vertical: UIConstants.paddingMedium, + horizontal: UIConstants.paddingXSmall, + ); + + String? _validator(String? value) { + for (FieldValidator validator in widget.validators) { + String? error = validator.validate(context, value); + if (error != null) return error; + } + return null; + } + + @override + Widget build(BuildContext context) => TextFormField( + controller: widget.controller, + focusNode: widget.focusNode, + onChanged: widget.onChanged, + onFieldSubmitted: widget.onFieldSubmitted, + textInputAction: widget.textInputAction, + keyboardType: widget.keyboardType, + obscureText: widget.obscureText, + minLines: widget.minLines, + maxLines: widget.maxLines, + autofillHints: widget.autofillHints, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + contentPadding: _contentPadding, + hintText: widget.hint, + labelText: widget.label, + alignLabelWithHint: true, + isDense: true, + prefixIconConstraints: const BoxConstraints(), + suffixIconConstraints: const BoxConstraints(), + prefixIcon: Padding( + padding: EdgeInsets.only( + left: UIConstants.paddingSmall, + right: _contentPadding.left, + ), + child: widget.prefix, + ), + suffixIcon: Padding( + padding: EdgeInsets.only( + right: UIConstants.paddingSmall, + left: _contentPadding.right, + ), + child: AnimatedSize( + duration: UIConstants.animationDurationShort, + curve: Curves.ease, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.canErase && (widget.controller?.text.isNotEmpty ?? false)) _buildClearTextButton(), + if (widget.suffix != null) ...[ + const SizedBox(width: UIConstants.paddingXSmall), + widget.suffix!, + ], + ], + ), + ), + ), + fillColor: widget.color ?? Theme.of(context).inputDecorationTheme.fillColor, + filled: true, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + borderSide: BorderSide.none, + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + borderSide: BorderSide.none, + ), + disabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + borderSide: BorderSide( + color: Theme.of(context).primaryColor, + width: UIConstants.thicknessMedium, + strokeAlign: BorderSide.strokeAlignOutside, + ), + ), + errorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + strokeAlign: BorderSide.strokeAlignOutside, + ), + ), + focusedErrorBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.error, + width: UIConstants.thicknessMedium, + strokeAlign: BorderSide.strokeAlignOutside, + ), + ), + errorStyle: TextStyle( + fontSize: 12, + fontStyle: FontStyle.italic, + color: context.theme.colorScheme.error, + ), + ), + validator: _validator, + ); + + Widget _buildClearTextButton() => GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => widget.controller?.clear(), + child: Icon( + Icons.clear, + color: Theme.of(context).colorScheme.secondary, + ), + ); +} diff --git a/lib/core/presentation/widgets/images/custom_network_image.dart b/lib/core/presentation/widgets/images/custom_network_image.dart new file mode 100644 index 0000000..748ef11 --- /dev/null +++ b/lib/core/presentation/widgets/images/custom_network_image.dart @@ -0,0 +1,151 @@ +import 'dart:io'; + +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/extensions/api_preferences_extension.dart'; +import 'package:piwigo_ng/network/api_client.dart'; +import 'package:piwigo_ng/services/preferences_service.dart'; +import 'package:piwigo_ng/utils/settings.dart'; + +class CustomNetworkImage extends StatefulWidget { + const CustomNetworkImage({ + super.key, + this.imageUrl, + this.fit, + }); + + final String? imageUrl; + final BoxFit? fit; + + @override + State createState() => _CustomNetworkImageState(); +} + +class _CustomNetworkImageState extends State with AppPreferencesMixin { + late final Future> _headers; + + @override + initState() { + super.initState(); + _headers = _getHeaders(); + } + + Future> _getHeaders() async { + String? serverUrl = prefs.instance.getString(Preferences.serverUrlKey); + + if (serverUrl == null) return {}; + + // Get server cookies + List cookies = await ApiClient.cookieJar.loadForRequest(Uri.parse(serverUrl)); + String cookiesStr = cookies.map((Cookie cookie) => '${cookie.name}=${cookie.value}').join('; '); + + // Get HTTP Basic id + String? basicAuth = prefs.apiBasicHeader; + + return { + HttpHeaders.cookieHeader: cookiesStr, + if (basicAuth != null) 'Authorization': basicAuth, + }; + } + + void _checkMemory() { + ImageCache imageCache = PaintingBinding.instance.imageCache; + if (imageCache.liveImageCount >= Settings.maxCacheLiveImages) { + imageCache + ..clear() + ..clearLiveImages(); + } + } + + @override + Widget build(BuildContext context) { + if (widget.imageUrl == null) { + return _buildNoImageWidget(context); + } + _checkMemory(); + return FutureBuilder>( + future: _headers, + builder: (BuildContext context, AsyncSnapshot> snapshot) { + if (snapshot.hasData) { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + double? cacheWidth = constraints.maxWidth.isInfinite ? constraints.maxWidth : null; + double? cacheHeight = constraints.maxHeight.isInfinite ? constraints.maxHeight : null; + return CachedNetworkImage( + imageUrl: widget.imageUrl!, + fadeInDuration: const Duration(milliseconds: 300), + fit: widget.fit ?? BoxFit.cover, + httpHeaders: snapshot.data!, + memCacheWidth: cacheWidth?.floor(), + memCacheHeight: cacheHeight?.floor(), + imageBuilder: (BuildContext context, ImageProvider provider) => Image( + image: provider, + fit: widget.fit ?? BoxFit.cover, + errorBuilder: (BuildContext context, Object o, StackTrace? s) { + debugPrint('$o\n$s'); + return _buildErrorWidget(context, widget.imageUrl, o); + }, + ), + progressIndicatorBuilder: _buildProgressIndicator, + errorWidget: _buildErrorWidget, + ); + }, + ); + } + if (snapshot.hasError) { + return _buildErrorWidget(context); + } + return const Center( + child: CircularProgressIndicator(), + ); + }, + ); + } + + Widget _buildProgressIndicator( + BuildContext context, + String url, + DownloadProgress download, + ) { + if (download.downloaded >= (download.totalSize ?? 0)) { + return const SizedBox(); + } + return Center( + child: CircularProgressIndicator( + value: download.progress, + ), + ); + } + + Widget _buildErrorWidget( + BuildContext context, [ + String? url, + dynamic error, + ]) { + debugPrint('[$url!] $error'); + return FittedBox( + fit: BoxFit.cover, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: const Icon(Icons.broken_image_outlined), + ), + ); + } + + Widget _buildNoImageWidget(BuildContext context) { + return FittedBox( + fit: BoxFit.cover, + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + ), + child: const Icon(Icons.image_not_supported), + ), + ); + } +} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart new file mode 100644 index 0000000..184ba44 --- /dev/null +++ b/lib/core/router/app_router.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/router/app_routes.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/presentation/pages/album_page.dart'; +import 'package:piwigo_ng/features/albums/presentation/pages/root_page.dart'; +import 'package:piwigo_ng/features/authentication/presentation/pages/login_page.dart'; +import 'package:piwigo_ng/features/authentication/presentation/pages/startup_page.dart'; +import 'package:piwigo_ng/features/images/presentation/pages/image_search_page.dart'; +import 'package:piwigo_ng/features/settings/presentation/pages/settings_page.dart'; + +part 'app_router.freezed.dart'; + +part 'page_route_arguments.dart'; + +class AppRouter { + const AppRouter(); + + Route generateRoute(RouteSettings settings) { + PageRouteArguments? route = settings.arguments as PageRouteArguments?; + + switch (settings.name) { + case AppRoutes.main: + return MaterialPageRoute(builder: (_) => const StartupPage()); + case AppRoutes.login: + return MaterialPageRoute(builder: (_) => const LoginPage()); + case AppRoutes.root: + return MaterialPageRoute(builder: (_) => const RootPage()); + case AppRoutes.settings: + return MaterialPageRoute(builder: (_) => const SettingsPage()); + case AppRoutes.searchImages: + return MaterialPageRoute(builder: (_) => const ImageSearchPage()); + case AppRoutes.album: + final _AlbumPageArgs args = route as _AlbumPageArgs; + return MaterialPageRoute(builder: (_) => AlbumPage(album: args.album)); + default: + return MaterialPageRoute( + builder: (_) => Scaffold( + body: Center( + child: Text("No route defined for ${settings.name}"), + ), + ), + ); + } + } +} diff --git a/lib/core/router/app_router.freezed.dart b/lib/core/router/app_router.freezed.dart new file mode 100644 index 0000000..8e158e1 --- /dev/null +++ b/lib/core/router/app_router.freezed.dart @@ -0,0 +1,144 @@ +// 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 'app_router.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 _$PageRouteArguments { + AlbumEntity get album => throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult when({ + required TResult Function(AlbumEntity album) album, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(AlbumEntity album)? album, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(AlbumEntity album)? album, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumPageArgs value) album, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumPageArgs value)? album, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumPageArgs value)? album, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc + +class _$AlbumPageArgsImpl implements _AlbumPageArgs { + const _$AlbumPageArgsImpl({required this.album}); + + @override + final AlbumEntity album; + + @override + String toString() { + return 'PageRouteArguments.album(album: $album)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumPageArgsImpl && + (identical(other.album, album) || other.album == album)); + } + + @override + int get hashCode => Object.hash(runtimeType, album); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(AlbumEntity album) album, + }) { + return album(this.album); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(AlbumEntity album)? album, + }) { + return album?.call(this.album); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(AlbumEntity album)? album, + required TResult orElse(), + }) { + if (album != null) { + return album(this.album); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumPageArgs value) album, + }) { + return album(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumPageArgs value)? album, + }) { + return album?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumPageArgs value)? album, + required TResult orElse(), + }) { + if (album != null) { + return album(this); + } + return orElse(); + } +} + +abstract class _AlbumPageArgs implements PageRouteArguments { + const factory _AlbumPageArgs({required final AlbumEntity album}) = _$AlbumPageArgsImpl; + + @override + AlbumEntity get album; +} diff --git a/lib/core/router/app_routes.dart b/lib/core/router/app_routes.dart new file mode 100644 index 0000000..56a97ca --- /dev/null +++ b/lib/core/router/app_routes.dart @@ -0,0 +1,8 @@ +class AppRoutes { + static const String main = '/'; + static const String login = '/login'; + static const String root = '/root'; + static const String settings = '/settings'; + static const String searchImages = '/images/search'; + static const String album = '/album'; +} diff --git a/lib/core/router/page_route_arguments.dart b/lib/core/router/page_route_arguments.dart new file mode 100644 index 0000000..b64c6c7 --- /dev/null +++ b/lib/core/router/page_route_arguments.dart @@ -0,0 +1,6 @@ +part of 'app_router.dart'; + +@Freezed(copyWith: false, fromJson: false, toJson: false) +class PageRouteArguments with _$PageRouteArguments { + const factory PageRouteArguments.album({required AlbumEntity album}) = _AlbumPageArgs; +} diff --git a/lib/core/utils/app_assets.dart b/lib/core/utils/app_assets.dart new file mode 100644 index 0000000..ce82fef --- /dev/null +++ b/lib/core/utils/app_assets.dart @@ -0,0 +1,5 @@ +class AppAssets { + static const String _logosPath = 'assets/logo'; + + static const String piwigoLogo = '$_logosPath/piwigo_logo.png'; +} diff --git a/lib/core/utils/app_colors.dart b/lib/core/utils/app_colors.dart new file mode 100644 index 0000000..8fcd0f8 --- /dev/null +++ b/lib/core/utils/app_colors.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; + +class AppColors { + static const Color accent = Color(0xFFFF7700); + static const Color white = Color(0xFFFFFFFF); + static const Color black = Color(0xFF000000); + static const Color error = Color(0xFFE02318); + static const Color success = Color(0xFF4CAF50); + static const Color disabled = Color(0xFF9E9E9E); + static const Color prefix = Color(0xFF393939); + static const Color textGrey = Color(0xFF282828); + static const Color transparent = Color(0x00000000); + + static const Color backgroundLight = Color(0xFFEEEEEE); + static const Color backgroundDark = Color(0xFF232323); + static const Color fieldLight = Color(0xFFE0E0E0); + static const Color fieldDark = Color(0xFF2D2D2D); + static const Color cardLight = Color(0xFFFFFFFF); + static const Color cardDark = Color(0xFF333333); + + static const Color grey = Color(0xFF4B4B4B); + static const Color lightGreen = Color(0xFFD6FFCF); + static const Color green = Color(0xFF6ECE5E); + static const Color lightOrange = Color(0xFFFFE9CF); + static const Color orange = Color(0xFFFFA744); + static const Color lightBlue = Color(0xFFCFEBFF); + static const Color blue = Color(0xFF2883C3); + static const Color lightPink = Color(0xFFFFCFCF); + static const Color pink = Color(0xFFFF5252); + + static const List foregroundColors = [ + AppColors.green, + AppColors.orange, + AppColors.blue, + AppColors.pink, + ]; +} diff --git a/lib/core/utils/app_regexp.dart b/lib/core/utils/app_regexp.dart new file mode 100644 index 0000000..89f806c --- /dev/null +++ b/lib/core/utils/app_regexp.dart @@ -0,0 +1,5 @@ +class AppRegexp { + static final RegExp urlCheck = RegExp( + r'[-a-zA-Z\d@:%._+~#=]{1,256}\.[a-zA-Z\d()]{1,256}\b([-a-zA-Z\d()@:%_+.~#?&/=]*)', + ); +} diff --git a/lib/core/utils/app_strings.dart b/lib/core/utils/app_strings.dart new file mode 100644 index 0000000..9f27768 --- /dev/null +++ b/lib/core/utils/app_strings.dart @@ -0,0 +1,11 @@ +class AppStrings { + static const String appName = 'Piwigo'; + + static const String piwigoUrlSample = 'example.piwigo.com'; + + static const String https = 'https'; + + static const String http = 'http'; + + static const String hostSeparator = '://'; +} diff --git a/lib/core/utils/app_text_styles.dart b/lib/core/utils/app_text_styles.dart new file mode 100644 index 0000000..0cf105c --- /dev/null +++ b/lib/core/utils/app_text_styles.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class AppTextStyles { + static const TextStyle button = TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.w500, + ); + + static const TextStyle textButton = TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.w500, + ); +} diff --git a/lib/core/utils/constants/api_constants.dart b/lib/core/utils/constants/api_constants.dart new file mode 100644 index 0000000..05db8aa --- /dev/null +++ b/lib/core/utils/constants/api_constants.dart @@ -0,0 +1,182 @@ +class ApiConstants { + static const String webServiceUrl = '/ws.php'; + static const String basicPrefix = 'Basic'; + + //region API Methods + + //region pcom + static const String imageFileConvertedMethod = 'pcom.images.fileConverted'; + + //endregion + + //region Activity + static const String downloadActivityLogMethod = 'pwg.activity.downloadLog'; + static const String getActivityListMethod = 'pwg.activity.getList'; + + //endregion + + //region Caddie + static const String addCaddieMethod = 'pwg.caddie.add'; + + //endregion + + //region Albums + static const String getAlbumsMethod = 'pwg.categories.getList'; + static const String getAdminAlbumsMethod = 'pwg.categories.getAdminList'; + static const String getAlbumImagesMethod = 'pwg.categories.getImageList'; + + static const String addAlbumMethod = 'pwg.categories.add'; + static const String editAlbumMethod = 'pwg.categories.setInfo'; + static const String moveAlbumMethod = 'pwg.categories.move'; + static const String deleteAlbumMethod = 'pwg.categories.delete'; + static const String setAlbumRankMethod = 'pwg.categories.setRank'; + static const String calculateAlbumOrphansMethod = 'pwg.categories.calculateOrphans'; + + static const String setRepresentativeMethod = 'pwg.categories.setRepresentative'; + static const String deleteRepresentativeMethod = 'pwg.categories.deleteRepresentative'; + static const String refreshRepresentativeMethod = 'pwg.categories.refreshRepresentative'; + + //endregion + + //region Extensions + static const String checkUpdatesMethod = 'pwg.extensions.checkUpdates'; + static const String ignoreUpdateMethod = 'pwg.extensions.ignoreUpdate'; + static const String updateMethod = 'pwg.extensions.update'; + + //endregion + + //region Groups + static const String getGroupsMethod = 'pwg.groups.getList'; + + static const String addGroupMethod = 'pwg.groups.add'; + static const String editGroupMethod = 'pwg.groups.setInfo'; + static const String deleteGroupMethod = 'pwg.groups.delete'; + static const String duplicateGroupMethod = 'pwg.groups.duplicate'; + static const String mergeGroupsMethod = 'pwg.groups.merge'; + + static const String addGroupUserMethod = 'pwg.groups.addUser'; + static const String deleteGroupUserMethod = 'pwg.groups.deleteUser'; + + //endregion + + //region History + static const String getHistoryMethod = 'pwg.history.log'; + static const String searchHistoryMethod = 'pwg.history.search'; + + //endregion + + //region Images + static const String addImageMethod = 'pwg.images.add'; + static const String addImageChunkMethod = 'pwg.images.addChunk'; + static const String addImageCommentMethod = 'pwg.images.addComment'; + static const String addFileMethod = 'pwg.images.addFile'; + static const String addSimpleMethod = 'pwg.images.addSimple'; + + static const String checkFilesMethod = 'pwg.images.checkFiles'; + static const String checkUploadMethod = 'pwg.images.checkUpload'; + + static const String deleteImageMethod = 'pwg.images.delete'; + static const String deleteOrphansMethod = 'pwg.images.deleteOrphans'; + static const String imageExistMethod = 'pwg.images.exist'; + static const String getImageMethod = 'pwg.images.getInfo'; + static const String rateImageMethod = 'pwg.images.rate'; + static const String searchImagesMethod = 'pwg.images.search'; + static const String editImageAlbumsMethod = 'pwg.images.setCategory'; + static const String editImageMethod = 'pwg.images.setInfo'; + static const String setImageMd5sumMethod = 'pwg.images.setMd5sum'; + static const String setImagePrivacyMethod = 'pwg.images.setPrivacyLevel'; + static const String setImageRankMethod = 'pwg.images.setRank'; + static const String syncImageMetadataMethod = 'pwg.images.syncMetadata'; + + static const String uploadImageMethod = 'pwg.images.upload'; + static const String uploadImageAsyncMethod = 'pwg.images.uploadAsync'; + static const String uploadCompletedMethod = 'pwg.images.uploadCompleted'; + static const String emptyLoungeMethod = 'pwg.images.emptyLounge'; + + // Filtered search + static const String filteredImageSearchMethod = 'pwg.images.filteredSearch.create'; + + // Format + static const String deleteImageFormatMethod = 'pwg.images.format.delete'; + static const String searchImageFormatMethod = 'pwg.images.format.searchImage'; + + //endregion + + //region Permissions + static const String addPermissionMethod = 'pwg.permissions.add'; + static const String getPermissionsMethod = 'pwg.permissions.getList'; + static const String revokePermissionMethod = 'pwg.permissions.remove'; + + //endregion + + //region Plugins + static const String getPluginsMethod = 'pwg.plugins.getList'; + static const String performPluginActionMethod = 'pwg.plugins.performAction'; + + //endregion + + //region Rates + static const String deleteUserRatesMethod = 'pwg.rates.delete'; + + //endregion + + //region Session + static const String loginMethod = 'pwg.session.login'; + static const String logoutMethod = 'pwg.session.logout'; + static const String getStatusMethod = 'pwg.session.getStatus'; + + //endregion + + //region Tags + static const String getTagsMethod = 'pwg.tags.getList'; + static const String getAdminTagsMethod = 'pwg.tags.getAdminList'; + + static const String addTagMethod = 'pwg.tags.add'; + static const String renameTagMethod = 'pwg.tags.rename'; + static const String deleteTagMethod = 'pwg.tags.delete'; + static const String duplicateTagMethod = 'pwg.tags.duplicate'; + static const String mergeTagsMethod = 'pwg.tags.merge'; + + static const String getTagImagesMethod = 'pwg.tags.getImages'; + +//endregion + + //region Themes + static const String performThemeActionMethod = 'pwg.themes.performAction'; + + //endregion + + //region Users + static const String getUsersMethod = 'pwg.users.getList'; + + static const String addUserMethod = 'pwg.users.add'; + static const String editUserMethod = 'pwg.users.setInfo'; + static const String deleteUserMethod = 'pwg.users.delete'; + static const String getUserAuthKeyMethod = 'pwg.users.getAuthKey'; + + static const String setUserPreferencesMethod = 'pwg.users.preferences.set'; + + //endregion + + //region Favorites + static const String getFavoritesMethod = 'pwg.users.favorites.getList'; + static const String addFavoriteMethod = 'pwg.users.favorites.add'; + static const String removeFavoriteMethod = 'pwg.users.favorites.remove'; + + //endregion + + //region Others + static const String getCacheSizeMethod = 'pwg.getCacheSize'; + static const String getPiwigoInfoMethod = 'pwg.getInfos'; + static const String getMissingDerivativesMethod = 'pwg.getMissingDerivatives'; + static const String getPiwigoVersionMethod = 'pwg.getVersion'; + + //endregion + + //region Reflexion + static const String getMethodDetailsMethod = 'reflection.getMethodDetails'; + static const String getMethodsMethod = 'reflection.getMethodList'; +//endregion + +//endregion +} diff --git a/lib/core/utils/constants/api_errors.dart b/lib/core/utils/constants/api_errors.dart new file mode 100644 index 0000000..981b8f7 --- /dev/null +++ b/lib/core/utils/constants/api_errors.dart @@ -0,0 +1,9 @@ +class ApiErrors { + //region Authentication + static const String invalidCredentials = 'Invalid username/password'; + static const int invalidCredentialsCode = 999; + + static const String missingUsername = 'Missing parameters: username'; + static const int missingUsernameCode = 1002; +//endregion +} diff --git a/lib/core/utils/constants/hero_tags.dart b/lib/core/utils/constants/hero_tags.dart new file mode 100644 index 0000000..1e593e8 --- /dev/null +++ b/lib/core/utils/constants/hero_tags.dart @@ -0,0 +1,5 @@ +class HeroTags { + static const String imageSearchField = ''; + + static String albumBackground(int albumId) => ''; +} diff --git a/lib/core/utils/constants/local_key_constants.dart b/lib/core/utils/constants/local_key_constants.dart new file mode 100644 index 0000000..93b9149 --- /dev/null +++ b/lib/core/utils/constants/local_key_constants.dart @@ -0,0 +1,30 @@ +class LocalKeyConstants { + //region Authentication + static const String serverUrlKey = 'SERVER_URL'; + static const String usernameKey = 'SERVER_USERNAME'; + static const String passwordKey = 'SERVER_PASSWORD'; + + static const String tokenKey = 'PWG_TOKEN'; + + // SSL + static const String enableSSLKey = 'ENABLE_SSL'; + + // Basic + static const String enableBasicAuthKey = 'ENABLE_BASIC_AUTH'; + static const String basicUsernameKey = 'BASIC_USERNAME'; + static const String basicPasswordKey = 'BASIC_PASSWORD'; + + //endregion + + static const String themeKey = 'THEME'; + + //region Album + static const String albumThumbnailSizeKey = 'ALBUM_THUMBNAIL_SIZE'; + +//endregion + +//region Image + static const String showThumbnailTitleKey = 'SHOW_THUMBNAIL_TITLE'; + static const String imageThumbnailSizeKey = 'IMAGE_THUMBNAIL_SIZE'; +//endregion +} diff --git a/lib/core/utils/constants/settings_constants.dart b/lib/core/utils/constants/settings_constants.dart new file mode 100644 index 0000000..2ee301b --- /dev/null +++ b/lib/core/utils/constants/settings_constants.dart @@ -0,0 +1,20 @@ +import 'package:piwigo_ng/core/enum/sort_method_enum.dart'; +import 'package:piwigo_ng/features/images/data/enums/image_size_enum.dart'; + +class SettingsConstants { + //region Albums + static const ImageSizeEnum defaultAlbumThumbnailSize = ImageSizeEnum.thumb; + + //endregion + + //region Images + static const ImageSizeEnum defaultImageThumbnailSize = ImageSizeEnum.thumb; + + static const SortMethodEnum defaultImageSort = SortMethodEnum.custom; + + static const bool defaultShowThumbnailTitle = false; + + //endregion + + static const int defaultElementPerPage = 100; +} diff --git a/lib/core/utils/constants/ui_constants.dart b/lib/core/utils/constants/ui_constants.dart new file mode 100644 index 0000000..e91eea7 --- /dev/null +++ b/lib/core/utils/constants/ui_constants.dart @@ -0,0 +1,29 @@ +class UIConstants { + static const double buttonHeight = 48.0; + + static const double paddingXXLarge = 64.0; + static const double paddingXLarge = 32.0; + static const double paddingLarge = 24.0; + static const double paddingMedium = 16.0; + static const double paddingSmall = 12.0; + static const double paddingXSmall = 8.0; + static const double paddingTiny = 4.0; + + static const double radiusXXLarge = 30.0; + static const double radiusXLarge = 20.0; + static const double radiusLarge = 15.0; + static const double radiusMedium = 10.0; + static const double radiusSmall = 5.0; + + static const double thicknessLarge = 2.0; + static const double thicknessMedium = 1.5; + static const double thicknessSmall = 1.0; + static const double thicknessXSmall = .5; + + static const double elevationLarge = 7.0; + static const double elevationMedium = 5.0; + + static const Duration animationDurationLong = Duration(milliseconds: 700); + static const Duration animationDuration = Duration(milliseconds: 300); + static const Duration animationDurationShort = Duration(milliseconds: 150); +} diff --git a/lib/core/utils/listeners/immersive_scroll_listener.dart b/lib/core/utils/listeners/immersive_scroll_listener.dart new file mode 100644 index 0000000..0b8268c --- /dev/null +++ b/lib/core/utils/listeners/immersive_scroll_listener.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/services.dart'; + +class ImmersiveScrollListener { + static void function( + ScrollController controller, [ + List enabledOverlays = const [], + ]) { + if (controller.position.userScrollDirection == ScrollDirection.reverse) { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: enabledOverlays); + } else { + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + } + } +} diff --git a/lib/core/utils/result.dart b/lib/core/utils/result.dart new file mode 100644 index 0000000..69c9203 --- /dev/null +++ b/lib/core/utils/result.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; + +part 'result.freezed.dart'; + +@freezed +class Result with _$Result { + const factory Result.success(T data) = ResultSuccess; + + const factory Result.failure(Failure failure) = ResultFailure; +} diff --git a/lib/core/utils/result.freezed.dart b/lib/core/utils/result.freezed.dart new file mode 100644 index 0000000..561301a --- /dev/null +++ b/lib/core/utils/result.freezed.dart @@ -0,0 +1,343 @@ +// 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 'result.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 _$Result { + @optionalTypeArgs + TResult when({ + required TResult Function(T data) success, + required TResult Function(Failure failure) failure, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(T data)? success, + TResult? Function(Failure failure)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(T data)? success, + TResult Function(Failure failure)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult map({ + required TResult Function(ResultSuccess value) success, + required TResult Function(ResultFailure value) failure, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ResultSuccess value)? success, + TResult? Function(ResultFailure value)? failure, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ResultSuccess value)? success, + TResult Function(ResultFailure value)? failure, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ResultCopyWith { + factory $ResultCopyWith(Result value, $Res Function(Result) then) = _$ResultCopyWithImpl>; +} + +/// @nodoc +class _$ResultCopyWithImpl> implements $ResultCopyWith { + _$ResultCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$ResultSuccessImplCopyWith { + factory _$$ResultSuccessImplCopyWith(_$ResultSuccessImpl value, $Res Function(_$ResultSuccessImpl) then) = + __$$ResultSuccessImplCopyWithImpl; + @useResult + $Res call({T data}); +} + +/// @nodoc +class __$$ResultSuccessImplCopyWithImpl extends _$ResultCopyWithImpl> + implements _$$ResultSuccessImplCopyWith { + __$$ResultSuccessImplCopyWithImpl(_$ResultSuccessImpl _value, $Res Function(_$ResultSuccessImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? data = freezed, + }) { + return _then(_$ResultSuccessImpl( + freezed == data + ? _value.data + : data // ignore: cast_nullable_to_non_nullable + as T, + )); + } +} + +/// @nodoc + +class _$ResultSuccessImpl implements ResultSuccess { + const _$ResultSuccessImpl(this.data); + + @override + final T data; + + @override + String toString() { + return 'Result<$T>.success(data: $data)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ResultSuccessImpl && + const DeepCollectionEquality().equals(other.data, data)); + } + + @override + int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(data)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ResultSuccessImplCopyWith> get copyWith => + __$$ResultSuccessImplCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(T data) success, + required TResult Function(Failure failure) failure, + }) { + return success(data); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(T data)? success, + TResult? Function(Failure failure)? failure, + }) { + return success?.call(data); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(T data)? success, + TResult Function(Failure failure)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(data); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ResultSuccess value) success, + required TResult Function(ResultFailure value) failure, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ResultSuccess value)? success, + TResult? Function(ResultFailure value)? failure, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ResultSuccess value)? success, + TResult Function(ResultFailure value)? failure, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class ResultSuccess implements Result { + const factory ResultSuccess(final T data) = _$ResultSuccessImpl; + + T get data; + @JsonKey(ignore: true) + _$$ResultSuccessImplCopyWith> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$ResultFailureImplCopyWith { + factory _$$ResultFailureImplCopyWith(_$ResultFailureImpl value, $Res Function(_$ResultFailureImpl) then) = + __$$ResultFailureImplCopyWithImpl; + @useResult + $Res call({Failure failure}); + + $FailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$ResultFailureImplCopyWithImpl extends _$ResultCopyWithImpl> + implements _$$ResultFailureImplCopyWith { + __$$ResultFailureImplCopyWithImpl(_$ResultFailureImpl _value, $Res Function(_$ResultFailureImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? failure = null, + }) { + return _then(_$ResultFailureImpl( + null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure, + )); + } + + @override + @pragma('vm:prefer-inline') + $FailureCopyWith<$Res> get failure { + return $FailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value)); + }); + } +} + +/// @nodoc + +class _$ResultFailureImpl implements ResultFailure { + const _$ResultFailureImpl(this.failure); + + @override + final Failure failure; + + @override + String toString() { + return 'Result<$T>.failure(failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ResultFailureImpl && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash(runtimeType, failure); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$ResultFailureImplCopyWith> get copyWith => + __$$ResultFailureImplCopyWithImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(T data) success, + required TResult Function(Failure failure) failure, + }) { + return failure(this.failure); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(T data)? success, + TResult? Function(Failure failure)? failure, + }) { + return failure?.call(this.failure); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(T data)? success, + TResult Function(Failure failure)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this.failure); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(ResultSuccess value) success, + required TResult Function(ResultFailure value) failure, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(ResultSuccess value)? success, + TResult? Function(ResultFailure value)? failure, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(ResultSuccess value)? success, + TResult Function(ResultFailure value)? failure, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class ResultFailure implements Result { + const factory ResultFailure(final Failure failure) = _$ResultFailureImpl; + + Failure get failure; + @JsonKey(ignore: true) + _$$ResultFailureImplCopyWith> get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/core/utils/themes/app_themes.dart b/lib/core/utils/themes/app_themes.dart new file mode 100644 index 0000000..43d67c0 --- /dev/null +++ b/lib/core/utils/themes/app_themes.dart @@ -0,0 +1,337 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/utils/app_colors.dart'; + +class AppThemes { + const AppThemes(); + + ThemeData get light => ThemeData.light(useMaterial3: true).copyWith( + primaryColor: AppColors.accent, + primaryColorLight: AppColors.white, + primaryColorDark: AppColors.black, + disabledColor: AppColors.disabled, + scaffoldBackgroundColor: AppColors.backgroundLight, + dialogBackgroundColor: AppColors.backgroundLight, + focusColor: AppColors.accent, + splashColor: AppColors.accent.withOpacity(0.3), + cardColor: AppColors.cardLight, + shadowColor: Colors.black54, + chipTheme: const ChipThemeData( + backgroundColor: AppColors.fieldLight, + ), + colorScheme: const ColorScheme.highContrastLight( + primary: AppColors.white, + secondary: AppColors.accent, + error: AppColors.error, + background: AppColors.backgroundLight, + outline: AppColors.fieldLight, + ), + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: AppColors.accent, + ), + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.backgroundLight, + surfaceTintColor: AppColors.backgroundLight, + shadowColor: Colors.black54, + elevation: 0.0, + scrolledUnderElevation: 5.0, + iconTheme: IconThemeData( + color: AppColors.accent, + ), + actionsIconTheme: IconThemeData( + color: AppColors.accent, + ), + foregroundColor: AppColors.accent, + titleTextStyle: TextStyle(fontSize: 20.0, color: AppColors.black), + ), + tabBarTheme: const TabBarTheme( + dividerColor: Colors.transparent, + labelStyle: TextStyle( + fontSize: 16, + color: AppColors.white, + fontWeight: FontWeight.bold, + ), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + selectedItemColor: AppColors.accent, + selectedLabelStyle: TextStyle( + fontSize: 14, + color: AppColors.accent, + fontWeight: FontWeight.w500, + ), + unselectedItemColor: AppColors.black, + unselectedLabelStyle: TextStyle( + fontSize: 14, + color: AppColors.black, + fontWeight: FontWeight.normal, + ), + ), + iconTheme: const IconThemeData( + color: AppColors.accent, + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: AppColors.accent, + foregroundColor: AppColors.white, + ), + buttonTheme: const ButtonThemeData( + colorScheme: ColorScheme.light( + primary: AppColors.accent, + ), + buttonColor: AppColors.accent, + ), + inputDecorationTheme: const InputDecorationTheme( + prefixStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColors.fieldDark, + ), + prefixIconColor: AppColors.prefix, + fillColor: AppColors.fieldLight, + focusColor: AppColors.accent, + hintStyle: TextStyle( + fontSize: 14, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.normal, + color: AppColors.disabled, + ), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: AppColors.accent, + selectionColor: AppColors.accent.withOpacity(0.3), + selectionHandleColor: AppColors.accent, + ), + bottomSheetTheme: BottomSheetThemeData( + surfaceTintColor: Colors.black.withOpacity(0), + backgroundColor: Colors.black.withOpacity(0), + ), + switchTheme: SwitchThemeData( + thumbColor: MaterialStateProperty.all(AppColors.backgroundLight), + overlayColor: MaterialStateProperty.all(AppColors.backgroundLight), + trackColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return AppColors.accent; + } else if (states.contains(MaterialState.disabled)) { + return AppColors.disabled; + } + return AppColors.fieldLight; + }), + ), + sliderTheme: const SliderThemeData( + activeTrackColor: AppColors.accent, + inactiveTrackColor: AppColors.fieldLight, + thumbColor: AppColors.backgroundLight, + activeTickMarkColor: AppColors.transparent, + inactiveTickMarkColor: AppColors.transparent, + overlayColor: Colors.transparent, + ), + dividerTheme: const DividerThemeData( + color: AppColors.backgroundLight, + ), + splashFactory: NoSplash.splashFactory, + textTheme: const TextTheme( + labelSmall: TextStyle( + fontSize: 11, + color: AppColors.black, + letterSpacing: 0, + ), + bodySmall: TextStyle( + fontSize: 14, + color: Color(0x80000000), + ), + bodyMedium: TextStyle( + fontSize: 14, + color: AppColors.black, + fontWeight: FontWeight.normal, + ), + bodyLarge: TextStyle( + fontSize: 14, + color: AppColors.black, + fontWeight: FontWeight.w500, + ), + titleSmall: TextStyle( + fontSize: 16, + color: AppColors.black, + fontWeight: FontWeight.normal, + ), + titleMedium: TextStyle( + fontSize: 16, + color: AppColors.black, + fontWeight: FontWeight.w500, + ), + titleLarge: TextStyle( + fontSize: 18, + color: AppColors.accent, + fontWeight: FontWeight.w500, + ), + displaySmall: TextStyle( + fontSize: 16, + color: AppColors.white, + fontWeight: FontWeight.bold, + ), + displayMedium: TextStyle( + fontSize: 20, + color: AppColors.black, + fontWeight: FontWeight.normal, + ), + ), + ); + + ThemeData get dark => ThemeData.dark(useMaterial3: true).copyWith( + primaryColor: AppColors.accent, + primaryColorLight: AppColors.white, + primaryColorDark: AppColors.black, + disabledColor: AppColors.disabled, + scaffoldBackgroundColor: AppColors.backgroundDark, + dialogBackgroundColor: AppColors.backgroundDark, + focusColor: AppColors.accent, + splashColor: AppColors.accent.withOpacity(0.3), + cardColor: AppColors.cardDark, + shadowColor: Colors.black54, + chipTheme: const ChipThemeData( + backgroundColor: AppColors.fieldDark, + ), + colorScheme: const ColorScheme.highContrastDark( + primary: AppColors.white, + secondary: AppColors.accent, + error: AppColors.error, + background: AppColors.backgroundDark, + outline: AppColors.fieldDark, + ), + progressIndicatorTheme: const ProgressIndicatorThemeData( + color: AppColors.accent, + ), + appBarTheme: const AppBarTheme( + backgroundColor: AppColors.backgroundDark, + surfaceTintColor: AppColors.backgroundDark, + shadowColor: Colors.black54, + elevation: 0.0, + scrolledUnderElevation: 5.0, + iconTheme: IconThemeData( + color: AppColors.accent, + ), + actionsIconTheme: IconThemeData( + color: AppColors.accent, + ), + foregroundColor: AppColors.accent, + titleTextStyle: TextStyle(fontSize: 20.0, color: AppColors.white), + ), + tabBarTheme: const TabBarTheme( + dividerColor: Colors.transparent, + labelStyle: TextStyle( + fontSize: 16, + color: AppColors.white, + fontWeight: FontWeight.bold, + ), + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + selectedItemColor: AppColors.accent, + selectedLabelStyle: TextStyle( + fontSize: 14, + color: AppColors.accent, + fontWeight: FontWeight.w500, + ), + unselectedItemColor: AppColors.white, + unselectedLabelStyle: TextStyle( + fontSize: 14, + color: AppColors.white, + fontWeight: FontWeight.normal, + ), + ), + iconTheme: const IconThemeData( + color: AppColors.accent, + ), + floatingActionButtonTheme: const FloatingActionButtonThemeData( + backgroundColor: AppColors.accent, + foregroundColor: AppColors.white, + ), + buttonTheme: const ButtonThemeData( + colorScheme: ColorScheme.dark( + primary: AppColors.accent, + ), + buttonColor: AppColors.accent, + ), + inputDecorationTheme: const InputDecorationTheme( + prefixStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: AppColors.white, + ), + prefixIconColor: AppColors.white, + fillColor: AppColors.fieldDark, + focusColor: AppColors.accent, + hintStyle: TextStyle( + fontSize: 14, + fontStyle: FontStyle.italic, + fontWeight: FontWeight.normal, + color: AppColors.disabled, + ), + ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: AppColors.accent, + selectionColor: Color(0x4DFF7700), + selectionHandleColor: AppColors.accent, + ), + bottomSheetTheme: BottomSheetThemeData( + surfaceTintColor: Colors.black.withOpacity(0), + backgroundColor: Colors.black.withOpacity(0), + ), + switchTheme: SwitchThemeData( + thumbColor: MaterialStateProperty.all(const Color(0x80FFFFFF)), + trackColor: MaterialStateProperty.resolveWith((Set states) { + if (states.contains(MaterialState.selected)) { + return AppColors.accent; + } else if (states.contains(MaterialState.disabled)) { + return AppColors.disabled; + } + return AppColors.backgroundDark; + }), + ), + sliderTheme: const SliderThemeData( + activeTrackColor: AppColors.accent, + inactiveTrackColor: AppColors.fieldDark, + thumbColor: Color(0xFF9E9E9E), + activeTickMarkColor: Color(0x00000000), + inactiveTickMarkColor: Color(0x00000000), + overlayColor: Colors.transparent, + ), + dividerTheme: const DividerThemeData( + color: AppColors.backgroundDark, + ), + splashFactory: NoSplash.splashFactory, + textTheme: const TextTheme( + labelSmall: TextStyle( + fontSize: 11, + color: AppColors.white, + letterSpacing: 0, + ), + bodySmall: TextStyle( + fontSize: 14, + color: Color(0x80FFFFFF), + ), + bodyMedium: TextStyle( + fontSize: 14, + color: AppColors.white, + fontWeight: FontWeight.normal, + ), + bodyLarge: TextStyle( + fontSize: 14, + color: AppColors.white, + fontWeight: FontWeight.w500, + ), + titleMedium: TextStyle( + fontSize: 16, + color: AppColors.white, + fontWeight: FontWeight.w500, + ), + titleLarge: TextStyle( + fontSize: 18, + color: AppColors.accent, + fontWeight: FontWeight.w500, + ), + displaySmall: TextStyle( + fontSize: 16, + color: AppColors.white, + fontWeight: FontWeight.bold, + ), + ), + ); +} diff --git a/lib/core/utils/validators/field_validator.dart b/lib/core/utils/validators/field_validator.dart new file mode 100644 index 0000000..f11cd9c --- /dev/null +++ b/lib/core/utils/validators/field_validator.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/utils/app_regexp.dart'; + +abstract class FieldValidator { + String? validate(BuildContext context, String? value); +} + +class RequiredValidator extends FieldValidator { + @override + String? validate(BuildContext context, String? value) { + if (value != null && value.isNotEmpty) { + return null; + } + return 'This field is required'; // todo: localization + } +} + +class UrlValidator extends FieldValidator { + @override + String? validate(BuildContext context, String? value) { + RegExp urlCheck = AppRegexp.urlCheck; + if (value != null && urlCheck.hasMatch(value)) { + return null; + } + return context.localizations.serverURLerror_message; + } +} diff --git a/lib/features/_template/data/datasources/.gitkeep b/lib/features/_template/data/datasources/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/data/models/.gitkeep b/lib/features/_template/data/models/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/data/repositories/.gitkeep b/lib/features/_template/data/repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/domain/entities/.gitkeep b/lib/features/_template/domain/entities/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/domain/repositories/.gitkeep b/lib/features/_template/domain/repositories/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/domain/usecases/.gitkeep b/lib/features/_template/domain/usecases/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/presentation/blocs/.gitkeep b/lib/features/_template/presentation/blocs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/presentation/pages/.gitkeep b/lib/features/_template/presentation/pages/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/_template/presentation/widgets/.gitkeep b/lib/features/_template/presentation/widgets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/albums/data/datasources/album_datasource.dart b/lib/features/albums/data/datasources/album_datasource.dart new file mode 100644 index 0000000..b7a708d --- /dev/null +++ b/lib/features/albums/data/datasources/album_datasource.dart @@ -0,0 +1,13 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/data/models/album_model.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_album_images_use_case.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_albums_use_case.dart'; +import 'package:piwigo_ng/features/images/data/models/image_model.dart'; + +abstract class AlbumDatasource { + const AlbumDatasource(); + + Future>> fetchAlbums(FetchAlbumsParams params); + + Future>> fetchAlbumImages(FetchAlbumImagesParams params); +} diff --git a/lib/features/albums/data/datasources/album_datasource.impl.dart b/lib/features/albums/data/datasources/album_datasource.impl.dart new file mode 100644 index 0000000..ce49bee --- /dev/null +++ b/lib/features/albums/data/datasources/album_datasource.impl.dart @@ -0,0 +1,68 @@ +import 'package:piwigo_ng/core/data/datasources/remote/remote_datasource.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/utils/constants/api_constants.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/data/datasources/album_datasource.dart'; +import 'package:piwigo_ng/features/albums/data/models/album_model.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_album_images_use_case.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_albums_use_case.dart'; +import 'package:piwigo_ng/features/images/data/models/image_model.dart'; + +class AlbumDatasourceImpl extends AlbumDatasource { + const AlbumDatasourceImpl(); + + final RemoteDatasource _remote = const RemoteDatasource(); + + @override + Future>> fetchAlbums(FetchAlbumsParams params) async { + final Map queries = { + 'cat_id': params.albumId, + 'thumbnail_size': params.thumbnailSize.value, + }; + + Result> response = await _remote.get>( + method: ApiConstants.getAlbumsMethod, + queryParameters: queries, + ); + + return response.when( + failure: (Failure failure) => Result>.failure(failure), + success: (Map data) { + List albums = data['categories'] + .map( + (dynamic album) => AlbumModel.fromJson(album), + ) + .where((AlbumModel album) => album.id != params.albumId) // remove current album + .toList(); + return Result>.success(albums); + }, + ); + } + + @override + Future>> fetchAlbumImages(FetchAlbumImagesParams params) async { + Map queries = { + 'cat_id': params.albumId, + 'order': params.sort.value, + 'per_page': params.count, + 'page': params.page, + }; + + Result> response = await _remote.get>( + method: ApiConstants.getAlbumImagesMethod, + queryParameters: queries, + ); + + return response.when( + failure: (Failure failure) => Result>.failure(failure), + success: (Map data) { + List images = data['images'] + .map( + (dynamic image) => ImageModel.fromJson(image), + ) + .toList(); + return Result>.success(images); + }, + ); + } +} diff --git a/lib/features/albums/data/models/album_model.dart b/lib/features/albums/data/models/album_model.dart new file mode 100644 index 0000000..029b4e7 --- /dev/null +++ b/lib/features/albums/data/models/album_model.dart @@ -0,0 +1,35 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/domain/enums/album_status_enum.dart'; + +part 'album_model.g.dart'; + +@JsonSerializable() +class AlbumModel extends AlbumEntity { + const AlbumModel({ + required super.id, + required super.name, + super.fullName, + super.comment, + required super.url, + super.urlRepresentative, + super.permalink, + required super.status, + required super.upperCategories, + super.idUpperCategory, + required super.globalRank, + required super.nbImages, + required super.nbTotalImages, + required super.nbCategories, + super.idRepresentative, + super.dateLast, + super.maxDateLast, + super.imageOrder, + }); + + //region Serialization + factory AlbumModel.fromJson(Map json) => _$AlbumModelFromJson(json); + + Map toJson() => _$AlbumModelToJson(this); +//end_region +} diff --git a/lib/features/albums/data/models/album_model.g.dart b/lib/features/albums/data/models/album_model.g.dart new file mode 100644 index 0000000..cfaa77f --- /dev/null +++ b/lib/features/albums/data/models/album_model.g.dart @@ -0,0 +1,54 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'album_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +AlbumModel _$AlbumModelFromJson(Map json) => AlbumModel( + id: json['id'] as int, + name: json['name'] as String, + fullName: json['fullname'] as String?, + comment: json['comment'] as String?, + url: json['url'] as String, + urlRepresentative: json['tn_url'] as String?, + permalink: json['permalink'] as String?, + status: $enumDecode(_$AlbumStatusEnumEnumMap, json['status']), + upperCategories: json['uppercats'] as String, + idUpperCategory: json['id_uppercat'] as String?, + globalRank: json['global_rank'] as String, + nbImages: json['nb_images'] as int, + nbTotalImages: json['total_nb_images'] as int, + nbCategories: json['nb_categories'] as int, + idRepresentative: json['representative_picture_id'] as String?, + dateLast: json['date_last'] == null ? null : DateTime.parse(json['date_last'] as String), + maxDateLast: json['max_date_last'] == null ? null : DateTime.parse(json['max_date_last'] as String), + imageOrder: json['image_order'] as String?, + ); + +Map _$AlbumModelToJson(AlbumModel instance) => { + 'id': instance.id, + 'name': instance.name, + 'fullname': instance.fullName, + 'comment': instance.comment, + 'url': instance.url, + 'tn_url': instance.urlRepresentative, + 'permalink': instance.permalink, + 'status': _$AlbumStatusEnumEnumMap[instance.status]!, + 'uppercats': instance.upperCategories, + 'id_uppercat': instance.idUpperCategory, + 'global_rank': instance.globalRank, + 'nb_images': instance.nbImages, + 'total_nb_images': instance.nbTotalImages, + 'nb_categories': instance.nbCategories, + 'representative_picture_id': instance.idRepresentative, + 'date_last': instance.dateLast?.toIso8601String(), + 'max_date_last': instance.maxDateLast?.toIso8601String(), + 'image_order': instance.imageOrder, + }; + +const _$AlbumStatusEnumEnumMap = { + AlbumStatusEnum.public: 'public', + AlbumStatusEnum.private: 'private', +}; diff --git a/lib/features/albums/data/repositories/album_repository.impl.dart b/lib/features/albums/data/repositories/album_repository.impl.dart new file mode 100644 index 0000000..957b1a1 --- /dev/null +++ b/lib/features/albums/data/repositories/album_repository.impl.dart @@ -0,0 +1,21 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/data/datasources/album_datasource.dart'; +import 'package:piwigo_ng/features/albums/data/datasources/album_datasource.impl.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/domain/repositories/album_repository.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_album_images_use_case.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_albums_use_case.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_entity.dart'; + +class AlbumRepositoryImpl extends AlbumRepository { + const AlbumRepositoryImpl(); + + final AlbumDatasource _datasource = const AlbumDatasourceImpl(); + + @override + Future>> fetchAlbums(FetchAlbumsParams params) => _datasource.fetchAlbums(params); + + @override + Future>> fetchAlbumImages(FetchAlbumImagesParams params) => + _datasource.fetchAlbumImages(params); +} diff --git a/lib/features/albums/domain/entities/album_entity.dart b/lib/features/albums/domain/entities/album_entity.dart new file mode 100644 index 0000000..c75a5d4 --- /dev/null +++ b/lib/features/albums/domain/entities/album_entity.dart @@ -0,0 +1,73 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/features/albums/domain/enums/album_status_enum.dart'; + +class AlbumEntity { + const AlbumEntity({ + required this.id, + required this.name, + this.fullName, + this.comment, + required this.url, + this.urlRepresentative, + this.permalink, + required this.status, + required this.upperCategories, + this.idUpperCategory, + required this.globalRank, + required this.nbImages, + required this.nbTotalImages, + required this.nbCategories, + this.idRepresentative, + this.dateLast, + this.maxDateLast, + this.imageOrder, + }); + + final int id; + + final String name; + + @JsonKey(name: 'fullname') + final String? fullName; + + final String? comment; + + final String url; + + @JsonKey(name: 'tn_url') + final String? urlRepresentative; + + final String? permalink; + + final AlbumStatusEnum status; + + @JsonKey(name: 'uppercats') + final String upperCategories; + + @JsonKey(name: 'id_uppercat') + final String? idUpperCategory; + + @JsonKey(name: 'global_rank') + final String globalRank; + + @JsonKey(name: 'nb_images') + final int nbImages; + + @JsonKey(name: 'total_nb_images') + final int nbTotalImages; + + @JsonKey(name: 'nb_categories') + final int nbCategories; + + @JsonKey(name: 'representative_picture_id') + final String? idRepresentative; + + @JsonKey(name: 'date_last') + final DateTime? dateLast; + + @JsonKey(name: 'max_date_last') + final DateTime? maxDateLast; + + @JsonKey(name: 'image_order') + final String? imageOrder; +} diff --git a/lib/features/albums/domain/enums/album_status_enum.dart b/lib/features/albums/domain/enums/album_status_enum.dart new file mode 100644 index 0000000..0c0026a --- /dev/null +++ b/lib/features/albums/domain/enums/album_status_enum.dart @@ -0,0 +1,11 @@ +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum(valueField: 'value') +enum AlbumStatusEnum { + public('public'), + private('private'); + + const AlbumStatusEnum(this.value); + + final String value; +} diff --git a/lib/features/albums/domain/repositories/album_repository.dart b/lib/features/albums/domain/repositories/album_repository.dart new file mode 100644 index 0000000..c976e59 --- /dev/null +++ b/lib/features/albums/domain/repositories/album_repository.dart @@ -0,0 +1,13 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_album_images_use_case.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_albums_use_case.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_entity.dart'; + +abstract class AlbumRepository { + const AlbumRepository(); + + Future>> fetchAlbums(FetchAlbumsParams params); + + Future>> fetchAlbumImages(FetchAlbumImagesParams params); +} diff --git a/lib/features/albums/domain/usecases/fetch_album_images_use_case.dart b/lib/features/albums/domain/usecases/fetch_album_images_use_case.dart new file mode 100644 index 0000000..ac0288b --- /dev/null +++ b/lib/features/albums/domain/usecases/fetch_album_images_use_case.dart @@ -0,0 +1,29 @@ +import 'package:piwigo_ng/core/enum/sort_method_enum.dart'; +import 'package:piwigo_ng/core/utils/constants/settings_constants.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/data/repositories/album_repository.impl.dart'; +import 'package:piwigo_ng/features/albums/domain/repositories/album_repository.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_entity.dart'; + +class FetchAlbumImagesUseCase { + const FetchAlbumImagesUseCase(); + + final AlbumRepository _repository = const AlbumRepositoryImpl(); + + Future>> execute(FetchAlbumImagesParams params) async => + await _repository.fetchAlbumImages(params); +} + +class FetchAlbumImagesParams { + const FetchAlbumImagesParams({ + required this.albumId, + this.page = 0, + this.count = SettingsConstants.defaultElementPerPage, + this.sort = SettingsConstants.defaultImageSort, + }); + + final int albumId; + final int page; + final int count; + final SortMethodEnum sort; +} diff --git a/lib/features/albums/domain/usecases/fetch_albums_use_case.dart b/lib/features/albums/domain/usecases/fetch_albums_use_case.dart new file mode 100644 index 0000000..48cdf08 --- /dev/null +++ b/lib/features/albums/domain/usecases/fetch_albums_use_case.dart @@ -0,0 +1,24 @@ +import 'package:piwigo_ng/core/utils/constants/settings_constants.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/data/repositories/album_repository.impl.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/domain/repositories/album_repository.dart'; +import 'package:piwigo_ng/features/images/data/enums/image_size_enum.dart'; + +class FetchAlbumsUseCase { + const FetchAlbumsUseCase(); + + final AlbumRepository _repository = const AlbumRepositoryImpl(); + + Future>> execute(FetchAlbumsParams params) async => await _repository.fetchAlbums(params); +} + +class FetchAlbumsParams { + const FetchAlbumsParams({ + required this.albumId, + this.thumbnailSize = SettingsConstants.defaultAlbumThumbnailSize, + }); + + final int albumId; + final ImageSizeEnum thumbnailSize; +} diff --git a/lib/features/albums/presentation/blocs/album_content/album_content_bloc.dart b/lib/features/albums/presentation/blocs/album_content/album_content_bloc.dart new file mode 100644 index 0000000..a678ac7 --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_content/album_content_bloc.dart @@ -0,0 +1,38 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_albums_use_case.dart'; + +part 'album_content_bloc.freezed.dart'; + +part 'album_content_event.dart'; + +part 'album_content_state.dart'; + +class AlbumContentBloc extends Bloc { + AlbumContentBloc() : super(AlbumContentState.initial()) { + on<_GetAlbumEvent>(_onGetAlbum); + } + + final FetchAlbumsUseCase _fetchAlbumsUseCase = const FetchAlbumsUseCase(); + + Future _onGetAlbum( + _GetAlbumEvent event, + Emitter emit, + ) async { + emit(AlbumContentState.loading()); + + Result> albumsResponse = await _fetchAlbumsUseCase.execute( + FetchAlbumsParams( + albumId: event.albumId, + ), + ); + + albumsResponse.when( + failure: (Failure failure) => emit(AlbumContentState.failure(failure)), + success: (List albums) => emit(AlbumContentState.success(subAlbums: albums)), + ); + } +} diff --git a/lib/features/albums/presentation/blocs/album_content/album_content_bloc.freezed.dart b/lib/features/albums/presentation/blocs/album_content/album_content_bloc.freezed.dart new file mode 100644 index 0000000..497e947 --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_content/album_content_bloc.freezed.dart @@ -0,0 +1,866 @@ +// 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 'album_content_bloc.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 _$AlbumContentEvent { + int get albumId => throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult when({ + required TResult Function(int albumId) getAlbum, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int albumId)? getAlbum, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int albumId)? getAlbum, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult map({ + required TResult Function(_GetAlbumEvent value) getAlbum, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetAlbumEvent value)? getAlbum, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetAlbumEvent value)? getAlbum, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $AlbumContentEventCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AlbumContentEventCopyWith<$Res> { + factory $AlbumContentEventCopyWith(AlbumContentEvent value, $Res Function(AlbumContentEvent) then) = + _$AlbumContentEventCopyWithImpl<$Res, AlbumContentEvent>; + @useResult + $Res call({int albumId}); +} + +/// @nodoc +class _$AlbumContentEventCopyWithImpl<$Res, $Val extends AlbumContentEvent> + implements $AlbumContentEventCopyWith<$Res> { + _$AlbumContentEventCopyWithImpl(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? albumId = null, + }) { + return _then(_value.copyWith( + albumId: null == albumId + ? _value.albumId + : albumId // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$GetAlbumEventImplCopyWith<$Res> implements $AlbumContentEventCopyWith<$Res> { + factory _$$GetAlbumEventImplCopyWith(_$GetAlbumEventImpl value, $Res Function(_$GetAlbumEventImpl) then) = + __$$GetAlbumEventImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int albumId}); +} + +/// @nodoc +class __$$GetAlbumEventImplCopyWithImpl<$Res> extends _$AlbumContentEventCopyWithImpl<$Res, _$GetAlbumEventImpl> + implements _$$GetAlbumEventImplCopyWith<$Res> { + __$$GetAlbumEventImplCopyWithImpl(_$GetAlbumEventImpl _value, $Res Function(_$GetAlbumEventImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? albumId = null, + }) { + return _then(_$GetAlbumEventImpl( + albumId: null == albumId + ? _value.albumId + : albumId // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$GetAlbumEventImpl implements _GetAlbumEvent { + const _$GetAlbumEventImpl({required this.albumId}); + + @override + final int albumId; + + @override + String toString() { + return 'AlbumContentEvent.getAlbum(albumId: $albumId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$GetAlbumEventImpl && + (identical(other.albumId, albumId) || other.albumId == albumId)); + } + + @override + int get hashCode => Object.hash(runtimeType, albumId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$GetAlbumEventImplCopyWith<_$GetAlbumEventImpl> get copyWith => + __$$GetAlbumEventImplCopyWithImpl<_$GetAlbumEventImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int albumId) getAlbum, + }) { + return getAlbum(albumId); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int albumId)? getAlbum, + }) { + return getAlbum?.call(albumId); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int albumId)? getAlbum, + required TResult orElse(), + }) { + if (getAlbum != null) { + return getAlbum(albumId); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GetAlbumEvent value) getAlbum, + }) { + return getAlbum(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetAlbumEvent value)? getAlbum, + }) { + return getAlbum?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetAlbumEvent value)? getAlbum, + required TResult orElse(), + }) { + if (getAlbum != null) { + return getAlbum(this); + } + return orElse(); + } +} + +abstract class _GetAlbumEvent implements AlbumContentEvent { + const factory _GetAlbumEvent({required final int albumId}) = _$GetAlbumEventImpl; + + @override + int get albumId; + @override + @JsonKey(ignore: true) + _$$GetAlbumEventImplCopyWith<_$GetAlbumEventImpl> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$AlbumContentState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? subAlbums) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List subAlbums) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? subAlbums)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List subAlbums)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? subAlbums)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List subAlbums)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumContentInitialState value) initial, + required TResult Function(_AlbumContentLoadingState value) loading, + required TResult Function(_AlbumContentErrorState value) failure, + required TResult Function(_AlbumContentSuccessState value) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumContentInitialState value)? initial, + TResult? Function(_AlbumContentLoadingState value)? loading, + TResult? Function(_AlbumContentErrorState value)? failure, + TResult? Function(_AlbumContentSuccessState value)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumContentInitialState value)? initial, + TResult Function(_AlbumContentLoadingState value)? loading, + TResult Function(_AlbumContentErrorState value)? failure, + TResult Function(_AlbumContentSuccessState value)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AlbumContentStateCopyWith<$Res> { + factory $AlbumContentStateCopyWith(AlbumContentState value, $Res Function(AlbumContentState) then) = + _$AlbumContentStateCopyWithImpl<$Res, AlbumContentState>; +} + +/// @nodoc +class _$AlbumContentStateCopyWithImpl<$Res, $Val extends AlbumContentState> + implements $AlbumContentStateCopyWith<$Res> { + _$AlbumContentStateCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$AlbumContentInitialStateImplCopyWith<$Res> { + factory _$$AlbumContentInitialStateImplCopyWith( + _$AlbumContentInitialStateImpl value, $Res Function(_$AlbumContentInitialStateImpl) then) = + __$$AlbumContentInitialStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AlbumContentInitialStateImplCopyWithImpl<$Res> + extends _$AlbumContentStateCopyWithImpl<$Res, _$AlbumContentInitialStateImpl> + implements _$$AlbumContentInitialStateImplCopyWith<$Res> { + __$$AlbumContentInitialStateImplCopyWithImpl( + _$AlbumContentInitialStateImpl _value, $Res Function(_$AlbumContentInitialStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$AlbumContentInitialStateImpl extends _AlbumContentInitialState { + _$AlbumContentInitialStateImpl() : super._(); + + @override + String toString() { + return 'AlbumContentState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$AlbumContentInitialStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? subAlbums) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List subAlbums) success, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? subAlbums)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List subAlbums)? success, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? subAlbums)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List subAlbums)? success, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumContentInitialState value) initial, + required TResult Function(_AlbumContentLoadingState value) loading, + required TResult Function(_AlbumContentErrorState value) failure, + required TResult Function(_AlbumContentSuccessState value) success, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumContentInitialState value)? initial, + TResult? Function(_AlbumContentLoadingState value)? loading, + TResult? Function(_AlbumContentErrorState value)? failure, + TResult? Function(_AlbumContentSuccessState value)? success, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumContentInitialState value)? initial, + TResult Function(_AlbumContentLoadingState value)? loading, + TResult Function(_AlbumContentErrorState value)? failure, + TResult Function(_AlbumContentSuccessState value)? success, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _AlbumContentInitialState extends AlbumContentState { + factory _AlbumContentInitialState() = _$AlbumContentInitialStateImpl; + _AlbumContentInitialState._() : super._(); +} + +/// @nodoc +abstract class _$$AlbumContentLoadingStateImplCopyWith<$Res> { + factory _$$AlbumContentLoadingStateImplCopyWith( + _$AlbumContentLoadingStateImpl value, $Res Function(_$AlbumContentLoadingStateImpl) then) = + __$$AlbumContentLoadingStateImplCopyWithImpl<$Res>; + @useResult + $Res call({List? subAlbums}); +} + +/// @nodoc +class __$$AlbumContentLoadingStateImplCopyWithImpl<$Res> + extends _$AlbumContentStateCopyWithImpl<$Res, _$AlbumContentLoadingStateImpl> + implements _$$AlbumContentLoadingStateImplCopyWith<$Res> { + __$$AlbumContentLoadingStateImplCopyWithImpl( + _$AlbumContentLoadingStateImpl _value, $Res Function(_$AlbumContentLoadingStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subAlbums = freezed, + }) { + return _then(_$AlbumContentLoadingStateImpl( + subAlbums: freezed == subAlbums + ? _value._subAlbums + : subAlbums // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +class _$AlbumContentLoadingStateImpl extends _AlbumContentLoadingState { + _$AlbumContentLoadingStateImpl({final List? subAlbums}) + : _subAlbums = subAlbums, + super._(); + + final List? _subAlbums; + @override + List? get subAlbums { + final value = _subAlbums; + if (value == null) return null; + if (_subAlbums is EqualUnmodifiableListView) return _subAlbums; +// ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'AlbumContentState.loading(subAlbums: $subAlbums)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumContentLoadingStateImpl && + const DeepCollectionEquality().equals(other._subAlbums, _subAlbums)); + } + + @override + int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(_subAlbums)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AlbumContentLoadingStateImplCopyWith<_$AlbumContentLoadingStateImpl> get copyWith => + __$$AlbumContentLoadingStateImplCopyWithImpl<_$AlbumContentLoadingStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? subAlbums) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List subAlbums) success, + }) { + return loading(subAlbums); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? subAlbums)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List subAlbums)? success, + }) { + return loading?.call(subAlbums); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? subAlbums)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List subAlbums)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(subAlbums); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumContentInitialState value) initial, + required TResult Function(_AlbumContentLoadingState value) loading, + required TResult Function(_AlbumContentErrorState value) failure, + required TResult Function(_AlbumContentSuccessState value) success, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumContentInitialState value)? initial, + TResult? Function(_AlbumContentLoadingState value)? loading, + TResult? Function(_AlbumContentErrorState value)? failure, + TResult? Function(_AlbumContentSuccessState value)? success, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumContentInitialState value)? initial, + TResult Function(_AlbumContentLoadingState value)? loading, + TResult Function(_AlbumContentErrorState value)? failure, + TResult Function(_AlbumContentSuccessState value)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _AlbumContentLoadingState extends AlbumContentState { + factory _AlbumContentLoadingState({final List? subAlbums}) = _$AlbumContentLoadingStateImpl; + _AlbumContentLoadingState._() : super._(); + + List? get subAlbums; + @JsonKey(ignore: true) + _$$AlbumContentLoadingStateImplCopyWith<_$AlbumContentLoadingStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$AlbumContentErrorStateImplCopyWith<$Res> { + factory _$$AlbumContentErrorStateImplCopyWith( + _$AlbumContentErrorStateImpl value, $Res Function(_$AlbumContentErrorStateImpl) then) = + __$$AlbumContentErrorStateImplCopyWithImpl<$Res>; + @useResult + $Res call({Failure failure}); + + $FailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$AlbumContentErrorStateImplCopyWithImpl<$Res> + extends _$AlbumContentStateCopyWithImpl<$Res, _$AlbumContentErrorStateImpl> + implements _$$AlbumContentErrorStateImplCopyWith<$Res> { + __$$AlbumContentErrorStateImplCopyWithImpl( + _$AlbumContentErrorStateImpl _value, $Res Function(_$AlbumContentErrorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? failure = null, + }) { + return _then(_$AlbumContentErrorStateImpl( + null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure, + )); + } + + @override + @pragma('vm:prefer-inline') + $FailureCopyWith<$Res> get failure { + return $FailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value)); + }); + } +} + +/// @nodoc + +class _$AlbumContentErrorStateImpl extends _AlbumContentErrorState { + _$AlbumContentErrorStateImpl(this.failure) : super._(); + + @override + final Failure failure; + + @override + String toString() { + return 'AlbumContentState.failure(failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumContentErrorStateImpl && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash(runtimeType, failure); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AlbumContentErrorStateImplCopyWith<_$AlbumContentErrorStateImpl> get copyWith => + __$$AlbumContentErrorStateImplCopyWithImpl<_$AlbumContentErrorStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? subAlbums) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List subAlbums) success, + }) { + return failure(this.failure); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? subAlbums)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List subAlbums)? success, + }) { + return failure?.call(this.failure); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? subAlbums)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List subAlbums)? success, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this.failure); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumContentInitialState value) initial, + required TResult Function(_AlbumContentLoadingState value) loading, + required TResult Function(_AlbumContentErrorState value) failure, + required TResult Function(_AlbumContentSuccessState value) success, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumContentInitialState value)? initial, + TResult? Function(_AlbumContentLoadingState value)? loading, + TResult? Function(_AlbumContentErrorState value)? failure, + TResult? Function(_AlbumContentSuccessState value)? success, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumContentInitialState value)? initial, + TResult Function(_AlbumContentLoadingState value)? loading, + TResult Function(_AlbumContentErrorState value)? failure, + TResult Function(_AlbumContentSuccessState value)? success, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _AlbumContentErrorState extends AlbumContentState { + factory _AlbumContentErrorState(final Failure failure) = _$AlbumContentErrorStateImpl; + _AlbumContentErrorState._() : super._(); + + Failure get failure; + @JsonKey(ignore: true) + _$$AlbumContentErrorStateImplCopyWith<_$AlbumContentErrorStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$AlbumContentSuccessStateImplCopyWith<$Res> { + factory _$$AlbumContentSuccessStateImplCopyWith( + _$AlbumContentSuccessStateImpl value, $Res Function(_$AlbumContentSuccessStateImpl) then) = + __$$AlbumContentSuccessStateImplCopyWithImpl<$Res>; + @useResult + $Res call({List subAlbums}); +} + +/// @nodoc +class __$$AlbumContentSuccessStateImplCopyWithImpl<$Res> + extends _$AlbumContentStateCopyWithImpl<$Res, _$AlbumContentSuccessStateImpl> + implements _$$AlbumContentSuccessStateImplCopyWith<$Res> { + __$$AlbumContentSuccessStateImplCopyWithImpl( + _$AlbumContentSuccessStateImpl _value, $Res Function(_$AlbumContentSuccessStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? subAlbums = null, + }) { + return _then(_$AlbumContentSuccessStateImpl( + subAlbums: null == subAlbums + ? _value._subAlbums + : subAlbums // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$AlbumContentSuccessStateImpl extends _AlbumContentSuccessState { + _$AlbumContentSuccessStateImpl({required final List subAlbums}) + : _subAlbums = subAlbums, + super._(); + + final List _subAlbums; + @override + List get subAlbums { + if (_subAlbums is EqualUnmodifiableListView) return _subAlbums; +// ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_subAlbums); + } + + @override + String toString() { + return 'AlbumContentState.success(subAlbums: $subAlbums)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumContentSuccessStateImpl && + const DeepCollectionEquality().equals(other._subAlbums, _subAlbums)); + } + + @override + int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(_subAlbums)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AlbumContentSuccessStateImplCopyWith<_$AlbumContentSuccessStateImpl> get copyWith => + __$$AlbumContentSuccessStateImplCopyWithImpl<_$AlbumContentSuccessStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? subAlbums) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List subAlbums) success, + }) { + return success(subAlbums); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? subAlbums)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List subAlbums)? success, + }) { + return success?.call(subAlbums); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? subAlbums)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List subAlbums)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(subAlbums); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumContentInitialState value) initial, + required TResult Function(_AlbumContentLoadingState value) loading, + required TResult Function(_AlbumContentErrorState value) failure, + required TResult Function(_AlbumContentSuccessState value) success, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumContentInitialState value)? initial, + TResult? Function(_AlbumContentLoadingState value)? loading, + TResult? Function(_AlbumContentErrorState value)? failure, + TResult? Function(_AlbumContentSuccessState value)? success, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumContentInitialState value)? initial, + TResult Function(_AlbumContentLoadingState value)? loading, + TResult Function(_AlbumContentErrorState value)? failure, + TResult Function(_AlbumContentSuccessState value)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _AlbumContentSuccessState extends AlbumContentState { + factory _AlbumContentSuccessState({required final List subAlbums}) = _$AlbumContentSuccessStateImpl; + _AlbumContentSuccessState._() : super._(); + + List get subAlbums; + @JsonKey(ignore: true) + _$$AlbumContentSuccessStateImplCopyWith<_$AlbumContentSuccessStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/albums/presentation/blocs/album_content/album_content_event.dart b/lib/features/albums/presentation/blocs/album_content/album_content_event.dart new file mode 100644 index 0000000..85a2514 --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_content/album_content_event.dart @@ -0,0 +1,6 @@ +part of 'album_content_bloc.dart'; + +@freezed +class AlbumContentEvent with _$AlbumContentEvent { + const factory AlbumContentEvent.getAlbum({required int albumId}) = _GetAlbumEvent; +} diff --git a/lib/features/albums/presentation/blocs/album_content/album_content_state.dart b/lib/features/albums/presentation/blocs/album_content/album_content_state.dart new file mode 100644 index 0000000..bc302c9 --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_content/album_content_state.dart @@ -0,0 +1,20 @@ +part of 'album_content_bloc.dart'; + +@freezed +class AlbumContentState with _$AlbumContentState { + factory AlbumContentState.initial() = _AlbumContentInitialState; + + factory AlbumContentState.loading({ + List? subAlbums, + }) = _AlbumContentLoadingState; + + factory AlbumContentState.failure(Failure failure) = _AlbumContentErrorState; + + factory AlbumContentState.success({ + required List subAlbums, + }) = _AlbumContentSuccessState; + + const AlbumContentState._(); + + bool get isLoading => this is _AlbumContentLoadingState; +} diff --git a/lib/features/albums/presentation/blocs/album_images/album_images_bloc.dart b/lib/features/albums/presentation/blocs/album_images/album_images_bloc.dart new file mode 100644 index 0000000..cf91b88 --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_images/album_images_bloc.dart @@ -0,0 +1,38 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/albums/domain/usecases/fetch_album_images_use_case.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_entity.dart'; + +part 'album_images_bloc.freezed.dart'; + +part 'album_images_event.dart'; + +part 'album_images_state.dart'; + +class AlbumImagesBloc extends Bloc { + AlbumImagesBloc() : super(AlbumImagesState.initial()) { + on<_FetchAlbumImagesEvent>(_onFetchAlbumImages); + } + + final FetchAlbumImagesUseCase _fetchAlbumImagesUseCase = const FetchAlbumImagesUseCase(); + + Future _onFetchAlbumImages( + _FetchAlbumImagesEvent event, + Emitter emit, + ) async { + emit(AlbumImagesState.loading()); + + Result> response = await _fetchAlbumImagesUseCase.execute( + FetchAlbumImagesParams( + albumId: event.albumId, + ), + ); + + response.when( + failure: (Failure failure) => emit(AlbumImagesState.failure(failure)), + success: (List images) => emit(AlbumImagesState.success(images: images)), + ); + } +} diff --git a/lib/features/albums/presentation/blocs/album_images/album_images_bloc.freezed.dart b/lib/features/albums/presentation/blocs/album_images/album_images_bloc.freezed.dart new file mode 100644 index 0000000..f23c52e --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_images/album_images_bloc.freezed.dart @@ -0,0 +1,866 @@ +// 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 'album_images_bloc.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 _$AlbumImagesEvent { + int get albumId => throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult when({ + required TResult Function(int albumId) fetchImages, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int albumId)? fetchImages, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int albumId)? fetchImages, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchAlbumImagesEvent value) fetchImages, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchAlbumImagesEvent value)? fetchImages, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchAlbumImagesEvent value)? fetchImages, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $AlbumImagesEventCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AlbumImagesEventCopyWith<$Res> { + factory $AlbumImagesEventCopyWith(AlbumImagesEvent value, $Res Function(AlbumImagesEvent) then) = + _$AlbumImagesEventCopyWithImpl<$Res, AlbumImagesEvent>; + @useResult + $Res call({int albumId}); +} + +/// @nodoc +class _$AlbumImagesEventCopyWithImpl<$Res, $Val extends AlbumImagesEvent> implements $AlbumImagesEventCopyWith<$Res> { + _$AlbumImagesEventCopyWithImpl(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? albumId = null, + }) { + return _then(_value.copyWith( + albumId: null == albumId + ? _value.albumId + : albumId // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$FetchAlbumImagesEventImplCopyWith<$Res> implements $AlbumImagesEventCopyWith<$Res> { + factory _$$FetchAlbumImagesEventImplCopyWith( + _$FetchAlbumImagesEventImpl value, $Res Function(_$FetchAlbumImagesEventImpl) then) = + __$$FetchAlbumImagesEventImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int albumId}); +} + +/// @nodoc +class __$$FetchAlbumImagesEventImplCopyWithImpl<$Res> + extends _$AlbumImagesEventCopyWithImpl<$Res, _$FetchAlbumImagesEventImpl> + implements _$$FetchAlbumImagesEventImplCopyWith<$Res> { + __$$FetchAlbumImagesEventImplCopyWithImpl( + _$FetchAlbumImagesEventImpl _value, $Res Function(_$FetchAlbumImagesEventImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? albumId = null, + }) { + return _then(_$FetchAlbumImagesEventImpl( + albumId: null == albumId + ? _value.albumId + : albumId // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$FetchAlbumImagesEventImpl implements _FetchAlbumImagesEvent { + const _$FetchAlbumImagesEventImpl({required this.albumId}); + + @override + final int albumId; + + @override + String toString() { + return 'AlbumImagesEvent.fetchImages(albumId: $albumId)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FetchAlbumImagesEventImpl && + (identical(other.albumId, albumId) || other.albumId == albumId)); + } + + @override + int get hashCode => Object.hash(runtimeType, albumId); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$FetchAlbumImagesEventImplCopyWith<_$FetchAlbumImagesEventImpl> get copyWith => + __$$FetchAlbumImagesEventImplCopyWithImpl<_$FetchAlbumImagesEventImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(int albumId) fetchImages, + }) { + return fetchImages(albumId); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(int albumId)? fetchImages, + }) { + return fetchImages?.call(albumId); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(int albumId)? fetchImages, + required TResult orElse(), + }) { + if (fetchImages != null) { + return fetchImages(albumId); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_FetchAlbumImagesEvent value) fetchImages, + }) { + return fetchImages(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_FetchAlbumImagesEvent value)? fetchImages, + }) { + return fetchImages?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_FetchAlbumImagesEvent value)? fetchImages, + required TResult orElse(), + }) { + if (fetchImages != null) { + return fetchImages(this); + } + return orElse(); + } +} + +abstract class _FetchAlbumImagesEvent implements AlbumImagesEvent { + const factory _FetchAlbumImagesEvent({required final int albumId}) = _$FetchAlbumImagesEventImpl; + + @override + int get albumId; + @override + @JsonKey(ignore: true) + _$$FetchAlbumImagesEventImplCopyWith<_$FetchAlbumImagesEventImpl> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$AlbumImagesState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? images) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List images) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? images)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List images)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? images)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List images)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumImagesInitialState value) initial, + required TResult Function(_AlbumImagesLoadingState value) loading, + required TResult Function(_AlbumImagesErrorState value) failure, + required TResult Function(_AlbumImagesSuccessState value) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumImagesInitialState value)? initial, + TResult? Function(_AlbumImagesLoadingState value)? loading, + TResult? Function(_AlbumImagesErrorState value)? failure, + TResult? Function(_AlbumImagesSuccessState value)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumImagesInitialState value)? initial, + TResult Function(_AlbumImagesLoadingState value)? loading, + TResult Function(_AlbumImagesErrorState value)? failure, + TResult Function(_AlbumImagesSuccessState value)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AlbumImagesStateCopyWith<$Res> { + factory $AlbumImagesStateCopyWith(AlbumImagesState value, $Res Function(AlbumImagesState) then) = + _$AlbumImagesStateCopyWithImpl<$Res, AlbumImagesState>; +} + +/// @nodoc +class _$AlbumImagesStateCopyWithImpl<$Res, $Val extends AlbumImagesState> implements $AlbumImagesStateCopyWith<$Res> { + _$AlbumImagesStateCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$AlbumImagesInitialStateImplCopyWith<$Res> { + factory _$$AlbumImagesInitialStateImplCopyWith( + _$AlbumImagesInitialStateImpl value, $Res Function(_$AlbumImagesInitialStateImpl) then) = + __$$AlbumImagesInitialStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AlbumImagesInitialStateImplCopyWithImpl<$Res> + extends _$AlbumImagesStateCopyWithImpl<$Res, _$AlbumImagesInitialStateImpl> + implements _$$AlbumImagesInitialStateImplCopyWith<$Res> { + __$$AlbumImagesInitialStateImplCopyWithImpl( + _$AlbumImagesInitialStateImpl _value, $Res Function(_$AlbumImagesInitialStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$AlbumImagesInitialStateImpl extends _AlbumImagesInitialState { + _$AlbumImagesInitialStateImpl() : super._(); + + @override + String toString() { + return 'AlbumImagesState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$AlbumImagesInitialStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? images) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List images) success, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? images)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List images)? success, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? images)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List images)? success, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumImagesInitialState value) initial, + required TResult Function(_AlbumImagesLoadingState value) loading, + required TResult Function(_AlbumImagesErrorState value) failure, + required TResult Function(_AlbumImagesSuccessState value) success, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumImagesInitialState value)? initial, + TResult? Function(_AlbumImagesLoadingState value)? loading, + TResult? Function(_AlbumImagesErrorState value)? failure, + TResult? Function(_AlbumImagesSuccessState value)? success, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumImagesInitialState value)? initial, + TResult Function(_AlbumImagesLoadingState value)? loading, + TResult Function(_AlbumImagesErrorState value)? failure, + TResult Function(_AlbumImagesSuccessState value)? success, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _AlbumImagesInitialState extends AlbumImagesState { + factory _AlbumImagesInitialState() = _$AlbumImagesInitialStateImpl; + _AlbumImagesInitialState._() : super._(); +} + +/// @nodoc +abstract class _$$AlbumImagesLoadingStateImplCopyWith<$Res> { + factory _$$AlbumImagesLoadingStateImplCopyWith( + _$AlbumImagesLoadingStateImpl value, $Res Function(_$AlbumImagesLoadingStateImpl) then) = + __$$AlbumImagesLoadingStateImplCopyWithImpl<$Res>; + @useResult + $Res call({List? images}); +} + +/// @nodoc +class __$$AlbumImagesLoadingStateImplCopyWithImpl<$Res> + extends _$AlbumImagesStateCopyWithImpl<$Res, _$AlbumImagesLoadingStateImpl> + implements _$$AlbumImagesLoadingStateImplCopyWith<$Res> { + __$$AlbumImagesLoadingStateImplCopyWithImpl( + _$AlbumImagesLoadingStateImpl _value, $Res Function(_$AlbumImagesLoadingStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? images = freezed, + }) { + return _then(_$AlbumImagesLoadingStateImpl( + images: freezed == images + ? _value._images + : images // ignore: cast_nullable_to_non_nullable + as List?, + )); + } +} + +/// @nodoc + +class _$AlbumImagesLoadingStateImpl extends _AlbumImagesLoadingState { + _$AlbumImagesLoadingStateImpl({final List? images}) + : _images = images, + super._(); + + final List? _images; + @override + List? get images { + final value = _images; + if (value == null) return null; + if (_images is EqualUnmodifiableListView) return _images; +// ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @override + String toString() { + return 'AlbumImagesState.loading(images: $images)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumImagesLoadingStateImpl && + const DeepCollectionEquality().equals(other._images, _images)); + } + + @override + int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(_images)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AlbumImagesLoadingStateImplCopyWith<_$AlbumImagesLoadingStateImpl> get copyWith => + __$$AlbumImagesLoadingStateImplCopyWithImpl<_$AlbumImagesLoadingStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? images) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List images) success, + }) { + return loading(images); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? images)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List images)? success, + }) { + return loading?.call(images); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? images)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List images)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(images); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumImagesInitialState value) initial, + required TResult Function(_AlbumImagesLoadingState value) loading, + required TResult Function(_AlbumImagesErrorState value) failure, + required TResult Function(_AlbumImagesSuccessState value) success, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumImagesInitialState value)? initial, + TResult? Function(_AlbumImagesLoadingState value)? loading, + TResult? Function(_AlbumImagesErrorState value)? failure, + TResult? Function(_AlbumImagesSuccessState value)? success, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumImagesInitialState value)? initial, + TResult Function(_AlbumImagesLoadingState value)? loading, + TResult Function(_AlbumImagesErrorState value)? failure, + TResult Function(_AlbumImagesSuccessState value)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _AlbumImagesLoadingState extends AlbumImagesState { + factory _AlbumImagesLoadingState({final List? images}) = _$AlbumImagesLoadingStateImpl; + _AlbumImagesLoadingState._() : super._(); + + List? get images; + @JsonKey(ignore: true) + _$$AlbumImagesLoadingStateImplCopyWith<_$AlbumImagesLoadingStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$AlbumImagesErrorStateImplCopyWith<$Res> { + factory _$$AlbumImagesErrorStateImplCopyWith( + _$AlbumImagesErrorStateImpl value, $Res Function(_$AlbumImagesErrorStateImpl) then) = + __$$AlbumImagesErrorStateImplCopyWithImpl<$Res>; + @useResult + $Res call({Failure failure}); + + $FailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$AlbumImagesErrorStateImplCopyWithImpl<$Res> + extends _$AlbumImagesStateCopyWithImpl<$Res, _$AlbumImagesErrorStateImpl> + implements _$$AlbumImagesErrorStateImplCopyWith<$Res> { + __$$AlbumImagesErrorStateImplCopyWithImpl( + _$AlbumImagesErrorStateImpl _value, $Res Function(_$AlbumImagesErrorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? failure = null, + }) { + return _then(_$AlbumImagesErrorStateImpl( + null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure, + )); + } + + @override + @pragma('vm:prefer-inline') + $FailureCopyWith<$Res> get failure { + return $FailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value)); + }); + } +} + +/// @nodoc + +class _$AlbumImagesErrorStateImpl extends _AlbumImagesErrorState { + _$AlbumImagesErrorStateImpl(this.failure) : super._(); + + @override + final Failure failure; + + @override + String toString() { + return 'AlbumImagesState.failure(failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumImagesErrorStateImpl && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash(runtimeType, failure); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AlbumImagesErrorStateImplCopyWith<_$AlbumImagesErrorStateImpl> get copyWith => + __$$AlbumImagesErrorStateImplCopyWithImpl<_$AlbumImagesErrorStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? images) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List images) success, + }) { + return failure(this.failure); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? images)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List images)? success, + }) { + return failure?.call(this.failure); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? images)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List images)? success, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this.failure); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumImagesInitialState value) initial, + required TResult Function(_AlbumImagesLoadingState value) loading, + required TResult Function(_AlbumImagesErrorState value) failure, + required TResult Function(_AlbumImagesSuccessState value) success, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumImagesInitialState value)? initial, + TResult? Function(_AlbumImagesLoadingState value)? loading, + TResult? Function(_AlbumImagesErrorState value)? failure, + TResult? Function(_AlbumImagesSuccessState value)? success, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumImagesInitialState value)? initial, + TResult Function(_AlbumImagesLoadingState value)? loading, + TResult Function(_AlbumImagesErrorState value)? failure, + TResult Function(_AlbumImagesSuccessState value)? success, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _AlbumImagesErrorState extends AlbumImagesState { + factory _AlbumImagesErrorState(final Failure failure) = _$AlbumImagesErrorStateImpl; + _AlbumImagesErrorState._() : super._(); + + Failure get failure; + @JsonKey(ignore: true) + _$$AlbumImagesErrorStateImplCopyWith<_$AlbumImagesErrorStateImpl> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$AlbumImagesSuccessStateImplCopyWith<$Res> { + factory _$$AlbumImagesSuccessStateImplCopyWith( + _$AlbumImagesSuccessStateImpl value, $Res Function(_$AlbumImagesSuccessStateImpl) then) = + __$$AlbumImagesSuccessStateImplCopyWithImpl<$Res>; + @useResult + $Res call({List images}); +} + +/// @nodoc +class __$$AlbumImagesSuccessStateImplCopyWithImpl<$Res> + extends _$AlbumImagesStateCopyWithImpl<$Res, _$AlbumImagesSuccessStateImpl> + implements _$$AlbumImagesSuccessStateImplCopyWith<$Res> { + __$$AlbumImagesSuccessStateImplCopyWithImpl( + _$AlbumImagesSuccessStateImpl _value, $Res Function(_$AlbumImagesSuccessStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? images = null, + }) { + return _then(_$AlbumImagesSuccessStateImpl( + images: null == images + ? _value._images + : images // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$AlbumImagesSuccessStateImpl extends _AlbumImagesSuccessState { + _$AlbumImagesSuccessStateImpl({required final List images}) + : _images = images, + super._(); + + final List _images; + @override + List get images { + if (_images is EqualUnmodifiableListView) return _images; +// ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_images); + } + + @override + String toString() { + return 'AlbumImagesState.success(images: $images)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AlbumImagesSuccessStateImpl && + const DeepCollectionEquality().equals(other._images, _images)); + } + + @override + int get hashCode => Object.hash(runtimeType, const DeepCollectionEquality().hash(_images)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$AlbumImagesSuccessStateImplCopyWith<_$AlbumImagesSuccessStateImpl> get copyWith => + __$$AlbumImagesSuccessStateImplCopyWithImpl<_$AlbumImagesSuccessStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(List? images) loading, + required TResult Function(Failure failure) failure, + required TResult Function(List images) success, + }) { + return success(images); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(List? images)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(List images)? success, + }) { + return success?.call(images); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(List? images)? loading, + TResult Function(Failure failure)? failure, + TResult Function(List images)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(images); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_AlbumImagesInitialState value) initial, + required TResult Function(_AlbumImagesLoadingState value) loading, + required TResult Function(_AlbumImagesErrorState value) failure, + required TResult Function(_AlbumImagesSuccessState value) success, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_AlbumImagesInitialState value)? initial, + TResult? Function(_AlbumImagesLoadingState value)? loading, + TResult? Function(_AlbumImagesErrorState value)? failure, + TResult? Function(_AlbumImagesSuccessState value)? success, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_AlbumImagesInitialState value)? initial, + TResult Function(_AlbumImagesLoadingState value)? loading, + TResult Function(_AlbumImagesErrorState value)? failure, + TResult Function(_AlbumImagesSuccessState value)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _AlbumImagesSuccessState extends AlbumImagesState { + factory _AlbumImagesSuccessState({required final List images}) = _$AlbumImagesSuccessStateImpl; + _AlbumImagesSuccessState._() : super._(); + + List get images; + @JsonKey(ignore: true) + _$$AlbumImagesSuccessStateImplCopyWith<_$AlbumImagesSuccessStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/albums/presentation/blocs/album_images/album_images_event.dart b/lib/features/albums/presentation/blocs/album_images/album_images_event.dart new file mode 100644 index 0000000..2af08fc --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_images/album_images_event.dart @@ -0,0 +1,6 @@ +part of 'album_images_bloc.dart'; + +@freezed +class AlbumImagesEvent with _$AlbumImagesEvent { + const factory AlbumImagesEvent.fetchImages({required int albumId}) = _FetchAlbumImagesEvent; +} diff --git a/lib/features/albums/presentation/blocs/album_images/album_images_state.dart b/lib/features/albums/presentation/blocs/album_images/album_images_state.dart new file mode 100644 index 0000000..bdd4af3 --- /dev/null +++ b/lib/features/albums/presentation/blocs/album_images/album_images_state.dart @@ -0,0 +1,20 @@ +part of 'album_images_bloc.dart'; + +@freezed +class AlbumImagesState with _$AlbumImagesState { + factory AlbumImagesState.initial() = _AlbumImagesInitialState; + + factory AlbumImagesState.loading({ + List? images, + }) = _AlbumImagesLoadingState; + + factory AlbumImagesState.failure(Failure failure) = _AlbumImagesErrorState; + + factory AlbumImagesState.success({ + required List images, + }) = _AlbumImagesSuccessState; + + const AlbumImagesState._(); + + bool get isLoading => this is _AlbumImagesLoadingState; +} diff --git a/lib/features/albums/presentation/pages/album_page.dart b/lib/features/albums/presentation/pages/album_page.dart new file mode 100644 index 0000000..319c1c4 --- /dev/null +++ b/lib/features/albums/presentation/pages/album_page.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/core/utils/listeners/immersive_scroll_listener.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/presentation/blocs/album_content/album_content_bloc.dart'; +import 'package:piwigo_ng/features/albums/presentation/widgets/album_content_widget.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; + +class AlbumPage extends StatefulWidget { + const AlbumPage({super.key, required this.album}); + + final AlbumEntity album; + + @override + State createState() => _AlbumPageState(); +} + +class _AlbumPageState extends State with UserStatusMixin { + final ScrollController _scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _scrollController.addListener(() => ImmersiveScrollListener.function(_scrollController)); + } + + @override + void dispose() { + _scrollController.removeListener(() => ImmersiveScrollListener.function(_scrollController)); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => AlbumContentBloc()..add(AlbumContentEvent.getAlbum(albumId: widget.album.id)), + child: Scaffold( + extendBody: true, + extendBodyBehindAppBar: true, + body: CustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppBar( + floating: true, + snap: true, + title: Text(widget.album.name), + ), + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: UIConstants.paddingMedium), + child: AlbumContentWidget(), + ), + ), + ], + ), + floatingActionButton: isAdmin(context) + ? FloatingActionButton( + shape: const CircleBorder(), + onPressed: () {}, + child: const Icon(Icons.add), + ) + : null, + ), + ); + } +} diff --git a/lib/features/albums/presentation/pages/root_page.dart b/lib/features/albums/presentation/pages/root_page.dart new file mode 100644 index 0000000..1275edb --- /dev/null +++ b/lib/features/albums/presentation/pages/root_page.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:piwigo_ng/components/fields/app_field.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/presentation/widgets/app_bars/expanded_app_bar.dart'; +import 'package:piwigo_ng/core/presentation/widgets/buttons/custom_popup_menu_button.dart'; +import 'package:piwigo_ng/core/router/app_routes.dart'; +import 'package:piwigo_ng/core/utils/constants/hero_tags.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/core/utils/listeners/immersive_scroll_listener.dart'; +import 'package:piwigo_ng/features/albums/presentation/blocs/album_content/album_content_bloc.dart'; +import 'package:piwigo_ng/features/albums/presentation/widgets/album_content_widget.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; + +class RootPage extends StatefulWidget { + const RootPage({super.key}); + + @override + State createState() => _RootPageState(); +} + +class _RootPageState extends State with UserStatusMixin { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + final int rootId = 0; + + @override + void initState() { + super.initState(); + _scrollController.addListener( + () => ImmersiveScrollListener.function( + _scrollController, + [SystemUiOverlay.top], + ), + ); + } + + @override + void dispose() { + _scrollController.removeListener( + () => ImmersiveScrollListener.function( + _scrollController, + [SystemUiOverlay.top], + ), + ); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (BuildContext context) => AlbumContentBloc()..add(AlbumContentEvent.getAlbum(albumId: rootId)), + child: Scaffold( + body: CustomScrollView( + controller: _scrollController, + slivers: [ + ExpandedAppBar( + scrollController: _scrollController, + leading: IconButton( + onPressed: () => context.navigator.pushNamed(AppRoutes.settings), + icon: const Icon(Icons.settings), + ), + title: context.localizations.tabBar_albums, + actions: [ + _buildPopupMenu(context), + ], + ), + SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingMedium, + vertical: UIConstants.paddingSmall, + ), + decoration: BoxDecoration( + color: context.theme.appBarTheme.backgroundColor, + ), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => context.navigator.pushNamed(AppRoutes.searchImages), + child: IgnorePointer( + child: Hero( + tag: HeroTags.imageSearchField, + child: Material( + color: Colors.transparent, + child: AppField( + controller: _searchController, + padding: const EdgeInsets.symmetric( + vertical: UIConstants.paddingXSmall, + horizontal: UIConstants.paddingSmall, + ), + prefix: const Icon(Icons.search), + hint: "Search...", + ), + ), + ), + ), + ), + ), + ), + const SliverToBoxAdapter( + child: AlbumContentWidget(), + ), + ], + ), + floatingActionButton: isAdmin(context) + ? FloatingActionButton( + shape: const CircleBorder(), + onPressed: () {}, + child: const Icon(Icons.add), + ) + : null, + ), + ); + } + + Widget _buildPopupMenu(BuildContext context) { + return CustomPopupMenuButton( + items: [ + CustomPopupMenuItem( + onTap: () {}, // todo: go to upload queue + label: context.localizations.uploadSection_queue, + icon: Icons.upload, + ), + if (!isGuest(context)) + CustomPopupMenuItem( + onTap: () {}, // todo: go to favorites + label: context.localizations.categoryDiscoverFavorites_title, + icon: Icons.favorite, + ), + ], + ); + } +} diff --git a/lib/features/albums/presentation/painters/album_card_painter.dart b/lib/features/albums/presentation/painters/album_card_painter.dart new file mode 100644 index 0000000..8f90969 --- /dev/null +++ b/lib/features/albums/presentation/painters/album_card_painter.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; + +class AlbumCardPainter extends CustomPainter { + AlbumCardPainter({ + required this.context, + this.showActions = false, + }); + + final bool showActions; + final BuildContext context; + + static double anchorRadius = UIConstants.paddingXSmall; + + @override + void paint(Canvas canvas, Size size) { + double height = size.height; + double width = size.width; + + Paint paint = Paint()..color = context.theme.cardColor; + + Path path = Path() + ..lineTo(height, 0.0) + ..arcToPoint( + Offset(height + anchorRadius * 2, 0), + radius: Radius.circular(anchorRadius), + clockwise: false, + ) + ..lineTo(width, 0.0) + ..lineTo(width, height) + ..lineTo(height + anchorRadius * 2, height) + ..arcToPoint( + Offset(height, height), + radius: Radius.circular(anchorRadius), + clockwise: false, + ) + ..lineTo(0, height) + ..close(); + + canvas.drawPath(path, paint); + + if (showActions) { + Paint indicatorPaint = Paint()..color = context.theme.colorScheme.secondary; + + Path indicatorPath = Path() + ..moveTo(width, height / 4) + ..arcToPoint( + Offset(width - anchorRadius, (height / 4) + anchorRadius), + radius: Radius.circular(anchorRadius), + clockwise: false, + ) + ..lineTo(width - anchorRadius, (height * 3 / 4) - anchorRadius) + ..arcToPoint( + Offset(width, height * 3 / 4), + radius: Radius.circular(anchorRadius), + clockwise: false, + ) + ..close(); + + canvas.drawPath(indicatorPath, indicatorPaint); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/features/albums/presentation/widgets/album_card_widget.dart b/lib/features/albums/presentation/widgets/album_card_widget.dart new file mode 100644 index 0000000..abcc6aa --- /dev/null +++ b/lib/features/albums/presentation/widgets/album_card_widget.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/extensions/string_extension.dart'; +import 'package:piwigo_ng/core/presentation/widgets/images/custom_network_image.dart'; +import 'package:piwigo_ng/core/utils/app_colors.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/presentation/painters/album_card_painter.dart'; + +class AlbumCardWidget extends StatelessWidget { + const AlbumCardWidget({ + super.key, + required this.album, + this.onTap, + this.showActions = false, + }); + + final AlbumEntity album; + final bool showActions; + final Function()? onTap; + + static const double maxWidth = 448.0; + static const double sizeRatio = 3.0; + + @override + Widget build(BuildContext context) => ClipRRect( + borderRadius: BorderRadius.circular(UIConstants.radiusLarge), + child: Slidable( + enabled: showActions, + endActionPane: _actionPane(context), + child: GestureDetector( + onTap: onTap, + child: CustomPaint( + painter: AlbumCardPainter(context: context, showActions: showActions), + child: Padding( + padding: const EdgeInsets.all(UIConstants.paddingXSmall), + child: _buildCardContent(context), + ), + ), + ), + ), + ); + + Widget _buildCardContent(BuildContext context) => Row( + children: [ + AspectRatio( + aspectRatio: 1, + child: ClipRRect( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + child: CustomNetworkImage( + imageUrl: album.urlRepresentative, + ), + ), + ), + const SizedBox(width: UIConstants.paddingXLarge), + Expanded( + child: Column( + children: [ + Text( + album.name, + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleLarge, + ), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: UIConstants.paddingTiny), + child: Align( + alignment: Alignment.bottomCenter, + child: Text( + album.comment?.isNotEmpty == true + ? album.comment! + : context.localizations.createNewAlbumDescription_noDescription.capitalize(), + softWrap: true, + overflow: TextOverflow.fade, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), + ), + Text( + context.localizations.imageCount(album.nbTotalImages), + maxLines: 1, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + ), + ], + ); + + ActionPane? _actionPane(BuildContext context) { + return ActionPane( + motion: const DrawerMotion(), + children: [ + CustomSlidableAction( + backgroundColor: AppColors.accent, + foregroundColor: AppColors.white, + padding: EdgeInsets.zero, + onPressed: (BuildContext context) {}, + // todo: edit album + child: const Icon(Icons.edit, size: 32.0), + ), + CustomSlidableAction( + backgroundColor: AppColors.grey, + foregroundColor: AppColors.white, + padding: EdgeInsets.zero, + onPressed: (BuildContext context) {}, + // todo: move album + child: const Icon(Icons.drive_file_move, size: 32.0), + ), + CustomSlidableAction( + backgroundColor: AppColors.error, + foregroundColor: AppColors.white, + padding: EdgeInsets.zero, + onPressed: (BuildContext context) {}, + // todo: delete album + child: const Icon(Icons.delete, size: 32.0), + ), + ], + ); + } +} diff --git a/lib/features/albums/presentation/widgets/album_content_widget.dart b/lib/features/albums/presentation/widgets/album_content_widget.dart new file mode 100644 index 0000000..a58f577 --- /dev/null +++ b/lib/features/albums/presentation/widgets/album_content_widget.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/router/app_router.dart'; +import 'package:piwigo_ng/core/router/app_routes.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/features/albums/domain/entities/album_entity.dart'; +import 'package:piwigo_ng/features/albums/presentation/blocs/album_content/album_content_bloc.dart'; +import 'package:piwigo_ng/features/albums/presentation/widgets/album_card_widget.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_entity.dart'; +import 'package:piwigo_ng/features/images/presentation/widgets/image_card_widget.dart'; + +class AlbumContentWidget extends StatelessWidget with UserStatusMixin { + const AlbumContentWidget({super.key}); + + @override + Widget build(BuildContext context) => BlocBuilder( + builder: (BuildContext context, AlbumContentState state) => state.maybeWhen( + orElse: () => _buildLoading(), + failure: (Failure failure) => _buildError(context, failure), + loading: (List? albums) { + if (albums == null) return _buildLoading(); + return _buildAlbumContent(context, albums, const []); + }, + success: (List albums) => _buildAlbumContent(context, albums, const []), + ), + ); + + Widget _buildLoading() => const Center( + child: CircularProgressIndicator(), + ); + + Widget _buildError(BuildContext context, Failure failure) => Center( + child: Text(failure.getMessage(context)), + ); + + Widget _buildAlbumContent(BuildContext context, List albums, List images) => Column( + children: [ + GridView.builder( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingMedium, + ), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: AlbumCardWidget.maxWidth, + mainAxisSpacing: UIConstants.paddingSmall, + crossAxisSpacing: UIConstants.paddingSmall, + childAspectRatio: AlbumCardWidget.sizeRatio, + ), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: albums.length, + itemBuilder: (BuildContext context, int index) { + final AlbumEntity album = albums[index]; + return AlbumCardWidget( + album: album, + showActions: isAdmin(context), + onTap: () => context.navigator.pushNamed( + AppRoutes.album, + arguments: PageRouteArguments.album(album: album), + ), + ); + }, + ), + if (images.isNotEmpty && albums.isNotEmpty) const SizedBox(height: UIConstants.paddingSmall), + ListView.builder( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingMedium, + ), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: images.length, + itemBuilder: (BuildContext context, int index) { + final ImageEntity image = images[index]; + return ImageCardWidget( + image: image, + ); + }, + ), + SizedBox( + height: context.screenPadding.bottom + UIConstants.paddingMedium, + ), + ], + ); +} diff --git a/lib/features/authentication/data/datasources/authentication_datasource.dart b/lib/features/authentication/data/datasources/authentication_datasource.dart new file mode 100644 index 0000000..3c1211c --- /dev/null +++ b/lib/features/authentication/data/datasources/authentication_datasource.dart @@ -0,0 +1,15 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/models/session_status_model.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/login_use_case.dart'; + +abstract class AuthenticationDatasource { + const AuthenticationDatasource(); + + Future> login(LoginParams params); + + Future> autoLogin(); + + Future> logout(); + + Future> getSessionStatus(); +} diff --git a/lib/features/authentication/data/datasources/authentication_datasource.impl.dart b/lib/features/authentication/data/datasources/authentication_datasource.impl.dart new file mode 100644 index 0000000..436bf0b --- /dev/null +++ b/lib/features/authentication/data/datasources/authentication_datasource.impl.dart @@ -0,0 +1,111 @@ +import 'package:dio/dio.dart'; +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/data/datasources/local/secure_storage_datasource.dart'; +import 'package:piwigo_ng/core/data/datasources/remote/remote_datasource.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/utils/constants/api_constants.dart'; +import 'package:piwigo_ng/core/utils/constants/local_key_constants.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/datasources/authentication_datasource.dart'; +import 'package:piwigo_ng/features/authentication/data/models/session_status_model.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/login_use_case.dart'; + +class AuthenticationDatasourceImpl extends AuthenticationDatasource { + const AuthenticationDatasourceImpl(); + + final RemoteDatasource _remote = const RemoteDatasource(); + final PreferencesDatasource _preferences = const PreferencesDatasource(); + final SecureStorageDatasource _secureStorage = const SecureStorageDatasource(); + + @override + Future> login(LoginParams params) async { + _preferences.instance.setString(LocalKeyConstants.serverUrlKey, params.url.toString()); + + if (params.isGuest) return const Result.success(null); + + // Save credentials + await _secureStorage.instance.write(key: LocalKeyConstants.usernameKey, value: params.username); + await _secureStorage.instance.write(key: LocalKeyConstants.passwordKey, value: params.password); + + Map data = { + 'username': params.username ?? '', + 'password': params.password ?? '', + }; + + Result response = await _remote.post( + method: ApiConstants.loginMethod, + data: data, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + + return response.when( + failure: (Failure failure) => Result.failure(failure), + success: (bool success) { + if (success) { + return const Result.success(null); + } + return const Result.failure(Failure.unknown()); + }, + ); + } + + @override + Future> autoLogin() async { + String? baseUrl = _preferences.instance.getString(LocalKeyConstants.serverUrlKey); + String? username = await _secureStorage.instance.read(key: LocalKeyConstants.usernameKey); + String? password = await _secureStorage.instance.read(key: LocalKeyConstants.passwordKey); + + // No server url registered + if (baseUrl == null) return const Result.failure(Failure.unknown()); + + // User is guest + if (username == null && password == null) return const Result.success(null); + + Result response = await login( + LoginParams( + url: Uri.parse(baseUrl), + username: username, + password: password, + ), + ); + + return response.when( + failure: (Failure failure) => Result.failure(failure), + success: (_) => const Result.success(null), + ); + } + + @override + Future> logout() async { + Result response = await _remote.get( + method: ApiConstants.logoutMethod, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + + return response.when( + failure: (Failure failure) => Result.failure(failure), + success: (bool success) async { + if (success) { + _preferences.instance.remove(LocalKeyConstants.serverUrlKey); + await _secureStorage.instance.delete(key: LocalKeyConstants.usernameKey); + await _secureStorage.instance.delete(key: LocalKeyConstants.passwordKey); + return const Result.success(null); + } + return const Result.failure(Failure.unknown()); + }, + ); + } + + @override + Future> getSessionStatus() async { + Result> response = await _remote.get>( + method: ApiConstants.getStatusMethod, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + + return response.when( + failure: (Failure failure) => Result.failure(failure), + success: (Map data) => Result.success(SessionStatusModel.fromJson(data)), + ); + } +} diff --git a/lib/features/authentication/data/enums/user_status_enum.dart b/lib/features/authentication/data/enums/user_status_enum.dart new file mode 100644 index 0000000..e4bb8ea --- /dev/null +++ b/lib/features/authentication/data/enums/user_status_enum.dart @@ -0,0 +1,18 @@ +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum() +enum UserStatusEnum { + guest, + webmaster, + admin, + user, +} + +extension UserStatusExtension on UserStatusEnum { + bool get isGuest => this == UserStatusEnum.guest; + + bool get isAdmin => [ + UserStatusEnum.admin, + UserStatusEnum.webmaster, + ].contains(this); +} diff --git a/lib/features/authentication/data/models/session_status_model.dart b/lib/features/authentication/data/models/session_status_model.dart new file mode 100644 index 0000000..c79cb1e --- /dev/null +++ b/lib/features/authentication/data/models/session_status_model.dart @@ -0,0 +1,30 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/features/authentication/data/enums/user_status_enum.dart'; +import 'package:piwigo_ng/features/authentication/domain/entities/session_status_entity.dart'; +import 'package:piwigo_ng/features/images/data/enums/image_size_enum.dart'; + +part 'session_status_model.g.dart'; + +@JsonSerializable() +class SessionStatusModel extends SessionStatusEntity { + const SessionStatusModel({ + required super.username, + required super.status, + required super.pwgToken, + required super.version, + super.theme, + super.languageCode, + super.charset, + super.currentDateTime, + super.saveVisits, + super.availableSizes = const [], + super.uploadFileTypes, + super.uploadFormChunkSize, + }); + + //region Serialization + factory SessionStatusModel.fromJson(Map json) => _$SessionStatusModelFromJson(json); + + Map toJson() => _$SessionStatusModelToJson(this); +//end_region +} diff --git a/lib/features/authentication/data/models/session_status_model.g.dart b/lib/features/authentication/data/models/session_status_model.g.dart new file mode 100644 index 0000000..ea0fe71 --- /dev/null +++ b/lib/features/authentication/data/models/session_status_model.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'session_status_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SessionStatusModel _$SessionStatusModelFromJson(Map json) => SessionStatusModel( + username: json['username'] as String, + status: $enumDecode(_$UserStatusEnumEnumMap, json['status']), + pwgToken: json['pwg_token'] as String?, + version: json['version'] as String, + theme: json['theme'] as String?, + languageCode: json['language'] as String?, + charset: json['charset'] as String?, + currentDateTime: json['current_datetime'] == null ? null : DateTime.parse(json['current_datetime'] as String), + saveVisits: json['saveVisits'] as bool?, + availableSizes: + (json['available_sizes'] as List?)?.map((e) => $enumDecode(_$ImageSizeEnumEnumMap, e)).toList() ?? + const [], + uploadFileTypes: json['upload_file_types'] as String?, + uploadFormChunkSize: json['upload_form_chunk_size'] as int?, + ); + +Map _$SessionStatusModelToJson(SessionStatusModel instance) => { + 'username': instance.username, + 'status': _$UserStatusEnumEnumMap[instance.status]!, + 'pwg_token': instance.pwgToken, + 'version': instance.version, + 'theme': instance.theme, + 'language': instance.languageCode, + 'charset': instance.charset, + 'current_datetime': instance.currentDateTime?.toIso8601String(), + 'saveVisits': instance.saveVisits, + 'available_sizes': instance.availableSizes.map((e) => _$ImageSizeEnumEnumMap[e]!).toList(), + 'upload_file_types': instance.uploadFileTypes, + 'upload_form_chunk_size': instance.uploadFormChunkSize, + }; + +const _$UserStatusEnumEnumMap = { + UserStatusEnum.guest: 'guest', + UserStatusEnum.webmaster: 'webmaster', + UserStatusEnum.admin: 'admin', + UserStatusEnum.user: 'user', +}; + +const _$ImageSizeEnumEnumMap = { + ImageSizeEnum.square: 'square', + ImageSizeEnum.thumb: 'thumb', + ImageSizeEnum.xxSmall: '2small', + ImageSizeEnum.xSmall: 'xsmall', + ImageSizeEnum.small: 'small', + ImageSizeEnum.medium: 'medium', + ImageSizeEnum.large: 'large', + ImageSizeEnum.xLarge: 'xlarge', + ImageSizeEnum.xxLarge: 'xxlarge', + ImageSizeEnum.full: 'full', +}; diff --git a/lib/features/authentication/data/repositories/authentication_repository.impl.dart b/lib/features/authentication/data/repositories/authentication_repository.impl.dart new file mode 100644 index 0000000..8c9272c --- /dev/null +++ b/lib/features/authentication/data/repositories/authentication_repository.impl.dart @@ -0,0 +1,24 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/datasources/authentication_datasource.dart'; +import 'package:piwigo_ng/features/authentication/data/datasources/authentication_datasource.impl.dart'; +import 'package:piwigo_ng/features/authentication/domain/entities/session_status_entity.dart'; +import 'package:piwigo_ng/features/authentication/domain/repositories/authentication_repository.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/login_use_case.dart'; + +class AuthenticationRepositoryImpl extends AuthenticationRepository { + const AuthenticationRepositoryImpl(); + + final AuthenticationDatasource _datasource = const AuthenticationDatasourceImpl(); + + @override + Future> login(LoginParams params) async => await _datasource.login(params); + + @override + Future> autoLogin() async => await _datasource.autoLogin(); + + @override + Future> logout() async => await _datasource.logout(); + + @override + Future> getSessionStatus() async => await _datasource.getSessionStatus(); +} diff --git a/lib/features/authentication/domain/entities/session_status_entity.dart b/lib/features/authentication/domain/entities/session_status_entity.dart new file mode 100644 index 0000000..d3b9365 --- /dev/null +++ b/lib/features/authentication/domain/entities/session_status_entity.dart @@ -0,0 +1,57 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:piwigo_ng/features/authentication/data/enums/user_status_enum.dart'; +import 'package:piwigo_ng/features/images/data/enums/image_size_enum.dart'; + +class SessionStatusEntity { + const SessionStatusEntity({ + required this.username, + required this.status, + this.pwgToken, + required this.version, + this.theme, + this.languageCode, + this.charset, + this.currentDateTime, + this.saveVisits, + this.availableSizes = const [], + this.uploadFileTypes, + this.uploadFormChunkSize, + }); + + /// Current user's username + final String username; + + /// Current user status + final UserStatusEnum status; + + /// PWG Token (for non-guest users) + @JsonKey(name: 'pwg_token') + final String? pwgToken; + + /// Piwigo Server version + final String version; + + final String? theme; + + @JsonKey(name: 'language') + final String? languageCode; + + final String? charset; + + @JsonKey(name: 'current_datetime') + final DateTime? currentDateTime; + + final bool? saveVisits; + + /// Available Piwigo image sizes + @JsonKey(name: 'available_sizes') + final List availableSizes; + + @JsonKey(name: 'upload_file_types') + final String? uploadFileTypes; + + @JsonKey(name: 'upload_form_chunk_size') + final int? uploadFormChunkSize; + + List get uploadFileTypeList => uploadFileTypes?.split(',') ?? []; +} diff --git a/lib/features/authentication/domain/repositories/authentication_repository.dart b/lib/features/authentication/domain/repositories/authentication_repository.dart new file mode 100644 index 0000000..f1269bd --- /dev/null +++ b/lib/features/authentication/domain/repositories/authentication_repository.dart @@ -0,0 +1,15 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/domain/entities/session_status_entity.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/login_use_case.dart'; + +abstract class AuthenticationRepository { + const AuthenticationRepository(); + + Future> login(LoginParams params); + + Future> autoLogin(); + + Future> logout(); + + Future> getSessionStatus(); +} diff --git a/lib/features/authentication/domain/usecases/auto_login_use_case.dart b/lib/features/authentication/domain/usecases/auto_login_use_case.dart new file mode 100644 index 0000000..dd424f8 --- /dev/null +++ b/lib/features/authentication/domain/usecases/auto_login_use_case.dart @@ -0,0 +1,11 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/repositories/authentication_repository.impl.dart'; +import 'package:piwigo_ng/features/authentication/domain/repositories/authentication_repository.dart'; + +class AutoLoginUseCase { + const AutoLoginUseCase(); + + final AuthenticationRepository _repository = const AuthenticationRepositoryImpl(); + + Future> execute() async => await _repository.autoLogin(); +} diff --git a/lib/features/authentication/domain/usecases/login_use_case.dart b/lib/features/authentication/domain/usecases/login_use_case.dart new file mode 100644 index 0000000..7cff2c1 --- /dev/null +++ b/lib/features/authentication/domain/usecases/login_use_case.dart @@ -0,0 +1,56 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/repositories/authentication_repository.impl.dart'; +import 'package:piwigo_ng/features/authentication/domain/repositories/authentication_repository.dart'; + +class LoginUseCase { + const LoginUseCase(); + + final AuthenticationRepository _repository = const AuthenticationRepositoryImpl(); + + Future> execute(LoginParams params) async => await _repository.login(params); +} + +class LoginParams { + const LoginParams({ + required this.url, + this.username, + this.password, + }); + + final Uri url; + final String? username; + final String? password; + + bool get isGuest => (username?.isEmpty ?? true) && (password?.isEmpty ?? true); +} + +enum UrlSchemeEnum { + http, + https; + + static UrlSchemeEnum fromJson(String json) { + switch (json) { + case 'http': + return UrlSchemeEnum.http; + case 'https': + return UrlSchemeEnum.https; + default: + throw UnimplementedError(); + } + } + + static String get domainSeparator => '://'; +} + +extension UrlSchemeExtension on UrlSchemeEnum { + String toJson() { + switch (this) { + case UrlSchemeEnum.http: + return 'http'; + case UrlSchemeEnum.https: + return 'https'; + } + } + + String get urlPrefix => '${toJson()}${UrlSchemeEnum.domainSeparator}'; +} diff --git a/lib/features/authentication/domain/usecases/logout_use_case.dart b/lib/features/authentication/domain/usecases/logout_use_case.dart new file mode 100644 index 0000000..76606b5 --- /dev/null +++ b/lib/features/authentication/domain/usecases/logout_use_case.dart @@ -0,0 +1,11 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/repositories/authentication_repository.impl.dart'; +import 'package:piwigo_ng/features/authentication/domain/repositories/authentication_repository.dart'; + +class LogOutUseCase { + const LogOutUseCase(); + + final AuthenticationRepository _repository = const AuthenticationRepositoryImpl(); + + Future> execute() async => await _repository.logout(); +} diff --git a/lib/features/authentication/domain/usecases/session_status_use_case.dart b/lib/features/authentication/domain/usecases/session_status_use_case.dart new file mode 100644 index 0000000..6e6cb3d --- /dev/null +++ b/lib/features/authentication/domain/usecases/session_status_use_case.dart @@ -0,0 +1,12 @@ +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/repositories/authentication_repository.impl.dart'; +import 'package:piwigo_ng/features/authentication/domain/entities/session_status_entity.dart'; +import 'package:piwigo_ng/features/authentication/domain/repositories/authentication_repository.dart'; + +class SessionStatusUseCase { + const SessionStatusUseCase(); + + final AuthenticationRepository _repository = const AuthenticationRepositoryImpl(); + + Future> execute() async => await _repository.getSessionStatus(); +} diff --git a/lib/features/authentication/presentation/blocs/login/login_bloc.dart b/lib/features/authentication/presentation/blocs/login/login_bloc.dart new file mode 100644 index 0000000..f7b46e8 --- /dev/null +++ b/lib/features/authentication/presentation/blocs/login/login_bloc.dart @@ -0,0 +1,75 @@ +import 'package:bloc/bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/auto_login_use_case.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/login_use_case.dart'; + +part 'login_bloc.freezed.dart'; + +part 'login_event.dart'; + +part 'login_state.dart'; + +class LoginBloc extends Bloc { + LoginBloc() : super(LoginState.initial()) { + on<_LoginUserEvent>(_onLoginUser); + on<_LoginGuestEvent>(_onLoginGuest); + on<_AutoLoginEvent>(_onAutoLogin); + } + + final LoginUseCase _loginUseCase = const LoginUseCase(); + final AutoLoginUseCase _autoLoginUseCase = const AutoLoginUseCase(); + + Future _onLoginUser( + _LoginUserEvent event, + Emitter emit, + ) async { + emit(LoginState.loading()); + + Result response = await _loginUseCase.execute( + LoginParams( + url: event.url, + username: event.username, + password: event.password, + ), + ); + + response.when( + failure: (Failure failure) => emit(LoginState.failure(failure)), + success: (_) => emit(LoginState.success()), + ); + } + + Future _onLoginGuest( + _LoginGuestEvent event, + Emitter emit, + ) async { + emit(LoginState.loading()); + + Result response = await _loginUseCase.execute( + LoginParams( + url: event.url, + ), + ); + + response.when( + failure: (Failure failure) => emit(LoginState.failure(failure)), + success: (_) => emit(LoginState.success()), + ); + } + + Future _onAutoLogin( + _AutoLoginEvent event, + Emitter emit, + ) async { + emit(LoginState.loading()); + + Result response = await _autoLoginUseCase.execute(); + + response.when( + failure: (Failure failure) => emit(LoginState.failure(failure)), + success: (_) => emit(LoginState.success()), + ); + } +} diff --git a/lib/features/authentication/presentation/blocs/login/login_bloc.freezed.dart b/lib/features/authentication/presentation/blocs/login/login_bloc.freezed.dart new file mode 100644 index 0000000..62c6e3f --- /dev/null +++ b/lib/features/authentication/presentation/blocs/login/login_bloc.freezed.dart @@ -0,0 +1,1030 @@ +// 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 'login_bloc.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 _$LoginEvent { + @optionalTypeArgs + TResult when({ + required TResult Function(Uri url, String username, String password) loginUser, + required TResult Function(Uri url) loginGuest, + required TResult Function() autoLogin, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Uri url, String username, String password)? loginUser, + TResult? Function(Uri url)? loginGuest, + TResult? Function()? autoLogin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Uri url, String username, String password)? loginUser, + TResult Function(Uri url)? loginGuest, + TResult Function()? autoLogin, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginUserEvent value) loginUser, + required TResult Function(_LoginGuestEvent value) loginGuest, + required TResult Function(_AutoLoginEvent value) autoLogin, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginUserEvent value)? loginUser, + TResult? Function(_LoginGuestEvent value)? loginGuest, + TResult? Function(_AutoLoginEvent value)? autoLogin, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginUserEvent value)? loginUser, + TResult Function(_LoginGuestEvent value)? loginGuest, + TResult Function(_AutoLoginEvent value)? autoLogin, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginEventCopyWith<$Res> { + factory $LoginEventCopyWith(LoginEvent value, $Res Function(LoginEvent) then) = + _$LoginEventCopyWithImpl<$Res, LoginEvent>; +} + +/// @nodoc +class _$LoginEventCopyWithImpl<$Res, $Val extends LoginEvent> implements $LoginEventCopyWith<$Res> { + _$LoginEventCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$LoginUserEventImplCopyWith<$Res> { + factory _$$LoginUserEventImplCopyWith(_$LoginUserEventImpl value, $Res Function(_$LoginUserEventImpl) then) = + __$$LoginUserEventImplCopyWithImpl<$Res>; + @useResult + $Res call({Uri url, String username, String password}); +} + +/// @nodoc +class __$$LoginUserEventImplCopyWithImpl<$Res> extends _$LoginEventCopyWithImpl<$Res, _$LoginUserEventImpl> + implements _$$LoginUserEventImplCopyWith<$Res> { + __$$LoginUserEventImplCopyWithImpl(_$LoginUserEventImpl _value, $Res Function(_$LoginUserEventImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + Object? username = null, + Object? password = null, + }) { + return _then(_$LoginUserEventImpl( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as Uri, + username: null == username + ? _value.username + : username // ignore: cast_nullable_to_non_nullable + as String, + password: null == password + ? _value.password + : password // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc + +class _$LoginUserEventImpl implements _LoginUserEvent { + const _$LoginUserEventImpl({required this.url, required this.username, required this.password}); + + @override + final Uri url; + @override + final String username; + @override + final String password; + + @override + String toString() { + return 'LoginEvent.loginUser(url: $url, username: $username, password: $password)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginUserEventImpl && + (identical(other.url, url) || other.url == url) && + (identical(other.username, username) || other.username == username) && + (identical(other.password, password) || other.password == password)); + } + + @override + int get hashCode => Object.hash(runtimeType, url, username, password); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoginUserEventImplCopyWith<_$LoginUserEventImpl> get copyWith => + __$$LoginUserEventImplCopyWithImpl<_$LoginUserEventImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(Uri url, String username, String password) loginUser, + required TResult Function(Uri url) loginGuest, + required TResult Function() autoLogin, + }) { + return loginUser(url, username, password); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Uri url, String username, String password)? loginUser, + TResult? Function(Uri url)? loginGuest, + TResult? Function()? autoLogin, + }) { + return loginUser?.call(url, username, password); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Uri url, String username, String password)? loginUser, + TResult Function(Uri url)? loginGuest, + TResult Function()? autoLogin, + required TResult orElse(), + }) { + if (loginUser != null) { + return loginUser(url, username, password); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginUserEvent value) loginUser, + required TResult Function(_LoginGuestEvent value) loginGuest, + required TResult Function(_AutoLoginEvent value) autoLogin, + }) { + return loginUser(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginUserEvent value)? loginUser, + TResult? Function(_LoginGuestEvent value)? loginGuest, + TResult? Function(_AutoLoginEvent value)? autoLogin, + }) { + return loginUser?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginUserEvent value)? loginUser, + TResult Function(_LoginGuestEvent value)? loginGuest, + TResult Function(_AutoLoginEvent value)? autoLogin, + required TResult orElse(), + }) { + if (loginUser != null) { + return loginUser(this); + } + return orElse(); + } +} + +abstract class _LoginUserEvent implements LoginEvent { + const factory _LoginUserEvent( + {required final Uri url, required final String username, required final String password}) = _$LoginUserEventImpl; + + Uri get url; + String get username; + String get password; + @JsonKey(ignore: true) + _$$LoginUserEventImplCopyWith<_$LoginUserEventImpl> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LoginGuestEventImplCopyWith<$Res> { + factory _$$LoginGuestEventImplCopyWith(_$LoginGuestEventImpl value, $Res Function(_$LoginGuestEventImpl) then) = + __$$LoginGuestEventImplCopyWithImpl<$Res>; + @useResult + $Res call({Uri url}); +} + +/// @nodoc +class __$$LoginGuestEventImplCopyWithImpl<$Res> extends _$LoginEventCopyWithImpl<$Res, _$LoginGuestEventImpl> + implements _$$LoginGuestEventImplCopyWith<$Res> { + __$$LoginGuestEventImplCopyWithImpl(_$LoginGuestEventImpl _value, $Res Function(_$LoginGuestEventImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? url = null, + }) { + return _then(_$LoginGuestEventImpl( + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as Uri, + )); + } +} + +/// @nodoc + +class _$LoginGuestEventImpl implements _LoginGuestEvent { + const _$LoginGuestEventImpl({required this.url}); + + @override + final Uri url; + + @override + String toString() { + return 'LoginEvent.loginGuest(url: $url)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginGuestEventImpl && + (identical(other.url, url) || other.url == url)); + } + + @override + int get hashCode => Object.hash(runtimeType, url); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoginGuestEventImplCopyWith<_$LoginGuestEventImpl> get copyWith => + __$$LoginGuestEventImplCopyWithImpl<_$LoginGuestEventImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(Uri url, String username, String password) loginUser, + required TResult Function(Uri url) loginGuest, + required TResult Function() autoLogin, + }) { + return loginGuest(url); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Uri url, String username, String password)? loginUser, + TResult? Function(Uri url)? loginGuest, + TResult? Function()? autoLogin, + }) { + return loginGuest?.call(url); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Uri url, String username, String password)? loginUser, + TResult Function(Uri url)? loginGuest, + TResult Function()? autoLogin, + required TResult orElse(), + }) { + if (loginGuest != null) { + return loginGuest(url); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginUserEvent value) loginUser, + required TResult Function(_LoginGuestEvent value) loginGuest, + required TResult Function(_AutoLoginEvent value) autoLogin, + }) { + return loginGuest(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginUserEvent value)? loginUser, + TResult? Function(_LoginGuestEvent value)? loginGuest, + TResult? Function(_AutoLoginEvent value)? autoLogin, + }) { + return loginGuest?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginUserEvent value)? loginUser, + TResult Function(_LoginGuestEvent value)? loginGuest, + TResult Function(_AutoLoginEvent value)? autoLogin, + required TResult orElse(), + }) { + if (loginGuest != null) { + return loginGuest(this); + } + return orElse(); + } +} + +abstract class _LoginGuestEvent implements LoginEvent { + const factory _LoginGuestEvent({required final Uri url}) = _$LoginGuestEventImpl; + + Uri get url; + @JsonKey(ignore: true) + _$$LoginGuestEventImplCopyWith<_$LoginGuestEventImpl> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$AutoLoginEventImplCopyWith<$Res> { + factory _$$AutoLoginEventImplCopyWith(_$AutoLoginEventImpl value, $Res Function(_$AutoLoginEventImpl) then) = + __$$AutoLoginEventImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$AutoLoginEventImplCopyWithImpl<$Res> extends _$LoginEventCopyWithImpl<$Res, _$AutoLoginEventImpl> + implements _$$AutoLoginEventImplCopyWith<$Res> { + __$$AutoLoginEventImplCopyWithImpl(_$AutoLoginEventImpl _value, $Res Function(_$AutoLoginEventImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$AutoLoginEventImpl implements _AutoLoginEvent { + const _$AutoLoginEventImpl(); + + @override + String toString() { + return 'LoginEvent.autoLogin()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$AutoLoginEventImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function(Uri url, String username, String password) loginUser, + required TResult Function(Uri url) loginGuest, + required TResult Function() autoLogin, + }) { + return autoLogin(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(Uri url, String username, String password)? loginUser, + TResult? Function(Uri url)? loginGuest, + TResult? Function()? autoLogin, + }) { + return autoLogin?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(Uri url, String username, String password)? loginUser, + TResult Function(Uri url)? loginGuest, + TResult Function()? autoLogin, + required TResult orElse(), + }) { + if (autoLogin != null) { + return autoLogin(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginUserEvent value) loginUser, + required TResult Function(_LoginGuestEvent value) loginGuest, + required TResult Function(_AutoLoginEvent value) autoLogin, + }) { + return autoLogin(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginUserEvent value)? loginUser, + TResult? Function(_LoginGuestEvent value)? loginGuest, + TResult? Function(_AutoLoginEvent value)? autoLogin, + }) { + return autoLogin?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginUserEvent value)? loginUser, + TResult Function(_LoginGuestEvent value)? loginGuest, + TResult Function(_AutoLoginEvent value)? autoLogin, + required TResult orElse(), + }) { + if (autoLogin != null) { + return autoLogin(this); + } + return orElse(); + } +} + +abstract class _AutoLoginEvent implements LoginEvent { + const factory _AutoLoginEvent() = _$AutoLoginEventImpl; +} + +/// @nodoc +mixin _$LoginState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Failure failure) failure, + required TResult Function() success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Failure failure)? failure, + TResult? Function()? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Failure failure)? failure, + TResult Function()? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginInitialState value) initial, + required TResult Function(_LoginLoadingState value) loading, + required TResult Function(_LoginErrorState value) failure, + required TResult Function(_LoginSuccessState value) success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginInitialState value)? initial, + TResult? Function(_LoginLoadingState value)? loading, + TResult? Function(_LoginErrorState value)? failure, + TResult? Function(_LoginSuccessState value)? success, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginInitialState value)? initial, + TResult Function(_LoginLoadingState value)? loading, + TResult Function(_LoginErrorState value)? failure, + TResult Function(_LoginSuccessState value)? success, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LoginStateCopyWith<$Res> { + factory $LoginStateCopyWith(LoginState value, $Res Function(LoginState) then) = + _$LoginStateCopyWithImpl<$Res, LoginState>; +} + +/// @nodoc +class _$LoginStateCopyWithImpl<$Res, $Val extends LoginState> implements $LoginStateCopyWith<$Res> { + _$LoginStateCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$LoginInitialStateImplCopyWith<$Res> { + factory _$$LoginInitialStateImplCopyWith(_$LoginInitialStateImpl value, $Res Function(_$LoginInitialStateImpl) then) = + __$$LoginInitialStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoginInitialStateImplCopyWithImpl<$Res> extends _$LoginStateCopyWithImpl<$Res, _$LoginInitialStateImpl> + implements _$$LoginInitialStateImplCopyWith<$Res> { + __$$LoginInitialStateImplCopyWithImpl(_$LoginInitialStateImpl _value, $Res Function(_$LoginInitialStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LoginInitialStateImpl extends _LoginInitialState { + _$LoginInitialStateImpl() : super._(); + + @override + String toString() { + return 'LoginState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$LoginInitialStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Failure failure) failure, + required TResult Function() success, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Failure failure)? failure, + TResult? Function()? success, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Failure failure)? failure, + TResult Function()? success, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginInitialState value) initial, + required TResult Function(_LoginLoadingState value) loading, + required TResult Function(_LoginErrorState value) failure, + required TResult Function(_LoginSuccessState value) success, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginInitialState value)? initial, + TResult? Function(_LoginLoadingState value)? loading, + TResult? Function(_LoginErrorState value)? failure, + TResult? Function(_LoginSuccessState value)? success, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginInitialState value)? initial, + TResult Function(_LoginLoadingState value)? loading, + TResult Function(_LoginErrorState value)? failure, + TResult Function(_LoginSuccessState value)? success, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _LoginInitialState extends LoginState { + factory _LoginInitialState() = _$LoginInitialStateImpl; + _LoginInitialState._() : super._(); +} + +/// @nodoc +abstract class _$$LoginLoadingStateImplCopyWith<$Res> { + factory _$$LoginLoadingStateImplCopyWith(_$LoginLoadingStateImpl value, $Res Function(_$LoginLoadingStateImpl) then) = + __$$LoginLoadingStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoginLoadingStateImplCopyWithImpl<$Res> extends _$LoginStateCopyWithImpl<$Res, _$LoginLoadingStateImpl> + implements _$$LoginLoadingStateImplCopyWith<$Res> { + __$$LoginLoadingStateImplCopyWithImpl(_$LoginLoadingStateImpl _value, $Res Function(_$LoginLoadingStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LoginLoadingStateImpl extends _LoginLoadingState { + _$LoginLoadingStateImpl() : super._(); + + @override + String toString() { + return 'LoginState.loading()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$LoginLoadingStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Failure failure) failure, + required TResult Function() success, + }) { + return loading(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Failure failure)? failure, + TResult? Function()? success, + }) { + return loading?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Failure failure)? failure, + TResult Function()? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginInitialState value) initial, + required TResult Function(_LoginLoadingState value) loading, + required TResult Function(_LoginErrorState value) failure, + required TResult Function(_LoginSuccessState value) success, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginInitialState value)? initial, + TResult? Function(_LoginLoadingState value)? loading, + TResult? Function(_LoginErrorState value)? failure, + TResult? Function(_LoginSuccessState value)? success, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginInitialState value)? initial, + TResult Function(_LoginLoadingState value)? loading, + TResult Function(_LoginErrorState value)? failure, + TResult Function(_LoginSuccessState value)? success, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _LoginLoadingState extends LoginState { + factory _LoginLoadingState() = _$LoginLoadingStateImpl; + _LoginLoadingState._() : super._(); +} + +/// @nodoc +abstract class _$$LoginErrorStateImplCopyWith<$Res> { + factory _$$LoginErrorStateImplCopyWith(_$LoginErrorStateImpl value, $Res Function(_$LoginErrorStateImpl) then) = + __$$LoginErrorStateImplCopyWithImpl<$Res>; + @useResult + $Res call({Failure failure}); + + $FailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$LoginErrorStateImplCopyWithImpl<$Res> extends _$LoginStateCopyWithImpl<$Res, _$LoginErrorStateImpl> + implements _$$LoginErrorStateImplCopyWith<$Res> { + __$$LoginErrorStateImplCopyWithImpl(_$LoginErrorStateImpl _value, $Res Function(_$LoginErrorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? failure = null, + }) { + return _then(_$LoginErrorStateImpl( + null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure, + )); + } + + @override + @pragma('vm:prefer-inline') + $FailureCopyWith<$Res> get failure { + return $FailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value)); + }); + } +} + +/// @nodoc + +class _$LoginErrorStateImpl extends _LoginErrorState { + _$LoginErrorStateImpl(this.failure) : super._(); + + @override + final Failure failure; + + @override + String toString() { + return 'LoginState.failure(failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LoginErrorStateImpl && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash(runtimeType, failure); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LoginErrorStateImplCopyWith<_$LoginErrorStateImpl> get copyWith => + __$$LoginErrorStateImplCopyWithImpl<_$LoginErrorStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Failure failure) failure, + required TResult Function() success, + }) { + return failure(this.failure); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Failure failure)? failure, + TResult? Function()? success, + }) { + return failure?.call(this.failure); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Failure failure)? failure, + TResult Function()? success, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this.failure); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginInitialState value) initial, + required TResult Function(_LoginLoadingState value) loading, + required TResult Function(_LoginErrorState value) failure, + required TResult Function(_LoginSuccessState value) success, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginInitialState value)? initial, + TResult? Function(_LoginLoadingState value)? loading, + TResult? Function(_LoginErrorState value)? failure, + TResult? Function(_LoginSuccessState value)? success, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginInitialState value)? initial, + TResult Function(_LoginLoadingState value)? loading, + TResult Function(_LoginErrorState value)? failure, + TResult Function(_LoginSuccessState value)? success, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _LoginErrorState extends LoginState { + factory _LoginErrorState(final Failure failure) = _$LoginErrorStateImpl; + _LoginErrorState._() : super._(); + + Failure get failure; + @JsonKey(ignore: true) + _$$LoginErrorStateImplCopyWith<_$LoginErrorStateImpl> get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$LoginSuccessStateImplCopyWith<$Res> { + factory _$$LoginSuccessStateImplCopyWith(_$LoginSuccessStateImpl value, $Res Function(_$LoginSuccessStateImpl) then) = + __$$LoginSuccessStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LoginSuccessStateImplCopyWithImpl<$Res> extends _$LoginStateCopyWithImpl<$Res, _$LoginSuccessStateImpl> + implements _$$LoginSuccessStateImplCopyWith<$Res> { + __$$LoginSuccessStateImplCopyWithImpl(_$LoginSuccessStateImpl _value, $Res Function(_$LoginSuccessStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LoginSuccessStateImpl extends _LoginSuccessState { + _$LoginSuccessStateImpl() : super._(); + + @override + String toString() { + return 'LoginState.success()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$LoginSuccessStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function() loading, + required TResult Function(Failure failure) failure, + required TResult Function() success, + }) { + return success(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function()? loading, + TResult? Function(Failure failure)? failure, + TResult? Function()? success, + }) { + return success?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function()? loading, + TResult Function(Failure failure)? failure, + TResult Function()? success, + required TResult orElse(), + }) { + if (success != null) { + return success(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_LoginInitialState value) initial, + required TResult Function(_LoginLoadingState value) loading, + required TResult Function(_LoginErrorState value) failure, + required TResult Function(_LoginSuccessState value) success, + }) { + return success(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_LoginInitialState value)? initial, + TResult? Function(_LoginLoadingState value)? loading, + TResult? Function(_LoginErrorState value)? failure, + TResult? Function(_LoginSuccessState value)? success, + }) { + return success?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_LoginInitialState value)? initial, + TResult Function(_LoginLoadingState value)? loading, + TResult Function(_LoginErrorState value)? failure, + TResult Function(_LoginSuccessState value)? success, + required TResult orElse(), + }) { + if (success != null) { + return success(this); + } + return orElse(); + } +} + +abstract class _LoginSuccessState extends LoginState { + factory _LoginSuccessState() = _$LoginSuccessStateImpl; + _LoginSuccessState._() : super._(); +} diff --git a/lib/features/authentication/presentation/blocs/login/login_event.dart b/lib/features/authentication/presentation/blocs/login/login_event.dart new file mode 100644 index 0000000..0ea4121 --- /dev/null +++ b/lib/features/authentication/presentation/blocs/login/login_event.dart @@ -0,0 +1,16 @@ +part of 'login_bloc.dart'; + +@freezed +class LoginEvent with _$LoginEvent { + const factory LoginEvent.loginUser({ + required Uri url, + required String username, + required String password, + }) = _LoginUserEvent; + + const factory LoginEvent.loginGuest({ + required Uri url, + }) = _LoginGuestEvent; + + const factory LoginEvent.autoLogin() = _AutoLoginEvent; +} diff --git a/lib/features/authentication/presentation/blocs/login/login_state.dart b/lib/features/authentication/presentation/blocs/login/login_state.dart new file mode 100644 index 0000000..c5e8132 --- /dev/null +++ b/lib/features/authentication/presentation/blocs/login/login_state.dart @@ -0,0 +1,16 @@ +part of 'login_bloc.dart'; + +@freezed +class LoginState with _$LoginState { + factory LoginState.initial() = _LoginInitialState; + + factory LoginState.loading() = _LoginLoadingState; + + factory LoginState.failure(Failure failure) = _LoginErrorState; + + factory LoginState.success() = _LoginSuccessState; + + const LoginState._(); + + bool get isLoading => this is _LoginLoadingState; +} diff --git a/lib/features/authentication/presentation/blocs/session_status/session_status_bloc.dart b/lib/features/authentication/presentation/blocs/session_status/session_status_bloc.dart new file mode 100644 index 0000000..d977d84 --- /dev/null +++ b/lib/features/authentication/presentation/blocs/session_status/session_status_bloc.dart @@ -0,0 +1,63 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/utils/result.dart'; +import 'package:piwigo_ng/features/authentication/data/enums/user_status_enum.dart'; +import 'package:piwigo_ng/features/authentication/domain/entities/session_status_entity.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/logout_use_case.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/session_status_use_case.dart'; + +part 'session_status_bloc.freezed.dart'; + +part 'session_status_event.dart'; + +part 'session_status_state.dart'; + +class SessionStatusBloc extends Bloc { + SessionStatusBloc() : super(SessionStatusState.initial()) { + on<_GetSessionStatusEvent>(_onGetSessionStatus); + on<_LogOutEvent>(_onLogout); + } + + final SessionStatusUseCase _sessionStatusUseCase = const SessionStatusUseCase(); + final LogOutUseCase _logoutUseCase = const LogOutUseCase(); + + Future _onGetSessionStatus( + _GetSessionStatusEvent event, + Emitter emit, + ) async { + emit(SessionStatusState.loading(state.currentStatus)); + + Result response = await _sessionStatusUseCase.execute(); + + response.when( + failure: (Failure failure) => emit(SessionStatusState.failure(failure)), + success: (SessionStatusEntity status) => emit(SessionStatusState.loggedIn(status)), + ); + } + + Future _onLogout( + _LogOutEvent event, + Emitter emit, + ) async { + emit(SessionStatusState.loading(state.currentStatus)); + + Result response = await _logoutUseCase.execute(); + + response.when( + failure: (Failure failure) => emit(SessionStatusState.failure(failure)), + success: (_) => emit(SessionStatusState.loggedOut()), + ); + } +} + +mixin UserStatusMixin { + UserStatusEnum getUserStatus(BuildContext context) => BlocProvider.of(context).state.userStatus; + + bool isGuest(BuildContext context) => getUserStatus(context) == UserStatusEnum.guest; + + bool isAdmin(BuildContext context) => [UserStatusEnum.admin, UserStatusEnum.webmaster].contains( + getUserStatus(context), + ); +} diff --git a/lib/features/authentication/presentation/blocs/session_status/session_status_bloc.freezed.dart b/lib/features/authentication/presentation/blocs/session_status/session_status_bloc.freezed.dart new file mode 100644 index 0000000..9bfaf69 --- /dev/null +++ b/lib/features/authentication/presentation/blocs/session_status/session_status_bloc.freezed.dart @@ -0,0 +1,1059 @@ +// 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 'session_status_bloc.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 _$SessionStatusEvent { + @optionalTypeArgs + TResult when({ + required TResult Function() getStatus, + required TResult Function() logout, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getStatus, + TResult? Function()? logout, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getStatus, + TResult Function()? logout, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult map({ + required TResult Function(_GetSessionStatusEvent value) getStatus, + required TResult Function(_LogOutEvent value) logout, + }) => + throw _privateConstructorUsedError; + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetSessionStatusEvent value)? getStatus, + TResult? Function(_LogOutEvent value)? logout, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetSessionStatusEvent value)? getStatus, + TResult Function(_LogOutEvent value)? logout, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SessionStatusEventCopyWith<$Res> { + factory $SessionStatusEventCopyWith(SessionStatusEvent value, $Res Function(SessionStatusEvent) then) = + _$SessionStatusEventCopyWithImpl<$Res, SessionStatusEvent>; +} + +/// @nodoc +class _$SessionStatusEventCopyWithImpl<$Res, $Val extends SessionStatusEvent> + implements $SessionStatusEventCopyWith<$Res> { + _$SessionStatusEventCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$GetSessionStatusEventImplCopyWith<$Res> { + factory _$$GetSessionStatusEventImplCopyWith( + _$GetSessionStatusEventImpl value, $Res Function(_$GetSessionStatusEventImpl) then) = + __$$GetSessionStatusEventImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$GetSessionStatusEventImplCopyWithImpl<$Res> + extends _$SessionStatusEventCopyWithImpl<$Res, _$GetSessionStatusEventImpl> + implements _$$GetSessionStatusEventImplCopyWith<$Res> { + __$$GetSessionStatusEventImplCopyWithImpl( + _$GetSessionStatusEventImpl _value, $Res Function(_$GetSessionStatusEventImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$GetSessionStatusEventImpl implements _GetSessionStatusEvent { + const _$GetSessionStatusEventImpl(); + + @override + String toString() { + return 'SessionStatusEvent.getStatus()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$GetSessionStatusEventImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getStatus, + required TResult Function() logout, + }) { + return getStatus(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getStatus, + TResult? Function()? logout, + }) { + return getStatus?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getStatus, + TResult Function()? logout, + required TResult orElse(), + }) { + if (getStatus != null) { + return getStatus(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GetSessionStatusEvent value) getStatus, + required TResult Function(_LogOutEvent value) logout, + }) { + return getStatus(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetSessionStatusEvent value)? getStatus, + TResult? Function(_LogOutEvent value)? logout, + }) { + return getStatus?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetSessionStatusEvent value)? getStatus, + TResult Function(_LogOutEvent value)? logout, + required TResult orElse(), + }) { + if (getStatus != null) { + return getStatus(this); + } + return orElse(); + } +} + +abstract class _GetSessionStatusEvent implements SessionStatusEvent { + const factory _GetSessionStatusEvent() = _$GetSessionStatusEventImpl; +} + +/// @nodoc +abstract class _$$LogOutEventImplCopyWith<$Res> { + factory _$$LogOutEventImplCopyWith(_$LogOutEventImpl value, $Res Function(_$LogOutEventImpl) then) = + __$$LogOutEventImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$LogOutEventImplCopyWithImpl<$Res> extends _$SessionStatusEventCopyWithImpl<$Res, _$LogOutEventImpl> + implements _$$LogOutEventImplCopyWith<$Res> { + __$$LogOutEventImplCopyWithImpl(_$LogOutEventImpl _value, $Res Function(_$LogOutEventImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$LogOutEventImpl implements _LogOutEvent { + const _$LogOutEventImpl(); + + @override + String toString() { + return 'SessionStatusEvent.logout()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$LogOutEventImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() getStatus, + required TResult Function() logout, + }) { + return logout(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? getStatus, + TResult? Function()? logout, + }) { + return logout?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? getStatus, + TResult Function()? logout, + required TResult orElse(), + }) { + if (logout != null) { + return logout(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_GetSessionStatusEvent value) getStatus, + required TResult Function(_LogOutEvent value) logout, + }) { + return logout(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_GetSessionStatusEvent value)? getStatus, + TResult? Function(_LogOutEvent value)? logout, + }) { + return logout?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_GetSessionStatusEvent value)? getStatus, + TResult Function(_LogOutEvent value)? logout, + required TResult orElse(), + }) { + if (logout != null) { + return logout(this); + } + return orElse(); + } +} + +abstract class _LogOutEvent implements SessionStatusEvent { + const factory _LogOutEvent() = _$LogOutEventImpl; +} + +/// @nodoc +mixin _$SessionStatusState { + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(SessionStatusEntity? status) loading, + required TResult Function(Failure failure) failure, + required TResult Function(SessionStatusEntity status) loggedIn, + required TResult Function() loggedOut, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(SessionStatusEntity? status)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(SessionStatusEntity status)? loggedIn, + TResult? Function()? loggedOut, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(SessionStatusEntity? status)? loading, + TResult Function(Failure failure)? failure, + TResult Function(SessionStatusEntity status)? loggedIn, + TResult Function()? loggedOut, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult map({ + required TResult Function(_SessionStatusInitialState value) initial, + required TResult Function(_SessionStatusLoadingState value) loading, + required TResult Function(_SessionStatusErrorState value) failure, + required TResult Function(_SessionStatusLoggedInState value) loggedIn, + required TResult Function(_SessionStatusLoggedOutState value) loggedOut, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SessionStatusInitialState value)? initial, + TResult? Function(_SessionStatusLoadingState value)? loading, + TResult? Function(_SessionStatusErrorState value)? failure, + TResult? Function(_SessionStatusLoggedInState value)? loggedIn, + TResult? Function(_SessionStatusLoggedOutState value)? loggedOut, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SessionStatusInitialState value)? initial, + TResult Function(_SessionStatusLoadingState value)? loading, + TResult Function(_SessionStatusErrorState value)? failure, + TResult Function(_SessionStatusLoggedInState value)? loggedIn, + TResult Function(_SessionStatusLoggedOutState value)? loggedOut, + required TResult orElse(), + }) => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SessionStatusStateCopyWith<$Res> { + factory $SessionStatusStateCopyWith(SessionStatusState value, $Res Function(SessionStatusState) then) = + _$SessionStatusStateCopyWithImpl<$Res, SessionStatusState>; +} + +/// @nodoc +class _$SessionStatusStateCopyWithImpl<$Res, $Val extends SessionStatusState> + implements $SessionStatusStateCopyWith<$Res> { + _$SessionStatusStateCopyWithImpl(this._value, this._then); + +// ignore: unused_field + final $Val _value; +// ignore: unused_field + final $Res Function($Val) _then; +} + +/// @nodoc +abstract class _$$SessionStatusInitialStateImplCopyWith<$Res> { + factory _$$SessionStatusInitialStateImplCopyWith( + _$SessionStatusInitialStateImpl value, $Res Function(_$SessionStatusInitialStateImpl) then) = + __$$SessionStatusInitialStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$SessionStatusInitialStateImplCopyWithImpl<$Res> + extends _$SessionStatusStateCopyWithImpl<$Res, _$SessionStatusInitialStateImpl> + implements _$$SessionStatusInitialStateImplCopyWith<$Res> { + __$$SessionStatusInitialStateImplCopyWithImpl( + _$SessionStatusInitialStateImpl _value, $Res Function(_$SessionStatusInitialStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$SessionStatusInitialStateImpl extends _SessionStatusInitialState { + _$SessionStatusInitialStateImpl() : super._(); + + @override + String toString() { + return 'SessionStatusState.initial()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$SessionStatusInitialStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(SessionStatusEntity? status) loading, + required TResult Function(Failure failure) failure, + required TResult Function(SessionStatusEntity status) loggedIn, + required TResult Function() loggedOut, + }) { + return initial(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(SessionStatusEntity? status)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(SessionStatusEntity status)? loggedIn, + TResult? Function()? loggedOut, + }) { + return initial?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(SessionStatusEntity? status)? loading, + TResult Function(Failure failure)? failure, + TResult Function(SessionStatusEntity status)? loggedIn, + TResult Function()? loggedOut, + required TResult orElse(), + }) { + if (initial != null) { + return initial(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SessionStatusInitialState value) initial, + required TResult Function(_SessionStatusLoadingState value) loading, + required TResult Function(_SessionStatusErrorState value) failure, + required TResult Function(_SessionStatusLoggedInState value) loggedIn, + required TResult Function(_SessionStatusLoggedOutState value) loggedOut, + }) { + return initial(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SessionStatusInitialState value)? initial, + TResult? Function(_SessionStatusLoadingState value)? loading, + TResult? Function(_SessionStatusErrorState value)? failure, + TResult? Function(_SessionStatusLoggedInState value)? loggedIn, + TResult? Function(_SessionStatusLoggedOutState value)? loggedOut, + }) { + return initial?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SessionStatusInitialState value)? initial, + TResult Function(_SessionStatusLoadingState value)? loading, + TResult Function(_SessionStatusErrorState value)? failure, + TResult Function(_SessionStatusLoggedInState value)? loggedIn, + TResult Function(_SessionStatusLoggedOutState value)? loggedOut, + required TResult orElse(), + }) { + if (initial != null) { + return initial(this); + } + return orElse(); + } +} + +abstract class _SessionStatusInitialState extends SessionStatusState { + factory _SessionStatusInitialState() = _$SessionStatusInitialStateImpl; + _SessionStatusInitialState._() : super._(); +} + +/// @nodoc +abstract class _$$SessionStatusLoadingStateImplCopyWith<$Res> { + factory _$$SessionStatusLoadingStateImplCopyWith( + _$SessionStatusLoadingStateImpl value, $Res Function(_$SessionStatusLoadingStateImpl) then) = + __$$SessionStatusLoadingStateImplCopyWithImpl<$Res>; + @useResult + $Res call({SessionStatusEntity? status}); +} + +/// @nodoc +class __$$SessionStatusLoadingStateImplCopyWithImpl<$Res> + extends _$SessionStatusStateCopyWithImpl<$Res, _$SessionStatusLoadingStateImpl> + implements _$$SessionStatusLoadingStateImplCopyWith<$Res> { + __$$SessionStatusLoadingStateImplCopyWithImpl( + _$SessionStatusLoadingStateImpl _value, $Res Function(_$SessionStatusLoadingStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? status = freezed, + }) { + return _then(_$SessionStatusLoadingStateImpl( + freezed == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as SessionStatusEntity?, + )); + } +} + +/// @nodoc + +class _$SessionStatusLoadingStateImpl extends _SessionStatusLoadingState { + _$SessionStatusLoadingStateImpl([this.status]) : super._(); + + @override + final SessionStatusEntity? status; + + @override + String toString() { + return 'SessionStatusState.loading(status: $status)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SessionStatusLoadingStateImpl && + (identical(other.status, status) || other.status == status)); + } + + @override + int get hashCode => Object.hash(runtimeType, status); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SessionStatusLoadingStateImplCopyWith<_$SessionStatusLoadingStateImpl> get copyWith => + __$$SessionStatusLoadingStateImplCopyWithImpl<_$SessionStatusLoadingStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(SessionStatusEntity? status) loading, + required TResult Function(Failure failure) failure, + required TResult Function(SessionStatusEntity status) loggedIn, + required TResult Function() loggedOut, + }) { + return loading(status); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(SessionStatusEntity? status)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(SessionStatusEntity status)? loggedIn, + TResult? Function()? loggedOut, + }) { + return loading?.call(status); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(SessionStatusEntity? status)? loading, + TResult Function(Failure failure)? failure, + TResult Function(SessionStatusEntity status)? loggedIn, + TResult Function()? loggedOut, + required TResult orElse(), + }) { + if (loading != null) { + return loading(status); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SessionStatusInitialState value) initial, + required TResult Function(_SessionStatusLoadingState value) loading, + required TResult Function(_SessionStatusErrorState value) failure, + required TResult Function(_SessionStatusLoggedInState value) loggedIn, + required TResult Function(_SessionStatusLoggedOutState value) loggedOut, + }) { + return loading(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SessionStatusInitialState value)? initial, + TResult? Function(_SessionStatusLoadingState value)? loading, + TResult? Function(_SessionStatusErrorState value)? failure, + TResult? Function(_SessionStatusLoggedInState value)? loggedIn, + TResult? Function(_SessionStatusLoggedOutState value)? loggedOut, + }) { + return loading?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SessionStatusInitialState value)? initial, + TResult Function(_SessionStatusLoadingState value)? loading, + TResult Function(_SessionStatusErrorState value)? failure, + TResult Function(_SessionStatusLoggedInState value)? loggedIn, + TResult Function(_SessionStatusLoggedOutState value)? loggedOut, + required TResult orElse(), + }) { + if (loading != null) { + return loading(this); + } + return orElse(); + } +} + +abstract class _SessionStatusLoadingState extends SessionStatusState { + factory _SessionStatusLoadingState([final SessionStatusEntity? status]) = _$SessionStatusLoadingStateImpl; + _SessionStatusLoadingState._() : super._(); + + SessionStatusEntity? get status; + @JsonKey(ignore: true) + _$$SessionStatusLoadingStateImplCopyWith<_$SessionStatusLoadingStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SessionStatusErrorStateImplCopyWith<$Res> { + factory _$$SessionStatusErrorStateImplCopyWith( + _$SessionStatusErrorStateImpl value, $Res Function(_$SessionStatusErrorStateImpl) then) = + __$$SessionStatusErrorStateImplCopyWithImpl<$Res>; + @useResult + $Res call({Failure failure}); + + $FailureCopyWith<$Res> get failure; +} + +/// @nodoc +class __$$SessionStatusErrorStateImplCopyWithImpl<$Res> + extends _$SessionStatusStateCopyWithImpl<$Res, _$SessionStatusErrorStateImpl> + implements _$$SessionStatusErrorStateImplCopyWith<$Res> { + __$$SessionStatusErrorStateImplCopyWithImpl( + _$SessionStatusErrorStateImpl _value, $Res Function(_$SessionStatusErrorStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? failure = null, + }) { + return _then(_$SessionStatusErrorStateImpl( + null == failure + ? _value.failure + : failure // ignore: cast_nullable_to_non_nullable + as Failure, + )); + } + + @override + @pragma('vm:prefer-inline') + $FailureCopyWith<$Res> get failure { + return $FailureCopyWith<$Res>(_value.failure, (value) { + return _then(_value.copyWith(failure: value)); + }); + } +} + +/// @nodoc + +class _$SessionStatusErrorStateImpl extends _SessionStatusErrorState { + _$SessionStatusErrorStateImpl(this.failure) : super._(); + + @override + final Failure failure; + + @override + String toString() { + return 'SessionStatusState.failure(failure: $failure)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SessionStatusErrorStateImpl && + (identical(other.failure, failure) || other.failure == failure)); + } + + @override + int get hashCode => Object.hash(runtimeType, failure); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SessionStatusErrorStateImplCopyWith<_$SessionStatusErrorStateImpl> get copyWith => + __$$SessionStatusErrorStateImplCopyWithImpl<_$SessionStatusErrorStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(SessionStatusEntity? status) loading, + required TResult Function(Failure failure) failure, + required TResult Function(SessionStatusEntity status) loggedIn, + required TResult Function() loggedOut, + }) { + return failure(this.failure); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(SessionStatusEntity? status)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(SessionStatusEntity status)? loggedIn, + TResult? Function()? loggedOut, + }) { + return failure?.call(this.failure); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(SessionStatusEntity? status)? loading, + TResult Function(Failure failure)? failure, + TResult Function(SessionStatusEntity status)? loggedIn, + TResult Function()? loggedOut, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this.failure); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SessionStatusInitialState value) initial, + required TResult Function(_SessionStatusLoadingState value) loading, + required TResult Function(_SessionStatusErrorState value) failure, + required TResult Function(_SessionStatusLoggedInState value) loggedIn, + required TResult Function(_SessionStatusLoggedOutState value) loggedOut, + }) { + return failure(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SessionStatusInitialState value)? initial, + TResult? Function(_SessionStatusLoadingState value)? loading, + TResult? Function(_SessionStatusErrorState value)? failure, + TResult? Function(_SessionStatusLoggedInState value)? loggedIn, + TResult? Function(_SessionStatusLoggedOutState value)? loggedOut, + }) { + return failure?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SessionStatusInitialState value)? initial, + TResult Function(_SessionStatusLoadingState value)? loading, + TResult Function(_SessionStatusErrorState value)? failure, + TResult Function(_SessionStatusLoggedInState value)? loggedIn, + TResult Function(_SessionStatusLoggedOutState value)? loggedOut, + required TResult orElse(), + }) { + if (failure != null) { + return failure(this); + } + return orElse(); + } +} + +abstract class _SessionStatusErrorState extends SessionStatusState { + factory _SessionStatusErrorState(final Failure failure) = _$SessionStatusErrorStateImpl; + _SessionStatusErrorState._() : super._(); + + Failure get failure; + @JsonKey(ignore: true) + _$$SessionStatusErrorStateImplCopyWith<_$SessionStatusErrorStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SessionStatusLoggedInStateImplCopyWith<$Res> { + factory _$$SessionStatusLoggedInStateImplCopyWith( + _$SessionStatusLoggedInStateImpl value, $Res Function(_$SessionStatusLoggedInStateImpl) then) = + __$$SessionStatusLoggedInStateImplCopyWithImpl<$Res>; + @useResult + $Res call({SessionStatusEntity status}); +} + +/// @nodoc +class __$$SessionStatusLoggedInStateImplCopyWithImpl<$Res> + extends _$SessionStatusStateCopyWithImpl<$Res, _$SessionStatusLoggedInStateImpl> + implements _$$SessionStatusLoggedInStateImplCopyWith<$Res> { + __$$SessionStatusLoggedInStateImplCopyWithImpl( + _$SessionStatusLoggedInStateImpl _value, $Res Function(_$SessionStatusLoggedInStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? status = null, + }) { + return _then(_$SessionStatusLoggedInStateImpl( + null == status + ? _value.status + : status // ignore: cast_nullable_to_non_nullable + as SessionStatusEntity, + )); + } +} + +/// @nodoc + +class _$SessionStatusLoggedInStateImpl extends _SessionStatusLoggedInState { + _$SessionStatusLoggedInStateImpl(this.status) : super._(); + + @override + final SessionStatusEntity status; + + @override + String toString() { + return 'SessionStatusState.loggedIn(status: $status)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SessionStatusLoggedInStateImpl && + (identical(other.status, status) || other.status == status)); + } + + @override + int get hashCode => Object.hash(runtimeType, status); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$SessionStatusLoggedInStateImplCopyWith<_$SessionStatusLoggedInStateImpl> get copyWith => + __$$SessionStatusLoggedInStateImplCopyWithImpl<_$SessionStatusLoggedInStateImpl>(this, _$identity); + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(SessionStatusEntity? status) loading, + required TResult Function(Failure failure) failure, + required TResult Function(SessionStatusEntity status) loggedIn, + required TResult Function() loggedOut, + }) { + return loggedIn(status); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(SessionStatusEntity? status)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(SessionStatusEntity status)? loggedIn, + TResult? Function()? loggedOut, + }) { + return loggedIn?.call(status); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(SessionStatusEntity? status)? loading, + TResult Function(Failure failure)? failure, + TResult Function(SessionStatusEntity status)? loggedIn, + TResult Function()? loggedOut, + required TResult orElse(), + }) { + if (loggedIn != null) { + return loggedIn(status); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SessionStatusInitialState value) initial, + required TResult Function(_SessionStatusLoadingState value) loading, + required TResult Function(_SessionStatusErrorState value) failure, + required TResult Function(_SessionStatusLoggedInState value) loggedIn, + required TResult Function(_SessionStatusLoggedOutState value) loggedOut, + }) { + return loggedIn(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SessionStatusInitialState value)? initial, + TResult? Function(_SessionStatusLoadingState value)? loading, + TResult? Function(_SessionStatusErrorState value)? failure, + TResult? Function(_SessionStatusLoggedInState value)? loggedIn, + TResult? Function(_SessionStatusLoggedOutState value)? loggedOut, + }) { + return loggedIn?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SessionStatusInitialState value)? initial, + TResult Function(_SessionStatusLoadingState value)? loading, + TResult Function(_SessionStatusErrorState value)? failure, + TResult Function(_SessionStatusLoggedInState value)? loggedIn, + TResult Function(_SessionStatusLoggedOutState value)? loggedOut, + required TResult orElse(), + }) { + if (loggedIn != null) { + return loggedIn(this); + } + return orElse(); + } +} + +abstract class _SessionStatusLoggedInState extends SessionStatusState { + factory _SessionStatusLoggedInState(final SessionStatusEntity status) = _$SessionStatusLoggedInStateImpl; + _SessionStatusLoggedInState._() : super._(); + + SessionStatusEntity get status; + @JsonKey(ignore: true) + _$$SessionStatusLoggedInStateImplCopyWith<_$SessionStatusLoggedInStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class _$$SessionStatusLoggedOutStateImplCopyWith<$Res> { + factory _$$SessionStatusLoggedOutStateImplCopyWith( + _$SessionStatusLoggedOutStateImpl value, $Res Function(_$SessionStatusLoggedOutStateImpl) then) = + __$$SessionStatusLoggedOutStateImplCopyWithImpl<$Res>; +} + +/// @nodoc +class __$$SessionStatusLoggedOutStateImplCopyWithImpl<$Res> + extends _$SessionStatusStateCopyWithImpl<$Res, _$SessionStatusLoggedOutStateImpl> + implements _$$SessionStatusLoggedOutStateImplCopyWith<$Res> { + __$$SessionStatusLoggedOutStateImplCopyWithImpl( + _$SessionStatusLoggedOutStateImpl _value, $Res Function(_$SessionStatusLoggedOutStateImpl) _then) + : super(_value, _then); +} + +/// @nodoc + +class _$SessionStatusLoggedOutStateImpl extends _SessionStatusLoggedOutState { + _$SessionStatusLoggedOutStateImpl() : super._(); + + @override + String toString() { + return 'SessionStatusState.loggedOut()'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || (other.runtimeType == runtimeType && other is _$SessionStatusLoggedOutStateImpl); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + @optionalTypeArgs + TResult when({ + required TResult Function() initial, + required TResult Function(SessionStatusEntity? status) loading, + required TResult Function(Failure failure) failure, + required TResult Function(SessionStatusEntity status) loggedIn, + required TResult Function() loggedOut, + }) { + return loggedOut(); + } + + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? initial, + TResult? Function(SessionStatusEntity? status)? loading, + TResult? Function(Failure failure)? failure, + TResult? Function(SessionStatusEntity status)? loggedIn, + TResult? Function()? loggedOut, + }) { + return loggedOut?.call(); + } + + @override + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? initial, + TResult Function(SessionStatusEntity? status)? loading, + TResult Function(Failure failure)? failure, + TResult Function(SessionStatusEntity status)? loggedIn, + TResult Function()? loggedOut, + required TResult orElse(), + }) { + if (loggedOut != null) { + return loggedOut(); + } + return orElse(); + } + + @override + @optionalTypeArgs + TResult map({ + required TResult Function(_SessionStatusInitialState value) initial, + required TResult Function(_SessionStatusLoadingState value) loading, + required TResult Function(_SessionStatusErrorState value) failure, + required TResult Function(_SessionStatusLoggedInState value) loggedIn, + required TResult Function(_SessionStatusLoggedOutState value) loggedOut, + }) { + return loggedOut(this); + } + + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(_SessionStatusInitialState value)? initial, + TResult? Function(_SessionStatusLoadingState value)? loading, + TResult? Function(_SessionStatusErrorState value)? failure, + TResult? Function(_SessionStatusLoggedInState value)? loggedIn, + TResult? Function(_SessionStatusLoggedOutState value)? loggedOut, + }) { + return loggedOut?.call(this); + } + + @override + @optionalTypeArgs + TResult maybeMap({ + TResult Function(_SessionStatusInitialState value)? initial, + TResult Function(_SessionStatusLoadingState value)? loading, + TResult Function(_SessionStatusErrorState value)? failure, + TResult Function(_SessionStatusLoggedInState value)? loggedIn, + TResult Function(_SessionStatusLoggedOutState value)? loggedOut, + required TResult orElse(), + }) { + if (loggedOut != null) { + return loggedOut(this); + } + return orElse(); + } +} + +abstract class _SessionStatusLoggedOutState extends SessionStatusState { + factory _SessionStatusLoggedOutState() = _$SessionStatusLoggedOutStateImpl; + _SessionStatusLoggedOutState._() : super._(); +} diff --git a/lib/features/authentication/presentation/blocs/session_status/session_status_event.dart b/lib/features/authentication/presentation/blocs/session_status/session_status_event.dart new file mode 100644 index 0000000..3d89c7f --- /dev/null +++ b/lib/features/authentication/presentation/blocs/session_status/session_status_event.dart @@ -0,0 +1,8 @@ +part of 'session_status_bloc.dart'; + +@freezed +class SessionStatusEvent with _$SessionStatusEvent { + const factory SessionStatusEvent.getStatus() = _GetSessionStatusEvent; + + const factory SessionStatusEvent.logout() = _LogOutEvent; +} diff --git a/lib/features/authentication/presentation/blocs/session_status/session_status_state.dart b/lib/features/authentication/presentation/blocs/session_status/session_status_state.dart new file mode 100644 index 0000000..2eb44ef --- /dev/null +++ b/lib/features/authentication/presentation/blocs/session_status/session_status_state.dart @@ -0,0 +1,29 @@ +part of 'session_status_bloc.dart'; + +@freezed +class SessionStatusState with _$SessionStatusState { + factory SessionStatusState.initial() = _SessionStatusInitialState; + + factory SessionStatusState.loading([SessionStatusEntity? status]) = _SessionStatusLoadingState; + + factory SessionStatusState.failure(Failure failure) = _SessionStatusErrorState; + + factory SessionStatusState.loggedIn(SessionStatusEntity status) = _SessionStatusLoggedInState; + + factory SessionStatusState.loggedOut() = _SessionStatusLoggedOutState; + + const SessionStatusState._(); + + bool get isLoading => this is _SessionStatusLoadingState; + + SessionStatusEntity? get currentStatus { + if (this is _SessionStatusLoggedInState) { + return (this as _SessionStatusLoggedInState).status; + } else if (this is _SessionStatusLoadingState) { + return (this as _SessionStatusLoadingState).status; + } + return null; + } + + UserStatusEnum get userStatus => currentStatus?.status ?? UserStatusEnum.guest; +} diff --git a/lib/features/authentication/presentation/pages/login_page.dart b/lib/features/authentication/presentation/pages/login_page.dart new file mode 100644 index 0000000..cb58e07 --- /dev/null +++ b/lib/features/authentication/presentation/pages/login_page.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/injector/injector.dart'; +import 'package:piwigo_ng/core/presentation/widgets/buttons/custom_text_button.dart'; +import 'package:piwigo_ng/core/router/app_routes.dart'; +import 'package:piwigo_ng/core/utils/app_assets.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/login/login_bloc.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; +import 'package:piwigo_ng/features/authentication/presentation/widgets/login_form_widget.dart'; + +class LoginPage extends StatefulWidget { + const LoginPage({super.key}); + + @override + State createState() => _LoginPageState(); +} + +class _LoginPageState extends State { + String get appVersion => serviceLocator().version; + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => LoginBloc(), + child: BlocListener( + listener: (BuildContext context, SessionStatusState state) => state.whenOrNull( + failure: (Failure failure) => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(failure.getMessage(context)), + ), + ), + loggedIn: (_) => context.navigator.pushReplacementNamed(AppRoutes.root), + ), + child: Scaffold( + resizeToAvoidBottomInset: true, + body: SafeArea( + child: SingleChildScrollView( + child: SizedBox( + height: context.screenSize(safeArea: true).height, + child: Column( + children: [ + Expanded( + flex: 5, + child: Stack( + alignment: Alignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingLarge, + vertical: UIConstants.paddingXSmall, + ), + child: Image.asset( + AppAssets.piwigoLogo, + fit: BoxFit.scaleDown, + ), + ), + Align( + alignment: Alignment.topRight, + child: IconButton( + onPressed: () {}, // todo: login settings + icon: const Icon(Icons.settings), + ), + ), + ], + ), + ), + const LoginFormView(), + Expanded( + flex: 4, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + CustomTextButton( + onTap: () {}, // todo: show privacy policy + text: context.localizations.settings_privacy, + ), + Text(appVersion), + const SizedBox(height: UIConstants.paddingXSmall), + ], + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/features/authentication/presentation/pages/startup_page.dart b/lib/features/authentication/presentation/pages/startup_page.dart new file mode 100644 index 0000000..f63a683 --- /dev/null +++ b/lib/features/authentication/presentation/pages/startup_page.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/router/app_routes.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/login/login_bloc.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; + +class StartupPage extends StatefulWidget { + const StartupPage({super.key}); + + @override + State createState() => _StartupPageState(); +} + +class _StartupPageState extends State { + @override + Widget build(BuildContext context) => BlocProvider( + create: (BuildContext context) => LoginBloc()..add(const LoginEvent.autoLogin()), + child: MultiBlocListener( + listeners: >[ + BlocListener( + listener: (BuildContext context, LoginState state) => state.whenOrNull( + failure: (Failure failure) => context.navigator.pushReplacementNamed(AppRoutes.login), + success: () => BlocProvider.of(context).add(const SessionStatusEvent.getStatus()), + ), + ), + BlocListener( + listener: (BuildContext context, SessionStatusState state) => state.whenOrNull( + failure: (Failure failure) => context.navigator.pushReplacementNamed(AppRoutes.login), + loggedIn: (_) => context.navigator.pushReplacementNamed(AppRoutes.root), + ), + ), + ], + child: const Scaffold( + body: Center( + child: CircularProgressIndicator(), + ), + ), + ), + ); +} diff --git a/lib/features/authentication/presentation/widgets/login_form_widget.dart b/lib/features/authentication/presentation/widgets/login_form_widget.dart new file mode 100644 index 0000000..34b7298 --- /dev/null +++ b/lib/features/authentication/presentation/widgets/login_form_widget.dart @@ -0,0 +1,234 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:piwigo_ng/core/errors/failures.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/presentation/widgets/animated/collapsible_widget.dart'; +import 'package:piwigo_ng/core/presentation/widgets/buttons/custom_button.dart'; +import 'package:piwigo_ng/core/presentation/widgets/form/custom_text_field.dart'; +import 'package:piwigo_ng/core/utils/app_strings.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/core/utils/validators/field_validator.dart'; +import 'package:piwigo_ng/features/authentication/domain/usecases/login_use_case.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/login/login_bloc.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; + +class LoginFormView extends StatefulWidget { + const LoginFormView({ + super.key, + this.autoLogin = false, + }); + + final bool autoLogin; + + @override + State createState() => _LoginFormViewState(); +} + +class _LoginFormViewState extends State { + final GlobalKey _formKey = GlobalKey(); + final TextEditingController _urlController = TextEditingController(); + final TextEditingController _usernameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + + UrlSchemeEnum _scheme = UrlSchemeEnum.https; + bool _showPassword = false; + bool _isGuest = false; + + @override + void dispose() { + _urlController.dispose(); + _usernameController.dispose(); + _passwordController.dispose(); + super.dispose(); + } + + bool get _isSecured => _scheme == UrlSchemeEnum.https; + + void _onLogin() { + if (!(_formKey.currentState?.validate() == true)) return; + + // Build uri from scheme and authority + late Uri url; + if (_scheme == UrlSchemeEnum.http) { + url = Uri.http(_urlController.text); + } else { + url = Uri.https(_urlController.text); + } + + if (_isGuest) { + BlocProvider.of(context).add( + LoginEvent.loginGuest(url: url), + ); + } else { + BlocProvider.of(context).add( + LoginEvent.loginUser( + url: url, + username: _usernameController.text, + password: _passwordController.text, + ), + ); + } + } + + void _switchProtocol() { + setState(() { + if (_scheme == UrlSchemeEnum.https) { + _scheme = UrlSchemeEnum.http; + } else { + _scheme = UrlSchemeEnum.https; + } + }); + } + + @override + Widget build(BuildContext context) { + return AutofillGroup( + child: DefaultTabController( + length: 2, + child: Form( + key: _formKey, + child: BlocBuilder( + builder: (BuildContext context, SessionStatusState sessionStatusState) => + BlocConsumer( + listener: (BuildContext context, LoginState loginState) => loginState.whenOrNull( + failure: (Failure failure) => ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(failure.getMessage(context)), + ), + ), + success: () => BlocProvider.of(context).add(const SessionStatusEvent.getStatus()), + ), + builder: (BuildContext context, LoginState state) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingMedium, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + decoration: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + ), + color: context.theme.inputDecorationTheme.fillColor, + ), + child: TabBar( + indicator: ShapeDecoration( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UIConstants.radiusMedium), + ), + color: context.theme.colorScheme.secondary, + ), + indicatorSize: TabBarIndicatorSize.tab, + tabs: const [ + Tab(text: 'User'), // todo: localization + Tab(text: 'Guest'), // todo: localization + ], + onTap: (int index) => setState(() => _isGuest = index == 1), + ), + ), + const SizedBox(height: UIConstants.paddingMedium), + CustomTextField( + padding: const EdgeInsets.symmetric( + vertical: UIConstants.paddingMedium, + horizontal: UIConstants.paddingXSmall, + ).copyWith(left: 0.0), + controller: _urlController, + textInputAction: TextInputAction.next, + autofillHints: const [AutofillHints.url], + prefix: _securedPrefix, + hint: AppStrings.piwigoUrlSample, + validators: [ + RequiredValidator(), + UrlValidator(), + ], + ), + CollapsibleWidget( + expanded: !_isGuest, + child: Column( + children: [ + const SizedBox(height: UIConstants.paddingXSmall), + CustomTextField( + controller: _usernameController, + textInputAction: TextInputAction.next, + autofillHints: const [AutofillHints.username], + hint: context.localizations.loginHTTPuser_placeholder, + prefix: const Icon(Icons.person), + canErase: true, + validators: [ + if (!_isGuest) RequiredValidator(), + ], + ), + const SizedBox(height: UIConstants.paddingXSmall), + CustomTextField( + controller: _passwordController, + textInputAction: TextInputAction.done, + autofillHints: const [AutofillHints.password], + obscureText: !_showPassword, + hint: context.localizations.loginHTTPpwd_placeholder, + prefix: GestureDetector( + onTap: () => setState(() { + _showPassword = !_showPassword; + }), + child: Icon(_showPassword ? Icons.lock_open : Icons.lock), + ), + canErase: true, + onFieldSubmitted: (String value) { + FocusScope.of(context).unfocus(); + _onLogin(); + }, + validators: [ + if (!_isGuest) RequiredValidator(), + ], + ), + ], + ), + ), + const SizedBox(height: UIConstants.paddingMedium), + CustomButton( + onTap: _onLogin, + text: context.localizations.login, + isLoading: state.isLoading || sessionStatusState.isLoading, + ), + ], + ), + ); + }, + ), + ), + ), + ), + ); + } + + Widget get _securedPrefix { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _switchProtocol, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.public), + const SizedBox(width: UIConstants.paddingXSmall), + Stack( + clipBehavior: Clip.none, + children: [ + Text( + '${_isSecured ? AppStrings.https : AppStrings.http}${AppStrings.hostSeparator}', + style: Theme.of(context).textTheme.bodyMedium, + ), + Positioned( + top: Theme.of(context).textTheme.bodyMedium?.fontSize, + child: Text( + !_isSecured ? AppStrings.https : AppStrings.http, + style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontSize: 11), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/features/images/data/enums/image_size_enum.dart b/lib/features/images/data/enums/image_size_enum.dart new file mode 100644 index 0000000..8a2f1cd --- /dev/null +++ b/lib/features/images/data/enums/image_size_enum.dart @@ -0,0 +1,46 @@ +import 'package:json_annotation/json_annotation.dart'; + +@JsonEnum(valueField: 'value') +enum ImageSizeEnum { + square('square'), + thumb('thumb'), + xxSmall('2small'), + xSmall('xsmall'), + small('small'), + medium('medium'), + large('large'), + xLarge('xlarge'), + xxLarge('xxlarge'), + full('full'); + + const ImageSizeEnum(this.value); + + final String value; + + static ImageSizeEnum fromJson(String json) { + switch (json) { + case 'square': + return ImageSizeEnum.square; + case 'thumb': + return ImageSizeEnum.thumb; + case '2small': + return ImageSizeEnum.xxSmall; + case 'xsmall': + return ImageSizeEnum.xSmall; + case 'small': + return ImageSizeEnum.small; + case 'medium': + return ImageSizeEnum.medium; + case 'large': + return ImageSizeEnum.large; + case 'xlarge': + return ImageSizeEnum.xLarge; + case 'xxlarge': + return ImageSizeEnum.xxLarge; + case 'full': + return ImageSizeEnum.full; + default: + throw UnimplementedError(); + } + } +} diff --git a/lib/features/images/data/models/image_derivative_model.dart b/lib/features/images/data/models/image_derivative_model.dart new file mode 100644 index 0000000..3482e11 --- /dev/null +++ b/lib/features/images/data/models/image_derivative_model.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_derivative_entity.dart'; + +part 'image_derivative_model.g.dart'; + +@JsonSerializable() +class ImageDerivativeModel extends ImageDerivativeEntity { + const ImageDerivativeModel({ + required super.url, + super.width, + super.height, + }); + + //region Serialization + factory ImageDerivativeModel.fromJson(Map json) => _$ImageDerivativeModelFromJson(json); + + Map toJson() => _$ImageDerivativeModelToJson(this); +//endregion +} diff --git a/lib/features/images/data/models/image_derivative_model.g.dart b/lib/features/images/data/models/image_derivative_model.g.dart new file mode 100644 index 0000000..04de74d --- /dev/null +++ b/lib/features/images/data/models/image_derivative_model.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_derivative_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageDerivativeModel _$ImageDerivativeModelFromJson(Map json) => ImageDerivativeModel( + url: json['url'] as String, + width: (json['width'] as num?)?.toDouble(), + height: (json['height'] as num?)?.toDouble(), + ); + +Map _$ImageDerivativeModelToJson(ImageDerivativeModel instance) => { + 'url': instance.url, + 'width': instance.width, + 'height': instance.height, + }; diff --git a/lib/features/images/data/models/image_model.dart b/lib/features/images/data/models/image_model.dart new file mode 100644 index 0000000..ebd5fec --- /dev/null +++ b/lib/features/images/data/models/image_model.dart @@ -0,0 +1,70 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/features/images/data/models/image_derivative_model.dart'; +import 'package:piwigo_ng/features/images/data/models/image_parent_model.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_entity.dart'; + +part 'image_model.g.dart'; + +@JsonSerializable() +class ImageModel extends ImageEntity { + const ImageModel({ + required super.id, + super.width, + super.height, + super.hit = 0, + super.favorite = false, + super.file = '', + required super.name, + super.comment, + super.dateCreation, + super.dateAvailable, + super.pageUrl, + super.elementUrl, + super.downloadUrl, + required ImageDerivativesModel derivatives, + List parents = const [], + }) : super( + derivatives: derivatives, + parents: parents, + ); + + //region Serialization + factory ImageModel.fromJson(Map json) => _$ImageModelFromJson(json); + + Map toJson() => _$ImageModelToJson(this); +//endregion +} + +@JsonSerializable() +class ImageDerivativesModel extends ImageDerivativesEntity { + const ImageDerivativesModel({ + required ImageDerivativeModel square, + required ImageDerivativeModel thumb, + ImageDerivativeModel? xxSmall, + ImageDerivativeModel? xSmall, + ImageDerivativeModel? small, + required ImageDerivativeModel medium, + ImageDerivativeModel? large, + ImageDerivativeModel? xLarge, + ImageDerivativeModel? xxLarge, + ImageDerivativeModel? full, + }) : super( + square: square, + thumb: thumb, + xxSmall: xxSmall, + xSmall: xSmall, + small: small, + medium: medium, + large: large, + xLarge: xLarge, + xxLarge: xxLarge, + full: full, + ); + + //region Serialization + factory ImageDerivativesModel.fromJson(Map json) => _$ImageDerivativesModelFromJson(json); + + @override + Map toJson() => _$ImageDerivativesModelToJson(this); +//endregion +} diff --git a/lib/features/images/data/models/image_model.g.dart b/lib/features/images/data/models/image_model.g.dart new file mode 100644 index 0000000..0a5da3b --- /dev/null +++ b/lib/features/images/data/models/image_model.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageModel _$ImageModelFromJson(Map json) => ImageModel( + id: json['id'] as int, + width: json['width'] as int?, + height: json['height'] as int?, + hit: json['hit'] as int? ?? 0, + favorite: json['favorite'] as bool? ?? false, + file: json['file'] as String? ?? '', + name: json['name'] as String, + comment: json['comment'] as String?, + dateCreation: json['date_creation'] as String?, + dateAvailable: json['date_available'] as String?, + pageUrl: json['page_url'] as String?, + elementUrl: json['element_url'] as String?, + downloadUrl: json['download_url'] as String?, + derivatives: ImageDerivativesModel.fromJson(json['derivatives'] as Map), + parents: (json['categories'] as List?) + ?.map((e) => ImageParentModel.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$ImageModelToJson(ImageModel instance) => { + 'id': instance.id, + 'width': instance.width, + 'height': instance.height, + 'hit': instance.hit, + 'favorite': instance.favorite, + 'file': instance.file, + 'name': instance.name, + 'comment': instance.comment, + 'date_creation': instance.dateCreation, + 'date_available': instance.dateAvailable, + 'page_url': instance.pageUrl, + 'element_url': instance.elementUrl, + 'download_url': instance.downloadUrl, + }; + +ImageDerivativesModel _$ImageDerivativesModelFromJson(Map json) => ImageDerivativesModel( + square: ImageDerivativeModel.fromJson(json['square'] as Map), + thumb: ImageDerivativeModel.fromJson(json['thumb'] as Map), + xxSmall: json['2small'] == null ? null : ImageDerivativeModel.fromJson(json['2small'] as Map), + xSmall: json['xsmall'] == null ? null : ImageDerivativeModel.fromJson(json['xsmall'] as Map), + small: json['small'] == null ? null : ImageDerivativeModel.fromJson(json['small'] as Map), + medium: ImageDerivativeModel.fromJson(json['medium'] as Map), + large: json['large'] == null ? null : ImageDerivativeModel.fromJson(json['large'] as Map), + xLarge: json['xlarge'] == null ? null : ImageDerivativeModel.fromJson(json['xlarge'] as Map), + xxLarge: json['xxlarge'] == null ? null : ImageDerivativeModel.fromJson(json['xxlarge'] as Map), + full: json['full'] == null ? null : ImageDerivativeModel.fromJson(json['full'] as Map), + ); + +Map _$ImageDerivativesModelToJson(ImageDerivativesModel instance) => { + 'square': instance.square, + 'thumb': instance.thumb, + '2small': instance.xxSmall, + 'xsmall': instance.xSmall, + 'small': instance.small, + 'medium': instance.medium, + 'large': instance.large, + 'xlarge': instance.xLarge, + 'xxlarge': instance.xxLarge, + 'full': instance.full, + }; diff --git a/lib/features/images/data/models/image_parent_model.dart b/lib/features/images/data/models/image_parent_model.dart new file mode 100644 index 0000000..f45c9c8 --- /dev/null +++ b/lib/features/images/data/models/image_parent_model.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_parent_entity.dart'; + +part 'image_parent_model.g.dart'; + +@JsonSerializable() +class ImageParentModel extends ImageParentEntity { + ImageParentModel({ + required super.id, + super.url, + super.pageUrl, + }); + + //region Serialization + factory ImageParentModel.fromJson(Map json) => _$ImageParentModelFromJson(json); + + Map toJson() => _$ImageParentModelToJson(this); +//endregion +} diff --git a/lib/features/images/data/models/image_parent_model.g.dart b/lib/features/images/data/models/image_parent_model.g.dart new file mode 100644 index 0000000..c3fe419 --- /dev/null +++ b/lib/features/images/data/models/image_parent_model.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_parent_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageParentModel _$ImageParentModelFromJson(Map json) => ImageParentModel( + id: json['id'] as int, + url: json['url'] as String?, + pageUrl: json['page_url'] as String?, + ); + +Map _$ImageParentModelToJson(ImageParentModel instance) => { + 'id': instance.id, + 'url': instance.url, + 'page_url': instance.pageUrl, + }; diff --git a/lib/features/images/domain/entities/image_derivative_entity.dart b/lib/features/images/domain/entities/image_derivative_entity.dart new file mode 100644 index 0000000..e9e6932 --- /dev/null +++ b/lib/features/images/domain/entities/image_derivative_entity.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'image_derivative_entity.g.dart'; + +@JsonSerializable() +class ImageDerivativeEntity { + const ImageDerivativeEntity({ + required this.url, + this.width, + this.height, + }); + + factory ImageDerivativeEntity.fromJson(Map json) => _$ImageDerivativeEntityFromJson(json); + + Map toJson() => _$ImageDerivativeEntityToJson(this); + + final String url; + final double? width; + final double? height; +} diff --git a/lib/features/images/domain/entities/image_derivative_entity.g.dart b/lib/features/images/domain/entities/image_derivative_entity.g.dart new file mode 100644 index 0000000..88148c8 --- /dev/null +++ b/lib/features/images/domain/entities/image_derivative_entity.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_derivative_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageDerivativeEntity _$ImageDerivativeEntityFromJson(Map json) => ImageDerivativeEntity( + url: json['url'] as String, + width: (json['width'] as num?)?.toDouble(), + height: (json['height'] as num?)?.toDouble(), + ); + +Map _$ImageDerivativeEntityToJson(ImageDerivativeEntity instance) => { + 'url': instance.url, + 'width': instance.width, + 'height': instance.height, + }; diff --git a/lib/features/images/domain/entities/image_entity.dart b/lib/features/images/domain/entities/image_entity.dart new file mode 100644 index 0000000..e891e93 --- /dev/null +++ b/lib/features/images/domain/entities/image_entity.dart @@ -0,0 +1,148 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:mime_type/mime_type.dart'; +import 'package:piwigo_ng/features/images/data/enums/image_size_enum.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_derivative_entity.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_parent_entity.dart'; + +part 'image_entity.g.dart'; + +@CopyWith() +@JsonSerializable() +class ImageEntity { + const ImageEntity({ + required this.id, + this.width, + this.height, + this.hit = 0, + this.favorite = false, + this.file = '', + required this.name, + this.comment, + this.dateCreation, + this.dateAvailable, + this.pageUrl, + this.elementUrl, + this.downloadUrl, + required this.derivatives, + this.parents = const [], + }); + + factory ImageEntity.fromJson(Map json) => _$ImageEntityFromJson(json); + + Map toJson() => _$ImageEntityToJson(this); + + final int id; + + final int? width; + + final int? height; + + final int hit; + + final bool favorite; + + final String file; + + final String name; + + final String? comment; + + @JsonKey(name: 'date_creation') + final String? dateCreation; + + @JsonKey(name: 'date_available') + final String? dateAvailable; + + @JsonKey(name: 'page_url') + final String? pageUrl; + + @JsonKey(name: 'element_url') + final String? elementUrl; + + @JsonKey(name: 'download_url') + final String? downloadUrl; + + @JsonKey(includeToJson: false) + final ImageDerivativesEntity derivatives; + + @JsonKey(name: 'categories', includeToJson: false) + final List parents; + + ImageDerivativeEntity getDerivative(ImageSizeEnum size) => derivatives.mapToSize(size) ?? derivatives.medium; + + bool get isVideo { + String? mimeType = mime(file) ?? mime(elementUrl) ?? mime(derivatives.medium.url); + return mimeType != null && mimeType.startsWith('video'); + } +} + +@CopyWith() +@JsonSerializable() +class ImageDerivativesEntity { + const ImageDerivativesEntity({ + required this.square, + required this.thumb, + this.xxSmall, + this.xSmall, + this.small, + required this.medium, + this.large, + this.xLarge, + this.xxLarge, + this.full, + }); + + factory ImageDerivativesEntity.fromJson(Map json) => _$ImageDerivativesEntityFromJson(json); + + Map toJson() => _$ImageDerivativesEntityToJson(this); + + final ImageDerivativeEntity square; + + final ImageDerivativeEntity thumb; + + @JsonKey(name: '2small') + final ImageDerivativeEntity? xxSmall; + + @JsonKey(name: 'xsmall') + final ImageDerivativeEntity? xSmall; + + final ImageDerivativeEntity? small; + + final ImageDerivativeEntity medium; + + final ImageDerivativeEntity? large; + + @JsonKey(name: 'xlarge') + final ImageDerivativeEntity? xLarge; + + @JsonKey(name: 'xxlarge') + final ImageDerivativeEntity? xxLarge; + + final ImageDerivativeEntity? full; + + ImageDerivativeEntity? mapToSize(ImageSizeEnum size) { + switch (size) { + case ImageSizeEnum.square: + return square; + case ImageSizeEnum.thumb: + return thumb; + case ImageSizeEnum.xxSmall: + return xxSmall; + case ImageSizeEnum.xSmall: + return xSmall; + case ImageSizeEnum.small: + return small; + case ImageSizeEnum.medium: + return medium; + case ImageSizeEnum.large: + return large; + case ImageSizeEnum.xLarge: + return xLarge; + case ImageSizeEnum.xxLarge: + return xxLarge; + case ImageSizeEnum.full: + return full; + } + } +} diff --git a/lib/features/images/domain/entities/image_entity.g.dart b/lib/features/images/domain/entities/image_entity.g.dart new file mode 100644 index 0000000..9745106 --- /dev/null +++ b/lib/features/images/domain/entities/image_entity.g.dart @@ -0,0 +1,70 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageEntity _$ImageEntityFromJson(Map json) => ImageEntity( + id: json['id'] as int, + width: json['width'] as int?, + height: json['height'] as int?, + hit: json['hit'] as int? ?? 0, + favorite: json['favorite'] as bool? ?? false, + file: json['file'] as String? ?? '', + name: json['name'] as String, + comment: json['comment'] as String?, + dateCreation: json['date_creation'] as String?, + dateAvailable: json['date_available'] as String?, + pageUrl: json['page_url'] as String?, + elementUrl: json['element_url'] as String?, + downloadUrl: json['download_url'] as String?, + derivatives: ImageDerivativesEntity.fromJson(json['derivatives'] as Map), + parents: (json['categories'] as List?) + ?.map((e) => ImageParentEntity.fromJson(e as Map)) + .toList() ?? + const [], + ); + +Map _$ImageEntityToJson(ImageEntity instance) => { + 'id': instance.id, + 'width': instance.width, + 'height': instance.height, + 'hit': instance.hit, + 'favorite': instance.favorite, + 'file': instance.file, + 'name': instance.name, + 'comment': instance.comment, + 'date_creation': instance.dateCreation, + 'date_available': instance.dateAvailable, + 'page_url': instance.pageUrl, + 'element_url': instance.elementUrl, + 'download_url': instance.downloadUrl, + }; + +ImageDerivativesEntity _$ImageDerivativesEntityFromJson(Map json) => ImageDerivativesEntity( + square: ImageDerivativeEntity.fromJson(json['square'] as Map), + thumb: ImageDerivativeEntity.fromJson(json['thumb'] as Map), + xxSmall: json['2small'] == null ? null : ImageDerivativeEntity.fromJson(json['2small'] as Map), + xSmall: json['xsmall'] == null ? null : ImageDerivativeEntity.fromJson(json['xsmall'] as Map), + small: json['small'] == null ? null : ImageDerivativeEntity.fromJson(json['small'] as Map), + medium: ImageDerivativeEntity.fromJson(json['medium'] as Map), + large: json['large'] == null ? null : ImageDerivativeEntity.fromJson(json['large'] as Map), + xLarge: json['xlarge'] == null ? null : ImageDerivativeEntity.fromJson(json['xlarge'] as Map), + xxLarge: json['xxlarge'] == null ? null : ImageDerivativeEntity.fromJson(json['xxlarge'] as Map), + full: json['full'] == null ? null : ImageDerivativeEntity.fromJson(json['full'] as Map), + ); + +Map _$ImageDerivativesEntityToJson(ImageDerivativesEntity instance) => { + 'square': instance.square, + 'thumb': instance.thumb, + '2small': instance.xxSmall, + 'xsmall': instance.xSmall, + 'small': instance.small, + 'medium': instance.medium, + 'large': instance.large, + 'xlarge': instance.xLarge, + 'xxlarge': instance.xxLarge, + 'full': instance.full, + }; diff --git a/lib/features/images/domain/entities/image_parent_entity.dart b/lib/features/images/domain/entities/image_parent_entity.dart new file mode 100644 index 0000000..1b17c4f --- /dev/null +++ b/lib/features/images/domain/entities/image_parent_entity.dart @@ -0,0 +1,23 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'image_parent_entity.g.dart'; + +@JsonSerializable() +class ImageParentEntity { + ImageParentEntity({ + required this.id, + this.url, + this.pageUrl, + }); + + factory ImageParentEntity.fromJson(Map json) => _$ImageParentEntityFromJson(json); + + Map toJson() => _$ImageParentEntityToJson(this); + + final int id; + + final String? url; + + @JsonKey(name: 'page_url') + final String? pageUrl; +} diff --git a/lib/features/images/domain/entities/image_parent_entity.g.dart b/lib/features/images/domain/entities/image_parent_entity.g.dart new file mode 100644 index 0000000..140631f --- /dev/null +++ b/lib/features/images/domain/entities/image_parent_entity.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_parent_entity.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageParentEntity _$ImageParentEntityFromJson(Map json) => ImageParentEntity( + id: json['id'] as int, + url: json['url'] as String?, + pageUrl: json['page_url'] as String?, + ); + +Map _$ImageParentEntityToJson(ImageParentEntity instance) => { + 'id': instance.id, + 'url': instance.url, + 'page_url': instance.pageUrl, + }; diff --git a/lib/features/images/presentation/pages/image_search_page.dart b/lib/features/images/presentation/pages/image_search_page.dart new file mode 100644 index 0000000..725361b --- /dev/null +++ b/lib/features/images/presentation/pages/image_search_page.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/components/fields/app_field.dart'; +import 'package:piwigo_ng/core/utils/constants/hero_tags.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; + +class ImageSearchPage extends StatefulWidget { + const ImageSearchPage({super.key}); + + @override + State createState() => _ImageSearchPageState(); +} + +class _ImageSearchPageState extends State { + final TextEditingController _searchController = TextEditingController(); + final FocusNode _searchFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) => Scaffold( + appBar: _buildSearchBar(context), + ); + + PreferredSizeWidget _buildSearchBar(BuildContext context) => AppBar( + titleSpacing: 0.0, + title: Hero( + tag: HeroTags.imageSearchField, + flightShuttleBuilder: ( + BuildContext flightContext, + Animation animation, + HeroFlightDirection flightDirection, + BuildContext fromHeroContext, + BuildContext toHeroContext, + ) { + animation.addStatusListener((AnimationStatus status) { + if (status == AnimationStatus.completed) { + // the end of hero animation end + Future.delayed( + const Duration(milliseconds: 1), + () => _searchFocusNode.requestFocus(), + ); + } + }); + + final Hero toHero = toHeroContext.widget as Hero; + + return AnimatedBuilder( + animation: animation, + builder: (BuildContext context, Widget? child) { + return toHero.child; + }, + ); + }, + child: Material( + color: Colors.transparent, + child: AppField( + controller: _searchController, + focusNode: _searchFocusNode, + padding: const EdgeInsets.symmetric( + vertical: UIConstants.paddingXSmall, + horizontal: UIConstants.paddingSmall, + ), + prefix: const Icon(Icons.search), + hint: "Search...", + ), + ), + ), + actions: [ + IconButton( + onPressed: () {}, + icon: const Icon(Icons.more_vert), + ), + ], + ); +} diff --git a/lib/features/images/presentation/widgets/image_card_widget.dart b/lib/features/images/presentation/widgets/image_card_widget.dart new file mode 100644 index 0000000..fddeac6 --- /dev/null +++ b/lib/features/images/presentation/widgets/image_card_widget.dart @@ -0,0 +1,160 @@ +import 'package:auto_size_text/auto_size_text.dart'; +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/extensions/album_preferences_extension.dart'; +import 'package:piwigo_ng/core/presentation/widgets/images/custom_network_image.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/features/images/domain/entities/image_entity.dart'; +import 'package:piwigo_ng/utils/resources.dart'; + +class ImageCardWidget extends StatelessWidget with AppPreferencesMixin { + const ImageCardWidget({ + super.key, + this.onPressed, + this.selected, + required this.image, + this.onLongPress, + }); + + final Function()? onPressed; + final Function()? onLongPress; + final bool? selected; + final ImageEntity image; + + static const Duration _selectDuration = Duration(milliseconds: 200); + static const Curve _selectCurve = Curves.easeInOut; + static const double _overlayOpacity = 0.5; + static const double _selectIconSize = 20.0; + + String? get _imageUrl => image.getDerivative(prefs.getImageThumbnailSize).url; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onPressed, + onLongPress: onLongPress, + child: ClipRRect( + borderRadius: BorderRadius.circular(UIConstants.radiusSmall), + child: Stack( + fit: StackFit.expand, + children: [ + CustomNetworkImage( + imageUrl: _imageUrl, + ), + _buildInfoOverlay(context), + ..._buildSelectOverlay(context), + ], + ), + ), + ); + } + + Widget _buildInfoOverlay(BuildContext context) => Positioned( + bottom: 0.0, + right: 0.0, + left: 0.0, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + if (image.isVideo) + Padding( + padding: const EdgeInsets.all(2.0), + child: Icon( + Icons.local_movies, + color: AppColors.white, + size: 12, + shadows: AppShadows.icon, + ), + ), + if (image.favorite) + Padding( + padding: const EdgeInsets.all(2.0), + child: Icon( + Icons.favorite, + color: AppColors.white, + size: 12, + shadows: AppShadows.icon, + ), + ), + ], + ), + if (prefs.getShowThumbnailTitle) + Container( + padding: const EdgeInsets.all(2.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.black, + Colors.black.withOpacity(0), + ], + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + ), + ), + child: AutoSizeText( + image.name, + maxLines: 1, + maxFontSize: 14, + minFontSize: 8, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: const TextStyle(color: AppColors.white, fontSize: 14), + ), + ), + ], + ), + ); + + List _buildSelectOverlay(BuildContext context) => [ + AnimatedOpacity( + duration: _selectDuration, + curve: _selectCurve, + opacity: selected == true ? _overlayOpacity : 0.0, + child: const Material( + color: Colors.black, + child: Center(), + ), + ), + Positioned( + top: UIConstants.paddingTiny, + right: UIConstants.paddingTiny, + child: Stack( + children: [ + AnimatedScale( + duration: _selectDuration, + curve: _selectCurve, + scale: selected == false ? 1 : 0, + child: AnimatedOpacity( + duration: _selectDuration, + curve: _selectCurve, + opacity: selected == false ? 1 : 0, + child: Container( + height: _selectIconSize, + width: _selectIconSize, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: Theme.of(context).primaryColor), + color: Colors.black.withOpacity(0.3), + ), + ), + ), + ), + AnimatedScale( + duration: _selectDuration, + curve: _selectCurve, + scale: selected == true ? 1 : 0, + child: AnimatedOpacity( + duration: _selectDuration, + curve: _selectCurve, + opacity: selected == true ? 1 : 0, + child: const Icon(Icons.check_circle, size: _selectIconSize), + ), + ), + ], + ), + ), + ]; +} diff --git a/lib/features/settings/data/datasources/settings_local_datasource.dart b/lib/features/settings/data/datasources/settings_local_datasource.dart new file mode 100644 index 0000000..24293e4 --- /dev/null +++ b/lib/features/settings/data/datasources/settings_local_datasource.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +abstract class SettingsLocalDatasource { + const SettingsLocalDatasource(); + + ThemeMode getThemeMode(); +} diff --git a/lib/features/settings/data/datasources/settings_local_datasource.impl.dart b/lib/features/settings/data/datasources/settings_local_datasource.impl.dart new file mode 100644 index 0000000..a98dade --- /dev/null +++ b/lib/features/settings/data/datasources/settings_local_datasource.impl.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/core/extensions/theme_mode_extension.dart'; +import 'package:piwigo_ng/core/utils/constants/local_key_constants.dart'; +import 'package:piwigo_ng/features/settings/data/datasources/settings_local_datasource.dart'; + +class SettingsLocalDatasourceImpl extends SettingsLocalDatasource with AppPreferencesMixin { + const SettingsLocalDatasourceImpl(); + + @override + ThemeMode getThemeMode() => themeModeFromJson(prefs.instance.getString(LocalKeyConstants.themeKey)); +} diff --git a/lib/features/settings/data/repositories/settings_repository.impl.dart b/lib/features/settings/data/repositories/settings_repository.impl.dart new file mode 100644 index 0000000..0fc27f9 --- /dev/null +++ b/lib/features/settings/data/repositories/settings_repository.impl.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/features/settings/data/datasources/settings_local_datasource.dart'; +import 'package:piwigo_ng/features/settings/data/datasources/settings_local_datasource.impl.dart'; +import 'package:piwigo_ng/features/settings/domain/repositories/settings_repository.dart'; + +class SettingsRepositoryImpl extends SettingsRepository { + const SettingsRepositoryImpl(); + + final SettingsLocalDatasource _localDatasource = const SettingsLocalDatasourceImpl(); + + @override + ThemeMode getThemeMode() => _localDatasource.getThemeMode(); +} diff --git a/lib/features/settings/domain/repositories/settings_repository.dart b/lib/features/settings/domain/repositories/settings_repository.dart new file mode 100644 index 0000000..7806889 --- /dev/null +++ b/lib/features/settings/domain/repositories/settings_repository.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +abstract class SettingsRepository { + const SettingsRepository(); + + ThemeMode getThemeMode(); +} diff --git a/lib/features/settings/domain/usecases/get_user_theme_mode_use_case.dart b/lib/features/settings/domain/usecases/get_user_theme_mode_use_case.dart new file mode 100644 index 0000000..ecab7ef --- /dev/null +++ b/lib/features/settings/domain/usecases/get_user_theme_mode_use_case.dart @@ -0,0 +1,11 @@ +import 'package:flutter/material.dart'; +import 'package:piwigo_ng/features/settings/data/repositories/settings_repository.impl.dart'; +import 'package:piwigo_ng/features/settings/domain/repositories/settings_repository.dart'; + +class GetUserThemeModeUseCase { + const GetUserThemeModeUseCase(); + + final SettingsRepository _repository = const SettingsRepositoryImpl(); + + ThemeMode execute() => _repository.getThemeMode(); +} diff --git a/lib/features/settings/presentation/blocs/theme/current_theme_bloc.dart b/lib/features/settings/presentation/blocs/theme/current_theme_bloc.dart new file mode 100644 index 0000000..30b4a37 --- /dev/null +++ b/lib/features/settings/presentation/blocs/theme/current_theme_bloc.dart @@ -0,0 +1,57 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:piwigo_ng/core/data/datasources/local/preferences_datasource.dart'; +import 'package:piwigo_ng/features/settings/domain/usecases/get_user_theme_mode_use_case.dart'; + +part 'current_theme_bloc.freezed.dart'; + +class CurrentThemeBloc extends Bloc with AppPreferencesMixin { + CurrentThemeBloc() : super(const CurrentThemeState(mode: ThemeMode.system)) { + on(_onInitTheme); + on(_onChangeTheme); + } + + final GetUserThemeModeUseCase _getUserThemeModeUseCase = const GetUserThemeModeUseCase(); + + Future _onInitTheme( + InitThemeEvent event, + Emitter emit, + ) async { + ThemeMode mode = _getUserThemeModeUseCase.execute(); + emit(CurrentThemeState(mode: mode)); + } + + Future _onChangeTheme( + ChangeThemeEvent event, + Emitter emit, + ) async { + emit(CurrentThemeState(mode: event.mode)); + } +} + +class CurrentThemeEvent extends Equatable { + const CurrentThemeEvent(); + + @override + List get props => []; +} + +class InitThemeEvent extends CurrentThemeEvent {} + +class ChangeThemeEvent extends CurrentThemeEvent { + const ChangeThemeEvent({required this.mode}); + + final ThemeMode mode; + + @override + List get props => [mode]; +} + +@freezed +class CurrentThemeState with _$CurrentThemeState { + const factory CurrentThemeState({ + required ThemeMode mode, + }) = _CurrentThemeState; +} diff --git a/lib/features/settings/presentation/blocs/theme/current_theme_bloc.freezed.dart b/lib/features/settings/presentation/blocs/theme/current_theme_bloc.freezed.dart new file mode 100644 index 0000000..e5081f0 --- /dev/null +++ b/lib/features/settings/presentation/blocs/theme/current_theme_bloc.freezed.dart @@ -0,0 +1,129 @@ +// 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 'current_theme_bloc.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 _$CurrentThemeState { + ThemeMode get mode => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $CurrentThemeStateCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CurrentThemeStateCopyWith<$Res> { + factory $CurrentThemeStateCopyWith(CurrentThemeState value, $Res Function(CurrentThemeState) then) = + _$CurrentThemeStateCopyWithImpl<$Res, CurrentThemeState>; + + @useResult + $Res call({ThemeMode mode}); +} + +/// @nodoc +class _$CurrentThemeStateCopyWithImpl<$Res, $Val extends CurrentThemeState> + implements $CurrentThemeStateCopyWith<$Res> { + _$CurrentThemeStateCopyWithImpl(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? mode = null, + }) { + return _then(_value.copyWith( + mode: null == mode + ? _value.mode + : mode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CurrentThemeStateImplCopyWith<$Res> implements $CurrentThemeStateCopyWith<$Res> { + factory _$$CurrentThemeStateImplCopyWith(_$CurrentThemeStateImpl value, $Res Function(_$CurrentThemeStateImpl) then) = + __$$CurrentThemeStateImplCopyWithImpl<$Res>; + + @override + @useResult + $Res call({ThemeMode mode}); +} + +/// @nodoc +class __$$CurrentThemeStateImplCopyWithImpl<$Res> extends _$CurrentThemeStateCopyWithImpl<$Res, _$CurrentThemeStateImpl> + implements _$$CurrentThemeStateImplCopyWith<$Res> { + __$$CurrentThemeStateImplCopyWithImpl(_$CurrentThemeStateImpl _value, $Res Function(_$CurrentThemeStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? mode = null, + }) { + return _then(_$CurrentThemeStateImpl( + mode: null == mode + ? _value.mode + : mode // ignore: cast_nullable_to_non_nullable + as ThemeMode, + )); + } +} + +/// @nodoc + +class _$CurrentThemeStateImpl implements _CurrentThemeState { + const _$CurrentThemeStateImpl({required this.mode}); + + @override + final ThemeMode mode; + + @override + String toString() { + return 'CurrentThemeState(mode: $mode)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CurrentThemeStateImpl && + (identical(other.mode, mode) || other.mode == mode)); + } + + @override + int get hashCode => Object.hash(runtimeType, mode); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$CurrentThemeStateImplCopyWith<_$CurrentThemeStateImpl> get copyWith => + __$$CurrentThemeStateImplCopyWithImpl<_$CurrentThemeStateImpl>(this, _$identity); +} + +abstract class _CurrentThemeState implements CurrentThemeState { + const factory _CurrentThemeState({required final ThemeMode mode}) = _$CurrentThemeStateImpl; + + @override + ThemeMode get mode; + + @override + @JsonKey(ignore: true) + _$$CurrentThemeStateImplCopyWith<_$CurrentThemeStateImpl> get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart new file mode 100644 index 0000000..b633d30 --- /dev/null +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:piwigo_ng/core/extensions/build_context_extension.dart'; +import 'package:piwigo_ng/core/presentation/widgets/buttons/custom_button.dart'; +import 'package:piwigo_ng/core/utils/constants/ui_constants.dart'; +import 'package:piwigo_ng/features/authentication/data/enums/user_status_enum.dart'; +import 'package:piwigo_ng/features/authentication/presentation/blocs/session_status/session_status_bloc.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage({super.key}); + + void _onLogout(BuildContext context) => + BlocProvider.of(context).add(const SessionStatusEvent.logout()); + + @override + Widget build(BuildContext context) => BlocBuilder( + builder: (BuildContext context, SessionStatusState sessionState) => Scaffold( + body: CustomScrollView( + slivers: [ + SliverAppBar( + pinned: true, + title: Text(context.localizations.tabBar_preferences), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: UIConstants.paddingMedium, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + CustomButton( + onTap: () => _onLogout(context), + isLoading: sessionState.isLoading, + text: sessionState.userStatus.isGuest + ? context.localizations.login + : context.localizations.settings_logout, + ), + ], + ), + ), + ), + ], + ), + ), + ); +} diff --git a/lib/main.dart b/lib/main.dart index 18fceca..575a16c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,43 +2,27 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:piwigo_ng/app.dart'; +import 'package:piwigo_ng/core/injector/injector.dart'; import 'package:piwigo_ng/network/api_client.dart'; -import 'package:piwigo_ng/services/auto_upload_manager.dart'; -import 'package:piwigo_ng/services/notification_service.dart'; -import 'package:piwigo_ng/services/preferences_service.dart'; -import 'package:piwigo_ng/services/theme_provider.dart'; -import 'package:shared_preferences/shared_preferences.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - _setUITheme(); HttpOverrides.global = SSLHttpOverrides(); - appPreferences = await SharedPreferences.getInstance(); + await init(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + // appPreferences = await SharedPreferences.getInstance(); runApp(const App()); - _clearUnusedStorage(); - initLocalNotifications(); - initializeWorkManager(); + // initLocalNotifications(); + // initializeWorkManager(); } -Future _clearUnusedStorage() async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - if (!prefs.containsKey('STORAGE_VERSION') && - prefs.getString('STORAGE_VERSION') != '2.0.0') { - prefs.clear(); - const FlutterSecureStorage().deleteAll(); - prefs.setString('STORAGE_VERSION', '2.0.0'); - } -} - -Future _setUITheme() async { - SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); - bool isDark = sharedPreferences.getBool(ThemeNotifier.themeKey) ?? - (ThemeMode.system == ThemeMode.dark); - SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( - systemNavigationBarColor: Colors.black.withOpacity(0.001), - statusBarColor: Colors.black.withOpacity(0.001), - statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, - )); -} +// Future _setUITheme() async { +// SharedPreferences sharedPreferences = await SharedPreferences.getInstance(); +// bool isDark = sharedPreferences.getBool(ThemeNotifier.themeKey) ?? (ThemeMode.system == ThemeMode.dark); +// SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle( +// systemNavigationBarColor: Colors.black.withOpacity(0.001), +// statusBarColor: Colors.black.withOpacity(0.001), +// statusBarIconBrightness: isDark ? Brightness.light : Brightness.dark, +// )); +// } diff --git a/lib/models/album_model.dart b/lib/models/album_model.dart index 72ec1c5..9138237 100644 --- a/lib/models/album_model.dart +++ b/lib/models/album_model.dart @@ -1,9 +1,7 @@ enum AlbumStatus { public, private } extension AlbumStatusSerialization on AlbumStatus { - String toJson() { - return this.name; - } + String toJson() => name; static AlbumStatus fromJson(String? json) { switch (json) { @@ -76,10 +74,7 @@ class AlbumModel { nbImages = json['nb_images'] ?? 0, nbTotalImages = json['total_nb_images'] ?? 0, nbCategories = json['nb_categories'] ?? 0, - children = json['sub_categories'] - ?.map((a) => AlbumModel.fromJson(a)) - .toList() ?? - [], + children = json['sub_categories']?.map((a) => AlbumModel.fromJson(a)).toList() ?? [], idRepresentative = json['representative_picture_id'], dateLast = json['date_last'], dateLastMax = json['max_date_last'], @@ -100,8 +95,7 @@ class AlbumModel { 'nb_images': nbImages, 'total_nb_images': nbTotalImages, 'nb_categories': nbCategories, - 'sub_categories': - List.generate(children.length, (i) => children[i].toJson()), + 'sub_categories': List.generate(children.length, (i) => children[i].toJson()), 'representative_picture_id': idRepresentative, 'date_last': dateLast, 'max_date_last': dateLastMax, diff --git a/pubspec.yaml b/pubspec.yaml index 4bc3198..0c8dd30 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: A Piwigo Android application publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 2.2.3+223 +version: 3.0.0+300 environment: sdk: ">=2.17.6 <3.0.0" @@ -34,6 +34,7 @@ dependencies: photo_view: ^0.14.0 # Zoom on fullscreen photos extended_text: ^11.0.0 # Text overflow on left side flutter_easyloading: ^3.0.5 # Show loading dialog + sliver_tools: ^0.2.12 # Storage package_info_plus: ^3.1.2 # Get project info (version) @@ -67,12 +68,25 @@ dependencies: intl: ^0.18.0 # Used for translations html_unescape: ^2.0.0 + # Clean archi packages + equatable: ^2.0.5 + bloc: ^8.1.2 + flutter_bloc: ^8.1.3 + get_it: ^7.6.7 + dartz: ^0.10.1 + freezed_annotation: ^2.4.1 + json_annotation: ^4.8.1 + copy_with_extension: ^5.0.4 + dev_dependencies: flutter_test: sdk: flutter dependency_validator: ^3.0.0 - flutter_lints: ^2.0.0 + flutter_lints: ^3.0.1 + build_runner: ^2.4.8 + json_serializable: ^6.7.1 + freezed: ^2.4.6 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec