diff --git a/fire_atlas_editor/analysis_options.yaml b/fire_atlas_editor/analysis_options.yaml index 1029fb1..04c15fc 100644 --- a/fire_atlas_editor/analysis_options.yaml +++ b/fire_atlas_editor/analysis_options.yaml @@ -1,6 +1,6 @@ -include: package:flame_lint/analysis_options.yaml +include: package:flame_lint/analysis_options_with_dcm.yaml linter: rules: avoid_catches_without_on_clauses: false - constant_identifier_names: false \ No newline at end of file + constant_identifier_names: false 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..9e792ce 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; + late 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..bea55f4 100644 --- a/fire_atlas_editor/lib/screens/open_screen/open_screen.dart +++ b/fire_atlas_editor/lib/screens/open_screen/open_screen.dart @@ -1,5 +1,3 @@ -import 'dart:io'; - import 'package:fire_atlas_editor/screens/open_screen/widgets/atlas_options_container.dart'; import 'package:fire_atlas_editor/screens/open_screen/widgets/support_container.dart'; import 'package:fire_atlas_editor/screens/widgets/scaffold.dart'; @@ -12,7 +10,6 @@ import 'package:fire_atlas_editor/widgets/button.dart'; import 'package:fire_atlas_editor/widgets/container.dart'; import 'package:fire_atlas_editor/widgets/icon_button.dart'; import 'package:fire_atlas_editor/widgets/text.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:slices/slices.dart'; @@ -73,6 +70,7 @@ class _OpenScreenState extends State { onConfirm: ( String atlasName, String imageData, + String imagePath, double tileWidth, double tileHeight, ) async { @@ -82,6 +80,7 @@ class _OpenScreenState extends State { CreateAtlasAction( id: atlasName, imageData: imageData, + imagePath: imagePath, tileWidth: tileWidth, tileHeight: tileHeight, ), @@ -155,14 +154,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..adb1f9a 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; + late 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..87c191e 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'; } @@ -51,4 +51,19 @@ class FireAtlasStorage extends FireAtlasStorageApi { Future getConfig(String key, String defaultValue) { throw 'Unsupported'; } + + @override + Future getProjectLastImageFile(String projectId) { + throw 'Unsupported'; + } + + @override + Future readImageData(String path) { + throw 'Unsupported'; + } + + @override + Future rememberProjectImageFile(String projectId, String imageFile) { + throw 'Unsupported'; + } } diff --git a/fire_atlas_editor/lib/services/storage/web.dart b/fire_atlas_editor/lib/services/storage/web.dart index 710fa8b..af78839 100644 --- a/fire_atlas_editor/lib/services/storage/web.dart +++ b/fire_atlas_editor/lib/services/storage/web.dart @@ -20,6 +20,7 @@ class FireAtlasStorage extends FireAtlasStorageApi { final entry = LoadedProjectEntry( path, _readBase64Project(value), + null, ); return entry; @@ -61,17 +62,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 +92,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 +136,17 @@ class FireAtlasStorage extends FireAtlasStorageApi { return FireAtlas.deserializeBytes(jsonRaw); } + + @override + Future rememberProjectImageFile( + String projectId, + String imageFile, + ) async { + // Noop on web + } + + @override + Future getProjectLastImageFile(String projectId) { + throw 'Unsupported'; + } } 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, + ); }, ), ], diff --git a/fire_atlas_editor/pubspec.lock b/fire_atlas_editor/pubspec.lock index 0e61109..fa25506 100644 --- a/fire_atlas_editor/pubspec.lock +++ b/fire_atlas_editor/pubspec.lock @@ -189,10 +189,10 @@ packages: dependency: "direct main" description: name: flame_lint - sha256: "69f30090f947a45948c0d8c7205f8ffa4b9745360d427d2de98bc69def9b7c42" + sha256: b5bdef4193438b3d6b5827aece830eb295e6b75620570d5013c93f7bef2cb279 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" flutter: dependency: "direct main" description: flutter @@ -252,10 +252,10 @@ packages: dependency: transitive description: name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "4.0.0" matcher: dependency: transitive description: diff --git a/fire_atlas_editor/pubspec.yaml b/fire_atlas_editor/pubspec.yaml index 9efc95d..aa922d5 100644 --- a/fire_atlas_editor/pubspec.yaml +++ b/fire_atlas_editor/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: file_selector_windows: ^0.9.1+4 flame: ^1.18.0 flame_fire_atlas: ^1.5.3 - flame_lint: ^1.0.0 + flame_lint: ^1.2.1 flutter: sdk: flutter shared_preferences: ^2.0.17