From 1888091e3fb13dbaac779bd3730579bc00a1abe2 Mon Sep 17 00:00:00 2001 From: Erick Zanardo Date: Wed, 9 Oct 2024 17:53:55 -0300 Subject: [PATCH] feat: Adding a quick auto replace image --- .../widgets/change_image_modal.dart | 9 ++- .../widgets/concat_image_modal.dart | 6 +- .../editor_screen/widgets/toolbar.dart | 17 ++++- .../lib/screens/open_screen/open_screen.dart | 10 +-- .../widgets/atlas_options_container.dart | 12 +++- .../lib/services/storage/desktop.dart | 43 ++++++++++-- .../lib/services/storage/storage.dart | 8 ++- .../lib/services/storage/unsupported.dart | 2 +- .../lib/services/storage/web.dart | 26 +++++++- .../lib/store/actions/atlas_actions.dart | 66 +++++++++++++++++-- fire_atlas_editor/lib/store/store.dart | 9 ++- .../widgets/image_selection_container.dart | 12 +++- 12 files changed, 189 insertions(+), 31 deletions(-) diff --git a/fire_atlas_editor/lib/screens/editor_screen/widgets/change_image_modal.dart b/fire_atlas_editor/lib/screens/editor_screen/widgets/change_image_modal.dart index 9855b09..2fe4eea 100644 --- a/fire_atlas_editor/lib/screens/editor_screen/widgets/change_image_modal.dart +++ b/fire_atlas_editor/lib/screens/editor_screen/widgets/change_image_modal.dart @@ -16,6 +16,7 @@ class ChangeImageModal extends StatefulWidget { class _ChangeImageModalState extends State { String? _imageData; + String? _imagePath; String? _imageName; @override @@ -29,9 +30,14 @@ class _ChangeImageModalState extends State { margin: const EdgeInsets.all(30), imageData: _imageData, imageName: _imageName, - onSelectImage: (imageName, imageData) { + onSelectImage: ({ + required imageName, + required imagePath, + required imageData, + }) { setState(() { _imageData = imageData; + _imagePath = imagePath; _imageName = imageName; }); }, @@ -56,6 +62,7 @@ class _ChangeImageModalState extends State { store.dispatchAsync( UpdateAtlasImageAction( imageData: _imageData!, + imagePath: _imagePath!, ), ); }, diff --git a/fire_atlas_editor/lib/screens/editor_screen/widgets/concat_image_modal.dart b/fire_atlas_editor/lib/screens/editor_screen/widgets/concat_image_modal.dart index 1a31ee1..4c9fd44 100644 --- a/fire_atlas_editor/lib/screens/editor_screen/widgets/concat_image_modal.dart +++ b/fire_atlas_editor/lib/screens/editor_screen/widgets/concat_image_modal.dart @@ -42,7 +42,11 @@ class _ConcatImageModalState extends State { ), imageData: _imageData, imageName: _imageName, - onSelectImage: (imageName, imageData) { + onSelectImage: ({ + required imageName, + required imagePath, + required imageData, + }) { setState(() { _imageData = imageData; _imageName = imageName; diff --git a/fire_atlas_editor/lib/screens/editor_screen/widgets/toolbar.dart b/fire_atlas_editor/lib/screens/editor_screen/widgets/toolbar.dart index 753418e..f7b2907 100644 --- a/fire_atlas_editor/lib/screens/editor_screen/widgets/toolbar.dart +++ b/fire_atlas_editor/lib/screens/editor_screen/widgets/toolbar.dart @@ -17,14 +17,20 @@ import 'package:slices/slices.dart'; class _ToolbarSlice extends Equatable { final FireAtlas? currentAtlas; + final String? lastUsedImage; final bool hasChanges; _ToolbarSlice.fromState(FireAtlasState state) : currentAtlas = state.currentAtlas, + lastUsedImage = state.loadedProject.value?.lastUsedImage, hasChanges = state.hasChanges; @override - List get props => [currentAtlas?.id, hasChanges]; + List get props => [ + currentAtlas?.id, + lastUsedImage, + hasChanges, + ]; } class Toolbar extends StatelessWidget { @@ -91,6 +97,15 @@ class Toolbar extends StatelessWidget { }, tooltip: 'Update base image', ), + if (!kIsWeb) + FIconButton( + disabled: slice.lastUsedImage == null, + iconData: Icons.fireplace, + onPress: () { + store.dispatchAsync(QuickReplaceImageAction()); + }, + tooltip: 'Quick replace base image', + ), FIconButton( iconData: Icons.add_photo_alternate, onPress: () { diff --git a/fire_atlas_editor/lib/screens/open_screen/open_screen.dart b/fire_atlas_editor/lib/screens/open_screen/open_screen.dart index 7a9ad39..e08137b 100644 --- a/fire_atlas_editor/lib/screens/open_screen/open_screen.dart +++ b/fire_atlas_editor/lib/screens/open_screen/open_screen.dart @@ -73,6 +73,7 @@ class _OpenScreenState extends State { onConfirm: ( String atlasName, String imageData, + String imagePath, double tileWidth, double tileHeight, ) async { @@ -82,6 +83,7 @@ class _OpenScreenState extends State { CreateAtlasAction( id: atlasName, imageData: imageData, + imagePath: imagePath, tileWidth: tileWidth, tileHeight: tileHeight, ), @@ -155,14 +157,6 @@ class _OpenScreenState extends State { Expanded( child: Image.asset('assets/Logo.png'), ), - if (!kIsWeb && Platform.isMacOS) - Container( - width: 300, - child: _Buttons( - importAtlas: _importAtlas, - newAtlas: _newAtlas, - ), - ), ], ), FContainer( diff --git a/fire_atlas_editor/lib/screens/open_screen/widgets/atlas_options_container.dart b/fire_atlas_editor/lib/screens/open_screen/widgets/atlas_options_container.dart index 8c1e207..6385b18 100644 --- a/fire_atlas_editor/lib/screens/open_screen/widgets/atlas_options_container.dart +++ b/fire_atlas_editor/lib/screens/open_screen/widgets/atlas_options_container.dart @@ -10,7 +10,7 @@ import 'package:slices/slices.dart'; class AtlasOptionsContainer extends StatefulWidget { final void Function() onCancel; - final Function(String, String, double, double) onConfirm; + final Function(String, String, String, double, double) onConfirm; const AtlasOptionsContainer({ required this.onCancel, @@ -25,6 +25,8 @@ class AtlasOptionsContainer extends StatefulWidget { class _AtlasOptionsContainerState extends State { String? _imageData; String? _imageName; + String? _imagePath; + late final TextEditingController atlasNameController; late final TextEditingController tileWidthController; late final TextEditingController tileHeightController; @@ -114,6 +116,7 @@ class _AtlasOptionsContainerState extends State { widget.onConfirm( atlasName, _imageData!, + _imagePath!, double.parse(tileWidthRaw), double.parse(tileHeightRaw), ); @@ -164,9 +167,14 @@ class _AtlasOptionsContainerState extends State { child: ImageSelectionContainer( imageData: _imageData, imageName: _imageName, - onSelectImage: (imageName, imageData) { + onSelectImage: ({ + required imageName, + required imagePath, + required imageData, + }) { setState(() { _imageData = imageData; + _imagePath = imagePath; _imageName = imageName; }); }, diff --git a/fire_atlas_editor/lib/services/storage/desktop.dart b/fire_atlas_editor/lib/services/storage/desktop.dart index ec42408..45a4303 100644 --- a/fire_atlas_editor/lib/services/storage/desktop.dart +++ b/fire_atlas_editor/lib/services/storage/desktop.dart @@ -13,7 +13,8 @@ class FireAtlasStorage extends FireAtlasStorageApi { final file = File(path); final raw = await file.readAsBytes(); final atlas = FireAtlas.deserializeBytes(raw); - return LoadedProjectEntry(path, atlas); + final lastUsedImage = await getProjectLastImageFile(atlas.id); + return LoadedProjectEntry(path, atlas, lastUsedImage); } @override @@ -52,13 +53,43 @@ class FireAtlasStorage extends FireAtlasStorageApi { prefs.setString('PROJECT_${entry.project.id}', path); } + @override + Future rememberProjectImageFile( + String projectId, + String imagePath, + ) async { + final prefs = await SharedPreferences.getInstance(); + prefs.setString('LAST_IMAGE_$projectId', imagePath); + } + + @override + Future getProjectLastImageFile(String projectId) async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getString('LAST_IMAGE_$projectId'); + } + @override Future selectProject() async { const typeGroup = XTypeGroup(label: 'fire atlas', extensions: ['fa']); final file = await _selectDialog(typeGroup); final bytes = await file.readAsBytes(); final atlas = FireAtlas.deserializeBytes(bytes); - return LoadedProjectEntry(file.path, atlas); + + final lastImage = await getProjectLastImageFile(atlas.id); + + return LoadedProjectEntry( + file.path, + atlas, + lastImage, + ); + } + + @override + Future readImageData(String path) async { + final file = File(path); + final bytes = await file.readAsBytes(); + + return base64Encode(bytes); } @override @@ -71,10 +102,14 @@ class FireAtlasStorage extends FireAtlasStorageApi { } @override - Future<(String, String)> selectFile() async { + Future<(String, String, String)> selectFile() async { final file = await _selectDialog(); final bytes = await file.readAsBytes(); - return (file.name, base64Encode(bytes)); + return ( + file.name, + file.path, + base64Encode(bytes), + ); } Future _selectDialog([XTypeGroup? typeGroup]) async { diff --git a/fire_atlas_editor/lib/services/storage/storage.dart b/fire_atlas_editor/lib/services/storage/storage.dart index 848bfb4..54a45d1 100644 --- a/fire_atlas_editor/lib/services/storage/storage.dart +++ b/fire_atlas_editor/lib/services/storage/storage.dart @@ -12,9 +12,15 @@ abstract class FireAtlasStorageApi { Future saveProject(LoadedProjectEntry entry); Future> lastUsedProjects(); Future rememberProject(LoadedProjectEntry entry); + Future rememberProjectImageFile( + String projectId, + String imageFile, + ); + Future getProjectLastImageFile(String projectId); Future selectNewProjectPath(FireAtlas atlas); Future selectProject(); - Future<(String, String)> selectFile(); + Future<(String, String, String)> selectFile(); + Future readImageData(String path); Future exportFile(List bytes, String fileName); Future setConfig(String key, String value); Future getConfig(String key, String defaultValue); diff --git a/fire_atlas_editor/lib/services/storage/unsupported.dart b/fire_atlas_editor/lib/services/storage/unsupported.dart index 5359a21..0485815 100644 --- a/fire_atlas_editor/lib/services/storage/unsupported.dart +++ b/fire_atlas_editor/lib/services/storage/unsupported.dart @@ -28,7 +28,7 @@ class FireAtlasStorage extends FireAtlasStorageApi { } @override - Future<(String, String)> selectFile() { + Future<(String, String, String)> selectFile() { throw 'Unsupported'; } diff --git a/fire_atlas_editor/lib/services/storage/web.dart b/fire_atlas_editor/lib/services/storage/web.dart index 710fa8b..f135177 100644 --- a/fire_atlas_editor/lib/services/storage/web.dart +++ b/fire_atlas_editor/lib/services/storage/web.dart @@ -61,17 +61,23 @@ class FireAtlasStorage extends FireAtlasStorageApi { return LoadedProjectEntry( 'ATLAS_${atlas.id}', atlas, + null, ); } + @override + Future readImageData(String path) async { + throw 'Unsupported'; + } + @override Future selectNewProjectPath(FireAtlas atlas) async { return 'ATLAS_${atlas.id}'; } @override - Future<(String, String)> selectFile() { - final completer = Completer<(String, String)>(); + Future<(String, String, String)> selectFile() { + final completer = Completer<(String, String, String)>(); final uploadInput = FileUploadInputElement(); uploadInput.click(); @@ -85,7 +91,13 @@ class FireAtlasStorage extends FireAtlasStorageApi { reader.onLoadEnd.listen((e) { final result = reader.result; if (result != null) { - completer.complete((file.name, result as String)); + completer.complete( + ( + file.name, + file.relativePath ?? file.name, + result as String, + ), + ); } }); reader.readAsDataUrl(file); @@ -123,4 +135,12 @@ class FireAtlasStorage extends FireAtlasStorageApi { return FireAtlas.deserializeBytes(jsonRaw); } + + @override + Future rememberProjectImageFile( + String projectId, + String imageFile, + ) async { + // Noop on web + } } diff --git a/fire_atlas_editor/lib/store/actions/atlas_actions.dart b/fire_atlas_editor/lib/store/actions/atlas_actions.dart index de0b73c..4b67185 100644 --- a/fire_atlas_editor/lib/store/actions/atlas_actions.dart +++ b/fire_atlas_editor/lib/store/actions/atlas_actions.dart @@ -9,12 +9,14 @@ import 'package:slices/slices.dart'; class CreateAtlasAction extends AsyncSlicesAction { final String id; final String imageData; + final String imagePath; final double tileWidth; final double tileHeight; CreateAtlasAction({ required this.id, required this.imageData, + required this.imagePath, required this.tileWidth, required this.tileHeight, }); @@ -33,7 +35,11 @@ class CreateAtlasAction extends AsyncSlicesAction { return state.copyWith( hasChanges: true, loadedProject: Nullable( - LoadedProjectEntry(null, atlas), + LoadedProjectEntry( + null, + atlas, + imagePath, + ), ), ); } @@ -41,22 +47,34 @@ class CreateAtlasAction extends AsyncSlicesAction { class UpdateAtlasImageAction extends AsyncSlicesAction { final String imageData; + final String? imagePath; - UpdateAtlasImageAction({required this.imageData}); + UpdateAtlasImageAction({ + required this.imageData, + this.imagePath, + }); @override Future perform(_, FireAtlasState state) async { if (state.currentAtlas != null) { final atlas = state.currentAtlas!; - Flame.images.clearCache(); - atlas.imageData = imageData; - await atlas.loadImage(clearImageData: false); + final sameImage = (atlas.imageData == imageData); + + if (!sameImage) { + Flame.images.clearCache(); + + atlas.imageData = imageData; + await atlas.loadImage(clearImageData: false); + } return state.copyWith( hasChanges: true, loadedProject: Nullable( - state.loadedProject.value?.copyWith(project: atlas), + state.loadedProject.value?.copyWith( + project: atlas, + lastUsedImage: imagePath, + ), ), ); } @@ -65,6 +83,42 @@ class UpdateAtlasImageAction extends AsyncSlicesAction { } } +class QuickReplaceImageAction extends AsyncSlicesAction { + @override + Future perform(_, FireAtlasState state) async { + if (state.currentAtlas != null) { + final atlas = state.currentAtlas!; + + final imagePath = state.loadedProject.value?.lastUsedImage; + + if (imagePath != null) { + final storage = FireAtlasStorage(); + final imageData = await storage.readImageData(imagePath); + + final sameImage = (atlas.imageData == imageData); + + if (!sameImage) { + Flame.images.clearCache(); + + atlas.imageData = imageData; + await atlas.loadImage(clearImageData: false); + } + + return state.copyWith( + hasChanges: true, + loadedProject: Nullable( + state.loadedProject.value?.copyWith( + project: atlas, + ), + ), + ); + } + } + + return state; + } +} + class UpdateSelectionGroup extends SlicesAction { UpdateSelectionGroup({ required this.selectionId, diff --git a/fire_atlas_editor/lib/store/store.dart b/fire_atlas_editor/lib/store/store.dart index 2994c81..a6c980d 100644 --- a/fire_atlas_editor/lib/store/store.dart +++ b/fire_atlas_editor/lib/store/store.dart @@ -12,9 +12,14 @@ class Nullable { @immutable class LoadedProjectEntry { final String? path; + final String? lastUsedImage; final FireAtlas project; - const LoadedProjectEntry(this.path, this.project); + const LoadedProjectEntry( + this.path, + this.project, + this.lastUsedImage, + ); LastProjectEntry toLastProjectEntry() { final currentPath = path; @@ -29,11 +34,13 @@ class LoadedProjectEntry { LoadedProjectEntry copyWith({ String? path, + String? lastUsedImage, FireAtlas? project, }) { return LoadedProjectEntry( path ?? this.path, project ?? this.project, + lastUsedImage ?? this.lastUsedImage, ); } } diff --git a/fire_atlas_editor/lib/widgets/image_selection_container.dart b/fire_atlas_editor/lib/widgets/image_selection_container.dart index c9391f9..865ed0b 100644 --- a/fire_atlas_editor/lib/widgets/image_selection_container.dart +++ b/fire_atlas_editor/lib/widgets/image_selection_container.dart @@ -8,7 +8,11 @@ import 'package:flame/flame.dart'; import 'package:flame/sprite.dart'; import 'package:flutter/material.dart' hide Image; -typedef OnSelectImage = void Function(String, String); +typedef OnSelectImage = void Function({ + required String imageName, + required String imagePath, + required String imageData, +}); class ImageSelectionContainer extends StatelessWidget { final String? imageData; @@ -62,7 +66,11 @@ class ImageSelectionContainer extends StatelessWidget { onSelect: () async { final storage = FireAtlasStorage(); final file = await storage.selectFile(); - onSelectImage(file.$1, file.$2); + onSelectImage( + imageName: file.$1, + imagePath: file.$2, + imageData: file.$3, + ); }, ), ],