Skip to content
This repository was archived by the owner on Nov 28, 2025. It is now read-only.

Commit 67a05e9

Browse files
committed
add automatic reloading on file updates
1 parent 544aeb1 commit 67a05e9

File tree

7 files changed

+200
-32
lines changed

7 files changed

+200
-32
lines changed

1.21.4/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ dependencies {
3232
officialMojangMappings {
3333
nameSyntheticMembers = true
3434
}
35-
parchment("org.parchmentmc.data:parchment-1.21.4:2024.12.22@zip")
35+
parchment("org.parchmentmc.data:parchment-1.21.4:2024.12.29@zip")
3636
})
3737

3838
modImplementation("net.fabricmc:fabric-loader:${project.property("fabric_loader")}")

1.21.4/src/main/java/io/github/axolotlclient/modules/screenshotUtils/GalleryScreen.java

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232

3333
import com.mojang.blaze3d.vertex.VertexConsumer;
3434
import io.github.axolotlclient.AxolotlClientConfig.api.util.Colors;
35+
import io.github.axolotlclient.api.API;
3536
import io.github.axolotlclient.api.requests.FriendRequest;
3637
import io.github.axolotlclient.api.requests.UserRequest;
38+
import io.github.axolotlclient.util.Watcher;
3739
import net.fabricmc.loader.api.FabricLoader;
3840
import net.minecraft.Util;
3941
import net.minecraft.client.Minecraft;
@@ -59,7 +61,7 @@
5961

