diff --git a/CHANGELOG.md b/CHANGELOG.md index a8b286b..39e0535 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ ### Added +- A World Backups icon sits at the far right of the square icons on the title + screen and on the pause screen. On the title screen it opens the list of + backed-up worlds; in a world it opens that world's backups. - The backup browser can select several backups at once. Ctrl or Cmd click toggles a row, Shift click extends the selection, and "Select all" picks every backup that matches the filter. One confirmation deletes all of them, diff --git a/src/client/java/dev/ishaanko/worldarchive/WorldArchiveClient.java b/src/client/java/dev/ishaanko/worldarchive/WorldArchiveClient.java index 6c8c65e..c4d8874 100644 --- a/src/client/java/dev/ishaanko/worldarchive/WorldArchiveClient.java +++ b/src/client/java/dev/ishaanko/worldarchive/WorldArchiveClient.java @@ -3,6 +3,7 @@ import dev.ishaanko.worldarchive.runtime.WorldArchiveRuntime; import dev.ishaanko.worldarchive.settings.ClientSettingsAccess; import dev.ishaanko.worldarchive.ui.EditWorldBackupIntegration; +import dev.ishaanko.worldarchive.ui.IconRowBackupIntegration; import dev.ishaanko.worldarchive.ui.SelectWorldBackupIntegration; import net.fabricmc.api.ClientModInitializer; import org.slf4j.Logger; @@ -17,6 +18,7 @@ public void onInitializeClient() { WorldArchiveRuntime runtime = WorldArchiveRuntime.initialize(); SelectWorldBackupIntegration.register(() -> runtime); EditWorldBackupIntegration.register(() -> runtime); + IconRowBackupIntegration.register(() -> runtime, runtime::openBrowser); LOGGER.info("{} initialized.", WorldArchiveMetadata.MOD_NAME); } } diff --git a/src/client/java/dev/ishaanko/worldarchive/runtime/RuntimeNavigation.java b/src/client/java/dev/ishaanko/worldarchive/runtime/RuntimeNavigation.java index 8f03a57..f0559a4 100644 --- a/src/client/java/dev/ishaanko/worldarchive/runtime/RuntimeNavigation.java +++ b/src/client/java/dev/ishaanko/worldarchive/runtime/RuntimeNavigation.java @@ -72,10 +72,11 @@ void playRestoredWorld(Screen returnTo, RestoreBackupResult result) { transitionToRestoredWorld(returnTo, result, true); } - void openBrowser() { + /** Opens the live world's browser; false when no world is resolved or the runtime is not ready. */ + boolean openBrowser() { BackupWorldContext world = runtime.currentLiveWorld(); if (world == null || runtime.unavailable()) { - return; + return false; } Minecraft minecraft = runtime.services().minecraft(); minecraft.execute(() -> { @@ -86,6 +87,7 @@ void openBrowser() { runtime)); } }); + return true; } boolean sourceDirectoryAvailable(BackupWorldContext world) { diff --git a/src/client/java/dev/ishaanko/worldarchive/runtime/WorldArchiveRuntime.java b/src/client/java/dev/ishaanko/worldarchive/runtime/WorldArchiveRuntime.java index f3e6453..dd9734d 100644 --- a/src/client/java/dev/ishaanko/worldarchive/runtime/WorldArchiveRuntime.java +++ b/src/client/java/dev/ishaanko/worldarchive/runtime/WorldArchiveRuntime.java @@ -467,8 +467,9 @@ public void playRestoredWorld(Screen returnTo, RestoreBackupResult result) { clientFacade.playRestoredWorld(returnTo, result); } - public void openBrowser() { - navigation.openBrowser(); + /** Opens the live world's backup browser; false when there is no resolved live world yet. */ + public boolean openBrowser() { + return navigation.openBrowser(); } private RuntimeState buildState(WorldArchiveConfig config) { diff --git a/src/client/java/dev/ishaanko/worldarchive/ui/IconRowBackupIntegration.java b/src/client/java/dev/ishaanko/worldarchive/ui/IconRowBackupIntegration.java new file mode 100644 index 0000000..817f842 --- /dev/null +++ b/src/client/java/dev/ishaanko/worldarchive/ui/IconRowBackupIntegration.java @@ -0,0 +1,145 @@ +package dev.ishaanko.worldarchive.ui; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.BooleanSupplier; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents; +import net.fabricmc.fabric.api.client.screen.v1.Screens; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.AbstractWidget; +import net.minecraft.client.gui.components.Button; +import net.minecraft.client.gui.components.FriendsButton; +import net.minecraft.client.gui.screens.PauseScreen; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.TitleScreen; + +/** + * Adds the WorldArchive shortcut at the far right of the square-icon row on the title screen + * and the pause screen. On the title screen it opens the world list; in a world it opens + * that world's backups, or the world list while the live world is still being resolved. + */ +public final class IconRowBackupIntegration { + private static final int GAP = 4; + + private static final AtomicBoolean REGISTERED = new AtomicBoolean(); + + private static volatile Supplier facadeSupplier; + + private static volatile BooleanSupplier openLiveWorldBackups; + + private IconRowBackupIntegration() { + } + + /** + * Registers the global Fabric screen hook. Repeated calls update the actions. + * + * @param facade supplies the facade the world list needs + * @param openLiveWorld opens the browser for the loaded world and reports whether it did + */ + public static void register( + Supplier facade, + BooleanSupplier openLiveWorld) { + facadeSupplier = Objects.requireNonNull(facade, "facade"); + openLiveWorldBackups = Objects.requireNonNull(openLiveWorld, "openLiveWorld"); + if (REGISTERED.compareAndSet(false, true)) { + ScreenEvents.AFTER_INIT.register(IconRowBackupIntegration::afterInit); + } + } + + private static void afterInit(Minecraft minecraft, Screen screen, int width, int height) { + Button.OnPress action; + if (screen instanceof TitleScreen) { + action = ignored -> minecraft.setScreenAndShow( + new BackupWorldsScreen(screen, currentFacade())); + } else if (screen instanceof PauseScreen && minecraft.hasSingleplayerServer()) { + action = ignored -> { + if (!currentLiveWorldAction().getAsBoolean()) { + minecraft.setScreenAndShow(new BackupWorldsScreen(screen, currentFacade())); + } + }; + } else { + return; + } + List