diff --git a/modules/canvas/api/src/main/java/net/hollowcube/canvas/View.java b/modules/canvas/api/src/main/java/net/hollowcube/canvas/View.java index 57e9362ca..88b6e4243 100644 --- a/modules/canvas/api/src/main/java/net/hollowcube/canvas/View.java +++ b/modules/canvas/api/src/main/java/net/hollowcube/canvas/View.java @@ -2,7 +2,6 @@ import net.hollowcube.canvas.internal.Context; import net.hollowcube.posthog.PostHog; -import net.minestom.server.MinecraftServer; import net.minestom.server.entity.Player; import org.jetbrains.annotations.*; import org.slf4j.Logger; @@ -121,6 +120,9 @@ public void popView(@NotNull String signal, Object... args) { context.performSignal(signal, args); } + public final void setDirty() { + context.markDirty(); + } /** * Called when this view is mounted (shown to a player). diff --git a/modules/canvas/api/src/main/java/net/hollowcube/canvas/internal/Context.java b/modules/canvas/api/src/main/java/net/hollowcube/canvas/internal/Context.java index 33bd074de..43c2cba4b 100644 --- a/modules/canvas/api/src/main/java/net/hollowcube/canvas/internal/Context.java +++ b/modules/canvas/api/src/main/java/net/hollowcube/canvas/internal/Context.java @@ -29,4 +29,5 @@ public interface Context { void popView(); + void markDirty(); } diff --git a/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/ElementContext.java b/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/ElementContext.java index 31f7fe95f..495a25f2f 100644 --- a/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/ElementContext.java +++ b/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/ElementContext.java @@ -7,5 +7,6 @@ public interface ElementContext extends Context { /** * Signals that this context has been modified and should be updated. */ + @Override void markDirty(); } diff --git a/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/RootContext.java b/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/RootContext.java index f8cc178bf..7d434f5e1 100644 --- a/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/RootContext.java +++ b/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/context/RootContext.java @@ -46,4 +46,9 @@ public void pushView(@NotNull View view, boolean isTransient) { public void popView() { throw new UnsupportedOperationException("Cannot pop view on root context"); } + + @Override + public void markDirty() { + throw new UnsupportedOperationException("Cannot mark dirty on root context"); + } } diff --git a/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/provider/InventoryViewHost.java b/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/provider/InventoryViewHost.java index 6a2a4b8ad..902138164 100644 --- a/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/provider/InventoryViewHost.java +++ b/modules/canvas/impl-standalone/src/main/java/net/hollowcube/canvas/internal/standalone/provider/InventoryViewHost.java @@ -282,6 +282,8 @@ public boolean addViewer(@NotNull Player player) { @Override public boolean removeViewer(@NotNull Player player) { + player.getInventory().setCursorItem(ItemStack.AIR); + var result = super.removeViewer(player); if (result) { player.getInventory().update(); diff --git a/modules/command/src/main/java/net/hollowcube/command/arg/Argument.java b/modules/command/src/main/java/net/hollowcube/command/arg/Argument.java index 7ecf14fb2..6bb5ef19a 100644 --- a/modules/command/src/main/java/net/hollowcube/command/arg/Argument.java +++ b/modules/command/src/main/java/net/hollowcube/command/arg/Argument.java @@ -64,6 +64,10 @@ public static > ArgumentEnum Enum(@NotNull String id, Class return new ArgumentItemStack(id); } + public static @NotNull ArgumentUUID UUID(@NotNull String id) { + return new ArgumentUUID(id); + } + // Impl diff --git a/modules/command/src/main/java/net/hollowcube/command/arg/ArgumentUUID.java b/modules/command/src/main/java/net/hollowcube/command/arg/ArgumentUUID.java new file mode 100644 index 000000000..c50dd3baa --- /dev/null +++ b/modules/command/src/main/java/net/hollowcube/command/arg/ArgumentUUID.java @@ -0,0 +1,27 @@ +package net.hollowcube.command.arg; + +import net.hollowcube.command.util.StringReader; +import net.hollowcube.command.util.WordType; +import net.minestom.server.command.CommandSender; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class ArgumentUUID extends Argument { + + protected ArgumentUUID(@NotNull String id) { + super(id); + } + + @Override + public @NotNull ParseResult parse(@NotNull CommandSender sender, @NotNull StringReader reader) { + var word = reader.readWord(WordType.BRIGADIER); + try { + return success(UUID.fromString(word)); + } catch (IllegalArgumentException e) { + return syntaxError(); + } + } + + +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/command/map/MapCollectionCommand.java b/modules/core/src/main/java/net/hollowcube/mapmaker/command/map/MapCollectionCommand.java new file mode 100644 index 000000000..6bc693a30 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/command/map/MapCollectionCommand.java @@ -0,0 +1,56 @@ +package net.hollowcube.mapmaker.command.map; + +import net.hollowcube.canvas.internal.Controller; +import net.hollowcube.command.CommandContext; +import net.hollowcube.command.arg.Argument; +import net.hollowcube.command.dsl.CommandDsl; +import net.hollowcube.mapmaker.ExceptionReporter; +import net.hollowcube.mapmaker.gui.play.collection.MapCollectionView; +import net.hollowcube.mapmaker.gui.play.collection.list.EditMapCollectionListView; +import net.hollowcube.mapmaker.map.MapService; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.UUID; + +public class MapCollectionCommand extends CommandDsl { + + private final Argument idArg = Argument.UUID("id") + .description("The ID of the collection to view") + .defaultValue((UUID) null); + + private final Controller controller; + private final MapService maps; + + public MapCollectionCommand(@NotNull Controller controller, @NotNull MapService maps) { + super("collection"); + this.controller = controller; + this.maps = maps; + + addSyntax(playerOnly(this::handleViewCollections)); + addSyntax(playerOnly(this::handleViewCollection), idArg); + } + + private void handleViewCollections(@NotNull Player player, @NotNull CommandContext ctx) { + this.controller.show(player, EditMapCollectionListView::new); + } + + private void handleViewCollection(@NotNull Player player, @NotNull CommandContext ctx) { + var collectionId = ctx.get(idArg); + if (collectionId == null) { + player.sendMessage("No collection ID specified"); + return; + } + + try { + var collection = this.maps.getMapCollection(player.getUuid().toString(), collectionId.toString()); + this.controller.show(player, c -> new MapCollectionView(c, collection)); + } catch (MapService.NotFoundError e) { + player.sendMessage("No collection found for ID " + collectionId); + } catch (Exception e) { + player.sendMessage("Failed to load collection"); + ExceptionReporter.reportException(e, player); + } + } + +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/command/map/MapCommand.java b/modules/core/src/main/java/net/hollowcube/mapmaker/command/map/MapCommand.java index 099837e60..f399d1eb5 100644 --- a/modules/core/src/main/java/net/hollowcube/mapmaker/command/map/MapCommand.java +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/command/map/MapCommand.java @@ -49,6 +49,8 @@ public MapCommand( .build() ); + addSubcommand(new MapCollectionCommand(guiController, mapService)); + // Permissioned commands addSubcommand(this.delete = new MapDeleteCommand(mapService, permManager)); addSubcommand(this.edit = new MapEditCommand(mapService, permManager, bridge)); diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/anvil/AbstractSearchView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/AbstractSearchView.java similarity index 98% rename from modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/anvil/AbstractSearchView.java rename to modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/AbstractSearchView.java index 54bc100d8..19325101a 100644 --- a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/anvil/AbstractSearchView.java +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/AbstractSearchView.java @@ -1,4 +1,4 @@ -package net.hollowcube.mapmaker.gui.common.anvil; +package net.hollowcube.mapmaker.gui.common.search; import net.hollowcube.canvas.Element; import net.hollowcube.canvas.Label; diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/ItemSearchEntry.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/ItemSearchEntry.java new file mode 100644 index 000000000..71c069b86 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/ItemSearchEntry.java @@ -0,0 +1,35 @@ +package net.hollowcube.mapmaker.gui.common.search; + +import net.hollowcube.canvas.Label; +import net.hollowcube.canvas.View; +import net.hollowcube.canvas.annotation.Action; +import net.hollowcube.canvas.annotation.Outlet; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.common.lang.LanguageProviderV2; +import net.hollowcube.mapmaker.util.ItemUtils; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +public class ItemSearchEntry extends View { + + public static final String SIGNAL = "search.selected"; + + private @Outlet("label") Label label; + private final @NotNull Material material; + + public ItemSearchEntry(@NotNull Context context, @NotNull Material material) { + super(context); + + this.material = material; + + this.label.setItemSprite(ItemUtils.asDisplay(material)); + this.label.setArgs(LanguageProviderV2.getVanillaTranslation(material)); + } + + @Action("label") + private void handleSelect() { + performSignal(SIGNAL, this.material); + } + + +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/ItemSearchView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/ItemSearchView.java new file mode 100644 index 000000000..be3bc9979 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/common/search/ItemSearchView.java @@ -0,0 +1,51 @@ +package net.hollowcube.mapmaker.gui.common.search; + +import net.hollowcube.canvas.annotation.Signal; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.util.Autocompletors; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; +import java.util.function.Predicate; + +public class ItemSearchView extends AbstractSearchView { + + private final Predicate filter; + private final Consumer callback; + + public ItemSearchView(@NotNull Context context, Predicate filter, Consumer callback) { + super(context); + + this.filter = filter; + this.callback = callback; + } + + @Override + protected List search(@NotNull Context context, int page, int size, @NotNull String input) { + if (input.isEmpty()) { + return ThreadLocalRandom.current().ints(1, Material.values().size()) + .mapToObj(Material::fromId) + .filter(Objects::nonNull) + .filter(this.filter) + .limit(size) + .map(m -> new ItemSearchEntry(context, m)) + .toList(); + } else { + return Autocompletors.searchMaterials(input, size, this.filter) + .stream() + .map(m -> new ItemSearchEntry(context, m)) + .toList(); + } + } + + @Signal(ItemSearchEntry.SIGNAL) + private void handleSelectIcon(@NotNull Material material) { + this.callback.accept(material); + popView(); + } + +} \ No newline at end of file diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapDetailsView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapDetailsView.java index fe65c3e50..be86011ba 100644 --- a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapDetailsView.java +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapDetailsView.java @@ -12,6 +12,7 @@ import net.hollowcube.common.lang.LanguageProviderV2; import net.hollowcube.common.lang.TimeComponent; import net.hollowcube.mapmaker.ExceptionReporter; +import net.hollowcube.mapmaker.gui.play.collection.list.AddMapCollectionListView; import net.hollowcube.mapmaker.gui.play.details.DetailsTimesTabView; import net.hollowcube.mapmaker.map.*; import net.hollowcube.mapmaker.map.runtime.ServerBridge; @@ -363,6 +364,11 @@ public void handlePlayMap(@NotNull Player player) { } } + @Action("add_to_collection") + public void handleAddToCollection(@NotNull Player player) { + pushTransientView(context -> new AddMapCollectionListView(context, map.id())); + } + @Action(value = "leave_map", async = true) public void handleLeaveMap(@NotNull Player player) { bridge.joinHub(player); diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapEntry.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapEntry.java index f6fe6ff49..a91f75a0c 100644 --- a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapEntry.java +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/MapEntry.java @@ -24,7 +24,7 @@ import java.util.List; import java.util.Map; -public class MapEntry extends View { +public class MapEntry extends View implements ProgressMapEntry { private @ContextObject PlayerService playerService; private @ContextObject ServerBridge bridge; @@ -44,10 +44,12 @@ public MapEntry(@NotNull Context context, @NotNull MapData map) { async(this::updateIcon); } + @Override public @NotNull MapData map() { return map; } + @Override public void setProgress(PersonalizedMapData.Progress progress, int playtime) { this.progress = progress; this.playtime = playtime; diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/ProgressMapEntry.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/ProgressMapEntry.java new file mode 100644 index 000000000..866360ea7 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/ProgressMapEntry.java @@ -0,0 +1,11 @@ +package net.hollowcube.mapmaker.gui.play; + +import net.hollowcube.mapmaker.map.MapData; +import net.hollowcube.mapmaker.map.PersonalizedMapData; + +public interface ProgressMapEntry { + + MapData map(); + + void setProgress(PersonalizedMapData.Progress progress, int playtime); +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/BaseMapCollectionView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/BaseMapCollectionView.java new file mode 100644 index 000000000..14697c121 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/BaseMapCollectionView.java @@ -0,0 +1,161 @@ +package net.hollowcube.mapmaker.gui.play.collection; + +import net.hollowcube.canvas.Pagination; +import net.hollowcube.canvas.Text; +import net.hollowcube.canvas.View; +import net.hollowcube.canvas.annotation.Action; +import net.hollowcube.canvas.annotation.ContextObject; +import net.hollowcube.canvas.annotation.Outlet; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.common.lang.LanguageProviderV2; +import net.hollowcube.common.util.OpUtils; +import net.hollowcube.mapmaker.ExceptionReporter; +import net.hollowcube.mapmaker.gui.play.ProgressMapEntry; +import net.hollowcube.mapmaker.map.MapCollection; +import net.hollowcube.mapmaker.map.MapData; +import net.hollowcube.mapmaker.map.MapProgressBatchResponse; +import net.hollowcube.mapmaker.map.MapService; +import net.hollowcube.mapmaker.player.DisplayName; +import net.hollowcube.mapmaker.player.PlayerService; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.kyori.adventure.text.format.TextColor; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class BaseMapCollectionView extends View { + + private static final TextColor PAGE_COLOR = TextColor.color(0xBAC7D2); + + protected @ContextObject PlayerService playerService; + protected @ContextObject MapService mapService; + protected @ContextObject Player player; + + protected @Outlet("paging") Pagination pagination; + private @Outlet("page_number") Text pageNumber; + + private final Map mapCache = new ConcurrentHashMap<>(); + private final Map progressCache = new ConcurrentHashMap<>(); + + protected final String id; + protected MapCollection collection; + protected Component ownerName; + + public BaseMapCollectionView(@NotNull Context context, @NotNull MapCollection collection) { + this(context, collection.collectionId()); + + this.collection = collection; + this.ownerName = playerService.getPlayerDisplayName2(collection.owner()).asComponent(); + this.onLoaded(collection); + } + + public BaseMapCollectionView(@NotNull Context context, @NotNull String id) { + super(context); + + this.id = id; + } + + protected void onLoaded(@NotNull MapCollection collection) { + + } + + @Action(value = "paging", async = true) + private void fetchPage(@NotNull Pagination.PageRequest request) { + if (this.collection == null) return; + + try { + var mapIds = this.collection.mapIds().subList( + Math.min(request.page() * request.pageSize(), this.collection.mapIds().size()), + Math.min(request.page() * request.pageSize() + request.pageSize(), this.collection.mapIds().size()) + ); + + var mapsToFetch = new HashSet<>(mapIds); + mapsToFetch.removeAll(this.mapCache.keySet()); + + if (!mapsToFetch.isEmpty()) { + for (MapData map : mapService.getMaps(this.player.getUuid().toString(), mapsToFetch.stream().toList())) { + this.mapCache.put(map.id(), map); + } + } + + var entries = new ArrayList(); + for (var id : mapIds) { + var data = this.mapCache.get(id); + if (data == null) continue; + + entries.add(createEntry(request.context(), data)); + } + + request.respond(entries, request.page() * request.pageSize() + request.pageSize() < this.collection.mapIds().size()); + + // Fetch the player's current progress on the maps + var idsToFetch = new HashSet<>(mapIds); + idsToFetch.removeAll(progressCache.keySet()); + + if (idsToFetch.isEmpty()) return; + final int page = request.page(); + async(() -> { + var resp = mapService.getMapProgress(player.getUuid().toString(), idsToFetch.stream().toList()); + resp.progress().forEach(e -> progressCache.put(e.mapId(), e)); + + player.scheduleNextTick(ignored -> { + if (page != pagination.page()) return; + pagination.forEachEntry(page, entry -> { + var progress = progressCache.get(entry.map().id()); + if (progress != null) entry.setProgress(progress.progress(), progress.playtime()); + }); + }); + }); + + updatePageNumber(page); + } catch (Exception e) { + ExceptionReporter.reportException(e, this.player); + } + } + + protected abstract T createEntry(@NotNull Context context, @NotNull MapData data); + + @Action("info") + private void info() { + if (this.collection == null) return; + + player.closeInventory(); + + Component authorName; + try { + authorName = playerService.getPlayerDisplayName2(this.collection.owner()).build(DisplayName.Context.DEFAULT); + } catch (Throwable t) { + ExceptionReporter.reportException(t, player); + authorName = Component.text("Unknown", NamedTextColor.RED); + } + + player.sendMessage(LanguageProviderV2.translateMultiMerged("chat.map_collection.info.id", List.of( + Component.text(collection.collectionId()), + OpUtils.mapOr(collection.name(), Component::text, Component.text("Unnamed")), + authorName + ))); + } + + @Action("back") + private void back() { + pagination.prevPage(); + updatePageNumber(pagination.page()); + } + + @Action("next") + private void next() { + pagination.nextPage(); + updatePageNumber(pagination.page()); + } + + private void updatePageNumber(int page) { + pageNumber.setArgs(Component.text(page + 1).color(PAGE_COLOR)); + pageNumber.setText(String.valueOf(page + 1), PAGE_COLOR); + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/MapCollectionView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/MapCollectionView.java new file mode 100644 index 000000000..56c1bc235 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/MapCollectionView.java @@ -0,0 +1,33 @@ +package net.hollowcube.mapmaker.gui.play.collection; + +import net.hollowcube.canvas.Text; +import net.hollowcube.canvas.annotation.Outlet; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.gui.play.MapEntry; +import net.hollowcube.mapmaker.map.MapCollection; +import net.hollowcube.mapmaker.map.MapData; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class MapCollectionView extends BaseMapCollectionView { + + protected @Outlet("name") Text name; + + public MapCollectionView(@NotNull Context context, @NotNull MapCollection collection) { + super(context, collection); + } + + @Override + protected void onLoaded(@NotNull MapCollection collection) { + this.name.setText(Objects.requireNonNullElse(collection.name(), "Unnamed Collection")); + this.name.setArgs(Objects.requireNonNullElse(this.ownerName, Component.text("Unknown").color(NamedTextColor.RED))); + } + + @Override + protected MapEntry createEntry(@NotNull Context context, @NotNull MapData data) { + return new MapEntry(context, data); + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/edit/EditMapCollectionView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/edit/EditMapCollectionView.java new file mode 100644 index 000000000..900112649 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/edit/EditMapCollectionView.java @@ -0,0 +1,137 @@ +package net.hollowcube.mapmaker.gui.play.collection.edit; + +import net.hollowcube.canvas.ClickType; +import net.hollowcube.canvas.Element; +import net.hollowcube.canvas.Label; +import net.hollowcube.canvas.Text; +import net.hollowcube.canvas.annotation.Action; +import net.hollowcube.canvas.annotation.Outlet; +import net.hollowcube.canvas.annotation.Signal; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.canvas.util.CursorRequest; +import net.hollowcube.common.lang.LanguageProviderV2; +import net.hollowcube.common.util.OpUtils; +import net.hollowcube.mapmaker.gui.common.ConfirmAction; +import net.hollowcube.mapmaker.gui.common.anvil.TextInputView; +import net.hollowcube.mapmaker.gui.common.search.ItemSearchView; +import net.hollowcube.mapmaker.gui.play.collection.BaseMapCollectionView; +import net.hollowcube.mapmaker.map.MapCollection; +import net.hollowcube.mapmaker.map.MapData; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.NamedTextColor; +import net.minestom.server.entity.Player; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Objects; +import java.util.function.Predicate; + +public class EditMapCollectionView extends BaseMapCollectionView { + + private static final Predicate<@Nullable Material> SEARCH_PREDICATE = material -> + material != null && + material != Material.AIR && + material != Material.SCULK_SENSOR && + material != Material.CALIBRATED_SCULK_SENSOR && + material != Material.RECOVERY_COMPASS && + !material.name().endsWith("glass_pane"); + + protected @Outlet("name") Text name; + protected @Outlet("icon") Label icon; + + private MapData selectedMap; + + public EditMapCollectionView(@NotNull Context context, @NotNull MapCollection collection) { + super(context, collection); + } + + @Override + protected void onLoaded(@NotNull MapCollection collection) { + this.name.setText(Objects.requireNonNullElse(collection.name(), "Unnamed Collection")); + this.name.setArgs(Objects.requireNonNullElse(this.ownerName, Component.text("Unknown").color(NamedTextColor.RED))); + this.icon.setArgs(OpUtils.mapOr(collection.icon(), LanguageProviderV2::getVanillaTranslation, Component.text("None").color(NamedTextColor.RED))); + } + + @Override + protected EditMapEntry createEntry(@NotNull Context context, @NotNull MapData data) { + return new EditMapEntry(context, data); + } + + @Action("name") + private void setName(@NotNull Player player) { + this.pushTransientView(context -> TextInputView.builder() + .title("Edit Collection Name") + .callback(name -> { + this.collection = this.collection.withName(name); + this.mapService.updateMapCollection(this.player.getUuid().toString(), this.collection); + this.onLoaded(this.collection); + }) + .build(context, this.collection.name()) + ); + } + + @Action("icon") + private void setIcon(@NotNull Player player) { + this.pushTransientView(context -> new ItemSearchView( + context, + SEARCH_PREDICATE, + icon -> { + this.collection = this.collection.withIcon(icon); + this.mapService.updateMapCollection(this.player.getUuid().toString(), this.collection); + this.onLoaded(this.collection); + } + )); + } + + @Action("delete") + private void deleteCollection(@NotNull Player player, int slot, ClickType type) { + if (type != ClickType.SHIFT_LEFT_CLICK) return; + + this.pushTransientView(context -> new ConfirmAction( + context, + () -> { + this.mapService.deleteMapCollection(this.player.getUuid().toString(), this.collection.collectionId()); + player.closeInventory(); + }, + Component.translatable("delete this collection.") + )); + } + + @Signal(EditMapEntry.SIGNAL_SELECT) + private void selectMap(@NotNull MapData map) { + if (this.selectedMap == null) { + this.selectedMap = map; + this.setDirty(); + } else if (map.equals(this.selectedMap)) { + this.selectedMap = null; + this.setDirty(); + } else { + var maps = this.collection.mapIds(); + var oldIndex = maps.indexOf(this.selectedMap.id()); + var newIndex = maps.indexOf(map.id()); + if (oldIndex != -1 && newIndex != -1) { + maps.remove(oldIndex); + maps.add(newIndex, this.selectedMap.id()); + } + this.mapService.updateMapCollection(this.player.getUuid().toString(), this.collection); + this.selectedMap = null; + + this.pagination.reset(); + } + } + + @Signal(EditMapEntry.SIGNAL_REMOVE) + private void removeMap(@NotNull MapData map) { + this.collection.mapIds().remove(map.id()); + this.mapService.updateMapCollection(this.player.getUuid().toString(), this.collection); + this.pagination.reset(); + } + + @Signal(Element.SIG_GET_CURSOR) + private void getCursor(@NotNull CursorRequest request) { + if (this.selectedMap == null) return; + request.respond(OpUtils.mapOr(this.selectedMap.settings().getIcon(), ItemStack::of, ItemStack.AIR)); + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/edit/EditMapEntry.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/edit/EditMapEntry.java new file mode 100644 index 000000000..7a3f1174a --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/edit/EditMapEntry.java @@ -0,0 +1,96 @@ +package net.hollowcube.mapmaker.gui.play.collection.edit; + +import net.hollowcube.canvas.ClickType; +import net.hollowcube.canvas.Label; +import net.hollowcube.canvas.View; +import net.hollowcube.canvas.annotation.Action; +import net.hollowcube.canvas.annotation.ContextObject; +import net.hollowcube.canvas.annotation.Outlet; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.common.lang.LanguageProviderV2; +import net.hollowcube.mapmaker.ExceptionReporter; +import net.hollowcube.mapmaker.gui.play.ProgressMapEntry; +import net.hollowcube.mapmaker.map.MapData; +import net.hollowcube.mapmaker.map.PersonalizedMapData; +import net.hollowcube.mapmaker.map.runtime.ServerBridge; +import net.hollowcube.mapmaker.player.DisplayName; +import net.hollowcube.mapmaker.player.PlayerService; +import net.hollowcube.mapmaker.util.ItemUtils; +import net.minestom.server.entity.Player; +import net.minestom.server.item.ItemStack; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.Blocking; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; + +public class EditMapEntry extends View implements ProgressMapEntry { + + public static final String SIGNAL_SELECT = "collection.edit.selected"; + public static final String SIGNAL_REMOVE = "collection.edit.remove"; + + private @ContextObject PlayerService playerService; + private @ContextObject ServerBridge bridge; + + private @Outlet("btn") Label label; + + private final MapData map; + + private PersonalizedMapData.Progress progress = null; // null is unknown + private int playtime = 0; + private DisplayName authorName = null; + + public EditMapEntry(@NotNull Context context, @NotNull MapData map) { + super(context); + this.map = map; + + label.setState(State.LOADING); + async(this::updateIcon); + } + + @Override + public @NotNull MapData map() { + return map; + } + + @Override + public void setProgress(PersonalizedMapData.Progress progress, int playtime) { + this.progress = progress; + this.playtime = playtime; + async(this::updateIcon); + } + + @Action("btn") + protected void handleClick(@NotNull Player player, int slot, @NotNull ClickType clickType) { + switch (clickType) { + case SHIFT_LEFT_CLICK -> performSignal(SIGNAL_REMOVE, this.map); + case LEFT_CLICK -> performSignal(SIGNAL_SELECT, this.map); + } + } + + private @Blocking void updateIcon() { + var icon = map.settings().getIcon(); + if (icon == null) { + label.setItemSprite(ItemStack.of(Material.PAPER)); + } else { + label.setItemSprite(ItemUtils.asDisplay(icon)); + } + + if (authorName == null) { + try { + authorName = playerService.getPlayerDisplayName2(map.owner()); + } catch (Exception e) { + ExceptionReporter.reportException(e, player()); + authorName = new DisplayName(List.of(new DisplayName.Part("username", "!error!", null))); + } + } + + var entry = MapData.createHoverComponents(map, authorName.build(), + progress == null ? null : Map.entry(progress, playtime)); + entry.getValue().addAll(LanguageProviderV2.translateMulti("gui.edit_map_collection.map_display.footer", List.of())); + label.setComponentsDirect(entry.getKey(), entry.getValue()); + + label.setState(State.ACTIVE); + } +} \ No newline at end of file diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/AddMapCollectionEntry.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/AddMapCollectionEntry.java new file mode 100644 index 000000000..215040f14 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/AddMapCollectionEntry.java @@ -0,0 +1,30 @@ +package net.hollowcube.mapmaker.gui.play.collection.list; + +import net.hollowcube.canvas.ClickType; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.gui.play.collection.edit.EditMapCollectionView; +import net.hollowcube.mapmaker.map.MapCollection; +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class AddMapCollectionEntry extends MapCollectionEntry { + + private final String mapId; + + public AddMapCollectionEntry(@NotNull Context context, @NotNull MapCollection collection, @NotNull Component playerName, String mapId) { + super(context, collection, playerName); + this.mapId = mapId; + } + + @Override + protected void onClick(Player player, ClickType click) { + if (Objects.requireNonNull(click) != ClickType.LEFT_CLICK) return; + + this.collection.mapIds().addFirst(this.mapId); + this.mapService.updateMapCollection(player.getUuid().toString(), this.collection); + this.pushView(context -> new EditMapCollectionView(context, this.collection)); + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/AddMapCollectionListView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/AddMapCollectionListView.java new file mode 100644 index 000000000..ac4c30af9 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/AddMapCollectionListView.java @@ -0,0 +1,47 @@ +package net.hollowcube.mapmaker.gui.play.collection.list; + +import net.hollowcube.canvas.annotation.Action; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.gui.common.anvil.TextInputView; +import net.hollowcube.mapmaker.map.MapCollection; +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; + +public class AddMapCollectionListView extends MapCollectionListView { + + private final String mapId; + + public AddMapCollectionListView(@NotNull Context context, String mapId) { + super(context); + this.mapId = mapId; + } + + @Override + protected @Nullable MapCollectionEntry createEntry(@NotNull Context context, @NotNull MapCollection collection, @NotNull Component playername) { + if (collection.mapIds().contains(this.mapId)) return null; + return new AddMapCollectionEntry(context, collection, playername, this.mapId); + } + + @Action("add") + public void handleAdd(@NotNull Player player) { + this.pushTransientView(context -> TextInputView.builder() + .title("Create Map Collection") + .callback(($1, text) -> { + this.mapService.createMapCollection( + player.getUuid().toString(), + text, + Material.PAPER.name(), + new ArrayList<>() + ); + this.pagination.reset(); + this.pushView(new AddMapCollectionListView(context, this.mapId)); + }) + .build(context) + ); + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/EditMapCollectionEntry.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/EditMapCollectionEntry.java new file mode 100644 index 000000000..8e396cd06 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/EditMapCollectionEntry.java @@ -0,0 +1,27 @@ +package net.hollowcube.mapmaker.gui.play.collection.list; + +import net.hollowcube.canvas.ClickType; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.gui.play.collection.MapCollectionView; +import net.hollowcube.mapmaker.gui.play.collection.edit.EditMapCollectionView; +import net.hollowcube.mapmaker.map.MapCollection; +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; + +public class EditMapCollectionEntry extends MapCollectionEntry { + + public EditMapCollectionEntry(@NotNull Context context, @NotNull MapCollection collection, @NotNull Component playerName) { + super(context, collection, playerName); + } + + @Override + protected void onClick(Player player, ClickType click) { + switch (click) { + case LEFT_CLICK -> this.pushView(context -> new MapCollectionView(context, this.collection)); + case SHIFT_LEFT_CLICK -> this.pushView(context -> new EditMapCollectionView(context, this.collection)); + } + } + + +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/EditMapCollectionListView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/EditMapCollectionListView.java new file mode 100644 index 000000000..5ee21dccb --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/EditMapCollectionListView.java @@ -0,0 +1,26 @@ +package net.hollowcube.mapmaker.gui.play.collection.list; + +import net.hollowcube.canvas.View; +import net.hollowcube.canvas.annotation.Signal; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.map.MapCollection; +import net.kyori.adventure.text.Component; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class EditMapCollectionListView extends MapCollectionListView { + + public EditMapCollectionListView(@NotNull Context context) { + super(context); + } + + @Override + protected @Nullable MapCollectionEntry createEntry(@NotNull Context context, @NotNull MapCollection collection, @NotNull Component playername) { + return new EditMapCollectionEntry(context, collection, playername); + } + + @Signal(View.SIG_MOUNT) + public void onMount() { + this.collections = null; + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/MapCollectionEntry.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/MapCollectionEntry.java new file mode 100644 index 000000000..e53184729 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/MapCollectionEntry.java @@ -0,0 +1,52 @@ +package net.hollowcube.mapmaker.gui.play.collection.list; + +import net.hollowcube.canvas.ClickType; +import net.hollowcube.canvas.Label; +import net.hollowcube.canvas.View; +import net.hollowcube.canvas.annotation.Action; +import net.hollowcube.canvas.annotation.ContextObject; +import net.hollowcube.canvas.annotation.Outlet; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.gui.play.collection.MapCollectionView; +import net.hollowcube.mapmaker.map.MapCollection; +import net.hollowcube.mapmaker.map.MapService; +import net.hollowcube.mapmaker.util.ItemUtils; +import net.kyori.adventure.text.Component; +import net.minestom.server.entity.Player; +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class MapCollectionEntry extends View { + + protected @ContextObject MapService mapService; + + protected @Outlet("label") Label label; + protected final @NotNull MapCollection collection; + + public MapCollectionEntry(@NotNull Context context, @NotNull MapCollection collection, @NotNull Component playerName) { + super(context); + + this.collection = collection; + + this.label.setItemSprite(ItemUtils.asDisplay(Objects.requireNonNullElse(collection.icon(), Material.BARRIER))); + this.label.setArgs( + Component.text(Objects.requireNonNullElse(this.collection.name(), "Unnamed Collection")), + playerName, + Component.text(this.collection.mapIds().size()) + ); + } + + @Action(value = "label", async = true) + private void handleSelect(Player player, int slot, ClickType click) { + onClick(player, click); + } + + protected void onClick(Player player, ClickType click) { + if (Objects.requireNonNull(click) == ClickType.LEFT_CLICK) { + this.pushView(context -> new MapCollectionView(context, this.collection)); + } + } + +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/MapCollectionListView.java b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/MapCollectionListView.java new file mode 100644 index 000000000..d8ef5ed8a --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/gui/play/collection/list/MapCollectionListView.java @@ -0,0 +1,108 @@ +package net.hollowcube.mapmaker.gui.play.collection.list; + +import net.hollowcube.canvas.Pagination; +import net.hollowcube.canvas.Text; +import net.hollowcube.canvas.View; +import net.hollowcube.canvas.annotation.Action; +import net.hollowcube.canvas.annotation.ContextObject; +import net.hollowcube.canvas.annotation.Outlet; +import net.hollowcube.canvas.internal.Context; +import net.hollowcube.mapmaker.ExceptionReporter; +import net.hollowcube.mapmaker.map.MapCollection; +import net.hollowcube.mapmaker.map.MapService; +import net.hollowcube.mapmaker.player.DisplayName; +import net.hollowcube.mapmaker.player.PlayerService; +import net.kyori.adventure.text.Component; +import net.kyori.adventure.text.format.TextColor; +import net.minestom.server.entity.Player; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class MapCollectionListView extends View { + + private static final TextColor PAGE_COLOR = TextColor.color(0xBAC7D2); + + protected @ContextObject PlayerService playerService; + protected @ContextObject MapService mapService; + protected @ContextObject Player player; + + protected @Outlet("paging") Pagination pagination; + protected @Outlet("title") Text title; + protected @Outlet("page_number") Text pageNumber; + + private final String target; + private DisplayName targetName; + + protected List collections; + + public MapCollectionListView(@NotNull Context context) { + this(context, context.player().getUuid().toString()); + } + + public MapCollectionListView(@NotNull Context context, @NotNull String target) { + super(context); + + this.target = target; + } + + protected @Nullable MapCollectionEntry createEntry(@NotNull Context context, @NotNull MapCollection collection, @NotNull Component playername) { + return new MapCollectionEntry(context, collection, playername); + } + + @Action(value = "paging", async = true) + private void fetchPage(@NotNull Pagination.PageRequest request) { + try { + if (this.collections == null) { + this.targetName = playerService.getPlayerDisplayName2(this.target); + this.collections = mapService.getMapCollections(this.target) + .stream() + .map(collection -> createEntry(request.context(), collection, this.targetName.asComponent())) + .filter(Objects::nonNull) + .toList(); + + if (this.target.equals(request.context().player().getUuid().toString())) { + this.title.setText("Your Collections"); + } else { + var component = Component.text() + .append(targetName.build(DisplayName.Context.PLAIN)) + .append(Component.text("'s Collections")) + .build(); + this.title.setText(component); + } + } + + var entries = new ArrayList(); + + for (int i = request.page() * request.pageSize(); i < Math.min((request.page() + 1) * request.pageSize(), this.collections.size()); i++) { + entries.add(this.collections.get(i)); + } + + request.respond(entries, (request.page() + 1) * request.pageSize() < this.collections.size()); + + updatePageNumber(request.page()); + } catch (Exception e) { + ExceptionReporter.reportException(e, this.player); + } + } + + @Action("back") + private void back() { + pagination.prevPage(); + updatePageNumber(pagination.page()); + } + + @Action("next") + private void next() { + pagination.nextPage(); + updatePageNumber(pagination.page()); + } + + private void updatePageNumber(int page) { + pageNumber.setArgs(Component.text(page + 1).color(PAGE_COLOR)); + pageNumber.setText(String.valueOf(page + 1), PAGE_COLOR); + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapCollection.java b/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapCollection.java new file mode 100644 index 000000000..afb1eb364 --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapCollection.java @@ -0,0 +1,24 @@ +package net.hollowcube.mapmaker.map; + +import net.minestom.server.item.Material; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public record MapCollection( + @NotNull String collectionId, + @NotNull String owner, + @Nullable String name, + @Nullable Material icon, + @NotNull List mapIds +) { + + public @NotNull MapCollection withName(@NotNull String name) { + return new MapCollection(collectionId, owner, name, icon, mapIds); + } + + public @NotNull MapCollection withIcon(@NotNull Material icon) { + return new MapCollection(collectionId, owner, name, icon, mapIds); + } +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapService.java b/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapService.java index 379500e9a..b5cd2c6b8 100644 --- a/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapService.java +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapService.java @@ -97,6 +97,18 @@ public interface MapService { @NotNull MapHistory getPlayerMapHistory(@NotNull String playerId, int page, int amount); + // Map Collections + + @NotNull List getMapCollections(@NotNull String playerId); + + void createMapCollection(@NotNull String playerId, @Nullable String name, @Nullable String icon, @NotNull List mapIds); + + @NotNull MapCollection getMapCollection(@NotNull String playerId, @NotNull String collectionId); + + void updateMapCollection(@NotNull String playerId, @NotNull MapCollection collection); + + void deleteMapCollection(@NotNull String playerId, @NotNull String collectionId); + // Legacy @NotNull List getLegacyMaps(@NotNull String authorizer, @NotNull String playerId); diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapServiceImpl.java b/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapServiceImpl.java index b6ebf7659..3526a383a 100644 --- a/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapServiceImpl.java +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/map/MapServiceImpl.java @@ -6,9 +6,12 @@ import com.mojang.serialization.JsonOps; import net.hollowcube.common.ServerRuntime; import net.hollowcube.common.util.FutureUtil; +import net.hollowcube.common.util.OpUtils; import net.hollowcube.mapmaker.map.requests.MapCreateRequest; import net.hollowcube.mapmaker.map.requests.MapSearchParams; +import net.hollowcube.mapmaker.map.responses.MapCollectionsResponse; import net.hollowcube.mapmaker.util.AbstractHttpService; +import net.minestom.server.item.Material; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -546,6 +549,81 @@ public void setMapRating(@NotNull String mapId, @NotNull String playerId, @NotNu }; } + @Override + public @NotNull List getMapCollections(@NotNull String playerId) { + var req = HttpRequest.newBuilder() + .uri(URI.create(urlV3 + "/map-players/" + playerId + "/collections")) + .header(AUTHORIZER_HEADER, playerId) + .build(); + + var res = doRequest(req, HttpResponse.BodyHandlers.ofString()); + return switch (res.statusCode()) { + case 200 -> GSON.fromJson(res.body(), MapCollectionsResponse.class).results(); + case 404 -> throw new NotFoundError(playerId); + default -> throw new InternalError("Failed to get map collections: " + res.body()); + }; + } + + @Override + public void createMapCollection(@NotNull String playerId, @Nullable String name, @Nullable String icon, @NotNull List mapIds) { + var body = new JsonObject(); + body.addProperty("name", name); + body.addProperty("icon", icon); + body.add("mapIds", GSON.toJsonTree(mapIds)); + + var req = HttpRequest.newBuilder() + .POST(HttpRequest.BodyPublishers.ofString(body.toString())) + .uri(URI.create(urlV3 + "/map-players/" + playerId + "/collections")) + .header(AUTHORIZER_HEADER, playerId) + .build(); + var res = doRequest(req, HttpResponse.BodyHandlers.ofString()); + if (res.statusCode() == 201) return; // Ok + throw new InternalError("Failed to create map collection: " + res.body()); + } + + @Override + public @NotNull MapCollection getMapCollection(@NotNull String playerId, @NotNull String collectionId) { + var req = HttpRequest.newBuilder() + .uri(URI.create(urlV3 + "/map-players/" + playerId + "/collections/" + collectionId)) + .header(AUTHORIZER_HEADER, playerId) + .build(); + var res = doRequest(req, HttpResponse.BodyHandlers.ofString()); + return switch (res.statusCode()) { + case 200 -> GSON.fromJson(res.body(), MapCollection.class); + case 404 -> throw new NotFoundError(collectionId); + default -> throw new InternalError("Failed to get map collection: " + res.body()); + }; + } + + @Override + public void updateMapCollection(@NotNull String playerId, @NotNull MapCollection collection) { + var body = new JsonObject(); + body.addProperty("name", collection.name()); + body.addProperty("icon", OpUtils.map(collection.icon(), Material::name)); + body.add("maps", GSON.toJsonTree(collection.mapIds())); + + var req = HttpRequest.newBuilder() + .method("PATCH", HttpRequest.BodyPublishers.ofString(body.toString())) + .uri(URI.create(urlV3 + "/map-players/" + playerId + "/collections/" + collection.collectionId())) + .header(AUTHORIZER_HEADER, playerId) + .build(); + var res = doRequest(req, HttpResponse.BodyHandlers.ofString()); + if (res.statusCode() == 204) return; // Ok + throw new InternalError("Failed to update map collection: " + res.body()); + } + + @Override + public void deleteMapCollection(@NotNull String playerId, @NotNull String collectionId) { + var req = HttpRequest.newBuilder() + .method("DELETE", HttpRequest.BodyPublishers.noBody()) + .uri(URI.create(urlV3 + "/map-players/" + playerId + "/collections/" + collectionId)) + .header(AUTHORIZER_HEADER, playerId) + .build(); + var res = doRequest(req, HttpResponse.BodyHandlers.ofString()); + if (res.statusCode() == 200) return; // Ok + throw new InternalError("Failed to delete map collection: " + res.body()); + } + @Override public @NotNull List getLegacyMaps(@NotNull String authorizer, @NotNull String playerId) { var req = HttpRequest.newBuilder() diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/map/responses/MapCollectionsResponse.java b/modules/core/src/main/java/net/hollowcube/mapmaker/map/responses/MapCollectionsResponse.java new file mode 100644 index 000000000..98de0407b --- /dev/null +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/map/responses/MapCollectionsResponse.java @@ -0,0 +1,11 @@ +package net.hollowcube.mapmaker.map.responses; + +import net.hollowcube.mapmaker.map.MapCollection; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + +public record MapCollectionsResponse( + @NotNull List results +) { +} diff --git a/modules/core/src/main/java/net/hollowcube/mapmaker/misc/noop/NoopMapService.java b/modules/core/src/main/java/net/hollowcube/mapmaker/misc/noop/NoopMapService.java index 7cabc67a7..5d62affba 100644 --- a/modules/core/src/main/java/net/hollowcube/mapmaker/misc/noop/NoopMapService.java +++ b/modules/core/src/main/java/net/hollowcube/mapmaker/misc/noop/NoopMapService.java @@ -192,6 +192,31 @@ public void setMapRating(@NotNull String mapId, @NotNull String playerId, @NotNu return new MapHistory(page, false, List.of(new MapHistory.Entry("62da0aaf-8cad-4c13-869c-02b07688988d"))); } + @Override + public void deleteMapCollection(@NotNull String playerId, @NotNull String collectionId) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void updateMapCollection(@NotNull String playerId, @NotNull MapCollection collection) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public @NotNull MapCollection getMapCollection(@NotNull String playerId, @NotNull String collectionId) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public void createMapCollection(@NotNull String playerId, @Nullable String name, @Nullable String icon, @NotNull List mapIds) { + throw new UnsupportedOperationException("not implemented"); + } + + @Override + public @NotNull List getMapCollections(@NotNull String playerId) { + throw new UnsupportedOperationException("not implemented"); + } + @Override public @NotNull List getLegacyMaps(@NotNull String authorizer, @NotNull String playerId) { throw new UnsupportedOperationException("not implemented"); diff --git a/modules/core/src/main/resources/net/hollowcube/mapmaker/gui/common/search/ItemSearchEntry.xml b/modules/core/src/main/resources/net/hollowcube/mapmaker/gui/common/search/ItemSearchEntry.xml new file mode 100644 index 000000000..ee4a3cf95 --- /dev/null +++ b/modules/core/src/main/resources/net/hollowcube/mapmaker/gui/common/search/ItemSearchEntry.xml @@ -0,0 +1,4 @@ + + +