Skip to content

Commit

Permalink
Adds Memos List - closes #221
Browse files Browse the repository at this point in the history
  • Loading branch information
ggirotto committed Nov 18, 2021
1 parent 56109e8 commit f57d943
Show file tree
Hide file tree
Showing 9 changed files with 276 additions and 40 deletions.
2 changes: 2 additions & 0 deletions lib/application/constants/animations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ const terminalAnimationDuration = Duration(milliseconds: 300);

const imageFadeDuration = Duration(seconds: 1);
const textFieldHelperTextDuration = Duration(milliseconds: 150);

const pageControllerAnimationDuration = Duration(milliseconds: 300);
4 changes: 2 additions & 2 deletions lib/application/constants/strings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ const saveCollection = 'Salvar Coleção';
const collectionName = 'Nome da Coleção';
const collectionDescription = 'Descrição da Coleção';

String updateMemoQuestionTitle(int memoIndex) => '### Pergunta $memoIndex';
String memoQuestionTitle(int memoIndex) => '### Pergunta $memoIndex';
const updateMemoQuestionPlaceholder = 'Digite a questão';

const updateMemoAnswer = '### Resposta';
const memoAnswerTitle = '### Resposta';
const updateMemoAnswerPlaceholder = 'Digite a resposta';

const remove = 'Remover';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,145 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:layoutr/common_layout.dart';
import 'package:memo/application/constants/animations.dart' as anims;
import 'package:memo/application/constants/dimensions.dart' as dimens;
import 'package:memo/application/constants/images.dart' as images;
import 'package:memo/application/constants/strings.dart' as strings;
import 'package:memo/application/theme/theme_controller.dart';
import 'package:memo/application/view-models/home/update_collection_memos_vm.dart';
import 'package:memo/application/view-models/home/update_collection_vm.dart';
import 'package:memo/application/widgets/material/asset_icon_button.dart';
import 'package:memo/application/widgets/theme/memo_terminal.dart';
import 'package:memo/application/widgets/theme/rich_text_field.dart';

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

final controller = usePageController(viewportFraction: 0.95);
final page = useState(0);

useEffect(() {
void onPageUpdate() => page.value = controller.page!.toInt();

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

// Uses `PageView.custom` to support pages reordering.
final pagesView = PageView.custom(
controller: controller,
childrenDelegate: SliverChildBuilderDelegate(
(context, index) {
if (index == state.memos.length) {
return _CreateMemoEmptyState(onTap: vm.createEmptyMemo);
}

final metadata = state.memos[index];

return _MemoPage(
key: ValueKey(metadata),
pageIndex: index,
metadata: metadata,
onRemove: state.memos.length > 1 ? () => vm.removeMemoAtIndex(index) : null,
).withOnlyPadding(context, right: Spacing.xSmall);
},
// Adds +1 to include creation empty state.
childCount: state.memos.length + 1,
findChildIndexCallback: (key) {
final valueKey = key as ValueKey<MemoMetadata>;
return state.memos.indexOf(valueKey.value);
},
),
);

void onPreviousTapped() =>
controller.previousPage(duration: anims.pageControllerAnimationDuration, curve: anims.defaultAnimationCurve);
void onNextTapped() =>
controller.nextPage(duration: anims.pageControllerAnimationDuration, curve: anims.defaultAnimationCurve);

return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(child: pagesView),
context.verticalBox(Spacing.large),
_NavigationIndicator(
currentMemoIndex: page.value,
memosAmount: state.memos.length,
onLeftTapped: page.value > 0 ? onPreviousTapped : null,
onRightTapped: page.value < state.memos.length ? onNextTapped : null,
),
],
).withSymmetricalPadding(context, vertical: Spacing.medium);
}
}

