Skip to content

Commit

Permalink
Adds support to update collection details - closes #216
Browse files Browse the repository at this point in the history
  • Loading branch information
ggirotto committed Nov 17, 2021
1 parent 63449c7 commit f400585
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 74 deletions.
9 changes: 9 additions & 0 deletions lib/application/constants/exception_strings.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import 'package:memo/core/faults/exceptions/base_exception.dart';
import 'package:memo/core/faults/exceptions/validation_exception.dart';

/// Returns a locale-suitable description for the following [exception].
String descriptionForException(BaseException exception) {
switch (exception.type) {
case ExceptionType.emptyField:
return 'Este campo é obrigatório';
case ExceptionType.fieldLengthExceeded:
final validationException = exception as ValidationException;
return 'Este campo tem limite máximo de ${validationException.amount!} caracteres';

case ExceptionType.failedToOpenUrl:
return 'Algo deu errado ao tentar abrir o link!';
default:
return 'Algo deu errado. Por favor tente novamente';
}
Expand Down
2 changes: 1 addition & 1 deletion lib/application/coordinator/routes_coordinator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'package:memo/application/pages/details/details_providers.dart';
import 'package:memo/application/pages/execution/collection_execution_page.dart';
import 'package:memo/application/pages/execution/execution_providers.dart';
import 'package:memo/application/pages/home/collections/update/update_collection_page.dart';
import 'package:memo/application/pages/home/collections/update/update_providers.dart';
import 'package:memo/application/pages/home/collections/update/update_collection_providers.dart';
import 'package:memo/application/pages/home/home_page.dart';
import 'package:memo/application/pages/settings/settings_page.dart';
import 'package:memo/core/faults/errors/inconsistent_state_error.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,127 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:layoutr/common_layout.dart';
import 'package:memo/application/constants/exception_strings.dart';
import 'package:memo/application/constants/strings.dart' as strings;
import 'package:memo/application/hooks/rich_text_field_controller_hook.dart';
import 'package:memo/application/hooks/tags_controller_hook.dart';
import 'package:memo/application/theme/theme_controller.dart';
import 'package:memo/application/view-models/home/update_collection_details_vm.dart';
import 'package:memo/application/widgets/theme/custom_text_field.dart';
import 'package:memo/application/widgets/theme/rich_text_field.dart';
import 'package:memo/application/widgets/theme/tags_field.dart';
import 'package:memo/application/widgets/unfocus_detector.dart';
import 'package:memo/domain/validators/collection_validators.dart' as validators;

class UpdateCollectionDetails extends StatelessWidget {
class UpdateCollectionDetails extends HookWidget {
@override
Widget build(BuildContext context) {
return Container(color: Colors.red, child: Center(child: Text('Detalhes')));
return UnfocusDetector(
child: SingleChildScrollView(
child: Column(
children: [
_NameField(),
context.verticalBox(Spacing.large),
_TagsField(),
context.verticalBox(Spacing.large),
_DescriptionField(),
],
).withSymmetricalPadding(context, vertical: Spacing.large, horizontal: Spacing.small),
),
);
}
}

class _NameField extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(updateCollectionDetailsVM.notifier);
final state = ref.watch(updateCollectionDetailsVM);

final controller = useTextEditingController(text: state.metadata.name);
final focus = useFocusNode();

useEffect(() {
void onNameUpdate() => vm.updateName(controller.text);

controller.addListener(onNameUpdate);
return () => controller.removeListener(onNameUpdate);
});

final nameLength = state.metadata.name.length;
return CustomTextField(
controller: controller,
focusNode: focus,
labelText: strings.collectionName,
inputFormatters: [
LengthLimitingTextInputFormatter(validators.collectionNameMaxLength),
],
helperText: '$nameLength/${validators.collectionNameMaxLength} caracteres',
errorText: state is UpdateDetailsInvalid && state.nameException != null && !focus.hasFocus
? descriptionForException(state.nameException!)
: null,
);
}
}

class _TagsField extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(updateCollectionDetailsVM.notifier);

final controller = useTagsController();

useEffect(() {
void onTagsUpdate() => vm.updateTags(controller.tags);

controller.addListener(onTagsUpdate);
return () => controller.removeListener(onTagsUpdate);
});

return const TagsField();
}
}

class _DescriptionField extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = useTheme(ref);
final textTheme = Theme.of(context).textTheme;
final vm = ref.read(updateCollectionDetailsVM.notifier);
final state = ref.watch(updateCollectionDetailsVM);

final controller = useRichTextEditingController(richText: state.metadata.description.richText);
final focus = useFocusNode();
final hasFocus = useState(focus.hasFocus);

useEffect(() {
void onDescriptionUpdate() => vm.updateDescription(controller.value);
void onFocusUpdate() => hasFocus.value = focus.hasFocus;

controller.addListener(onDescriptionUpdate);
focus.addListener(onFocusUpdate);

return () {
controller.removeListener(onDescriptionUpdate);
focus.removeListener(onFocusUpdate);
};
});

