Skip to content

Commit

Permalink
Initial support for adding & removing maps
Browse files Browse the repository at this point in the history
BlueMap's default maps folder becomes the map-templates folder, instead, and a new empty maps folder gets created.
Users can click on the "New map" button to create a new map config from one of those templates.

There is also a delete button which closes the config file in the editor if it's open, and then deletes the file the next frame.

The config tree UI is updated through a file watcher.
  • Loading branch information
TechnicJelle committed Aug 31, 2024
1 parent 7596712 commit 6e22c2f
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 1 deletion.
37 changes: 36 additions & 1 deletion lib/config_tree.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import "package:path/path.dart" as p;
import "config_tile.dart";
import "dual_pane.dart";
import "main.dart";
import "map_tile.dart";
import "new_map_button.dart";

class ConfigTree extends ConsumerStatefulWidget {
const ConfigTree({super.key});
Expand Down Expand Up @@ -45,13 +47,45 @@ class _ConfigTreeState extends ConsumerState<ConfigTree> {
maps.add(map);
}
}

//watch for changes to files in the maps directory
entity
.watch(events: FileSystemEvent.create | FileSystemEvent.delete)
.listen((FileSystemEvent event) {
switch (event.type) {
case FileSystemEvent.create:
addMap(File(event.path));
break;
case FileSystemEvent.delete:
removeMap(File(event.path));
break;
}
});
}
}
}

//sort all lists alphabetically
configs.sort((a, b) => a.path.compareTo(b.path));
storages.sort((a, b) => a.path.compareTo(b.path));
sortMaps();
}

void addMap(File newMap) {
setState(() {
maps.add(newMap);
sortMaps();
});
}

void removeMap(File toRemoveMap) {
setState(() {
maps.removeWhere((File map) => p.equals(map.path, toRemoveMap.path));
sortMaps();
});
}

void sortMaps() {
//TODO: Sort maps based on internal `sorting` property
maps.sort((a, b) => a.path.compareTo(b.path));
}
Expand All @@ -66,7 +100,8 @@ class _ConfigTreeState extends ConsumerState<ConfigTree> {
const Text("Storages"),
for (final File storage in storages) ConfigTile(storage),
const Text("Maps"),
for (final File map in maps) ConfigTile(map),
for (final File map in maps) MapTile(map),
const NewMapButton(),
],
);
}
Expand Down
47 changes: 47 additions & 0 deletions lib/map_tile.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import "dart:io";

import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path/path.dart" as p;

import "dual_pane.dart";

class MapTile extends ConsumerWidget {
final File configFile;

const MapTile(this.configFile, {super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final File? openConfig = ref.watch(openConfigProvider);

return ListTile(
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () {
//TODO: Show confirmation dialog

//close the editor if it's open on that file
if (openConfig != null && p.equals(openConfig.path, configFile.path)) {
ref.read(openConfigProvider.notifier).close();
}
//delete the file next frame, to ensure the editor is closed
WidgetsBinding.instance.addPostFrameCallback((_) {
configFile.delete();
});
},
),
title: Text(_toHuman(configFile)),
onTap: () {
ref.read(openConfigProvider.notifier).open(configFile);
},
selected: openConfig != null && p.equals(openConfig.path, configFile.path),
);
}

static String _toHuman(File file) {
final String name = p.basename(file.path).replaceAll(".conf", "");
if (name == "Sql") return "SQL";
return name;
}
}
53 changes: 53 additions & 0 deletions lib/new_map_button.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import "dart:io";
import "dart:math";

import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:path/path.dart" as p;

import "main.dart";

class NewMapButton extends ConsumerWidget {
const NewMapButton({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
return Padding(
padding: const EdgeInsets.all(8),
child: ListTile(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(80),
),
tileColor: Theme.of(context).colorScheme.secondary,
title: const Row(
children: [
Icon(Icons.add),
SizedBox(width: 8),
Text("New map"),
],
),
onTap: () {
Directory? projectDirectory = ref.read(projectDirectoryProvider);
if (projectDirectory == null) return;

File templateConfig = File(
p.join(projectDirectory.path, "config", "map-templates", "overworld.conf"),
);

File newConfig = File(
p.join(projectDirectory.path, "config", "maps",
"new-map-${Random().nextInt(999)}.conf"),
);

templateConfig.copySync(newConfig.path);

//TODO: Show options dialog
// - which template? (overworld, nether, end)
// - map name? (gets turned into a map ID and compared against existing map IDs)
// other options won't be in this initial dialog, but in the actual config screen
// which will get opened after this dialog
},
),
);
}
}
6 changes: 6 additions & 0 deletions lib/path_picker_button.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,12 @@ class _PathPickerButtonState extends ConsumerState<PathPickerButton> {
throw Exception("BlueMap CLI JAR failed to run!");
}

// == Turn default maps directory into templates directory ==
final Directory mapsDir =
Directory(p.join(projectDirectory.path, "config", "maps"));
mapsDir.renameSync(p.join(projectDirectory.path, "config", "map-templates"));
mapsDir.createSync(); //recreate maps dir (now empty)

Prefs.instance.projectPath = projectDirectory.path;
ref.invalidate(projectDirectoryProvider);
},
Expand Down

0 comments on commit 6e22c2f

Please sign in to comment.