class _MemoPage extends HookConsumerWidget {
const _MemoPage({required this.pageIndex, required this.metadata, this.onRemove, Key? key}) : super(key: key);

final int pageIndex;
final MemoMetadata metadata;
final VoidCallback? onRemove;

@override
Widget build(BuildContext context, WidgetRef ref) {
final vm = ref.read(updateCollectionMemosVM.notifier);
final questionController = RichTextFieldController.fromValue(metadata.question);
final answerController = RichTextFieldController.fromValue(metadata.answer);

useEffect(() {
void onQuestionUpdate() =>
vm.updateMemoAtIndex(pageIndex, metadata: metadata.copyWith(question: questionController.value));
void onAnswerUpdate() =>
vm.updateMemoAtIndex(pageIndex, metadata: metadata.copyWith(answer: answerController.value));

questionController.addListener(onQuestionUpdate);
answerController.addListener(onAnswerUpdate);

return () {
questionController.removeListener(onQuestionUpdate);
answerController.removeListener(onAnswerUpdate);
};
});

return MemoTerminal(
memoIndex: pageIndex + 1,
questionController: questionController,
answerController: answerController,
onRemove: onRemove,
);
}
}

class _NavigationIndicator extends StatelessWidget {
const _NavigationIndicator({
required this.currentMemoIndex,
required this.memosAmount,
this.onLeftTapped,
this.onRightTapped,
});

final int currentMemoIndex;
final int memosAmount;

final VoidCallback? onLeftTapped;
final VoidCallback? onRightTapped;

class UpdateCollectionMemos extends StatelessWidget {
@override
Widget build(BuildContext context) {
return _CreateMemoEmptyState(
onTap: () {
print("teste");
},
final textTheme = Theme.of(context).textTheme;

return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AssetIconButton(images.chevronLeftAsset, onPressed: onLeftTapped),
Text('$currentMemoIndex/$memosAmount', style: textTheme.subtitle2),
AssetIconButton(images.chevronRightAsset, onPressed: onRightTapped),
],
);
}
}
Expand All @@ -29,33 +156,33 @@ class _CreateMemoEmptyState extends ConsumerWidget {
final textTheme = Theme.of(context).textTheme;

return Material(
borderRadius: dimens.executionsTerminalBorderRadius,
child: InkWell(
borderRadius: dimens.executionsTerminalBorderRadius,
onTap: onTap,
child: Ink(
decoration: BoxDecoration(
border: Border.all(color: theme.neutralSwatch.shade700, width: dimens.genericBorderHeight),
borderRadius: dimens.executionsTerminalBorderRadius,
color: theme.neutralSwatch.shade800,
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomPaint(
size: const Size(dimens.createMemoCtaSide, dimens.createMemoCtaSide),
painter: _CreateButtonPainter(
backgroundColor: theme.primarySwatch.shade400,
createColor: theme.neutralSwatch.shade800,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CustomPaint(
size: const Size(dimens.createMemoCtaSide, dimens.createMemoCtaSide),
painter: _CreateButtonPainter(
backgroundColor: theme.primarySwatch.shade400,
createColor: theme.neutralSwatch.shade800,
),
context.verticalBox(Spacing.small),
Text(
strings.newMemo.toUpperCase(),
style: textTheme.button?.copyWith(color: theme.primarySwatch.shade400),
textAlign: TextAlign.center,
)
],
),
),
context.verticalBox(Spacing.small),
Text(
strings.newMemo.toUpperCase(),
style: textTheme.button?.copyWith(color: theme.primarySwatch.shade400),
textAlign: TextAlign.center,
)
],
),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:memo/application/pages/home/collections/update/update_collection
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_memos_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';
Expand Down Expand Up @@ -42,7 +43,6 @@ class UpdateCollectionPage extends HookConsumerWidget {
children: [
ThemedTabBar(controller: tabController, tabs: tabs),
Expanded(child: _UpdateCollectionContents(selectedSegment: selectedSegment.value)),
context.verticalBox(Spacing.large),
_BottomActionContainer(
onSegmentSwapRequested: (segment) => tabController.animateTo(_Segment.values.indexOf(segment)),
selectedSegment: selectedSegment.value,
Expand Down Expand Up @@ -72,17 +72,24 @@ class _UpdateCollectionContents extends ConsumerWidget {
}

if (state is UpdateCollectionLoaded) {
late Widget body;

switch (selectedSegment) {
case _Segment.details:
return ProviderScope(
overrides: [
updateDetailsMetadata.overrideWithValue(state.collectionMetadata),
],
child: _UpdateCollectionDetails(),
);
body = _UpdateCollectionDetails();
break;
case _Segment.memos:
return UpdateCollectionMemos();
body = _UpdateCollectionMemos();
break;
}

return ProviderScope(
overrides: [
updateDetailsMetadata.overrideWithValue(state.collectionMetadata),
updateMemosMetadata.overrideWithValue(state.memosMetadata),
],
child: body,
);
}

throw InconsistentStateError.layout('Unsupported subtype (${state.runtimeType}) of `UpdateCollectionState`');
Expand All @@ -103,6 +110,17 @@ class _UpdateCollectionDetails extends ConsumerWidget {
}
}

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

ref.listen<UpdateMemosState>(updateCollectionMemosVM, (_, state) => vm.updateMemos(memos: state.memos));

return UpdateCollectionMemos();
}
}

extension on _Segment {
String get title {
switch (this) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ final updateCollectionId = Provider<String?>((_) => throw UnimplementedError(),
/// Overridable collection metadata used in the scope of a collection update.
final updateDetailsMetadata =
Provider<CollectionMetadata>((_) => throw UnimplementedError(), name: 'updateDetailsMetadata');

/// Overridable memos metadata used in the scope of a collection update.
final updateMemosMetadata =
Provider<List<MemoMetadata>>((_) => throw UnimplementedError(), name: 'updateMemosMetadata');
72 changes: 72 additions & 0 deletions lib/application/view-models/home/update_collection_memos_vm.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:memo/application/pages/home/collections/update/update_collection_providers.dart';
import 'package:memo/application/view-models/home/update_collection_vm.dart';

final updateCollectionMemosVM = StateNotifierProvider.autoDispose<UpdateCollectionMemosVM, UpdateMemosState>(
(ref) => UpdateCollectionMemosVMImpl(memos: ref.read(updateMemosMetadata)),
dependencies: [updateMemosMetadata],
name: 'updateCollectionMemosVM',
);

abstract class UpdateCollectionMemosVM extends StateNotifier<UpdateMemosState> {
UpdateCollectionMemosVM(UpdateMemosState state) : super(state);

/// Replaces the current saved memos with [memos].
void updateMemos(List<MemoMetadata> memos);

/// Creates a new empty Memo in the collection.
void createEmptyMemo();

/// Updates memo at [index] metadata with [metadata].
void updateMemoAtIndex(int index, {required MemoMetadata metadata});

/// Removes the memo at given [index].
///
/// If [index] is not in range `0 <= index < memos.length` does nothing.
void removeMemoAtIndex(int index);
}

class UpdateCollectionMemosVMImpl extends UpdateCollectionMemosVM {
UpdateCollectionMemosVMImpl({required List<MemoMetadata> memos}) : super(UpdateMemosState(memos: memos));

@override
void updateMemos(List<MemoMetadata> memos) => state = state.copyWith(memos: memos);

@override
void createEmptyMemo() {
final updatedMemos = List<MemoMetadata>.from(state.memos)..add(MemoMetadata.empty());
state = state.copyWith(memos: updatedMemos);
}

@override
void updateMemoAtIndex(int index, {required MemoMetadata metadata}) {
final updatedMemos = state.memos
..removeAt(index)
..insert(index, metadata);

state = state.copyWith(memos: updatedMemos);
}

@override
void removeMemoAtIndex(int index) {
if (index < 0 || index >= state.memos.length) {
return;
}

final updatedMemos = List<MemoMetadata>.from(state.memos)..removeAt(index);
state = state.copyWith(memos: updatedMemos);
}
}

class UpdateMemosState extends Equatable {
const UpdateMemosState({required this.memos});

final List<MemoMetadata> memos;

UpdateMemosState copyWith({required List<MemoMetadata> memos}) => UpdateMemosState(memos: memos);

@override
List<Object?> get props => [memos];
}
Loading

0 comments on commit f57d943

Please sign in to comment.