diff --git a/app/lib/app/app.dart b/app/lib/app/app.dart index c8a7f3b..fedaa0c 100644 --- a/app/lib/app/app.dart +++ b/app/lib/app/app.dart @@ -14,6 +14,7 @@ import 'package:chuckle_chest/shared/widgets/_widgets.dart'; import 'package:chuckle_chest/shared/widgets/client_provider.dart'; import 'package:cperson_repository/cperson_repository.dart'; import 'package:cplatform_client/cplatform_client.dart'; +import 'package:cstorage_client/cstorage_client.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -47,6 +48,7 @@ class _ChuckleChestAppState extends State { late CPlatformClient platformClient; late CAuthClient authClient; late CPersonClient personClient; + late CStorageClient storageClient; late CAuthRepository authRepository; late CChestRepository chestRepository; @@ -77,6 +79,7 @@ class _ChuckleChestAppState extends State { peopleTable: peopleTable, avatarsTable: avatarsTable, ); + storageClient = CStorageClient(supabaseClient: supabaseClient); authRepository = CAuthRepository( authClient: CAuthClient(authClient: supabaseClient.auth), @@ -86,7 +89,11 @@ class _ChuckleChestAppState extends State { gemClient: gemClient, platformClient: platformClient, ); - personRepository = CPersonRepository(personClient: personClient); + personRepository = CPersonRepository( + personClient: personClient, + storageClient: storageClient, + platformClient: platformClient, + ); appRouter = CAppRouter(authRepository: authRepository); @@ -103,6 +110,7 @@ class _ChuckleChestAppState extends State { CClientProvider.value(value: chestClient), CClientProvider.value(value: gemClient), CClientProvider.value(value: personClient), + CClientProvider.value(value: storageClient), ], child: MultiRepositoryProvider( providers: [ diff --git a/app/lib/app/router.dart b/app/lib/app/router.dart index 4443e4c..ebdd630 100644 --- a/app/lib/app/router.dart +++ b/app/lib/app/router.dart @@ -100,6 +100,10 @@ class CAppRouter extends _$CAppRouter implements AutoRouteGuard { path: 'edit-person', page: CEditPersonRoute.page, ), + AutoRoute( + path: 'edit-avatar', + page: CEditAvatarRoute.page, + ), ], ), ], diff --git a/app/lib/app/router.gr.dart b/app/lib/app/router.gr.dart index b042da6..e636904 100644 --- a/app/lib/app/router.gr.dart +++ b/app/lib/app/router.gr.dart @@ -47,13 +47,25 @@ abstract class _$CAppRouter extends RootStackRouter { child: WrappedRoute(child: const CCreateGemPage()), ); }, + CEditAvatarRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: CEditAvatarPage( + personID: args.personID, + avatarURL: args.avatarURL, + key: args.key, + )), + ); + }, CEditGemRoute.name: (routeData) { final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, child: WrappedRoute( child: CEditGemPage( - gem: args.gem, + initialGem: args.initialGem, key: args.key, )), ); @@ -259,17 +271,60 @@ class CCreateGemRoute extends PageRouteInfo { static const PageInfo page = PageInfo(name); } +/// generated route for +/// [CEditAvatarPage] +class CEditAvatarRoute extends PageRouteInfo { + CEditAvatarRoute({ + required BigInt personID, + required CAvatarURL avatarURL, + Key? key, + List? children, + }) : super( + CEditAvatarRoute.name, + args: CEditAvatarRouteArgs( + personID: personID, + avatarURL: avatarURL, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'CEditAvatarRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class CEditAvatarRouteArgs { + const CEditAvatarRouteArgs({ + required this.personID, + required this.avatarURL, + this.key, + }); + + final BigInt personID; + + final CAvatarURL avatarURL; + + final Key? key; + + @override + String toString() { + return 'CEditAvatarRouteArgs{personID: $personID, avatarURL: $avatarURL, key: $key}'; + } +} + /// generated route for /// [CEditGemPage] class CEditGemRoute extends PageRouteInfo { CEditGemRoute({ - required CGem? gem, + required CGem? initialGem, Key? key, List? children, }) : super( CEditGemRoute.name, args: CEditGemRouteArgs( - gem: gem, + initialGem: initialGem, key: key, ), initialChildren: children, @@ -283,17 +338,17 @@ class CEditGemRoute extends PageRouteInfo { class CEditGemRouteArgs { const CEditGemRouteArgs({ - required this.gem, + required this.initialGem, this.key, }); - final CGem? gem; + final CGem? initialGem; final Key? key; @override String toString() { - return 'CEditGemRouteArgs{gem: $gem, key: $key}'; + return 'CEditGemRouteArgs{initialGem: $initialGem, key: $key}'; } } diff --git a/app/lib/localization/arb/intl_de.arb b/app/lib/localization/arb/intl_de.arb index 0def65e..264de3b 100644 --- a/app/lib/localization/arb/intl_de.arb +++ b/app/lib/localization/arb/intl_de.arb @@ -10,6 +10,15 @@ "collectionsPage_section_title_other": "Sonstige", "collectionsPage_section_title_years": "Springe zu Jahr", "delete": "Löschen", + "editAvatarPage_pickPhotoButton": "Foto auswählen", + "editAvatarPage_title": "Bild Für {year} Bearbeiten", + "@editAvatarPage_title": { + "placeholders": { + "year": { + "type": "int" + } + } + }, "editGemPage_addNarrationButton": "Neue Erzählung", "editGemPage_addQuoteButton": "Neues Zitat", "editGemPage_dateTile_title": "Datum", @@ -22,7 +31,7 @@ "editGemPage_helperMessage": "Tippe auf etwas, um es zu bearbeiten.", "editGemPage_title_create": "Gemme erstellen", "editGemPage_title_edit": "Gemme bearbeiten", - "editPersonPage_banner_message": "Änderungen werden beim nächsten Öffnen der App auf andere Benutzer übertragen.", + "editPersonPage_banner_message": "Das Schließen dieser Seite wird die App neu laden, wenn Änderungen vorgenommen wurden.", "editPersonPage_dateOfBirthTile_title": "Geburtsdatum", "editPersonPage_editNicknameDialog_title": "Spitzname bearbeiten", "editPersonPage_nicknameTile_title": "Spitzname", diff --git a/app/lib/localization/arb/intl_en.arb b/app/lib/localization/arb/intl_en.arb index 6ac443d..52ea1d2 100644 --- a/app/lib/localization/arb/intl_en.arb +++ b/app/lib/localization/arb/intl_en.arb @@ -10,6 +10,15 @@ "collectionsPage_section_title_other": "Other", "collectionsPage_section_title_years": "Jump to year", "delete": "Delete", + "editAvatarPage_pickPhotoButton": "Pick a photo", + "editAvatarPage_title": "Edit Photo for {year}", + "@editAvatarPage_title": { + "placeholders": { + "year": { + "type": "int" + } + } + }, "editGemPage_addNarrationButton": "Add a narration", "editGemPage_addQuoteButton": "Add a quote", "editGemPage_dateTile_title": "Date", @@ -22,7 +31,7 @@ "editGemPage_helperMessage": "Tap anything to edit.", "editGemPage_title_create": "Create a gem", "editGemPage_title_edit": "Edit the gem", - "editPersonPage_banner_message": "Changes will propagate to other users next time they open the app.", + "editPersonPage_banner_message": "Closing this page will reload the app if changes have been made.", "editPersonPage_dateOfBirthTile_title": "Date of birth", "editPersonPage_editNicknameDialog_title": "Edit nickname", "editPersonPage_nicknameTile_title": "Nickname", diff --git a/app/lib/localization/generated/localizations.g.dart b/app/lib/localization/generated/localizations.g.dart index 9e2f418..a493424 100644 --- a/app/lib/localization/generated/localizations.g.dart +++ b/app/lib/localization/generated/localizations.g.dart @@ -155,6 +155,18 @@ abstract class CAppL10n { /// **'Delete'** String get delete; + /// No description provided for @editAvatarPage_pickPhotoButton. + /// + /// In en, this message translates to: + /// **'Pick a photo'** + String get editAvatarPage_pickPhotoButton; + + /// No description provided for @editAvatarPage_title. + /// + /// In en, this message translates to: + /// **'Edit Photo for {year}'** + String editAvatarPage_title(int year); + /// No description provided for @editGemPage_addNarrationButton. /// /// In en, this message translates to: @@ -230,7 +242,7 @@ abstract class CAppL10n { /// No description provided for @editPersonPage_banner_message. /// /// In en, this message translates to: - /// **'Changes will propagate to other users next time they open the app.'** + /// **'Closing this page will reload the app if changes have been made.'** String get editPersonPage_banner_message; /// No description provided for @editPersonPage_dateOfBirthTile_title. diff --git a/app/lib/localization/generated/localizations_de.g.dart b/app/lib/localization/generated/localizations_de.g.dart index a3b8955..464096d 100644 --- a/app/lib/localization/generated/localizations_de.g.dart +++ b/app/lib/localization/generated/localizations_de.g.dart @@ -38,6 +38,14 @@ class CAppL10nDe extends CAppL10n { @override String get delete => 'Löschen'; + @override + String get editAvatarPage_pickPhotoButton => 'Foto auswählen'; + + @override + String editAvatarPage_title(int year) { + return 'Bild Für $year Bearbeiten'; + } + @override String get editGemPage_addNarrationButton => 'Neue Erzählung'; @@ -75,7 +83,7 @@ class CAppL10nDe extends CAppL10n { String get editGemPage_title_edit => 'Gemme bearbeiten'; @override - String get editPersonPage_banner_message => 'Änderungen werden beim nächsten Öffnen der App auf andere Benutzer übertragen.'; + String get editPersonPage_banner_message => 'Das Schließen dieser Seite wird die App neu laden, wenn Änderungen vorgenommen wurden.'; @override String get editPersonPage_dateOfBirthTile_title => 'Geburtsdatum'; diff --git a/app/lib/localization/generated/localizations_en.g.dart b/app/lib/localization/generated/localizations_en.g.dart index 84f2e66..6ca8ad8 100644 --- a/app/lib/localization/generated/localizations_en.g.dart +++ b/app/lib/localization/generated/localizations_en.g.dart @@ -38,6 +38,14 @@ class CAppL10nEn extends CAppL10n { @override String get delete => 'Delete'; + @override + String get editAvatarPage_pickPhotoButton => 'Pick a photo'; + + @override + String editAvatarPage_title(int year) { + return 'Edit Photo for $year'; + } + @override String get editGemPage_addNarrationButton => 'Add a narration'; @@ -75,7 +83,7 @@ class CAppL10nEn extends CAppL10n { String get editGemPage_title_edit => 'Edit the gem'; @override - String get editPersonPage_banner_message => 'Changes will propagate to other users next time they open the app.'; + String get editPersonPage_banner_message => 'Closing this page will reload the app if changes have been made.'; @override String get editPersonPage_dateOfBirthTile_title => 'Date of birth'; diff --git a/app/lib/pages/_pages.dart b/app/lib/pages/_pages.dart index 5fc3b70..b8eae2d 100644 --- a/app/lib/pages/_pages.dart +++ b/app/lib/pages/_pages.dart @@ -2,6 +2,7 @@ export 'base/page.dart'; export 'chest/page.dart'; export 'collections/page.dart'; export 'create_gem/page.dart'; +export 'edit_avatar/page.dart'; export 'edit_gem/page.dart'; export 'edit_person/page.dart'; export 'gem/page.dart'; diff --git a/app/lib/pages/collections/widgets/year_collections_section.dart b/app/lib/pages/collections/widgets/year_collections_section.dart index 4c58a14..a7b2430 100644 --- a/app/lib/pages/collections/widgets/year_collections_section.dart +++ b/app/lib/pages/collections/widgets/year_collections_section.dart @@ -65,7 +65,7 @@ class _CYearCollectionCard extends StatelessWidget { final people = context.read().state.people; final avatars = people - .map((p) => p.avatarURLForDate(DateTime(year))) + .map((p) => p.avatarURLForDate(DateTime(year))?.url) .where((a) => a != null) .toList() .cast() diff --git a/app/lib/pages/create_gem/page.dart b/app/lib/pages/create_gem/page.dart index 3428d88..2ce8705 100644 --- a/app/lib/pages/create_gem/page.dart +++ b/app/lib/pages/create_gem/page.dart @@ -37,5 +37,5 @@ class CCreateGemPage extends StatelessWidget implements AutoRouteWrapper { } @override - Widget build(BuildContext context) => const CEditGemPage(gem: null); + Widget build(BuildContext context) => const CEditGemPage(initialGem: null); } diff --git a/app/lib/pages/edit_avatar/logic/_logic.dart b/app/lib/pages/edit_avatar/logic/_logic.dart new file mode 100644 index 0000000..50ef852 --- /dev/null +++ b/app/lib/pages/edit_avatar/logic/_logic.dart @@ -0,0 +1,2 @@ +export 'avatar_pick_cubit.dart'; +export 'avatar_update_cubit.dart'; diff --git a/app/lib/pages/edit_avatar/logic/avatar_pick_cubit.dart b/app/lib/pages/edit_avatar/logic/avatar_pick_cubit.dart new file mode 100644 index 0000000..7c4867c --- /dev/null +++ b/app/lib/pages/edit_avatar/logic/avatar_pick_cubit.dart @@ -0,0 +1,55 @@ +import 'package:bloc/bloc.dart'; +import 'package:bobs_jobs/bobs_jobs.dart'; +import 'package:chuckle_chest/shared/logic/_logic.dart'; +import 'package:cperson_repository/cperson_repository.dart'; +import 'package:flutter/foundation.dart'; + +/// {@template CAvatarPickState} +/// +/// The state for the [CAvatarPickCubit]. +/// +/// {@endtemplate} +class CAvatarPickState + extends CRequestCubitState> { + /// {@macro CAvatarPickState} + /// + /// The initial state. + CAvatarPickState.initial() : super.initial(); + + /// {@macro CAvatarPickState} + /// + /// The in progress state. + CAvatarPickState.inProgress() : super.inProgress(); + + /// {@macro CAvatarPickState} + /// + /// The completed state. + CAvatarPickState.completed({required super.outcome}) : super.completed(); + + /// The image to be uploaded. + BobsMaybe get image => success; +} + +/// {@template CAvatarPickCubit} +/// +/// A cubit for allowing the user to pick an avatar. +/// +/// {@endtemplate} +class CAvatarPickCubit extends Cubit { + /// {@macro CAvatarPickCubit} + CAvatarPickCubit({required this.personRepository}) + : super(CAvatarPickState.initial()); + + /// The repository this cubit uses to pick an avatar. + final CPersonRepository personRepository; + + /// Allows the user to pick an image from their gallery. + Future pickAvatar() async { + emit(CAvatarPickState.inProgress()); + + final result = + await personRepository.pickAvatar().run(isDebugMode: kDebugMode); + + emit(CAvatarPickState.completed(outcome: result)); + } +} diff --git a/app/lib/pages/edit_avatar/logic/avatar_update_cubit.dart b/app/lib/pages/edit_avatar/logic/avatar_update_cubit.dart new file mode 100644 index 0000000..0a2ee64 --- /dev/null +++ b/app/lib/pages/edit_avatar/logic/avatar_update_cubit.dart @@ -0,0 +1,65 @@ +import 'package:bloc/bloc.dart'; +import 'package:chuckle_chest/shared/logic/_logic.dart'; +import 'package:cperson_repository/cperson_repository.dart'; +import 'package:flutter/foundation.dart'; + +/// {@template CAvatarUpdateState} +/// +/// The state for the [CAvatarUpdateCubit]. +/// +/// {@endtemplate} +class CAvatarUpdateState + extends CRequestCubitState { + /// {@macro CAvatarUpdateState} + /// + /// The initial state. + CAvatarUpdateState.initial() : super.initial(); + + /// {@macro CAvatarUpdateState} + /// + /// The in progress state. + CAvatarUpdateState.inProgress() : super.inProgress(); + + /// {@macro CAvatarUpdateState} + /// + /// The completed state. + CAvatarUpdateState.completed({required super.outcome}) : super.completed(); + + /// The URL of the avatar that was updated. + String get url => success; +} + +/// {@template CAvatarUpdateCubit} +/// +/// A cubit for updating a person's avatar for a given year. +/// +/// {@endtemplate} +class CAvatarUpdateCubit extends Cubit { + /// {@macro CAvatarUpdateCubit} + CAvatarUpdateCubit({required this.personRepository}) + : super(CAvatarUpdateState.initial()); + + /// The repository this cubit uses to update the person's avatar. + final CPersonRepository personRepository; + + /// Updates the image for the given person for the given year. + Future updateAvatarForYear({ + required Uint8List image, + required String chestID, + required BigInt personID, + required int year, + }) async { + emit(CAvatarUpdateState.inProgress()); + + final result = await personRepository + .updateAvatar( + personID: personID, + chestID: chestID, + year: year, + image: image, + ) + .run(isDebugMode: kDebugMode); + + emit(CAvatarUpdateState.completed(outcome: result)); + } +} diff --git a/app/lib/pages/edit_avatar/page.dart b/app/lib/pages/edit_avatar/page.dart new file mode 100644 index 0000000..4cecebd --- /dev/null +++ b/app/lib/pages/edit_avatar/page.dart @@ -0,0 +1,113 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:chuckle_chest/localization/l10n.dart'; +import 'package:chuckle_chest/pages/edit_avatar/logic/_logic.dart'; +import 'package:chuckle_chest/pages/edit_avatar/widgets/_widgets.dart'; +import 'package:chuckle_chest/shared/_shared.dart'; +import 'package:cperson_repository/cperson_repository.dart'; +import 'package:crop_your_image/crop_your_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:transparent_image/transparent_image.dart'; + +/// {@template CEditAvatarPage} +/// +/// The page that allows the user to select, crop and upload an avatar for a +/// person. +/// +/// {@endtemplate} +@RoutePage() +class CEditAvatarPage extends StatelessWidget implements AutoRouteWrapper { + /// {@macro CEditAvatarPage} + CEditAvatarPage({ + required this.personID, + required this.avatarURL, + super.key, + }); + + /// The unique identifier of the person to edit the avatar for. + final BigInt personID; + + /// The URL of the avatar to edit. + final CAvatarURL avatarURL; + + final _cropController = CropController(); + + @override + Widget wrappedRoute(BuildContext context) { + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => CAvatarUpdateCubit( + personRepository: context.read(), + ), + ), + BlocProvider( + create: (context) => CAvatarPickCubit( + personRepository: context.read(), + ), + ), + ], + child: MultiBlocListener( + listeners: [ + BlocListener( + listener: (context, state) => switch (state.status) { + CRequestCubitStatus.initial => null, + CRequestCubitStatus.inProgress => null, + CRequestCubitStatus.failed => + const CErrorSnackBar().show(context), + CRequestCubitStatus.succeeded => context.router.maybePop( + CAvatarURL(year: avatarURL.year, url: state.url), + ), + }, + ), + BlocListener( + listenWhen: (_, state) => + state.status == CRequestCubitStatus.failed, + listener: (context, _) => const CErrorSnackBar().show(context), + ), + ], + child: this, + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: CAppBar( + context: context, + title: Text(context.cAppL10n.editAvatarPage_title(avatarURL.year)), + ), + body: Stack( + children: [ + Positioned.fill( + child: avatarURL.url.isNotEmpty + ? Center( + child: Container( + decoration: const BoxDecoration(shape: BoxShape.circle), + clipBehavior: Clip.hardEdge, + child: FadeInImage.memoryNetwork( + placeholder: kTransparentImage, + image: avatarURL.url, + ), + ), + ) + : const SizedBox(), + ), + Positioned.fill( + child: CEditAvatarPageCrop( + cropController: _cropController, + personID: personID, + avatarURL: avatarURL, + ), + ), + ], + ), + bottomNavigationBar: CEditAvatarBottomAppBar( + personID: personID, + avatarURL: avatarURL, + cropController: _cropController, + ), + ); + } +} diff --git a/app/lib/pages/edit_avatar/widgets/_widgets.dart b/app/lib/pages/edit_avatar/widgets/_widgets.dart new file mode 100644 index 0000000..afe46df --- /dev/null +++ b/app/lib/pages/edit_avatar/widgets/_widgets.dart @@ -0,0 +1,3 @@ +export 'bottom_app_bar.dart'; +export 'crop.dart'; +export 'save_button.dart'; diff --git a/app/lib/pages/edit_avatar/widgets/bottom_app_bar.dart b/app/lib/pages/edit_avatar/widgets/bottom_app_bar.dart new file mode 100644 index 0000000..b1748d4 --- /dev/null +++ b/app/lib/pages/edit_avatar/widgets/bottom_app_bar.dart @@ -0,0 +1,57 @@ +import 'package:chuckle_chest/localization/l10n.dart'; +import 'package:chuckle_chest/pages/edit_avatar/logic/_logic.dart'; +import 'package:chuckle_chest/pages/edit_avatar/widgets/_widgets.dart'; +import 'package:cperson_repository/cperson_repository.dart'; +import 'package:crop_your_image/crop_your_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// {@template CEditAvatarBottomAppBar} +/// +/// The bottom app bar on the edit avatar page that allows the user to pick a +/// photo and save the selected avatar. +/// +/// {@endtemplate} +class CEditAvatarBottomAppBar extends StatelessWidget { + /// {@macro CEditAvatarBottomAppBar} + const CEditAvatarBottomAppBar({ + required this.personID, + required this.avatarURL, + required this.cropController, + super.key, + }); + + /// The unique identifier of the person to edit the avatar for. + final BigInt personID; + + /// The URL of the avatar to edit. + final CAvatarURL avatarURL; + + /// The controller for the crop. + final CropController cropController; + + @override + Widget build(BuildContext context) { + return BottomAppBar( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: TextButton.icon( + onPressed: () => context.read().pickAvatar(), + icon: const Icon(Icons.photo_library_rounded), + label: Text(context.cAppL10n.editAvatarPage_pickPhotoButton), + ), + ), + Expanded( + child: CEditAvatarSaveButton( + personID: personID, + avatarURL: avatarURL, + cropController: cropController, + ), + ), + ], + ), + ); + } +} diff --git a/app/lib/pages/edit_avatar/widgets/crop.dart b/app/lib/pages/edit_avatar/widgets/crop.dart new file mode 100644 index 0000000..3b359d8 --- /dev/null +++ b/app/lib/pages/edit_avatar/widgets/crop.dart @@ -0,0 +1,63 @@ +import 'package:chuckle_chest/pages/edit_avatar/logic/_logic.dart'; +import 'package:chuckle_chest/shared/_shared.dart'; +import 'package:cperson_repository/cperson_repository.dart'; +import 'package:crop_your_image/crop_your_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// {@template CEditAvatarPageCrop} +/// +/// The crop widget on the edit avatar page that allows the user to crop the +/// selected avatar. +/// +/// {@endtemplate} +class CEditAvatarPageCrop extends StatelessWidget { + /// {@macro CEditAvatarPageCrop} + const CEditAvatarPageCrop({ + required this.cropController, + required this.personID, + required this.avatarURL, + super.key, + }); + + /// The unique identifier of the person to edit the avatar for. + final BigInt personID; + + /// The URL of the avatar to edit. + final CAvatarURL avatarURL; + + /// The controller for the crop. + final CropController cropController; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) => switch (state.status) { + CRequestCubitStatus.initial => const SizedBox(), + CRequestCubitStatus.inProgress => ColoredBox( + color: Colors.white.withOpacity(0.75), + child: const Center(child: CCradleLoadingIndicator()), + ), + CRequestCubitStatus.failed => const SizedBox(), + CRequestCubitStatus.succeeded => state.image.evaluate( + onAbsent: () => const SizedBox(), + onPresent: (image) => Padding( + padding: const EdgeInsets.all(16), + child: Crop( + controller: cropController, + image: image, + withCircleUi: true, + onCropped: (cropped) => + context.read().updateAvatarForYear( + personID: personID, + image: cropped, + year: avatarURL.year, + chestID: context.read().state.id, + ), + ), + ), + ), + }, + ); + } +} diff --git a/app/lib/pages/edit_avatar/widgets/save_button.dart b/app/lib/pages/edit_avatar/widgets/save_button.dart new file mode 100644 index 0000000..0500de5 --- /dev/null +++ b/app/lib/pages/edit_avatar/widgets/save_button.dart @@ -0,0 +1,60 @@ +import 'package:ccore/ccore.dart'; +import 'package:chuckle_chest/localization/l10n.dart'; +import 'package:chuckle_chest/pages/edit_avatar/logic/_logic.dart'; +import 'package:chuckle_chest/shared/_shared.dart'; +import 'package:cperson_repository/cperson_repository.dart'; +import 'package:crop_your_image/crop_your_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +/// {@template CEditAvatarSaveButton} +/// +/// The button on the edit avatar page that allows the user to save the +/// selected avatar. +/// +/// {@endtemplate} +class CEditAvatarSaveButton extends StatelessWidget { + /// {@macro CEditAvatarSaveButton} + const CEditAvatarSaveButton({ + required this.personID, + required this.avatarURL, + required this.cropController, + super.key, + }); + + /// The unique identifier of the person to update the avatar for. + final BigInt personID; + + /// The avatar URL to update. + final CAvatarURL avatarURL; + + /// The controller for the crop. + final CropController cropController; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, pickState) => + BlocBuilder( + builder: (context, saveState) => ElevatedButton.icon( + onPressed: saveState.status != CRequestCubitStatus.inProgress + ? pickState.status == CRequestCubitStatus.succeeded + ? pickState.image.evaluate( + onAbsent: () => null, + onPresent: (image) => cropController.crop, + ) + : null + : null, + style: ElevatedButton.styleFrom( + backgroundColor: context.cColorScheme.primary, + foregroundColor: context.cColorScheme.onPrimary, + ), + icon: const Icon(Icons.save_rounded), + label: saveState.status != CRequestCubitStatus.inProgress + ? Text(context.cAppL10n.save) + : const CCradleLoadingIndicator(), + ), + ), + ); + } +} diff --git a/app/lib/pages/edit_gem/logic/gem_edit_cubit.dart b/app/lib/pages/edit_gem/logic/gem_edit_cubit.dart index 5028fbe..18a2bce 100644 --- a/app/lib/pages/edit_gem/logic/gem_edit_cubit.dart +++ b/app/lib/pages/edit_gem/logic/gem_edit_cubit.dart @@ -1,6 +1,5 @@ import 'package:bloc/bloc.dart'; import 'package:cgem_repository/cgem_repository.dart'; -import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; /// {@template CGemEditState} @@ -8,7 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; /// The state for the [CGemEditCubit]. /// /// {@endtemplate} -class CGemEditState extends Equatable { +class CGemEditState { /// {@macro CGemEditState} const CGemEditState({required this.gem, required this.deletedLines}); @@ -17,9 +16,6 @@ class CGemEditState extends Equatable { /// The lines that have been deleted. final List deletedLines; - - @override - List get props => [gem, deletedLines]; } /// {@template CGemEditCubit} @@ -44,7 +40,7 @@ class CGemEditCubit extends Cubit { lines: [], chestID: chestID, ), - deletedLines: const [], + deletedLines: [], ), ); diff --git a/app/lib/pages/edit_gem/page.dart b/app/lib/pages/edit_gem/page.dart index 9faabf1..d97f9d4 100644 --- a/app/lib/pages/edit_gem/page.dart +++ b/app/lib/pages/edit_gem/page.dart @@ -17,12 +17,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; @RoutePage() class CEditGemPage extends StatelessWidget implements AutoRouteWrapper { /// {@macro CEditGemPage} - const CEditGemPage({required this.gem, super.key}); + const CEditGemPage({required this.initialGem, super.key}); /// The gem to edit. /// /// If `null`, a new gem will be created. - final CGem? gem; + final CGem? initialGem; @override Widget wrappedRoute(BuildContext context) { @@ -30,7 +30,7 @@ class CEditGemPage extends StatelessWidget implements AutoRouteWrapper { providers: [ BlocProvider( create: (context) => CGemEditCubit( - gem: gem, + gem: initialGem?.copyWith(), chestID: context.read().state.id, ), ), @@ -66,11 +66,11 @@ class CEditGemPage extends StatelessWidget implements AutoRouteWrapper { context.read().deleteLastLine(); void _onSaved(BuildContext context, String gemID) { - if (gem == null) { + if (initialGem == null) { context.router.replace(CGemRoute(gemID: gemID)); return; } - context.router.maybePop(gem); + context.router.maybePop(context.read().state.gem); } @override @@ -86,7 +86,7 @@ class CEditGemPage extends StatelessWidget implements AutoRouteWrapper { appBar: CAppBar( context: context, title: Text( - gem == null + initialGem == null ? context.cAppL10n.editGemPage_title_create : context.cAppL10n.editGemPage_title_edit, ), diff --git a/app/lib/pages/edit_person/dialogs/edit_nickname.dart b/app/lib/pages/edit_person/dialogs/edit_nickname.dart index b52e735..35b29f4 100644 --- a/app/lib/pages/edit_person/dialogs/edit_nickname.dart +++ b/app/lib/pages/edit_person/dialogs/edit_nickname.dart @@ -6,7 +6,7 @@ import 'package:flutter/material.dart'; /// {@template CNicknameTile} /// -/// A tile for displaying and editing a person's nickname. +/// The dialog on the edit person page for editing a person's nickname. /// /// {@endtemplate} class CEditNicknameDialog extends StatelessWidget with CDialogMixin { diff --git a/app/lib/pages/edit_person/logic/person_update_cubit.dart b/app/lib/pages/edit_person/logic/person_update_cubit.dart index 819d4b4..c6bf786 100644 --- a/app/lib/pages/edit_person/logic/person_update_cubit.dart +++ b/app/lib/pages/edit_person/logic/person_update_cubit.dart @@ -11,25 +11,56 @@ import 'package:flutter/foundation.dart'; /// {@endtemplate} class CPersonUpdateState extends CRequestCubitState { + CPersonUpdateState._({ + required this.person, + required this.haveAvatarsChanged, + required super.status, + super.outcome, + }) : super(); + /// {@macro CPersonUpdateState} /// /// The initial state. - CPersonUpdateState.initial({required this.person}) : super.initial(); + CPersonUpdateState.initial({ + required this.person, + }) : haveAvatarsChanged = false, + super.initial(); /// {@macro CPersonUpdateState} /// /// The in progress state. - CPersonUpdateState.inProgress({required this.person}) : super.inProgress(); + CPersonUpdateState.inProgress({ + required this.person, + required this.haveAvatarsChanged, + }) : super.inProgress(); /// {@macro CPersonUpdateState} /// /// The completed state. - CPersonUpdateState.completed({required super.outcome, required this.person}) - : super.completed(); + CPersonUpdateState.completed({ + required super.outcome, + required this.person, + required this.haveAvatarsChanged, + }) : super.completed(); /// The person being updated. final CPerson person; + /// Whether the avatars have changed. + final bool haveAvatarsChanged; + + /// {@macro CPersonUpdateState} + /// + /// Returns a copy of this state with the given fields replaced by the new + /// values. + CPersonUpdateState copyWith({CPerson? person, bool? haveAvatarsChanged}) => + CPersonUpdateState._( + outcome: outcome, + status: status, + person: person ?? this.person, + haveAvatarsChanged: haveAvatarsChanged ?? this.haveAvatarsChanged, + ); + @override List get props => super.props..add(person); } @@ -49,7 +80,12 @@ class CPersonUpdateCubit extends Cubit { /// Updates the person's nickname. Future updateNickname({required String nickname}) async { - emit(CPersonUpdateState.inProgress(person: state.person)); + emit( + CPersonUpdateState.inProgress( + person: state.person, + haveAvatarsChanged: state.haveAvatarsChanged, + ), + ); final newPerson = state.person.copyWith(nickname: nickname); @@ -60,6 +96,7 @@ class CPersonUpdateCubit extends Cubit { emit( CPersonUpdateState.completed( outcome: result, + haveAvatarsChanged: state.haveAvatarsChanged, person: result.evaluate( onFailure: (_) => state.person, onSuccess: (_) => newPerson, @@ -70,7 +107,12 @@ class CPersonUpdateCubit extends Cubit { /// Updates the person's date of birth. Future updateDateOfBirth({required DateTime dateOfBirth}) async { - emit(CPersonUpdateState.inProgress(person: state.person)); + emit( + CPersonUpdateState.inProgress( + person: state.person, + haveAvatarsChanged: state.haveAvatarsChanged, + ), + ); final newPerson = state.person.copyWith(dateOfBirth: dateOfBirth); @@ -85,6 +127,19 @@ class CPersonUpdateCubit extends Cubit { onFailure: (_) => state.person, onSuccess: (_) => newPerson, ), + haveAvatarsChanged: state.haveAvatarsChanged, + ), + ); + } + + /// Creates or replaces an avatar for the person. + void updateAvatar({required CAvatarURL avatarURL}) { + emit( + state.copyWith( + haveAvatarsChanged: true, + person: state.person + ..avatarURLs.removeWhere((url) => url.year == avatarURL.year) + ..avatarURLs.add(avatarURL), ), ); } diff --git a/app/lib/pages/edit_person/page.dart b/app/lib/pages/edit_person/page.dart index d9ad019..637df8c 100644 --- a/app/lib/pages/edit_person/page.dart +++ b/app/lib/pages/edit_person/page.dart @@ -31,17 +31,6 @@ class CEditPersonPage extends StatelessWidget implements AutoRouteWrapper { /// If true, the session will be refreshed when the page is popped. final bool isPersonNew; - void _onPopped(BuildContext context) { - final hasPersonChanged = context.read().state.status != - CRequestCubitStatus.initial; - - if (hasPersonChanged || isPersonNew) { - context.router.replaceAll([CChestRoute(chestID: person.chestID)]); - } else { - context.router.maybePop(); - } - } - @override Widget wrappedRoute(BuildContext context) { return MultiBlocProvider( @@ -66,6 +55,18 @@ class CEditPersonPage extends StatelessWidget implements AutoRouteWrapper { ); } + void _onPopped(BuildContext context) { + final state = context.read().state; + final hasPersonChanged = + state.status != CRequestCubitStatus.initial || state.haveAvatarsChanged; + + if (hasPersonChanged || isPersonNew) { + context.router.replaceAll([CChestRoute(chestID: person.chestID)]); + } else { + context.router.maybePop(); + } + } + @override Widget build(BuildContext context) { return PopScope( diff --git a/app/lib/pages/edit_person/widgets/avatar_section.dart b/app/lib/pages/edit_person/widgets/avatar_section.dart index 626db9f..8f14be7 100644 --- a/app/lib/pages/edit_person/widgets/avatar_section.dart +++ b/app/lib/pages/edit_person/widgets/avatar_section.dart @@ -1,5 +1,8 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:chuckle_chest/app/router.dart'; import 'package:chuckle_chest/pages/edit_person/logic/_logic.dart'; import 'package:chuckle_chest/shared/_shared.dart'; +import 'package:cperson_repository/cperson_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:signed_spacing_flex/signed_spacing_flex.dart'; @@ -23,22 +26,65 @@ class CAvatarSection extends StatelessWidget { alignment: WrapAlignment.center, children: List.generate( DateTime.now().year + 1 - state.person.dateOfBirth.year, - (index) => SignedSpacingColumn( - spacing: 4, - mainAxisSize: MainAxisSize.min, - children: [ - CAvatar.fromPerson( - person: state.person, - date: DateTime(state.person.dateOfBirth.year + index), - diameter: 64, - onPressed: () {}, - icon: const Icon(Icons.add_photo_alternate_rounded), - ), - Text((state.person.dateOfBirth.year + index).toString()), - ], + (index) => CEditableAvatar( + person: state.person, + date: DateTime(state.person.dateOfBirth.year + index), ), ), ), ); } } + +/// {@template CEditableAvatar} +/// +/// An avatar in the avatar section on the edit person page that allows the user +/// to edit the avatar. +/// +/// {@endtemplate} +class CEditableAvatar extends StatelessWidget { + /// {@macro CEditableAvatar} + const CEditableAvatar({ + required this.person, + required this.date, + super.key, + }); + + /// The person to edit the avatar for. + final CPerson person; + + /// The date of the avatar. + final DateTime date; + + Future _onAvatarPressed(BuildContext context, int year) async { + final updateCubit = context.read(); + final result = await context.router.push( + CEditAvatarRoute( + personID: person.id, + avatarURL: + person.avatarURLForDate(date) ?? CAvatarURL(year: year, url: ''), + ), + ); + if (result != null) { + updateCubit.updateAvatar(avatarURL: result as CAvatarURL); + } + } + + @override + Widget build(BuildContext context) { + return SignedSpacingColumn( + spacing: 4, + mainAxisSize: MainAxisSize.min, + children: [ + CAvatar.fromPerson( + person: person, + date: date, + diameter: 64, + onPressed: () => _onAvatarPressed(context, date.year), + icon: const Icon(Icons.add_photo_alternate_rounded), + ), + Text(date.year.toString()), + ], + ); + } +} diff --git a/app/lib/shared/dialogs/_dialogs.dart b/app/lib/shared/dialogs/_dialogs.dart index 268aae4..de1e264 100644 --- a/app/lib/shared/dialogs/_dialogs.dart +++ b/app/lib/shared/dialogs/_dialogs.dart @@ -1 +1,2 @@ +export 'avatar_dialog.dart'; export 'chest_creation.dart'; diff --git a/app/lib/shared/dialogs/avatar_dialog.dart b/app/lib/shared/dialogs/avatar_dialog.dart new file mode 100644 index 0000000..398e524 --- /dev/null +++ b/app/lib/shared/dialogs/avatar_dialog.dart @@ -0,0 +1,48 @@ +import 'package:chuckle_chest/localization/l10n.dart'; +import 'package:chuckle_chest/shared/_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:transparent_image/transparent_image.dart'; + +/// {@template CAvatarDialog} +/// +/// A dialog is opened when an avatar is tapped and displays the avatar in a +/// larger format. +/// +/// {@endtemplate} +class CAvatarDialog extends StatelessWidget with CDialogMixin { + /// {@macro CAvatarDialog} + const CAvatarDialog({ + required this.nickname, + required this.year, + required this.imageURL, + super.key, + }); + + /// The nickname of the person the avatar corresponds to. + final String nickname; + + /// The year the avatar corresponds to. + final int year; + + /// The URL of the avatar to display. + final String imageURL; + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text('$nickname - $year'), + content: FadeInImage.memoryNetwork( + placeholder: kTransparentImage, + image: imageURL, + height: 200, + width: 200, + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(context.cAppL10n.close), + ), + ], + ); + } +} diff --git a/app/lib/shared/logic/request_cubit.dart b/app/lib/shared/logic/request_cubit.dart index c19986f..7293fd9 100644 --- a/app/lib/shared/logic/request_cubit.dart +++ b/app/lib/shared/logic/request_cubit.dart @@ -26,6 +26,9 @@ enum CRequestCubitStatus { /// /// {@endtemplate} class CRequestCubitState with EquatableMixin { + /// {@macro CRequestCubitState} + CRequestCubitState({required this.status, this.outcome}); + /// {@macro CRequestCubitState} /// /// The initial state. It sets `status` to `CRequestCubitStatus.initial`. diff --git a/app/lib/shared/views/collection/logic/collection_view_cubit.dart b/app/lib/shared/views/collection/logic/collection_view_cubit.dart index fed1d3e..159302c 100644 --- a/app/lib/shared/views/collection/logic/collection_view_cubit.dart +++ b/app/lib/shared/views/collection/logic/collection_view_cubit.dart @@ -27,7 +27,7 @@ class CCollectionViewState { int? currentIndex, }) { return CCollectionViewState( - gems: gems ?? this.gems, + gems: gems != null ? [...gems] : [...this.gems], currentIndex: currentIndex ?? this.currentIndex, ); } diff --git a/app/lib/shared/views/collection/view.dart b/app/lib/shared/views/collection/view.dart index 0d6bae3..5dd2522 100644 --- a/app/lib/shared/views/collection/view.dart +++ b/app/lib/shared/views/collection/view.dart @@ -26,7 +26,7 @@ class CCollectionView extends StatelessWidget { final bloc = context.read(); final result = await context.router.push( - CEditGemRoute(gem: bloc.state.currentGem), + CEditGemRoute(initialGem: bloc.state.currentGem), ); if (context.mounted && result != null) { @@ -80,14 +80,21 @@ class CCollectionView extends StatelessWidget { itemBuilder: (context, index) => BlocBuilder( buildWhen: (_, state) => state.gemID == gemIDs[index], - builder: (context, state) => switch (state.status) { + builder: (context, fetchState) => switch (fetchState.status) { CRequestCubitStatus.initial => const Center(child: CCradleLoadingIndicator()), CRequestCubitStatus.inProgress => const Center(child: CCradleLoadingIndicator()), CRequestCubitStatus.failed => const Center(child: Icon(Icons.error_rounded)), - CRequestCubitStatus.succeeded => CAnimatedGem(gem: state.gem), + CRequestCubitStatus.succeeded => + BlocBuilder( + buildWhen: (_, state) => + state.currentGem?.id == fetchState.gemID, + builder: (context, state) => state.currentGem != null + ? CAnimatedGem(gem: state.currentGem!) + : const SizedBox(), + ), }, ), ), diff --git a/app/lib/shared/widgets/avatar.dart b/app/lib/shared/widgets/avatar.dart index 8009581..cfc40a9 100644 --- a/app/lib/shared/widgets/avatar.dart +++ b/app/lib/shared/widgets/avatar.dart @@ -1,5 +1,6 @@ import 'package:ccore/ccore.dart'; import 'package:chuckle_chest/shared/_shared.dart'; +import 'package:chuckle_chest/shared/dialogs/_dialogs.dart'; import 'package:cperson_repository/cperson_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -61,7 +62,7 @@ class CAvatar extends StatelessWidget { String? url; if (person != null) { - url = person?.avatarURLForDate(date); + url = person?.avatarURLForDate(date)?.url; } else if (personID != null) { CPerson? person2; @@ -71,23 +72,28 @@ class CAvatar extends StatelessWidget { person2 = context.read().fetchPerson(personID!); } - url = person2?.avatarURLForDate(date); + url = person2?.avatarURLForDate(date)?.url; } return CircleAvatar( radius: diameter != null ? diameter! / 2 : null, foregroundImage: url != null ? NetworkImage(url) : null, - child: onPressed != null - ? Material( - type: MaterialType.transparency, - clipBehavior: Clip.antiAlias, - shape: const CircleBorder(), - child: InkWell( - onTap: onPressed, - child: Center(child: icon), - ), - ) - : Center(child: icon), + child: Material( + type: MaterialType.transparency, + clipBehavior: Clip.antiAlias, + shape: const CircleBorder(), + child: InkWell( + onTap: onPressed ?? + (url != null + ? () => CAvatarDialog( + nickname: person?.nickname ?? '', + year: date.year, + imageURL: url!, + ).show(context) + : null), + child: Center(child: icon), + ), + ), ); } } diff --git a/app/pubspec.lock b/app/pubspec.lock index 2ba3497..ae59a3c 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -62,6 +62,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -114,10 +122,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: @@ -302,6 +310,14 @@ packages: relative: true source: path version: "0.0.0" + crop_your_image: + dependency: "direct main" + description: + name: crop_your_image + sha256: "9ae3b33042de5bda5321fc48aad41054c196bf2cc28350cd30cb8a85c1a7b1bd" + url: "https://pub.dev" + source: hosted + version: "1.1.0" cross_file: dependency: transitive description: @@ -318,6 +334,13 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + cstorage_client: + dependency: "direct main" + description: + path: "../packages/data/storage_client" + relative: true + source: path + version: "0.0.0" dart_jsonwebtoken: dependency: transitive description: @@ -374,6 +397,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" + url: "https://pub.dev" + source: hosted + version: "0.9.3" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" fixnum: dependency: transitive description: @@ -400,6 +455,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" flutter_test: dependency: "direct dev" description: flutter @@ -482,6 +545,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643 + url: "https://pub.dev" + source: hosted + version: "0.8.12+15" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 5f4c8fd..fa382b8 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -1,7 +1,7 @@ name: chuckle_chest description: "A new Flutter project." publish_to: "none" -version: 0.8.1+1 +version: 0.9.0+1 environment: sdk: ">=3.4.0 <4.0.0" @@ -10,7 +10,7 @@ dependencies: auto_route: ^8.1.4 bloc: ^8.1.4 bloc_concurrency: ^0.2.5 - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 cauth_client: cauth_repository: cchest_repository: @@ -19,6 +19,8 @@ dependencies: cgem_repository: cperson_repository: cplatform_client: + crop_your_image: ^1.1.0 + cstorage_client: equatable: ^2.0.5 flutter: sdk: flutter diff --git a/melos.yaml b/melos.yaml index 9db226b..55205f5 100644 --- a/melos.yaml +++ b/melos.yaml @@ -91,6 +91,6 @@ scripts: packageFilters: fileExists: pubspec.yaml - diff_db: - description: Creates new migration file with the chagnes made in the Supabase studio. - run: supabase db diff --local -f new + diff_supabase: + description: Creates new migration file with the changes made in the Supabase studio. + run: supabase db diff --local -f new && supabase db diff --local -f new --schema storage diff --git a/packages/data/auth_client/pubspec.lock b/packages/data/auth_client/pubspec.lock index 1a944d8..8354ecf 100644 --- a/packages/data/auth_client/pubspec.lock +++ b/packages/data/auth_client/pubspec.lock @@ -50,10 +50,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: diff --git a/packages/data/auth_client/pubspec.yaml b/packages/data/auth_client/pubspec.yaml index ba18ff8..c95246a 100644 --- a/packages/data/auth_client/pubspec.yaml +++ b/packages/data/auth_client/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=3.1.0 <4.0.0" dependencies: - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 ccore: dart_jsonwebtoken: ^2.14.0 equatable: ^2.0.5 diff --git a/packages/data/database_client/pubspec.lock b/packages/data/database_client/pubspec.lock index 8cdff1b..d921b58 100644 --- a/packages/data/database_client/pubspec.lock +++ b/packages/data/database_client/pubspec.lock @@ -42,10 +42,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: diff --git a/packages/data/database_client/pubspec.yaml b/packages/data/database_client/pubspec.yaml index 0848022..bf4baf4 100644 --- a/packages/data/database_client/pubspec.yaml +++ b/packages/data/database_client/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=3.1.0 <4.0.0" dependencies: - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 ccore: supabase: ^2.3.0 typesafe_supabase: ^0.0.1-dev.18 diff --git a/packages/data/platform_client/lib/src/exceptions/_exceptions.dart b/packages/data/platform_client/lib/src/exceptions/_exceptions.dart index f718295..ba9cad9 100644 --- a/packages/data/platform_client/lib/src/exceptions/_exceptions.dart +++ b/packages/data/platform_client/lib/src/exceptions/_exceptions.dart @@ -1,2 +1,3 @@ export 'clipboard_copy.dart'; +export 'image_pick.dart'; export 'share.dart'; diff --git a/packages/data/platform_client/lib/src/exceptions/image_pick.dart b/packages/data/platform_client/lib/src/exceptions/image_pick.dart new file mode 100644 index 0000000..8363461 --- /dev/null +++ b/packages/data/platform_client/lib/src/exceptions/image_pick.dart @@ -0,0 +1,13 @@ +import 'dart:developer'; + +/// Represents an exception that occurs when picking an image from the user's +/// gallery. +enum CImagePickException { + /// The failure was unitentifiable. + unknown; + + factory CImagePickException.fromError(Object e, StackTrace s) { + log(e.toString(), error: e, stackTrace: s, name: 'CImagePickException'); + return CImagePickException.unknown; + } +} diff --git a/packages/data/platform_client/lib/src/platform_client.dart b/packages/data/platform_client/lib/src/platform_client.dart index 4cc0a6a..0764856 100644 --- a/packages/data/platform_client/lib/src/platform_client.dart +++ b/packages/data/platform_client/lib/src/platform_client.dart @@ -2,6 +2,7 @@ import 'package:bobs_jobs/bobs_jobs.dart'; import 'package:cplatform_client/cplatform_client.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:share_plus/share_plus.dart'; /// A client to interact with the platform's API. @@ -69,4 +70,24 @@ class CPlatformClient { /// The type of device the app is running on. CDeviceType get deviceType => CPlatformClient.staticDeviceType; + + /// Allows the user to pick an image from their gallery. + BobsJob> pickImage({ + double? maxWidth, + double? maxHeight, + }) => + BobsJob.attempt( + run: () async { + final picker = ImagePicker(); + final image = await picker.pickImage( + source: ImageSource.gallery, + maxWidth: maxWidth, + maxHeight: maxHeight, + ); + if (image == null) return bobsAbsent(); + final bytes = await image.readAsBytes(); + return bobsPresent(bytes); + }, + onError: CImagePickException.fromError, + ); } diff --git a/packages/data/platform_client/pubspec.lock b/packages/data/platform_client/pubspec.lock index 0fca968..093708e 100644 --- a/packages/data/platform_client/pubspec.lock +++ b/packages/data/platform_client/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: @@ -128,6 +128,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" + url: "https://pub.dev" + source: hosted + version: "0.9.3" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" fixnum: dependency: transitive description: @@ -146,6 +178,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" flutter_web_plugins: dependency: transitive description: flutter @@ -167,6 +207,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -183,6 +231,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643 + url: "https://pub.dev" + source: hosted + version: "0.8.12+15" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: transitive description: @@ -621,5 +733,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/packages/data/platform_client/pubspec.yaml b/packages/data/platform_client/pubspec.yaml index f6f30e0..75cd85b 100644 --- a/packages/data/platform_client/pubspec.yaml +++ b/packages/data/platform_client/pubspec.yaml @@ -4,9 +4,10 @@ environment: sdk: ">=3.1.0 <4.0.0" dependencies: - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 ccore: flutter: sdk: flutter + image_picker: ^1.1.2 share_plus: ^10.0.2 very_good_analysis: ^6.0.0 diff --git a/packages/data/storage_client/lib/cstorage_client.dart b/packages/data/storage_client/lib/cstorage_client.dart new file mode 100644 index 0000000..c94e4b8 --- /dev/null +++ b/packages/data/storage_client/lib/cstorage_client.dart @@ -0,0 +1,5 @@ +/// A library to interact with the storage API. +library cstorage_client; + +export 'src/exceptions/_exceptions.dart'; +export 'src/storage_client.dart'; diff --git a/packages/data/storage_client/lib/src/exceptions/_exceptions.dart b/packages/data/storage_client/lib/src/exceptions/_exceptions.dart new file mode 100644 index 0000000..0be30fc --- /dev/null +++ b/packages/data/storage_client/lib/src/exceptions/_exceptions.dart @@ -0,0 +1 @@ +export 'avatar_upload_exception.dart'; diff --git a/packages/data/storage_client/lib/src/exceptions/avatar_upload_exception.dart b/packages/data/storage_client/lib/src/exceptions/avatar_upload_exception.dart new file mode 100644 index 0000000..b7bfe8a --- /dev/null +++ b/packages/data/storage_client/lib/src/exceptions/avatar_upload_exception.dart @@ -0,0 +1,28 @@ +import 'dart:developer'; + +import 'package:supabase/supabase.dart'; + +/// Represents an exception that occurs when uploading an avatar fails. +enum CRawAvatarUploadException { + /// The failure was unitentifiable. + unknown; + + factory CRawAvatarUploadException.fromError(Object e, StackTrace s) { + if (e is PostgrestException) { + log( + e.message, + error: e, + stackTrace: s, + name: 'CRawAvatarUploadException', + ); + } else { + log( + e.toString(), + error: e, + stackTrace: s, + name: 'CRawAvatarUploadException', + ); + } + return CRawAvatarUploadException.unknown; + } +} diff --git a/packages/data/storage_client/lib/src/storage_client.dart b/packages/data/storage_client/lib/src/storage_client.dart new file mode 100644 index 0000000..c46524d --- /dev/null +++ b/packages/data/storage_client/lib/src/storage_client.dart @@ -0,0 +1,48 @@ +import 'dart:typed_data'; + +import 'package:bobs_jobs/bobs_jobs.dart'; +import 'package:cstorage_client/cstorage_client.dart'; +import 'package:supabase/supabase.dart'; + +/// {@template CStorageClient} +/// +/// The client that interacts with the Supabase storage API. +/// +/// It is used to upload avatars for people in the chest. +/// +/// {@endtemplate} +class CStorageClient { + /// {@macro CStorageClient} + const CStorageClient({required this.supabaseClient}); + + /// The Supabase client to use for storage operations. + final SupabaseClient supabaseClient; + + /// Uploads the avatar file for the person with the given [personID] in the + /// chest with the given [chestID] and [year]. + /// + /// Returns the signed URL for the uploaded file. + /// + /// If the user already has an avatar for the given year, it will be + /// overwritten. + BobsJob uploadAvatar({ + required String chestID, + required BigInt personID, + required int year, + required Uint8List avatarFile, + }) => + BobsJob.attempt( + run: () => supabaseClient.storage.from('avatars').uploadBinary( + 'chests/$chestID/$personID-$year.jpg', + avatarFile, + fileOptions: const FileOptions(upsert: true), + ), + onError: CRawAvatarUploadException.fromError, + ).thenAttempt( + run: (path) => supabaseClient.storage.from('avatars').createSignedUrl( + path.replaceFirst('avatars/', ''), + 60 * 60 * 24 * 365 * 100, + ), + onError: CRawAvatarUploadException.fromError, + ); +} diff --git a/packages/data/storage_client/pubspec.lock b/packages/data/storage_client/pubspec.lock new file mode 100644 index 0000000..dab6cb9 --- /dev/null +++ b/packages/data/storage_client/pubspec.lock @@ -0,0 +1,750 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" + source: hosted + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + args: + dependency: transitive + description: + name: args + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" + source: hosted + version: "2.5.0" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + bobs_jobs: + dependency: "direct main" + description: + name: bobs_jobs + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" + url: "https://pub.dev" + source: hosted + version: "0.0.1-dev.4" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb + url: "https://pub.dev" + source: hosted + version: "8.9.2" + ccore: + dependency: "direct main" + description: + path: "../../core" + relative: true + source: path + version: "0.0.0" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 + url: "https://pub.dev" + source: hosted + version: "4.10.0" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + coverage: + dependency: transitive + description: + name: coverage + sha256: c1fb2dce3c0085f39dc72668e85f8e0210ec7de05345821ff58530567df345a5 + url: "https://pub.dev" + source: hosted + version: "1.9.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 + url: "https://pub.dev" + source: hosted + version: "3.0.5" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + flutter: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + functions_client: + dependency: transitive + description: + name: functions_client + sha256: "61597ed93be197b1be6387855e4b760e6aac2355fcfc4df6d20d2b4579982158" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + glob: + dependency: transitive + description: + name: glob + sha256: "0e7014b3b7d4dac1ca4d6114f82bf1782ee86745b9b42a92c9289c23d8a0ab63" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + gotrue: + dependency: transitive + description: + name: gotrue + sha256: "74b29f10ef7239e254847d52ecce1eac2a08c2cfced6a78280859391f5157e8b" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + http: + dependency: transitive + description: + name: http + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" + source: hosted + version: "1.2.2" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + js: + dependency: transitive + description: + name: js + sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf + url: "https://pub.dev" + source: hosted + version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + jwt_decode: + dependency: transitive + description: + name: jwt_decode + sha256: d2e9f68c052b2225130977429d30f187aa1981d789c76ad104a32243cfdebfbb + url: "https://pub.dev" + source: hosted + version: "0.3.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + mocktail: + dependency: "direct dev" + description: + name: mocktail + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" + source: hosted + version: "1.0.4" + node_preamble: + dependency: transitive + description: + name: node_preamble + sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + postgrest: + dependency: transitive + description: + name: postgrest + sha256: c6ddc0a2c238c5a686b00094edad728a49645579bf5868a06e1ad95c4918fe2c + url: "https://pub.dev" + source: hosted + version: "2.3.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + realtime_client: + dependency: transitive + description: + name: realtime_client + sha256: "173c3dc40922bb0ae19a558ae1a0acf7b28ec8a458b54390c499bbe5ff15f049" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + retry: + dependency: transitive + description: + name: retry + sha256: "822e118d5b3aafed083109c72d5f484c6dc66707885e07c0fbcb8b986bba7efc" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_packages_handler: + dependency: transitive + description: + name: shelf_packages_handler + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + shelf_static: + dependency: transitive + description: + name: shelf_static + sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3 + url: "https://pub.dev" + source: hosted + version: "1.1.3" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_map_stack_trace: + dependency: transitive + description: + name: source_map_stack_trace + sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b + url: "https://pub.dev" + source: hosted + version: "2.1.2" + source_maps: + dependency: transitive + description: + name: source_maps + sha256: "708b3f6b97248e5781f493b765c3337db11c5d2c81c3094f10904bfa8004c703" + url: "https://pub.dev" + source: hosted + version: "0.10.12" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + storage_client: + dependency: transitive + description: + name: storage_client + sha256: "833666edcd804aaf434f3b13165fd553a8c6371a488618a58be8914856795c04" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + supabase: + dependency: "direct main" + description: + name: supabase + sha256: dccda29b5bda0a04f6725480e180353486dfd5d9dd2b8b17a30a7a7d094f7042 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test: + dependency: transitive + description: + name: test + sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + url: "https://pub.dev" + source: hosted + version: "1.25.7" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + test_beautifier: + dependency: "direct dev" + description: + name: test_beautifier + sha256: "97b6ec1d8ba13806c6956066c0f66569c9536c38601895e43f52d88e8a0557e0" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + test_core: + dependency: transitive + description: + name: test_core + sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + url: "https://pub.dev" + source: hosted + version: "0.6.4" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + very_good_analysis: + dependency: "direct dev" + description: + name: very_good_analysis + sha256: "1fb637c0022034b1f19ea2acb42a3603cbd8314a470646a59a2fb01f5f3a8629" + url: "https://pub.dev" + source: hosted + version: "6.0.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + url: "https://pub.dev" + source: hosted + version: "14.2.5" + watcher: + dependency: transitive + description: + name: watcher + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web: + dependency: transitive + description: + name: web + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb + url: "https://pub.dev" + source: hosted + version: "1.1.0" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + webkit_inspection_protocol: + dependency: transitive + description: + name: webkit_inspection_protocol + sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + yaml: + dependency: transitive + description: + name: yaml + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + yet_another_json_isolate: + dependency: transitive + description: + name: yet_another_json_isolate + sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9" + url: "https://pub.dev" + source: hosted + version: "2.0.3" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/packages/data/storage_client/pubspec.yaml b/packages/data/storage_client/pubspec.yaml new file mode 100644 index 0000000..ba8152f --- /dev/null +++ b/packages/data/storage_client/pubspec.yaml @@ -0,0 +1,17 @@ +name: cstorage_client + +environment: + sdk: ">=3.1.0 <4.0.0" + +dependencies: + bobs_jobs: ^0.0.1-dev.4 + ccore: + supabase: ^2.3.0 + +dev_dependencies: + build_runner: ^2.4.12 + flutter_test: + sdk: flutter + mocktail: ^1.0.4 + test_beautifier: ^1.1.0 + very_good_analysis: ^6.0.0 diff --git a/packages/domain/auth_repository/pubspec.lock b/packages/domain/auth_repository/pubspec.lock index 168a434..6914be7 100644 --- a/packages/domain/auth_repository/pubspec.lock +++ b/packages/domain/auth_repository/pubspec.lock @@ -50,10 +50,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: diff --git a/packages/domain/auth_repository/pubspec.yaml b/packages/domain/auth_repository/pubspec.yaml index ddd4bdb..d050603 100644 --- a/packages/domain/auth_repository/pubspec.yaml +++ b/packages/domain/auth_repository/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=3.1.0 <4.0.0" dependencies: - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 cauth_client: ccore: equatable: ^2.0.5 diff --git a/packages/domain/chest_repository/pubspec.lock b/packages/domain/chest_repository/pubspec.lock index 78b8644..68d39d8 100644 --- a/packages/domain/chest_repository/pubspec.lock +++ b/packages/domain/chest_repository/pubspec.lock @@ -42,10 +42,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: diff --git a/packages/domain/chest_repository/pubspec.yaml b/packages/domain/chest_repository/pubspec.yaml index ba61683..4af96f0 100644 --- a/packages/domain/chest_repository/pubspec.yaml +++ b/packages/domain/chest_repository/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=3.1.0 <4.0.0" dependencies: - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 ccore: cdatabase_client: diff --git a/packages/domain/gem_repository/lib/src/models/gem.dart b/packages/domain/gem_repository/lib/src/models/gem.dart index 7c1fa7e..d29a991 100644 --- a/packages/domain/gem_repository/lib/src/models/gem.dart +++ b/packages/domain/gem_repository/lib/src/models/gem.dart @@ -54,7 +54,7 @@ class CGem with EquatableMixin { id: id, number: number, occurredAt: occurredAt ?? this.occurredAt, - lines: lines, + lines: [...lines], chestID: chestID, ); diff --git a/packages/domain/gem_repository/pubspec.lock b/packages/domain/gem_repository/pubspec.lock index 6d536e3..3015091 100644 --- a/packages/domain/gem_repository/pubspec.lock +++ b/packages/domain/gem_repository/pubspec.lock @@ -42,10 +42,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: @@ -179,6 +179,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" + url: "https://pub.dev" + source: hosted + version: "0.9.3" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" fixnum: dependency: transitive description: @@ -197,6 +229,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" flutter_test: dependency: "direct dev" description: flutter @@ -263,6 +303,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643 + url: "https://pub.dev" + source: hosted + version: "0.8.12+15" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: transitive description: @@ -829,5 +933,5 @@ packages: source: hosted version: "2.0.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.19.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/packages/domain/gem_repository/pubspec.yaml b/packages/domain/gem_repository/pubspec.yaml index 04d57f9..91d29de 100644 --- a/packages/domain/gem_repository/pubspec.yaml +++ b/packages/domain/gem_repository/pubspec.yaml @@ -4,7 +4,7 @@ environment: sdk: ">=3.1.0 <4.0.0" dependencies: - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 ccore: cdatabase_client: cplatform_client: diff --git a/packages/domain/person_repository/lib/src/exceptions/_exceptions.dart b/packages/domain/person_repository/lib/src/exceptions/_exceptions.dart index 54e89cf..355f5d7 100644 --- a/packages/domain/person_repository/lib/src/exceptions/_exceptions.dart +++ b/packages/domain/person_repository/lib/src/exceptions/_exceptions.dart @@ -1,4 +1,5 @@ -export 'avatar_upsert.dart'; +export 'avatar_pick.dart'; +export 'avatar_update.dart'; export 'chest_people_fetch.dart'; export 'person_creation.dart'; export 'person_stream.dart'; diff --git a/packages/domain/person_repository/lib/src/exceptions/avatar_pick.dart b/packages/domain/person_repository/lib/src/exceptions/avatar_pick.dart new file mode 100644 index 0000000..49672f1 --- /dev/null +++ b/packages/domain/person_repository/lib/src/exceptions/avatar_pick.dart @@ -0,0 +1,23 @@ +import 'package:cdatabase_client/cdatabase_client.dart'; +import 'package:cplatform_client/cplatform_client.dart'; + +/// Represents an exception that occurs when picking an avatar from the user's +/// gallery fails. +enum CAvatarPickException { + /// The failure was unitentifiable. + unknown; + + /// Converts the raw exception to a [CAvatarPickException]. + static CAvatarPickException fromRaw(Object e) { + if (e is CRawAvatarUpsertException) { + return switch (e) { + CRawAvatarUpsertException.unknown => CAvatarPickException.unknown, + }; + } else if (e is CImagePickException) { + return switch (e) { + CImagePickException.unknown => CAvatarPickException.unknown, + }; + } + return CAvatarPickException.unknown; + } +} diff --git a/packages/domain/person_repository/lib/src/exceptions/avatar_update.dart b/packages/domain/person_repository/lib/src/exceptions/avatar_update.dart new file mode 100644 index 0000000..f8fe081 --- /dev/null +++ b/packages/domain/person_repository/lib/src/exceptions/avatar_update.dart @@ -0,0 +1,30 @@ +import 'dart:developer'; + +import 'package:cdatabase_client/cdatabase_client.dart'; +import 'package:cplatform_client/cplatform_client.dart'; + +/// Represents an exception that occurs when upserting an avatar fails. +enum CAvatarUpdateException { + /// The failure was unitentifiable. + unknown; + + /// Converts the error to a [CAvatarUpdateException]. + static CAvatarUpdateException fromError(Object e, StackTrace s) { + log(e.toString(), error: e, stackTrace: s, name: 'CAvatarUpdateException'); + return CAvatarUpdateException.unknown; + } + + /// Converts the raw exception to a [CAvatarUpdateException]. + static CAvatarUpdateException fromRaw(Object e) { + if (e is CRawAvatarUpsertException) { + return switch (e) { + CRawAvatarUpsertException.unknown => CAvatarUpdateException.unknown, + }; + } else if (e is CImagePickException) { + return switch (e) { + CImagePickException.unknown => CAvatarUpdateException.unknown, + }; + } + return CAvatarUpdateException.unknown; + } +} diff --git a/packages/domain/person_repository/lib/src/exceptions/avatar_upsert.dart b/packages/domain/person_repository/lib/src/exceptions/avatar_upsert.dart deleted file mode 100644 index 1b74b23..0000000 --- a/packages/domain/person_repository/lib/src/exceptions/avatar_upsert.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:cdatabase_client/cdatabase_client.dart'; - -/// Represents an exception that occurs when upserting an avatar fails. -enum CAvatarUpsertException { - /// The failure was unitentifiable. - unknown; - - /// Converts the raw exception to a [CAvatarUpsertException]. - static CAvatarUpsertException fromRaw(CRawAvatarUpsertException e) { - return switch (e) { - CRawAvatarUpsertException.unknown => CAvatarUpsertException.unknown, - }; - } -} diff --git a/packages/domain/person_repository/lib/src/models/person.dart b/packages/domain/person_repository/lib/src/models/person.dart index 96f56a7..071a5b1 100644 --- a/packages/domain/person_repository/lib/src/models/person.dart +++ b/packages/domain/person_repository/lib/src/models/person.dart @@ -68,8 +68,8 @@ class CPerson with EquatableMixin { } /// The URL of the person's avatar for the given date. - String? avatarURLForDate(DateTime? date) => - avatarURLs.cFirstWhereOrNull((a) => a.year == date?.year)?.url; + CAvatarURL? avatarURLForDate(DateTime? date) => + avatarURLs.cFirstWhereOrNull((a) => a.year == date?.year); /// Creates a copy of the person with the given fields updated. CPerson copyWith({String? nickname, DateTime? dateOfBirth}) { diff --git a/packages/domain/person_repository/lib/src/person_repository.dart b/packages/domain/person_repository/lib/src/person_repository.dart index 16b7a9d..e1d0196 100644 --- a/packages/domain/person_repository/lib/src/person_repository.dart +++ b/packages/domain/person_repository/lib/src/person_repository.dart @@ -1,8 +1,12 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:bobs_jobs/bobs_jobs.dart'; import 'package:cdatabase_client/cdatabase_client.dart'; import 'package:cperson_repository/cperson_repository.dart'; +import 'package:cplatform_client/cplatform_client.dart'; +import 'package:cstorage_client/cstorage_client.dart'; +import 'package:image/image.dart' as img; /// {@template CPersonRepository} /// @@ -13,11 +17,19 @@ class CPersonRepository { /// {@macro CPersonRepository} const CPersonRepository({ required this.personClient, + required this.storageClient, + required this.platformClient, }); /// The client for interacting with the people API. final CPersonClient personClient; + /// The client for interacting with the storage API. + final CStorageClient storageClient; + + /// The client for interacting with the platform API. + final CPlatformClient platformClient; + /// Fetches all the people belonging to the chest with the given `chestID`. BobsJob> fetchChestPeople({ required String chestID, @@ -53,25 +65,63 @@ class CPersonRepository { ), ); - /// Upserts an avatar for the given person. - BobsJob upsertAvatar({ - required CPerson person, - required String imageURL, + /// Updates the avatar for the given `year` for the given `person` and returns + /// the URL of the new avatar. + /// + /// If the an image for the given `year` already exists, it will be replaced. + BobsJob updateAvatar({ + required BigInt personID, + required String chestID, required int year, + required Uint8List image, }) => - personClient - .upsertAvatar( - avatar: CAvatarsTableInsert( - personID: person.id, + BobsJob.attempt( + run: () async { + final command = img.Command() + ..decodeImage(image) + ..copyResize( + width: 200, + height: 200, + maintainAspect: true, + ) + ..encodeJpg(); + return (await command.executeThread()).outputBytes!; + }, + onError: CAvatarUpdateException.fromError, + ).chainOnSuccess( + onFailure: (f) => f, + nextJob: (resizedImage) => storageClient + .uploadAvatar( + chestID: chestID, + personID: personID, year: year, - imageURL: imageURL, - chestID: person.chestID, + avatarFile: resizedImage, + ) + .chainOnSuccess( + onFailure: CAvatarUpdateException.fromRaw, + nextJob: (imageURL) => personClient + .upsertAvatar( + avatar: CAvatarsTableInsert( + personID: personID, + year: year, + imageURL: imageURL, + chestID: chestID, + ), + ) + .thenEvaluate( + onFailure: CAvatarUpdateException.fromRaw, + onSuccess: (_) => imageURL, + ), ), - ) - .thenEvaluate( - onFailure: CAvatarUpsertException.fromRaw, - onSuccess: (_) => bobsNothing, - ); + ); + + /// Allows the user to pick an image from their gallery. + /// + /// If the user cancels the image pick it will return a `BobsAbsent`. + BobsJob> pickAvatar() => + platformClient + .pickImage(maxHeight: 1000, maxWidth: 1000) + .thenEvaluateOnFailure(CAvatarPickException.fromRaw); /// Creates a default person with the given `chestID`. BobsJob createPerson({ diff --git a/packages/domain/person_repository/pubspec.lock b/packages/domain/person_repository/pubspec.lock index d060773..5803307 100644 --- a/packages/domain/person_repository/pubspec.lock +++ b/packages/domain/person_repository/pubspec.lock @@ -22,6 +22,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.0" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -42,10 +50,10 @@ packages: dependency: "direct main" description: name: bobs_jobs - sha256: dc7937f6a06f960c231a354310ff18cde33663a176e7f66db1ef53f6ee5ef843 + sha256: "85524c701541576f4cffb136a09fea60726cac13dfec94617d6c53d619648531" url: "https://pub.dev" source: hosted - version: "0.0.1-dev.3" + version: "0.0.1-dev.4" boolean_selector: dependency: transitive description: @@ -116,6 +124,21 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.2" + cplatform_client: + dependency: "direct main" + description: + path: "../../data/platform_client" + relative: true + source: path + version: "0.0.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: dependency: transitive description: @@ -124,6 +147,13 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.5" + cstorage_client: + dependency: "direct main" + description: + path: "../../data/storage_client" + relative: true + source: path + version: "0.0.0" dart_style: dependency: transitive description: @@ -148,6 +178,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" file: dependency: transitive description: @@ -156,6 +194,46 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" + url: "https://pub.dev" + source: hosted + version: "0.9.3" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: transitive description: flutter @@ -166,11 +244,24 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + url: "https://pub.dev" + source: hosted + version: "2.0.23" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" frontend_server_client: dependency: transitive description: @@ -227,6 +318,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: "direct main" + description: + name: image + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" + source: hosted + version: "4.2.0" + image_picker: + dependency: transitive + description: + name: image_picker + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: d3e5e00fdfeca8fd4ffb3227001264d449cc8950414c2ff70b0e06b9c628e643 + url: "https://pub.dev" + source: hosted + version: "0.8.12+15" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" + url: "https://pub.dev" + source: hosted + version: "0.8.12" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" intl: dependency: transitive description: @@ -363,6 +526,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + url: "https://pub.dev" + source: hosted + version: "2.2.12" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + url: "https://pub.dev" + source: hosted + version: "2.4.0" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" + source: hosted + version: "6.0.2" + platform: + dependency: transitive + description: + name: platform + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + url: "https://pub.dev" + source: hosted + version: "3.1.5" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" pool: dependency: transitive description: @@ -411,6 +646,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.28.0" + share_plus: + dependency: transitive + description: + name: share_plus + sha256: "468c43f285207c84bcabf5737f33b914ceb8eb38398b91e5e3ad1698d1b72a52" + url: "https://pub.dev" + source: hosted + version: "10.0.2" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" + url: "https://pub.dev" + source: hosted + version: "5.0.0" shelf: dependency: transitive description: @@ -480,6 +731,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -576,6 +835,46 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.1-dev.18" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + url: "https://pub.dev" + source: hosted + version: "3.2.0" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_math: dependency: transitive description: @@ -612,10 +911,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" web_socket: dependency: transitive description: @@ -640,6 +939,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec" + url: "https://pub.dev" + source: hosted + version: "5.5.5" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + xml: + dependency: transitive + description: + name: xml + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" + source: hosted + version: "6.5.0" yaml: dependency: transitive description: @@ -657,5 +980,5 @@ packages: source: hosted version: "2.0.2" sdks: - dart: ">=3.4.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/packages/domain/person_repository/pubspec.yaml b/packages/domain/person_repository/pubspec.yaml index 51198bc..bbc9ccb 100644 --- a/packages/domain/person_repository/pubspec.yaml +++ b/packages/domain/person_repository/pubspec.yaml @@ -4,10 +4,13 @@ environment: sdk: ">=3.1.0 <4.0.0" dependencies: - bobs_jobs: ^0.0.1-dev.3 + bobs_jobs: ^0.0.1-dev.4 ccore: cdatabase_client: + cplatform_client: + cstorage_client: equatable: ^2.0.5 + image: ^4.2.0 dev_dependencies: flutter_test: diff --git a/supabase/migrations/20241011105715_avatars_bucket.sql b/supabase/migrations/20241011105715_avatars_bucket.sql new file mode 100644 index 0000000..3bea87f --- /dev/null +++ b/supabase/migrations/20241011105715_avatars_bucket.sql @@ -0,0 +1,14 @@ +alter table "storage"."objects" drop column "user_metadata"; + +alter table "storage"."s3_multipart_uploads" drop column "user_metadata"; + +create policy "Enable insert for authenticated users only" +on "storage"."objects" +as permissive +for all +to authenticated +using (true) +with check (true); + + +