From bc56d56c7090b9fd9e456ea7cc98ebbea962b553 Mon Sep 17 00:00:00 2001 From: Lenkomotive Date: Thu, 10 Oct 2024 22:00:07 +0200 Subject: [PATCH] PAINTROID-454: Flutter: Add Layers - reorder positions, select, delete, add layer --- .../state/layer_menu_state_data.dart | 2 + .../state/layer_menu_state_data.freezed.dart | 41 ++++- .../state/layer_menu_state_provider.dart | 49 +++++- .../state/layer_menu_state_provider.g.dart | 2 +- .../providers/state/layer_state_data.dart | 12 ++ .../state/layer_state_data.freezed.dart | 152 ++++++++++++++++++ .../components/layer_menu/layer.dart | 50 ++++++ .../components/layer_menu/layer_menu.dart | 43 +++++ 8 files changed, 338 insertions(+), 13 deletions(-) create mode 100644 lib/core/providers/state/layer_state_data.dart create mode 100644 lib/core/providers/state/layer_state_data.freezed.dart create mode 100644 lib/ui/pages/workspace_page/components/layer_menu/layer.dart diff --git a/lib/core/providers/state/layer_menu_state_data.dart b/lib/core/providers/state/layer_menu_state_data.dart index e5ca684c..e0e8b1a3 100644 --- a/lib/core/providers/state/layer_menu_state_data.dart +++ b/lib/core/providers/state/layer_menu_state_data.dart @@ -1,4 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:paintroid/core/providers/state/layer_state_data.dart'; part 'layer_menu_state_data.freezed.dart'; @@ -7,5 +8,6 @@ part 'layer_menu_state_data.freezed.dart'; class LayerMenuStateData with _$LayerMenuStateData { const factory LayerMenuStateData({ required bool isVisible, + required List layer, }) = _LayerMenuStateData; } diff --git a/lib/core/providers/state/layer_menu_state_data.freezed.dart b/lib/core/providers/state/layer_menu_state_data.freezed.dart index 4b19bd3d..02214554 100644 --- a/lib/core/providers/state/layer_menu_state_data.freezed.dart +++ b/lib/core/providers/state/layer_menu_state_data.freezed.dart @@ -17,6 +17,7 @@ final _privateConstructorUsedError = UnsupportedError( /// @nodoc mixin _$LayerMenuStateData { bool get isVisible => throw _privateConstructorUsedError; + List get layer => throw _privateConstructorUsedError; @JsonKey(ignore: true) $LayerMenuStateDataCopyWith get copyWith => @@ -29,7 +30,7 @@ abstract class $LayerMenuStateDataCopyWith<$Res> { LayerMenuStateData value, $Res Function(LayerMenuStateData) then) = _$LayerMenuStateDataCopyWithImpl<$Res, LayerMenuStateData>; @useResult - $Res call({bool isVisible}); + $Res call({bool isVisible, List layer}); } /// @nodoc @@ -46,12 +47,17 @@ class _$LayerMenuStateDataCopyWithImpl<$Res, $Val extends LayerMenuStateData> @override $Res call({ Object? isVisible = null, + Object? layer = null, }) { return _then(_value.copyWith( isVisible: null == isVisible ? _value.isVisible : isVisible // ignore: cast_nullable_to_non_nullable as bool, + layer: null == layer + ? _value.layer + : layer // ignore: cast_nullable_to_non_nullable + as List, ) as $Val); } } @@ -64,7 +70,7 @@ abstract class _$$LayerMenuStateDataImplCopyWith<$Res> __$$LayerMenuStateDataImplCopyWithImpl<$Res>; @override @useResult - $Res call({bool isVisible}); + $Res call({bool isVisible, List layer}); } /// @nodoc @@ -79,12 +85,17 @@ class __$$LayerMenuStateDataImplCopyWithImpl<$Res> @override $Res call({ Object? isVisible = null, + Object? layer = null, }) { return _then(_$LayerMenuStateDataImpl( isVisible: null == isVisible ? _value.isVisible : isVisible // ignore: cast_nullable_to_non_nullable as bool, + layer: null == layer + ? _value._layer + : layer // ignore: cast_nullable_to_non_nullable + as List, )); } } @@ -92,14 +103,23 @@ class __$$LayerMenuStateDataImplCopyWithImpl<$Res> /// @nodoc class _$LayerMenuStateDataImpl implements _LayerMenuStateData { - const _$LayerMenuStateDataImpl({required this.isVisible}); + const _$LayerMenuStateDataImpl( + {required this.isVisible, required final List layer}) + : _layer = layer; @override final bool isVisible; + final List _layer; + @override + List get layer { + if (_layer is EqualUnmodifiableListView) return _layer; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_layer); + } @override String toString() { - return 'LayerMenuStateData(isVisible: $isVisible)'; + return 'LayerMenuStateData(isVisible: $isVisible, layer: $layer)'; } @override @@ -108,11 +128,13 @@ class _$LayerMenuStateDataImpl implements _LayerMenuStateData { (other.runtimeType == runtimeType && other is _$LayerMenuStateDataImpl && (identical(other.isVisible, isVisible) || - other.isVisible == isVisible)); + other.isVisible == isVisible) && + const DeepCollectionEquality().equals(other._layer, _layer)); } @override - int get hashCode => Object.hash(runtimeType, isVisible); + int get hashCode => Object.hash( + runtimeType, isVisible, const DeepCollectionEquality().hash(_layer)); @JsonKey(ignore: true) @override @@ -123,12 +145,15 @@ class _$LayerMenuStateDataImpl implements _LayerMenuStateData { } abstract class _LayerMenuStateData implements LayerMenuStateData { - const factory _LayerMenuStateData({required final bool isVisible}) = - _$LayerMenuStateDataImpl; + const factory _LayerMenuStateData( + {required final bool isVisible, + required final List layer}) = _$LayerMenuStateDataImpl; @override bool get isVisible; @override + List get layer; + @override @JsonKey(ignore: true) _$$LayerMenuStateDataImplCopyWith<_$LayerMenuStateDataImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/core/providers/state/layer_menu_state_provider.dart b/lib/core/providers/state/layer_menu_state_provider.dart index b4625ced..3cfe10f3 100644 --- a/lib/core/providers/state/layer_menu_state_provider.dart +++ b/lib/core/providers/state/layer_menu_state_provider.dart @@ -1,4 +1,5 @@ import 'package:paintroid/core/providers/state/layer_menu_state_data.dart'; +import 'package:paintroid/core/providers/state/layer_state_data.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'layer_menu_state_provider.g.dart'; @@ -9,14 +10,54 @@ class LayerMenuStateProvider extends _$LayerMenuStateProvider { LayerMenuStateData build() { return const LayerMenuStateData( isVisible: false, + layer: [ + LayerStateData(id: 0, isSelected: false), + LayerStateData(id: 1, isSelected: false), + LayerStateData(id: 2, isSelected: false), + LayerStateData(id: 3, isSelected: false), + LayerStateData(id: 4, isSelected: false), + LayerStateData(id: 5, isSelected: false), + LayerStateData(id: 6, isSelected: false), + ], ); } - void toggleVisibility() { - state = state.copyWith(isVisible: !state.isVisible); + void toggleVisibility() => + state = state.copyWith(isVisible: !state.isVisible); + + void hide() => state = state.copyWith(isVisible: false); + + void reorder(int oldIndex, int newIndex) { + List layerList = List.from(state.layer); + if (oldIndex < newIndex) { + newIndex -= 1; + } + final movedLayer = layerList.removeAt(oldIndex); + layerList.insert(newIndex, movedLayer); + state = state.copyWith(layer: layerList); + } + + void toggleSelection(int layerId) { + final updatedLayerList = state.layer.map((layer) { + if (layer.id == layerId) { + return layer.copyWith(isSelected: !layer.isSelected); + } + return layer; + }).toList(); + state = state.copyWith(layer: updatedLayerList); + } + + void addLayer() { + final newLayer = LayerStateData( + id: state.layer.length + 1, + isSelected: false, + ); + state = state.copyWith(layer: [...state.layer, newLayer]); } - void hide() { - state = state.copyWith(isVisible: false); + void deleteLayer() { + final updatedLayerList = + state.layer.where((layer) => !layer.isSelected).toList(); + state = state.copyWith(layer: updatedLayerList); } } diff --git a/lib/core/providers/state/layer_menu_state_provider.g.dart b/lib/core/providers/state/layer_menu_state_provider.g.dart index e906cb19..b301c9d7 100644 --- a/lib/core/providers/state/layer_menu_state_provider.g.dart +++ b/lib/core/providers/state/layer_menu_state_provider.g.dart @@ -7,7 +7,7 @@ part of 'layer_menu_state_provider.dart'; // ************************************************************************** String _$layerMenuStateProviderHash() => - r'1dc3f0fa55df98057c2b981e7b215ccea251542a'; + r'311c058569eb9676b09e9de5c21a44516f2ee183'; /// See also [LayerMenuStateProvider]. @ProviderFor(LayerMenuStateProvider) diff --git a/lib/core/providers/state/layer_state_data.dart b/lib/core/providers/state/layer_state_data.dart new file mode 100644 index 00000000..b03533c7 --- /dev/null +++ b/lib/core/providers/state/layer_state_data.dart @@ -0,0 +1,12 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'layer_state_data.freezed.dart'; + +@immutable +@freezed +class LayerStateData with _$LayerStateData { + const factory LayerStateData({ + required int id, + required bool isSelected, + }) = _LayerStateData; +} diff --git a/lib/core/providers/state/layer_state_data.freezed.dart b/lib/core/providers/state/layer_state_data.freezed.dart new file mode 100644 index 00000000..c03ca7e8 --- /dev/null +++ b/lib/core/providers/state/layer_state_data.freezed.dart @@ -0,0 +1,152 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'layer_state_data.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$LayerStateData { + int get id => throw _privateConstructorUsedError; + bool get isSelected => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $LayerStateDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $LayerStateDataCopyWith<$Res> { + factory $LayerStateDataCopyWith( + LayerStateData value, $Res Function(LayerStateData) then) = + _$LayerStateDataCopyWithImpl<$Res, LayerStateData>; + @useResult + $Res call({int id, bool isSelected}); +} + +/// @nodoc +class _$LayerStateDataCopyWithImpl<$Res, $Val extends LayerStateData> + implements $LayerStateDataCopyWith<$Res> { + _$LayerStateDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? isSelected = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + isSelected: null == isSelected + ? _value.isSelected + : isSelected // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$LayerStateDataImplCopyWith<$Res> + implements $LayerStateDataCopyWith<$Res> { + factory _$$LayerStateDataImplCopyWith(_$LayerStateDataImpl value, + $Res Function(_$LayerStateDataImpl) then) = + __$$LayerStateDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({int id, bool isSelected}); +} + +/// @nodoc +class __$$LayerStateDataImplCopyWithImpl<$Res> + extends _$LayerStateDataCopyWithImpl<$Res, _$LayerStateDataImpl> + implements _$$LayerStateDataImplCopyWith<$Res> { + __$$LayerStateDataImplCopyWithImpl( + _$LayerStateDataImpl _value, $Res Function(_$LayerStateDataImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? isSelected = null, + }) { + return _then(_$LayerStateDataImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + isSelected: null == isSelected + ? _value.isSelected + : isSelected // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$LayerStateDataImpl implements _LayerStateData { + const _$LayerStateDataImpl({required this.id, required this.isSelected}); + + @override + final int id; + @override + final bool isSelected; + + @override + String toString() { + return 'LayerStateData(id: $id, isSelected: $isSelected)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$LayerStateDataImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.isSelected, isSelected) || + other.isSelected == isSelected)); + } + + @override + int get hashCode => Object.hash(runtimeType, id, isSelected); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$LayerStateDataImplCopyWith<_$LayerStateDataImpl> get copyWith => + __$$LayerStateDataImplCopyWithImpl<_$LayerStateDataImpl>( + this, _$identity); +} + +abstract class _LayerStateData implements LayerStateData { + const factory _LayerStateData( + {required final int id, + required final bool isSelected}) = _$LayerStateDataImpl; + + @override + int get id; + @override + bool get isSelected; + @override + @JsonKey(ignore: true) + _$$LayerStateDataImplCopyWith<_$LayerStateDataImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/ui/pages/workspace_page/components/layer_menu/layer.dart b/lib/ui/pages/workspace_page/components/layer_menu/layer.dart new file mode 100644 index 00000000..a25dfbba --- /dev/null +++ b/lib/ui/pages/workspace_page/components/layer_menu/layer.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:paintroid/core/providers/state/layer_menu_state_provider.dart'; + +List customColors = [ + const Color(0xFFFF5733), // Fiery Red + const Color(0xFF33C1FF), // Sky Blue + const Color(0xFF75FF33), // Lime Green + const Color(0xFFFF33A8), // Hot Pink + const Color(0xFF33FFF5), // Aqua + const Color(0xFFFFD133), // Golden Yellow + const Color(0xFF8D33FF), // Deep Purple + const Color(0xFFFF8333), // Vibrant Orange + const Color(0xFF33FF8D), // Mint Green + const Color(0xFF3361FF), // Bright Indigo +]; + +class Layer extends ConsumerWidget { + const Layer({ + super.key, + this.isSelected = false, + required this.id, + }); + + final int id; + final bool isSelected; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return GestureDetector( + onTap: () { + ref.read(layerMenuStateProvider.notifier).toggleSelection(id); + }, + child: Container( + height: 80, + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: customColors[id], + borderRadius: BorderRadius.circular(12), + border: isSelected + ? Border.all( + color: Colors.blue, + width: 5.0, + ) + : null, + ), + ), + ); + } +} diff --git a/lib/ui/pages/workspace_page/components/layer_menu/layer_menu.dart b/lib/ui/pages/workspace_page/components/layer_menu/layer_menu.dart index 9bd9630a..8d3b0a6a 100644 --- a/lib/ui/pages/workspace_page/components/layer_menu/layer_menu.dart +++ b/lib/ui/pages/workspace_page/components/layer_menu/layer_menu.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:paintroid/core/providers/state/layer_menu_state_provider.dart'; +import 'package:paintroid/ui/pages/workspace_page/components/layer_menu/layer.dart'; import 'package:paintroid/ui/shared/fade_in_out_widget.dart'; class LayerMenu extends ConsumerWidget { @@ -11,6 +12,9 @@ class LayerMenu extends ConsumerWidget { final isVisible = ref.watch( layerMenuStateProvider.select((state) => state.isVisible), ); + final layers = ref.watch( + layerMenuStateProvider.select((state) => state.layer), + ); return Positioned( top: 54, bottom: 54, @@ -23,6 +27,45 @@ class LayerMenu extends ConsumerWidget { color: Colors.white, borderRadius: BorderRadius.circular(12), ), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () { + ref.read(layerMenuStateProvider.notifier).addLayer(); + }, + ), + IconButton( + icon: const Icon(Icons.delete), + onPressed: () { + ref.read(layerMenuStateProvider.notifier).deleteLayer(); + }, + ), + ], + ), + Expanded( + child: ReorderableListView( + padding: const EdgeInsets.all(8), + onReorder: (int oldIndex, int newIndex) { + ref.read(layerMenuStateProvider.notifier).reorder( + oldIndex, + newIndex, + ); + }, + children: List.generate(layers.length, (index) { + return Layer( + id: layers[index].id, + key: ValueKey(layers[index].id), + isSelected: layers[index].isSelected, + ); + }), + ), + ), + ], + ), ), ), );