final descriptionLength = state.metadata.description.plainText.length;
return RichTextField(
controller: controller,
focus: focus,
modalTitle: Text(
strings.detailsDescription,
style: textTheme.bodyText1?.copyWith(color: theme.primarySwatch.shade400),
),
placeholder: strings.collectionDescription,
helperText: '$descriptionLength/${validators.collectionDescriptionMaxLength} caracteres',
errorText: state is UpdateDetailsInvalid && state.descriptionException != null && !focus.hasFocus
? descriptionForException(state.descriptionException!)
: null,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,22 @@ import 'package:layoutr/common_layout.dart';
import 'package:memo/application/constants/strings.dart' as strings;
import 'package:memo/application/pages/home/collections/update/update_collection_details.dart';
import 'package:memo/application/pages/home/collections/update/update_collection_memos.dart';
import 'package:memo/application/pages/home/collections/update/update_providers.dart';
import 'package:memo/application/pages/home/collections/update/update_collection_providers.dart';
import 'package:memo/application/theme/theme_controller.dart';
import 'package:memo/application/view-models/home/update_collection_details_vm.dart';
import 'package:memo/application/view-models/home/update_collection_vm.dart';
import 'package:memo/application/widgets/theme/custom_button.dart';
import 'package:memo/application/widgets/theme/exception_retry_container.dart';
import 'package:memo/application/widgets/theme/themed_container.dart';
import 'package:memo/application/widgets/theme/themed_tab_bar.dart';
import 'package:memo/core/faults/errors/inconsistent_state_error.dart';

enum _Segment { details, memos }

class UpdateCollectionPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = useUpdateCollectionVM(ref);
final vm = ref.read(updateCollectionVM.notifier);

final selectedSegment = useState(_Segment.details);
final tabController = useTabController(initialLength: _Segment.values.length);
Expand Down Expand Up @@ -58,19 +60,46 @@ class _UpdateCollectionContents extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = useUpdateCollectionVM(ref);
final state = useUpdateCollectionState(ref);
final vm = ref.read(updateCollectionVM.notifier);
final state = ref.watch(updateCollectionVM);

if (state is UpdateCollectionFailedLoading) {
return Center(child: ExceptionRetryContainer(exception: state.exception, onRetry: vm.loadInitialContent));
return Center(child: ExceptionRetryContainer(exception: state.exception, onRetry: vm.loadContent));
}

switch (selectedSegment) {
case _Segment.details:
return UpdateCollectionDetails();
case _Segment.memos:
return UpdateCollectionMemos();
if (state is UpdateCollectionLoading) {
return const Center(child: CircularProgressIndicator());
}

if (state is UpdateCollectionLoaded) {
switch (selectedSegment) {
case _Segment.details:
return ProviderScope(
overrides: [
updateDetailsMetadata.overrideWithValue(state.collectionMetadata),
],
child: _UpdateCollectionDetails(),
);
case _Segment.memos:
return UpdateCollectionMemos();
}
}

throw InconsistentStateError.layout('Unsupported subtype (${state.runtimeType}) of `UpdateCollectionState`');
}
}

class _UpdateCollectionDetails extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(updateCollectionVM.notifier);

ref.listen<UpdatedDetailsState>(
updateCollectionDetailsVM,
(_, state) => vm.updateMetadata(metadata: state.metadata),
);

return UpdateCollectionDetails();
}
}

Expand Down Expand Up @@ -124,38 +153,29 @@ class _DetailsActionButton extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final state = useUpdateCollectionState(ref);
final vm = ref.read(updateCollectionVM.notifier);
final state = ref.watch(updateCollectionVM);

if (state is! UpdateCollectionLoaded) {
return const Center(child: CircularProgressIndicator());
}

final vm = useUpdateCollectionVM(ref);

void onPressed() {
if (state.hasMemos) {
vm.saveCollection();
} else {
onSegmentSwapRequested(_Segment.memos);
}
}

void onPressed() => state.hasMemos ? vm.saveCollection : onSegmentSwapRequested(_Segment.memos);
final buttonTitle = state.hasMemos ? strings.saveCollection : strings.next;

return PrimaryElevatedButton(onPressed: state.hasDetails ? onPressed : null, text: buttonTitle.toUpperCase());
}
}

class _MemosActionButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final state = useUpdateCollectionState(ref);
final vm = ref.read(updateCollectionVM.notifier);
final state = ref.watch(updateCollectionVM);

if (state is! UpdateCollectionLoaded) {
return const Center(child: CircularProgressIndicator());
}

final vm = useUpdateCollectionVM(ref);
final canSave = state.hasDetails && state.hasMemos;

return PrimaryElevatedButton(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:memo/application/view-models/home/update_collection_vm.dart';

/// Overridable collection id used in the scope of a collection update.
final updateCollectionId = Provider<String?>((_) => throw UnimplementedError(), name: 'updateCollectionId');

/// Overridable collection metadata used in the scope of a collection update.
final updateDetailsMetadata =
Provider<CollectionMetadata>((_) => throw UnimplementedError(), name: 'updateDetailsMetadata');

This file was deleted.

10 changes: 2 additions & 8 deletions lib/application/utils/scaffold_messenger.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:memo/application/constants/exception_strings.dart';
import 'package:memo/core/faults/exceptions/base_exception.dart';

/// Unique [GlobalKey] that handles the application's global [ScaffoldMessengerState].
Expand All @@ -14,14 +15,7 @@ void showSnackBar(WidgetRef ref, SnackBar snackBar) => ref.read(scaffoldMessenge

/// Shows a message for the [exception] using [showSnackBar].
void showExceptionSnackBar(WidgetRef ref, BaseException exception) {
final content = Text(_descriptionForException(exception));
final content = Text(descriptionForException(exception));
final exceptionSnackBar = SnackBar(content: content);
showSnackBar(ref, exceptionSnackBar);
}

String _descriptionForException(BaseException exception) {
switch (exception.type) {
case ExceptionType.failedToOpenUrl:
return 'Algo deu errado ao tentar abrir o link!';
}
}
Loading

0 comments on commit f400585

Please sign in to comment.