feat: route switcher on home card, subtler transitions, flat press feedback; add ServerEndpoint tests
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
06ef27f0e0
commit
0269e54b95
5 files changed
+85
-16
No files matched your search
@@ -121,7 +121,7 @@ public enum ContainerAnimations implements TransitionPane.AnimationProducer {
|
||||
/// Animates forward navigation with temporary incoming-page caching.
|
||||
@Override
|
||||
public Timeline animate(Pane container, Node previousNode, Node nextNode) {
|
||||
double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
|
||||
double offset = 12.0;
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.cacheProperty(), isCacheAllowed(nextNode), Interpolator.DISCRETE),
|
||||
@@ -152,7 +152,7 @@ public enum ContainerAnimations implements TransitionPane.AnimationProducer {
|
||||
/// Animates backward navigation with temporary incoming-page caching.
|
||||
@Override
|
||||
public Timeline animate(Pane container, Node previousNode, Node nextNode) {
|
||||
double offset = container.getWidth() > 0 ? container.getWidth() * 0.2 : 50;
|
||||
double offset = 12.0;
|
||||
return new Timeline(
|
||||
new KeyFrame(Duration.ZERO,
|
||||
new KeyValue(nextNode.cacheProperty(), isCacheAllowed(nextNode), Interpolator.DISCRETE),
|
||||
|
||||
@@ -23,10 +23,10 @@ import org.jetbrains.annotations.Nullable;
|
||||
public class TransitionPane extends StackPane {
|
||||
|
||||
/// The default easing curve for page transitions.
|
||||
public static final Interpolator DEFAULT_INTERPOLATOR = Motion.EASE;
|
||||
public static final Interpolator DEFAULT_INTERPOLATOR = Motion.EASE_OUT_CUBIC;
|
||||
|
||||
/// The default duration for page transitions.
|
||||
public static final Duration DEFAULT_DURATION = Motion.SHORT4;
|
||||
public static final Duration DEFAULT_DURATION = Duration.millis(320);
|
||||
|
||||
/// Half the default duration for transitions with two phases.
|
||||
public static final Duration DEFAULT_DURATION_HALF = DEFAULT_DURATION.divide(2);
|
||||
|
||||
@@ -12,6 +12,7 @@ import javafx.collections.ObservableList;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
@@ -31,6 +32,8 @@ import org.jackhuang.hmcl.ui.Controllers;
|
||||
import org.jackhuang.hmcl.ui.FXUtils;
|
||||
import org.jackhuang.hmcl.ui.SVG;
|
||||
import org.jackhuang.hmcl.ui.animation.AnimationUtils;
|
||||
import org.jackhuang.hmcl.ui.construct.IconedMenuItem;
|
||||
import org.jackhuang.hmcl.ui.construct.PopupMenu;
|
||||
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
|
||||
import org.jackhuang.hmcl.ui.instances.GameListPopupMenu;
|
||||
import org.jackhuang.hmcl.ui.instances.Instances;
|
||||
@@ -40,6 +43,8 @@ import org.jackhuang.hmcl.util.javafx.BindingMapping;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
|
||||
@@ -148,6 +153,27 @@ public final class MainPage extends StackPane implements DecoratorPage {
|
||||
routeFact.setVisible(route != null);
|
||||
routeFact.setManaged(route != null);
|
||||
routeValue.setText(route != null ? route.name() : "");
|
||||
|
||||
// With more than one route (for example direct and CDN), the route can be switched.
|
||||
List<ServerEndpoint.Route> routes = ServerEndpoint.routes(currentGame.getModpackConfigurationFile());
|
||||
boolean switchable = routes.size() > 1;
|
||||
routeFact.setCursor(switchable ? Cursor.HAND : Cursor.DEFAULT);
|
||||
if (switchable && route != null) {
|
||||
routeValue.setText(route.name() + " ▾");
|
||||
}
|
||||
routeFact.setOnMouseClicked(switchable ? event -> {
|
||||
PopupMenu list = new PopupMenu();
|
||||
JFXPopup popup = new JFXPopup(list);
|
||||
List<Node> items = new ArrayList<>();
|
||||
for (ServerEndpoint.Route candidate : routes) {
|
||||
items.add(new IconedMenuItem(null, candidate.name(), () -> {
|
||||
ServerEndpoint.select(currentGame.getModpackConfigurationFile(), candidate);
|
||||
routeValue.setText(candidate.name() + " ▾");
|
||||
}, popup));
|
||||
}
|
||||
list.getContent().setAll(items);
|
||||
popup.show(routeFact, JFXPopup.PopupVPosition.BOTTOM, JFXPopup.PopupHPosition.LEFT, 0, -routeFact.getHeight());
|
||||
} : null);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -460,7 +460,7 @@
|
||||
}
|
||||
|
||||
.launch-pane > .jfx-button > StackPane > .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-on-primary-container;
|
||||
-jfx-rippler-fill: transparent;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
-fx-padding: 0;
|
||||
}
|
||||
@@ -494,11 +494,11 @@
|
||||
|
||||
/* TODO: It seems not actually used */
|
||||
.tab-rippler {
|
||||
-jfx-rippler-fill: derive(-monet-primary-container, 30%);
|
||||
-jfx-rippler-fill: transparent;
|
||||
}
|
||||
|
||||
.jfx-tab-pane .jfx-rippler {
|
||||
-jfx-rippler-fill: white;
|
||||
-jfx-rippler-fill: transparent;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
@@ -746,7 +746,7 @@
|
||||
}
|
||||
|
||||
.jfx-tool-bar-second .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-on-surface;
|
||||
-jfx-rippler-fill: transparent;
|
||||
}
|
||||
|
||||
.jfx-tool-bar-button {
|
||||
@@ -864,7 +864,7 @@
|
||||
}
|
||||
|
||||
.jfx-button .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-on-surface;
|
||||
-jfx-rippler-fill: transparent;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
-fx-padding: 0.0;
|
||||
}
|
||||
@@ -874,7 +874,7 @@
|
||||
}
|
||||
|
||||
.jfx-button-raised .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-on-primary;
|
||||
-jfx-rippler-fill: transparent;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
-fx-padding: 0.0;
|
||||
}
|
||||
@@ -997,7 +997,7 @@
|
||||
}
|
||||
|
||||
.jfx-list-cell .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-primary-container;
|
||||
-jfx-rippler-fill: transparent;
|
||||
}
|
||||
|
||||
/*.list-cell:odd:selected > .jfx-rippler > StackPane,*/
|
||||
@@ -1350,7 +1350,7 @@
|
||||
}
|
||||
|
||||
.toggle-icon4 .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-primary;
|
||||
-jfx-rippler-fill: transparent;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
@@ -1379,7 +1379,7 @@
|
||||
}
|
||||
|
||||
.toggle-icon-tiny .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-primary;
|
||||
-jfx-rippler-fill: transparent;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
@@ -1396,7 +1396,7 @@
|
||||
}
|
||||
|
||||
.announcement-close-button .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-primary;
|
||||
-jfx-rippler-fill: transparent;
|
||||
-jfx-mask-type: CIRCLE;
|
||||
}
|
||||
|
||||
@@ -1607,7 +1607,7 @@
|
||||
}
|
||||
|
||||
/*.combo-box-popup .list-view .jfx-list-cell .jfx-rippler {*/
|
||||
/* -jfx-rippler-fill: -monet-primary-container;*/
|
||||
/* -jfx-rippler-fill: transparent;*/
|
||||
/*}*/
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -1654,7 +1654,7 @@
|
||||
}
|
||||
|
||||
.custom-color-dialog .tab .jfx-rippler {
|
||||
-jfx-rippler-fill: -monet-on-surface;
|
||||
-jfx-rippler-fill: transparent;
|
||||
}
|
||||
|
||||
.custom-color-dialog .tab-selected-line {
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 The HMCL Authors
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later WITH AdditionRef-HMCL
|
||||
package org.jackhuang.hmcl.game;
|
||||
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/// Tests how [ServerEndpoint] resolves routes without an installed modpack manifest.
|
||||
public final class ServerEndpointTest {
|
||||
@AfterEach
|
||||
void clearProperty() {
|
||||
System.clearProperty("hmcl.server.address");
|
||||
}
|
||||
|
||||
@Test
|
||||
void noConfigurationMeansNoRoute(@TempDir Path dir) {
|
||||
assertTrue(ServerEndpoint.routes(dir.resolve("modpack.json")).isEmpty());
|
||||
assertNull(ServerEndpoint.selected(dir.resolve("modpack.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fallsBackToDefaultAddress(@TempDir Path dir) {
|
||||
System.setProperty("hmcl.server.address", " play.example.net:25565 ");
|
||||
var routes = ServerEndpoint.routes(dir.resolve("modpack.json"));
|
||||
assertEquals(1, routes.size());
|
||||
assertEquals("play.example.net:25565", routes.get(0).address());
|
||||
assertEquals(routes.get(0), ServerEndpoint.selected(dir.resolve("modpack.json")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void selectionIsRememberedNextToTheConfiguration(@TempDir Path dir) throws Exception {
|
||||
System.setProperty("hmcl.server.address", "wss://cdn.example.net/mc");
|
||||
Path file = dir.resolve("modpack.json");
|
||||
ServerEndpoint.select(file, new ServerEndpoint.Route("cdn", "wss://cdn.example.net/mc"));
|
||||
assertEquals("cdn", Files.readString(dir.resolve("server-route.txt")));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user