6062
public class GalleryScreen extends Screen {
6163

62-
public static final Path SCREENSHOT_DIR = FabricLoader.getInstance().getGameDir().resolve(Screenshot.SCREENSHOT_DIR);
64+
public static final Path SCREENSHOTS_DIR = FabricLoader.getInstance().getGameDir().resolve(Screenshot.SCREENSHOT_DIR);
6365

6466
private record Tab<T>(Component title, Callable<List<T>> list, Map<T, ImageInstance> loadingCache,
6567
GalleryScreen.Tab.Loader<T> loader) {
@@ -77,7 +79,7 @@ private static <T> Tab<T> of(Component title, Callable<List<T>> list, GalleryScr
7779
}
7880

7981
private static final Tab<Path> LOCAL = of(Component.translatable("gallery.title.local"), () -> {
80-
try (Stream<Path> screenshots = Files.list(SCREENSHOT_DIR)) {
82+
try (Stream<Path> screenshots = Files.list(SCREENSHOTS_DIR)) {
8183
return screenshots.sorted(Comparator.<Path>comparingLong(p -> {
8284
try {
8385
return Files.getLastModifiedTime(p).toMillis();
@@ -100,37 +102,46 @@ private static <T> Tab<T> of(Component title, Callable<List<T>> list, GalleryScr
100102
})).join(), url -> ImageShare.getInstance().downloadImage(url).join());
101103

102104
interface Loader<T> {
103-
ImageInstance load(T obj) throws Exception;
105+
ImageInstance load(T obj) throws Exception;
104106
}
105107
}
106108

107109
private Tab<?> current;
108110

109111
private final Screen parent;
112+
private final Watcher watcher;
110113

111114
public GalleryScreen(Screen parent) {
112115
super(Component.translatable("gallery.title"));
113116
this.parent = parent;
114117
this.current = Tab.LOCAL;
118+
this.watcher = Watcher.createSelfTicking(SCREENSHOTS_DIR, () -> {
119+
if (current == Tab.LOCAL) {
120+
rebuildWidgets();
121+
}
122+
});
115123
}
116124

117-
private static final int entrySpacing = 10,
125+
private static final int entrySpacing = 4,
118126
entryWidth = 100,
119127
entryHeight = 75,
120128
marginLeftRight = 10;
121129

122130
@Override
123131
protected void init() {
132+
boolean online = API.getInstance().isAuthenticated();
124133
HeaderAndFooterLayout layout = new HeaderAndFooterLayout(this);
125134
layout.setHeaderHeight(40);
126135
LinearLayout header = layout.addToHeader(LinearLayout.vertical().spacing(4));
127136
header.defaultCellSetting().alignHorizontallyCenter();
128137
header.addChild(new StringWidget(title, font));
129-
header.addChild(new StringWidget(current.title(), font));
138+
if (online) {
139+
header.addChild(new StringWidget(current.title(), font));
140+
}
130141

131-
int columnCount = (width - (marginLeftRight * 2)) / (entryWidth + entrySpacing);
142+
int columnCount = (width - (marginLeftRight * 2) + entrySpacing - 13) / (entryWidth + entrySpacing); // -13 to always have enough space for the scrollbar
132143

133-
final var area = new ImageList(minecraft, layout.getWidth(), layout.getContentHeight(), layout.getHeaderHeight(), entryHeight + entrySpacing, columnCount * (entryWidth + entrySpacing) + marginLeftRight);
144+
final var area = new ImageList(minecraft, layout.getWidth(), layout.getContentHeight(), layout.getHeaderHeight(), entryHeight + entrySpacing, columnCount);
134145

135146
layout.addToContents(area, LayoutSettings::alignHorizontallyLeft);
136147
setInitialFocus(area);
@@ -147,17 +158,21 @@ protected void init() {
147158
});
148159

149160
var footer = layout.addToFooter(LinearLayout.horizontal()).spacing(4);
150-
Button.Builder switchTab;
151-
if (current == Tab.SHARED) {
152-
switchTab = Button.builder(Component.translatable("gallery.tab.local"), b -> setTab(Tab.LOCAL));
153-
} else {
154-
switchTab = Button.builder(Component.translatable("gallery.tab.shared"), b -> setTab(Tab.SHARED));
161+
footer.defaultCellSetting().alignHorizontallyCenter();
162+
int buttonWidth = columnCount <= 5 && online ? 100 : 150;
163+
if (online) {
164+
Button.Builder switchTab;
165+
if (current == Tab.SHARED) {
166+
switchTab = Button.builder(Component.translatable("gallery.tab.local"), b -> setTab(Tab.LOCAL));
167+
} else {
168+
switchTab = Button.builder(Component.translatable("gallery.tab.shared"), b -> setTab(Tab.SHARED));
169+
}
170+
footer.addChild(switchTab.width(buttonWidth).build());
155171
}
156-
footer.addChild(switchTab.width(100).build());
157172
footer.addChild(Button.builder(Component.translatable("gallery.download_external"), b -> minecraft.setScreen(new DownloadImageScreen(this)))
158-
.width(100).build());
173+
.width(buttonWidth).build());
159174
footer.addChild(Button.builder(CommonComponents.GUI_BACK, b -> onClose())
160-
.width(100).build());
175+
.width(buttonWidth).build());
161176

162177
layout.arrangeElements();
163178
layout.visitWidgets(this::addRenderableWidget);
@@ -169,6 +184,7 @@ public void onClose() {
169184
Tab.LOCAL.loadingCache().clear();
170185
Tab.SHARED.loadingCache().forEach((s, instance) -> minecraft.getTextureManager().release(instance.id()));
171186
Tab.SHARED.loadingCache().clear();
187+
Watcher.close(watcher);
172188
minecraft.setScreen(parent);
173189
}
174190

@@ -359,9 +375,9 @@ private static class ImageList extends ContainerObjectSelectionList<ImageListEnt
359375

360376
private final int rowWidth;
361377

362-
public ImageList(Minecraft minecraft, int i, int j, int k, int l, int rowWidth) {
378+
public ImageList(Minecraft minecraft, int i, int j, int k, int l, int columns) {
363379
super(minecraft, i, j, k, l);
364-
this.rowWidth = rowWidth;
380+
this.rowWidth = columns * (entryWidth + entrySpacing) - entrySpacing;
365381
}
366382

367383
@Override
@@ -374,6 +390,11 @@ public int getRowWidth() {
374390
return rowWidth;
375391
}
376392

393+
@Override
394+
public int getRowLeft() {
395+
return this.getX() + this.width / 2 - this.getRowWidth() / 2;
396+
}
397+
377398
@Override
378399
public boolean removeEntry(ImageListEntry entry) {
379400
return super.removeEntry(entry);

1.21.4/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ImageScreen.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import net.minecraft.client.gui.components.StringWidget;
4646
import net.minecraft.client.gui.layouts.HeaderAndFooterLayout;
4747
import net.minecraft.client.gui.layouts.LinearLayout;
48+
import net.minecraft.client.gui.layouts.SpacerElement;
4849
import net.minecraft.client.gui.narration.NarrationElementOutput;
4950
import net.minecraft.client.gui.screens.Screen;
5051
import net.minecraft.client.renderer.RenderType;
@@ -61,7 +62,11 @@ static Screen create(Screen parent, CompletableFuture<ImageInstance> future, boo
6162
if (future.isDone()) {
6263
return new ImageScreen(parent, future.join(), freeOnClose);
6364
}
64-
return new LoadingImageScreen(parent, future.thenAccept(i -> Minecraft.getInstance().execute(() -> Minecraft.getInstance().setScreen(new ImageScreen(parent, i, freeOnClose)))), freeOnClose);
65+
return new LoadingImageScreen(parent, future.thenAccept(i -> {
66+
if (i != null) {
67+
Minecraft.getInstance().execute(() -> Minecraft.getInstance().setScreen(new ImageScreen(parent, i, freeOnClose)));
68+
}
69+
}), freeOnClose);
6570
}
6671

6772
private ImageScreen(Screen parent, ImageInstance instance, boolean freeOnClose) {
@@ -78,13 +83,20 @@ protected void init() {
7883
header.defaultCellSetting().alignHorizontallyCenter();
7984
header.addChild(new StringWidget(getTitle(), font));
8085

86+
if (image instanceof ImageInstance.Remote remote) {
87+
layout.setHeaderHeight(38);
88+
header.addChild(new StringWidget(Component.translatable("gallery.image.upload_details", UUIDHelper.getUsername(remote.uploader()), remote.sharedAt().atZone(ZoneId.systemDefault()).format(AxolotlClientCommon.getInstance().formatter)), font));
89+
}
90+
8191
int buttonWidth = 75;
8292
double imgAspectRatio = image.image().getWidth() / (double) image.image().getHeight();
8393
int imageWidth = Math.min((int) (layout.getContentHeight() * imgAspectRatio), layout.getWidth() - buttonWidth - 4 - 10);
8494
int imageHeight = (int) (imageWidth / imgAspectRatio);
85-
layout.setHeaderHeight(height - layout.getFooterHeight() - imageHeight);
8695

8796
var contents = layout.addToContents(LinearLayout.horizontal().spacing(4));
97+
if (width/2 > (imageWidth / 2) + buttonWidth+4) {
98+
contents.addChild(new SpacerElement(buttonWidth + 4, imageHeight));
99+
}
88100
var footer = layout.addToFooter(LinearLayout.horizontal().spacing(4));
89101
contents.addChild(new ImageElement(imageWidth, imageHeight));
90102
var actions = contents.addChild(LinearLayout.vertical()).spacing(4);
@@ -107,7 +119,6 @@ protected void init() {
107119
actions.addChild(Button.builder(Component.translatable("gallery.image.open.external"), b -> Util.getPlatform().openPath(local.location())).width(buttonWidth).build());
108120
}
109121
if (image instanceof ImageInstance.Remote remote) {
110-
header.addChild(new StringWidget(Component.translatable("gallery.image.upload_details", UUIDHelper.getUsername(remote.uploader()), remote.sharedAt().atZone(ZoneId.systemDefault()).format(AxolotlClientCommon.getInstance().formatter)), font));
111122
if (!(image instanceof ImageInstance.Local)) {
112123
actions.addChild(Button.builder(Component.translatable("gallery.image.save"), b -> {
113124
b.active = false;
@@ -147,7 +158,7 @@ public void onClose() {
147158
}
148159

149160
private Path saveSharedImage(ImageInstance.Remote remote) throws IOException {
150-
Path out = GalleryScreen.SCREENSHOT_DIR.resolve("shared")
161+
Path out = GalleryScreen.SCREENSHOTS_DIR.resolve("shared")
151162
.resolve(remote.uploader())
152163
.resolve(remote.filename());
153164
Path infoJson = out.resolveSibling(remote.filename() + ".json");

1.21.4/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ImageShare.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ public CompletableFuture<ImageInstance> downloadImage(String url) {
6969
try {
7070
ImageInstance.Remote remote = new ImageInstance.RemoteImpl(NativeImage.read(new ByteArrayInputStream(data.data())), data.name(), data.uploader(), data.sharedAt(), ensureUrl(url).orElseThrow());
7171
try {
72-
Path local = GalleryScreen.SCREENSHOT_DIR.resolve(remote.filename());
72+
Path local = GalleryScreen.SCREENSHOTS_DIR.resolve(remote.filename());
7373
HashFunction hash = Hashing.goodFastHash(32);
7474
if (Files.exists(local) && hash.hashBytes(data.data()).equals(hash.hashBytes(Files.readAllBytes(local)))) {
7575
return remote.toShared(local);

1.21.4/src/main/java/io/github/axolotlclient/modules/screenshotUtils/ScreenshotUtils.java

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.util.LinkedHashMap;
3030
import java.util.List;
3131
import java.util.Map;
32+
import java.util.concurrent.CompletableFuture;
3233
import java.util.function.BooleanSupplier;
3334

3435
import io.github.axolotlclient.AxolotlClient;
@@ -75,17 +76,20 @@ public class ScreenshotUtils extends AbstractModule {
7576
"open_image",
7677
new CustomClickEvent((file) -> Util.getPlatform().openUri(file.toUri()))));
7778

78-
actions.put(() -> API.getInstance().isAuthenticated(), new Action("uploadAction", ChatFormatting.LIGHT_PURPLE,
79-
"upload_image",
79+
actions.put(() -> true, new Action("viewInGalleryAction", ChatFormatting.LIGHT_PURPLE, "view_in_gallery",
8080
new CustomClickEvent(file -> {
81-
new Thread("Image Uploader") {
82-
@Override
83-
public void run() {
84-
ImageShare.getInstance().uploadImage(file);
85-
}
86-
}.start();
81+
try {
82+
ImageInstance instance = new ImageInstance.LocalImpl(file);
83+
Minecraft.getInstance().execute(() -> Minecraft.getInstance().setScreen(ImageScreen.create(null, CompletableFuture.completedFuture(instance), true)));
84+
} catch (Exception ignored) {
85+
io.github.axolotlclient.util.Util.sendChatMessage(Component.translatable("screenshot.gallery.view.error"));
86+
}
8787
})));
8888

89+
actions.put(() -> API.getInstance().isAuthenticated(), new Action("uploadAction", ChatFormatting.AQUA,
90+
"upload_image",
91+
new CustomClickEvent(ImageShare.getInstance()::uploadImage)));
92+
8993
// If you have further ideas to what actions could be added here, please let us know!
9094

9195
return actions;
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Copyright © 2024 moehreag <[email protected]> & Contributors
3+
*
4+
* This file is part of AxolotlClient.
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the GNU Lesser General Public
8+
* License as published by the Free Software Foundation; either
9+
* version 3 of the License, or (at your option) any later version.
10+
*
11+
* This program is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14+
* Lesser General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU Lesser General Public License
17+
* along with this program; if not, write to the Free Software Foundation,
18+
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19+
*
20+
* For more information, see the LICENSE file.
21+
*/
22+
23+
package io.github.axolotlclient.util;
24+
25+
import java.io.IOException;
26+
import java.nio.file.*;
27+
import java.util.concurrent.Executors;
28+
import java.util.concurrent.ScheduledExecutorService;
29+
import java.util.concurrent.TimeUnit;
30+
31+
import io.github.axolotlclient.AxolotlClientCommon;
32+
import org.jetbrains.annotations.Nullable;
33+
34+
35+
public class Watcher implements AutoCloseable {
36+
private static final ScheduledExecutorService thread = Executors.newSingleThreadScheduledExecutor();
37+
private final WatchService watcher;
38+
private final Path path;
39+
40+
public Watcher(Path root) throws IOException {
41+
this.path = root;
42+
this.watcher = path.getFileSystem().newWatchService();
43+
44+
try {
45+
this.watchDir(path);
46+
try (DirectoryStream<Path> directoryStream = Files.newDirectoryStream(path)) {
47+
48+
for (Path path : directoryStream) {
49+
if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
50+
this.watchDir(path);
51+
}
52+
}
53+
}
54+
} catch (Exception e) {
55+
this.watcher.close();
56+
throw e;
57+
}
58+
}
59+
60+
@Nullable
61+
public static Watcher create(Path path) {
62+
try {
63+
return new Watcher(path);
64+
} catch (IOException var2) {
65+
AxolotlClientCommon.getInstance().getLogger().warn("Failed to initialize directory {} monitoring", path, var2);
66+
return null;
67+
}
68+
}
69+
70+
public static Watcher createSelfTicking(Path path, Runnable onUpdate) {
71+
var watcher = create(path);
72+
if (watcher != null) {
73+
thread.scheduleAtFixedRate(() -> {
74+
try {
75+
if (watcher.pollForChanges()) {
76+
onUpdate.run();
77+
}
78+
} catch (IOException e) {
79+
try {
80+
watcher.close();
81+
} catch (IOException ignored) {
82+
}
83+
}
84+
}, 100, 100, TimeUnit.MILLISECONDS);
85+
}
86+
return watcher;
87+
}
88+
89+
private void watchDir(Path path) throws IOException {
90+
path.register(this.watcher, StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY);
91+
}
92+
93+
public boolean pollForChanges() throws IOException {
94+
boolean bl = false;
95+
96+
WatchKey watchKey;
97+
while ((watchKey = this.watcher.poll()) != null) {
98+
for (WatchEvent<?> watchEvent : watchKey.pollEvents()) {
99+
bl = true;
100+
if (watchKey.watchable() == this.path && watchEvent.kind() == StandardWatchEventKinds.ENTRY_CREATE) {
101+
Path path = this.path.resolve((Path) watchEvent.context());
102+
if (Files.isDirectory(path, LinkOption.NOFOLLOW_LINKS)) {
103+
this.watchDir(path);
104+
}
105+
}
106+
}
107+
108+
watchKey.reset();
109+
}
110+
111+
return bl;
112+
}
113+
114+
public void close() throws IOException {
115+
this.watcher.close();
116+
}
117+
118+
public void safeClose() {
119+
try {
120+
close();
121+
} catch (IOException ignored) {
122+
}
123+
}
124+
125+
public static void close(Watcher watcher) {
126+
if (watcher != null) {
127+
watcher.safeClose();
128+
}
129+
}
130+
}

common/src/main/resources/assets/axolotlclient/lang/en_us.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -624,5 +624,7 @@
624624
"gallery.tab.shared": "View shared images",
625625
"gallery.download_external": "View image from url",
626626
"gallery.image.view": "View image",
627-
"gallery.image.loading.title": "Loading image..."
627+
"gallery.image.loading.title": "Loading image...",
628+
"viewInGalleryAction": "[Gallery]",
629+
"view_in_gallery": "View in Gallery"
628630
}

0 commit comments

Comments
 (0)