Authlib Injector support

This commit is contained in:
huanghongxun 2018-02-17 23:28:05 +08:00
parent 83bc6748a3
commit 24ec0adacf
33 changed files with 716 additions and 137 deletions

View File

@ -45,7 +45,7 @@ public final class AccountHelper {
public static final AccountHelper INSTANCE = new AccountHelper(); public static final AccountHelper INSTANCE = new AccountHelper();
private AccountHelper() {} private AccountHelper() {}
public static final File SKIN_DIR = new File(Main.APPDATA, "skins"); public static final File SKIN_DIR = new File(Main.HMCL_DIRECTORY, "skins");
public static void loadSkins() { public static void loadSkins() {
loadSkins(Proxy.NO_PROXY); loadSkins(Proxy.NO_PROXY);

View File

@ -17,6 +17,7 @@
*/ */
package org.jackhuang.hmcl.setting; package org.jackhuang.hmcl.setting;
import com.google.gson.JsonParseException;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AccountFactory;
@ -24,12 +25,12 @@ import org.jackhuang.hmcl.auth.OfflineAccount;
import org.jackhuang.hmcl.auth.OfflineAccountFactory; import org.jackhuang.hmcl.auth.OfflineAccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.*; import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.task.FileDownloadTask; import org.jackhuang.hmcl.task.FileDownloadTask;
import org.jackhuang.hmcl.util.FileUtils; import org.jackhuang.hmcl.util.*;
import org.jackhuang.hmcl.util.Lang;
import org.jackhuang.hmcl.util.Pair;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.net.URL; import java.net.URL;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -49,6 +50,8 @@ public final class Accounts {
new Pair<>(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector)) new Pair<>(AUTHLIB_INJECTOR_ACCOUNT_KEY, new AuthlibInjectorAccountFactory(Accounts::downloadAuthlibInjector))
); );
private static final Map<String, String> AUTHLIB_INJECTOR_SERVER_NAMES = new HashMap<>();
public static String getAccountType(Account account) { public static String getAccountType(Account account) {
if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY; if (account instanceof OfflineAccount) return OFFLINE_ACCOUNT_KEY;
else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY; else if (account instanceof AuthlibInjectorAccount) return AUTHLIB_INJECTOR_ACCOUNT_KEY;
@ -87,11 +90,29 @@ public final class Accounts {
AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo(); AuthlibInjectorBuildInfo buildInfo = AuthlibInjectorBuildInfo.requestBuildInfo();
File jar = new File(Main.HMCL_DIRECTORY, "authlib-injector.jar"); File jar = new File(Main.HMCL_DIRECTORY, "authlib-injector.jar");
File local = new File(Main.HMCL_DIRECTORY, "authlib-injector.txt"); File local = new File(Main.HMCL_DIRECTORY, "authlib-injector.txt");
int buildNumber = Integer.parseInt(FileUtils.readText(local)); int buildNumber = 0;
try {
buildNumber = Integer.parseInt(FileUtils.readText(local));
} catch (IOException | NumberFormatException ignore) {
}
if (buildNumber < buildInfo.getBuildNumber()) { if (buildNumber < buildInfo.getBuildNumber()) {
new FileDownloadTask(new URL(buildInfo.getUrl()), jar).run(); new FileDownloadTask(new URL(buildInfo.getUrl()), jar).run();
FileUtils.writeText(local, String.valueOf(buildInfo.getBuildNumber())); FileUtils.writeText(local, String.valueOf(buildInfo.getBuildNumber()));
} }
return jar.getAbsolutePath(); return jar.getAbsolutePath();
} }
public static String getAuthlibInjectorServerName(String serverIp) {
if (AUTHLIB_INJECTOR_SERVER_NAMES.containsKey(serverIp))
return AUTHLIB_INJECTOR_SERVER_NAMES.get(serverIp);
else {
try {
AuthlibInjectorServerResponse response = Constants.GSON.fromJson(NetworkUtils.doGet(NetworkUtils.toURL(serverIp)), AuthlibInjectorServerResponse.class);
AUTHLIB_INJECTOR_SERVER_NAMES.put(serverIp, response.getMeta().getServerName());
return response.getMeta().getServerName();
} catch (JsonParseException | IOException | NullPointerException e) {
return null;
}
}
}
} }

View File

@ -19,15 +19,11 @@ package org.jackhuang.hmcl.setting;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorBuildInfo;
import org.jackhuang.hmcl.util.JavaVersion; import org.jackhuang.hmcl.util.JavaVersion;
import java.util.LinkedList; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
public final class Config { final class Config {
@SerializedName("last") @SerializedName("last")
private String selectedProfile = ""; private String selectedProfile = "";
@ -92,8 +88,8 @@ public final class Config {
@SerializedName("logLines") @SerializedName("logLines")
private int logLines = 100; private int logLines = 100;
@SerializedName("authlibInjectorServerURL") @SerializedName("authlibInjectorServerURLs")
private String authlibInjectorServerURL = AuthlibInjectorBuildInfo.UPDATE_URL; private Set<String> authlibInjectorServerURLs = new HashSet<>();
public String getSelectedProfile() { public String getSelectedProfile() {
return selectedProfile; return selectedProfile;
@ -283,17 +279,12 @@ public final class Config {
this.logLines = logLines; this.logLines = logLines;
} }
public String getAuthlibInjectorServerURL() { public Set<String> getAuthlibInjectorServerURLs() {
return authlibInjectorServerURL; return authlibInjectorServerURLs;
} }
/** public void setAuthlibInjectorServerURLs(Set<String> authlibInjectorServerURLs) {
* Will not invoke Settings.INSTANCE.save() this.authlibInjectorServerURLs = authlibInjectorServerURLs;
* @param authlibInjectorServerURL new server url. Settings.INSTANCE.save();
*/
public void setAuthlibInjectorServerURL(String authlibInjectorServerURL) {
this.authlibInjectorServerURL = authlibInjectorServerURL;
// Do not invoke Settings.INSTANCE.save()
// Because we want users set it theirself.
} }
} }

View File

@ -28,6 +28,7 @@ import javafx.scene.text.Font;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory; import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorBuildInfo; import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorBuildInfo;
import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider; import org.jackhuang.hmcl.download.BMCLAPIDownloadProvider;
import org.jackhuang.hmcl.download.DownloadProvider; import org.jackhuang.hmcl.download.DownloadProvider;
@ -87,10 +88,12 @@ public class Settings {
accounts.put(Accounts.getAccountId(account), account); accounts.put(Accounts.getAccountId(account), account);
} }
save(); checkAuthlibInjectorServerURLs();
checkAuthlibInjectorAccounts();
checkProfileMap(); checkProfileMap();
save();
for (Map.Entry<String, Profile> entry2 : getProfileMap().entrySet()) { for (Map.Entry<String, Profile> entry2 : getProfileMap().entrySet()) {
entry2.getValue().setName(entry2.getKey()); entry2.getValue().setName(entry2.getKey());
entry2.getValue().nameProperty().setChangedListener(this::profileNameChanged); entry2.getValue().nameProperty().setChangedListener(this::profileNameChanged);
@ -104,13 +107,6 @@ public class Settings {
}); });
loadProxy(); loadProxy();
try {
new URL(SETTINGS.getAuthlibInjectorServerURL());
} catch (MalformedURLException ex) {
Logging.LOG.log(Level.SEVERE, "Authlib injector server URL is malformed, using official update url.", ex);
SETTINGS.setAuthlibInjectorServerURL(AuthlibInjectorBuildInfo.UPDATE_URL);
}
} }
private Config initSettings() { private Config initSettings() {
@ -280,8 +276,39 @@ public class Settings {
SETTINGS.setLogLines(logLines); SETTINGS.setLogLines(logLines);
} }
public String getAuthlibInjectorServerURL() { public Set<String> getAuthlibInjectorServerURLs() {
return SETTINGS.getAuthlibInjectorServerURL(); return SETTINGS.getAuthlibInjectorServerURLs();
}
public void removeAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().remove(serverURL);
checkAuthlibInjectorAccounts();
save();
}
public void addAuthlibInjectorServerURL(String serverURL) {
checkAuthlibInjectorServerURLs();
SETTINGS.getAuthlibInjectorServerURLs().add(serverURL);
save();
}
private void checkAuthlibInjectorServerURLs() {
if (SETTINGS.getAuthlibInjectorServerURLs() == null)
SETTINGS.setAuthlibInjectorServerURLs(new HashSet<>());
}
private void checkAuthlibInjectorAccounts() {
for (Account account : getAccounts()) {
if (account instanceof AuthlibInjectorAccount) {
AuthlibInjectorAccount injectorAccount = (AuthlibInjectorAccount) account;
if (!SETTINGS.getAuthlibInjectorServerURLs().contains(injectorAccount.getServerBaseURL()))
deleteAccount(account);
}
}
} }
public DownloadProvider getDownloadProvider() { public DownloadProvider getDownloadProvider() {
@ -490,6 +517,8 @@ public class Settings {
getProfileMap().put(ver.getName(), ver); getProfileMap().put(ver.getName(), ver);
ver.nameProperty().setChangedListener(this::profileNameChanged); ver.nameProperty().setChangedListener(this::profileNameChanged);
save();
} }
public void deleteProfile(Profile profile) { public void deleteProfile(Profile profile) {

View File

@ -36,11 +36,13 @@ import javafx.scene.layout.VBox;
import javafx.scene.paint.Color; import javafx.scene.paint.Color;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.OfflineAccount; import org.jackhuang.hmcl.auth.OfflineAccount;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers; import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
public final class AccountItem extends StackPane { public final class AccountItem extends StackPane {
@ -56,6 +58,7 @@ public final class AccountItem extends StackPane {
@FXML private Label lblUser; @FXML private Label lblUser;
@FXML private Label lblType; @FXML private Label lblType;
@FXML private Label lblEmail; @FXML private Label lblEmail;
@FXML private Label lblServer;
@FXML private Label lblCurrentAccount; @FXML private Label lblCurrentAccount;
@FXML private JFXRadioButton chkSelected; @FXML private JFXRadioButton chkSelected;
@FXML private JFXProgressBar pgsSkin; @FXML private JFXProgressBar pgsSkin;
@ -73,12 +76,8 @@ public final class AccountItem extends StackPane {
btnDelete.setGraphic(SVG.delete("black", 15, 15)); btnDelete.setGraphic(SVG.delete("black", 15, 15));
btnRefresh.setGraphic(SVG.refresh("black", 15, 15)); btnRefresh.setGraphic(SVG.refresh("black", 15, 15));
// create content
String headerColor = getDefaultColor(i % 12);
header.setStyle("-fx-background-radius: 2 2 0 0; -fx-background-color: " + headerColor);
// create image view // create image view
icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 32.0, header.boundsInParentProperty(), icon.heightProperty())); icon.translateYProperty().bind(Bindings.createDoubleBinding(() -> header.getBoundsInParent().getHeight() - icon.getHeight() / 2 - 16, header.boundsInParentProperty(), icon.heightProperty()));
chkSelected.getProperties().put("account", account); chkSelected.getProperties().put("account", account);
setSelected(Settings.INSTANCE.getSelectedAccount() == account); setSelected(Settings.INSTANCE.getSelectedAccount() == account);
@ -87,6 +86,13 @@ public final class AccountItem extends StackPane {
lblType.setText(AccountsPage.accountType(account)); lblType.setText(AccountsPage.accountType(account));
lblEmail.setText(account.getUsername()); lblEmail.setText(account.getUsername());
if (account instanceof AuthlibInjectorAccount) {
Task.ofResult("serverName", () -> Accounts.getAuthlibInjectorServerName(((AuthlibInjectorAccount) account).getServerBaseURL()))
.subscribe(Schedulers.javafx(), variables -> {
lblServer.setText(variables.get("serverName"));
});
}
if (account instanceof YggdrasilAccount) { if (account instanceof YggdrasilAccount) {
btnRefresh.setOnMouseClicked(e -> { btnRefresh.setOnMouseClicked(e -> {
pgsSkin.setVisible(true); pgsSkin.setVisible(true);
@ -113,25 +119,6 @@ public final class AccountItem extends StackPane {
FXUtils.limitSize(portraitView, 32, 32); FXUtils.limitSize(portraitView, 32, 32);
} }
private String getDefaultColor(int i) {
switch (i) {
case 0: return "#8F3F7E";
case 1: return "#B5305F";
case 2: return "#CE584A";
case 3: return "#DB8D5C";
case 4: return "#DA854E";
case 5: return "#E9AB44";
case 6: return "#FEE435";
case 7: return "#99C286";
case 8: return "#01A05E";
case 9: return "#4A8895";
case 10: return "#16669B";
case 11: return "#2F65A5";
case 12: return "#4E6A9C";
default: return "#FFFFFF";
}
}
public Account getAccount() { public Account getAccount() {
return account; return account;
} }

View File

@ -23,7 +23,6 @@ import com.jfoenix.controls.JFXProgressBar;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.scene.control.Label; import javafx.scene.control.Label;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
@ -34,8 +33,8 @@ import org.jackhuang.hmcl.task.Task;
import java.util.function.Consumer; import java.util.function.Consumer;
public class YggdrasilAccountLoginPane extends StackPane { public class AccountLoginPane extends StackPane {
private final YggdrasilAccount oldAccount; private final Account oldAccount;
private final Consumer<AuthInfo> success; private final Consumer<AuthInfo> success;
private final Runnable failed; private final Runnable failed;
@ -46,12 +45,12 @@ public class YggdrasilAccountLoginPane extends StackPane {
@FXML private JFXProgressBar progressBar; @FXML private JFXProgressBar progressBar;
private JFXDialog dialog; private JFXDialog dialog;
public YggdrasilAccountLoginPane(YggdrasilAccount oldAccount, Consumer<AuthInfo> success, Runnable failed) { public AccountLoginPane(Account oldAccount, Consumer<AuthInfo> success, Runnable failed) {
this.oldAccount = oldAccount; this.oldAccount = oldAccount;
this.success = success; this.success = success;
this.failed = failed; this.failed = failed;
FXUtils.loadFXML(this, "/assets/fxml/yggdrasil-account-login.fxml"); FXUtils.loadFXML(this, "/assets/fxml/account-login.fxml");
lblUsername.setText(oldAccount.getUsername()); lblUsername.setText(oldAccount.getUsername());
txtPassword.setOnAction(e -> onAccept()); txtPassword.setOnAction(e -> onAccept());
@ -59,14 +58,12 @@ public class YggdrasilAccountLoginPane extends StackPane {
@FXML @FXML
private void onAccept() { private void onAccept() {
String username = oldAccount.getUsername();
String password = txtPassword.getText(); String password = txtPassword.getText();
progressBar.setVisible(true); progressBar.setVisible(true);
lblCreationWarning.setText(""); lblCreationWarning.setText("");
Task.ofResult("login", () -> { Task.ofResult("login", () -> {
try { try {
Account account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password); return oldAccount.logInWithPassword(new SpecificCharacterSelector(Accounts.getCurrentCharacter(oldAccount)), password, Settings.INSTANCE.getProxy());
return account.logIn(new SpecificCharacterSelector(Accounts.getCurrentCharacter(oldAccount)), Settings.INSTANCE.getProxy());
} catch (Exception e) { } catch (Exception e) {
return e; return e;
} }

View File

@ -25,20 +25,17 @@ import javafx.beans.property.StringProperty;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.geometry.Pos; import javafx.geometry.Pos;
import javafx.scene.Node; import javafx.scene.Node;
import javafx.scene.control.Label; import javafx.scene.control.*;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.ToggleGroup;
import javafx.scene.image.Image; import javafx.scene.image.Image;
import javafx.scene.image.ImageView; import javafx.scene.image.ImageView;
import javafx.scene.layout.BorderPane; import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox; import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane; import javafx.scene.layout.StackPane;
import javafx.util.Callback;
import org.jackhuang.hmcl.Main; import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.*; import org.jackhuang.hmcl.auth.*;
import org.jackhuang.hmcl.auth.yggdrasil.GameProfile; import org.jackhuang.hmcl.auth.yggdrasil.*;
import org.jackhuang.hmcl.auth.InvalidCredentialsException; import org.jackhuang.hmcl.auth.InvalidCredentialsException;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccountFactory;
import org.jackhuang.hmcl.game.AccountHelper; import org.jackhuang.hmcl.game.AccountHelper;
import org.jackhuang.hmcl.setting.Accounts; import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings; import org.jackhuang.hmcl.setting.Settings;
@ -48,10 +45,14 @@ import org.jackhuang.hmcl.ui.construct.AdvancedListBox;
import org.jackhuang.hmcl.ui.construct.IconedItem; import org.jackhuang.hmcl.ui.construct.IconedItem;
import org.jackhuang.hmcl.ui.construct.Validator; import org.jackhuang.hmcl.ui.construct.Validator;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage; import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import org.jackhuang.hmcl.util.Pair;
import org.jackhuang.hmcl.util.ReflectionHelper;
import java.util.Collection;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import java.util.concurrent.CountDownLatch; import java.util.concurrent.CountDownLatch;
import java.util.stream.Collectors;
public final class AccountsPage extends StackPane implements DecoratorPage { public final class AccountsPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("account")); private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("account"));
@ -64,7 +65,10 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
@FXML private JFXPasswordField txtPassword; @FXML private JFXPasswordField txtPassword;
@FXML private Label lblCreationWarning; @FXML private Label lblCreationWarning;
@FXML private JFXComboBox<String> cboType; @FXML private JFXComboBox<String> cboType;
@FXML private JFXComboBox<TwoLineListItem> cboServers;
@FXML private JFXProgressBar progressBar; @FXML private JFXProgressBar progressBar;
@FXML private Label lblAddInjectorServer;
@FXML private Hyperlink linkAddInjectorServer;
{ {
FXUtils.loadFXML(this, "/assets/fxml/account.fxml"); FXUtils.loadFXML(this, "/assets/fxml/account.fxml");
@ -74,12 +78,19 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
FXUtils.smoothScrolling(scrollPane); FXUtils.smoothScrolling(scrollPane);
cboType.getItems().setAll(Main.i18n("account.methods.offline"), Main.i18n("account.methods.yggdrasil")); cboType.getItems().setAll(Main.i18n("account.methods.offline"), Main.i18n("account.methods.yggdrasil"), Main.i18n("account.methods.authlib_injector"));
cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> { cboType.getSelectionModel().selectedIndexProperty().addListener((a, b, newValue) -> {
txtPassword.setVisible(newValue.intValue() != 0); txtPassword.setVisible(newValue.intValue() != 0);
cboServers.setVisible(newValue.intValue() == 2);
linkAddInjectorServer.setVisible(newValue.intValue() == 2);
lblAddInjectorServer.setVisible(newValue.intValue() == 2);
}); });
cboType.getSelectionModel().select(0); cboType.getSelectionModel().select(0);
// These two lines can eliminate black, don't know why.
cboServers.getItems().setAll(new TwoLineListItem("", ""));
cboServers.getSelectionModel().select(0);
txtPassword.setOnAction(e -> onCreationAccept()); txtPassword.setOnAction(e -> onCreationAccept());
txtUsername.setOnAction(e -> onCreationAccept()); txtUsername.setOnAction(e -> onCreationAccept());
txtUsername.getValidators().add(new Validator(Main.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@"))); txtUsername.getValidators().add(new Validator(Main.i18n("input.email"), str -> !txtPassword.isVisible() || str.contains("@")));
@ -91,6 +102,7 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
}); });
loadAccounts(); loadAccounts();
loadServers();
if (Settings.INSTANCE.getAccounts().isEmpty()) if (Settings.INSTANCE.getAccounts().isEmpty())
addNewAccount(); addNewAccount();
@ -114,6 +126,17 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
}); });
} }
public void loadServers() {
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.map(serverURL -> new TwoLineListItem(Accounts.getAuthlibInjectorServerName(serverURL), serverURL))
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
cboServers.getItems().setAll(variables.<Collection<TwoLineListItem>>get("list"));
if (!cboServers.getItems().isEmpty())
cboServers.getSelectionModel().select(0);
}));
}
private Node buildNode(int i, Account account, ToggleGroup group) { private Node buildNode(int i, Account account, ToggleGroup group) {
AccountItem item = new AccountItem(i, account, group); AccountItem item = new AccountItem(i, account, group);
item.setOnDeleteButtonMouseClicked(e -> { item.setOnDeleteButtonMouseClicked(e -> {
@ -130,6 +153,11 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
dialog.show(); dialog.show();
} }
@FXML
private void onAddInjecterServer() {
Controllers.navigate(Controllers.getServersPage());
}
@FXML @FXML
private void onCreationAccept() { private void onCreationAccept() {
int type = cboType.getSelectionModel().getSelectedIndex(); int type = cboType.getSelectionModel().getSelectedIndex();
@ -141,8 +169,9 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
try { try {
Account account; Account account;
switch (type) { switch (type) {
case 0: account = OfflineAccountFactory.INSTANCE.fromUsername(username); break; case 0: account = Accounts.ACCOUNT_FACTORY.get(Accounts.OFFLINE_ACCOUNT_KEY).fromUsername(username); break;
case 1: account = YggdrasilAccountFactory.INSTANCE.fromUsername(username, password); break; case 1: account = Accounts.ACCOUNT_FACTORY.get(Accounts.YGGDRASIL_ACCOUNT_KEY).fromUsername(username, password); break;
case 2: account = Accounts.ACCOUNT_FACTORY.get(Accounts.AUTHLIB_INJECTOR_ACCOUNT_KEY).fromUsername(username, password, cboServers.getSelectionModel().getSelectedItem().getSubtitle()); break;
default: throw new Error(); default: throw new Error();
} }
@ -203,6 +232,7 @@ public final class AccountsPage extends StackPane implements DecoratorPage {
public static String accountType(Account account) { public static String accountType(Account account) {
if (account instanceof OfflineAccount) return Main.i18n("account.methods.offline"); if (account instanceof OfflineAccount) return Main.i18n("account.methods.offline");
else if (account instanceof AuthlibInjectorAccount) return Main.i18n("account.methods.authlib_injector");
else if (account instanceof YggdrasilAccount) return Main.i18n("account.methods.yggdrasil"); else if (account instanceof YggdrasilAccount) return Main.i18n("account.methods.yggdrasil");
else throw new Error(Main.i18n("account.methods.no_method") + ": " + account); else throw new Error(Main.i18n("account.methods.no_method") + ": " + account);
} }

View File

@ -0,0 +1,65 @@
/*
* Hello Minecraft! Launcher.
* Copyright (C) 2017 huangyuhui <huanghongxun2008@126.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see {http://www.gnu.org/licenses/}.
*/
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.effects.JFXDepthManager;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorServerInfo;
import org.jackhuang.hmcl.mod.ModInfo;
import java.util.function.Consumer;
public final class AuthlibInjectorServerItem extends BorderPane {
private final AuthlibInjectorServerInfo info;
private final Label lblServerName = new Label();
private final Label lblServerIp = new Label();
public AuthlibInjectorServerItem(AuthlibInjectorServerInfo info, Consumer<AuthlibInjectorServerItem> deleteCallback) {
this.info = info;
lblServerName.setStyle("-fx-font-size: 15;");
lblServerIp.setStyle("-fx-font-size: 10;");
VBox center = new VBox();
BorderPane.setAlignment(center, Pos.CENTER);
center.getChildren().addAll(lblServerName, lblServerIp);
setCenter(center);
JFXButton right = new JFXButton();
right.setOnMouseClicked(e -> deleteCallback.accept(this));
right.getStyleClass().add("toggle-icon4");
BorderPane.setAlignment(right, Pos.CENTER);
right.setGraphic(SVG.close("black", 15, 15));
setRight(right);
setStyle("-fx-background-radius: 2; -fx-background-color: white; -fx-padding: 8;");
JFXDepthManager.setDepth(this, 1);
lblServerName.setText(info.getServerName());
lblServerIp.setText(info.getServerIp());
}
public AuthlibInjectorServerInfo getInfo() {
return info;
}
}

View File

@ -0,0 +1,150 @@
package org.jackhuang.hmcl.ui;
import com.jfoenix.controls.*;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.control.ScrollPane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.Main;
import org.jackhuang.hmcl.auth.yggdrasil.AuthlibInjectorServerInfo;
import org.jackhuang.hmcl.setting.Accounts;
import org.jackhuang.hmcl.setting.Settings;
import org.jackhuang.hmcl.task.Schedulers;
import org.jackhuang.hmcl.task.Task;
import org.jackhuang.hmcl.ui.animation.ContainerAnimations;
import org.jackhuang.hmcl.ui.animation.TransitionHandler;
import org.jackhuang.hmcl.ui.wizard.DecoratorPage;
import java.util.Collection;
import java.util.Objects;
import java.util.stream.Collectors;
public class AuthlibInjectorServersPage extends StackPane implements DecoratorPage {
private final StringProperty title = new SimpleStringProperty(this, "title", Main.i18n("account.injector.server"));
@FXML private ScrollPane scrollPane;
@FXML private StackPane addServerContainer;
@FXML private Label lblServerIp;
@FXML private Label lblServerName;
@FXML private Label lblCreationWarning;
@FXML private VBox listPane;
@FXML private JFXTextField txtServerIp;
@FXML private JFXDialogLayout addServerPane;
@FXML private JFXDialogLayout confirmServerPane;
@FXML private JFXDialog dialog;
@FXML private StackPane contentPane;
@FXML private JFXSpinner spinner;
@FXML private JFXProgressBar progressBar;
@FXML private JFXButton btnAddNext;
private TransitionHandler transitionHandler;
{
FXUtils.loadFXML(this, "/assets/fxml/authlib-injector-servers.fxml");
FXUtils.smoothScrolling(scrollPane);
transitionHandler = new TransitionHandler(addServerContainer);
getChildren().remove(dialog);
dialog.setDialogContainer(this);
txtServerIp.textProperty().addListener((a, b, newValue) -> {
btnAddNext.setDisable(!txtServerIp.validate());
});
loading();
}
private void removeServer(AuthlibInjectorServerItem item) {
Settings.INSTANCE.removeAuthlibInjectorServerURL(item.getInfo().getServerIp());
loading();
}
private void loading() {
getChildren().remove(contentPane);
spinner.setVisible(true);
Task.ofResult("list", () -> Settings.INSTANCE.getAuthlibInjectorServerURLs().parallelStream()
.map(serverURL -> new AuthlibInjectorServerItem(new AuthlibInjectorServerInfo(serverURL, Accounts.getAuthlibInjectorServerName(serverURL)), this::removeServer))
.collect(Collectors.toList()))
.subscribe(Task.of(Schedulers.javafx(), variables -> {
listPane.getChildren().setAll(variables.<Collection<? extends Node>>get("list"));
loadingCompleted();
}));
}
private void loadingCompleted() {
getChildren().add(contentPane);
spinner.setVisible(false);
if (Settings.INSTANCE.getAuthlibInjectorServerURLs().isEmpty())
onAdd();
}
@FXML
private void onAdd() {
transitionHandler.setContent(addServerPane, ContainerAnimations.NONE.getAnimationProducer());
txtServerIp.setText("");
addServerPane.setDisable(false);
progressBar.setVisible(false);
dialog.show();
}
@FXML
private void onAddCancel() {
dialog.close();
}
@FXML
private void onAddNext() {
progressBar.setVisible(true);
addServerPane.setDisable(true);
Task.ofResult("serverName", () -> Objects.requireNonNull(Accounts.getAuthlibInjectorServerName(txtServerIp.getText())))
.finalized(Schedulers.javafx(), (variables, isDependentsSucceeded) -> {
progressBar.setVisible(false);
addServerPane.setDisable(false);
if (isDependentsSucceeded) {
lblServerName.setText(variables.get("serverName"));
lblServerIp.setText(txtServerIp.getText());
transitionHandler.setContent(confirmServerPane, ContainerAnimations.SWIPE_LEFT.getAnimationProducer());
} else
lblCreationWarning.setText(variables.<Exception>get("lastException").getLocalizedMessage());
}).start();
}
@FXML
private void onAddPrev() {
transitionHandler.setContent(addServerPane, ContainerAnimations.SWIPE_RIGHT.getAnimationProducer());
}
@FXML
private void onAddFinish() {
String ip = txtServerIp.getText();
if (!ip.endsWith("/"))
ip += "/";
Settings.INSTANCE.addAuthlibInjectorServerURL(ip);
loading();
dialog.close();
}
public String getTitle() {
return title.get();
}
@Override
public StringProperty titleProperty() {
return title;
}
public void setTitle(String title) {
this.title.set(title);
}
}

View File

@ -42,6 +42,7 @@ public final class Controllers {
private static MainPage mainPage = new MainPage(); private static MainPage mainPage = new MainPage();
private static SettingsPage settingsPage = null; private static SettingsPage settingsPage = null;
private static VersionPage versionPage = null; private static VersionPage versionPage = null;
private static AuthlibInjectorServersPage serversPage = null;
private static LeftPaneController leftPaneController; private static LeftPaneController leftPaneController;
private static Decorator decorator; private static Decorator decorator;
@ -67,6 +68,13 @@ public final class Controllers {
return versionPage; return versionPage;
} }
// FXThread
public static AuthlibInjectorServersPage getServersPage() {
if (serversPage == null)
serversPage = new AuthlibInjectorServersPage();
return serversPage;
}
public static Decorator getDecorator() { public static Decorator getDecorator() {
return decorator; return decorator;
} }

View File

@ -19,6 +19,7 @@ package org.jackhuang.hmcl.ui;
import com.jfoenix.concurrency.JFXUtilities; import com.jfoenix.concurrency.JFXUtilities;
import org.jackhuang.hmcl.auth.Account; import org.jackhuang.hmcl.auth.Account;
import org.jackhuang.hmcl.auth.AccountFactory;
import org.jackhuang.hmcl.auth.AuthInfo; import org.jackhuang.hmcl.auth.AuthInfo;
import org.jackhuang.hmcl.auth.MultiCharacterSelector; import org.jackhuang.hmcl.auth.MultiCharacterSelector;
import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount; import org.jackhuang.hmcl.auth.yggdrasil.YggdrasilAccount;
@ -35,7 +36,7 @@ public final class DialogController {
CountDownLatch latch = new CountDownLatch(1); CountDownLatch latch = new CountDownLatch(1);
AtomicReference<AuthInfo> res = new AtomicReference<>(null); AtomicReference<AuthInfo> res = new AtomicReference<>(null);
JFXUtilities.runInFX(() -> { JFXUtilities.runInFX(() -> {
YggdrasilAccountLoginPane pane = new YggdrasilAccountLoginPane((YggdrasilAccount) account, it -> { AccountLoginPane pane = new AccountLoginPane(account, it -> {
res.set(it); res.set(it);
latch.countDown(); latch.countDown();
Controllers.closeDialog(); Controllers.closeDialog();

View File

@ -29,7 +29,7 @@ import org.jackhuang.hmcl.mod.ModInfo;
import java.util.function.Consumer; import java.util.function.Consumer;
public final class ModItem extends BorderPane { public final class ModItem extends BorderPane {
private final Label lblModFileName = new Label(); private final Label lblModFileName = new Label();
private final Label lblModAuthor = new Label(); private final Label lblModAuthor = new Label();
private final JFXCheckBox chkEnabled = new JFXCheckBox(); private final JFXCheckBox chkEnabled = new JFXCheckBox();

View File

@ -0,0 +1,45 @@
package org.jackhuang.hmcl.ui;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.util.Pair;
public class TwoLineListItem extends StackPane {
private final Label lblTitle = new Label();
private final Label lblSubtitle = new Label();
private final String title;
private final String subtitle;
public TwoLineListItem(Pair<String, String> pair) {
this(pair.getKey(), pair.getValue());
}
public TwoLineListItem(String title, String subtitle) {
lblTitle.setStyle("-fx-font-size: 15;");
lblSubtitle.setStyle("-fx-font-size: 10; -fx-text-fill: gray;");
this.title = title;
this.subtitle = subtitle;
lblTitle.setText(title);
lblSubtitle.setText(subtitle);
VBox vbox = new VBox();
vbox.getChildren().setAll(lblTitle, lblSubtitle);
getChildren().setAll(vbox);
}
public String getTitle() {
return title;
}
public String getSubtitle() {
return subtitle;
}
@Override
public String toString() {
return getTitle();
}
}

View File

@ -0,0 +1,42 @@
package org.jackhuang.hmcl.ui.construct;
import com.jfoenix.validation.base.ValidatorBase;
import javafx.beans.NamedArg;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TextInputControl;
import java.net.MalformedURLException;
import java.net.URL;
public class URLValidator extends ValidatorBase {
private final ObservableList<String> protocols = FXCollections.observableArrayList();
public URLValidator() {
super();
}
public URLValidator(@NamedArg("message") String message) {
super(message);
}
public ObservableList<String> getProtocols() {
return protocols;
}
@Override
protected void eval() {
if (srcControl.get() instanceof TextInputControl) {
try {
URL url = new URL(((TextInputControl) srcControl.get()).getText());
if (protocols.isEmpty())
hasErrors.set(false);
else
hasErrors.set(!protocols.contains(url.getProtocol()));
} catch (MalformedURLException e) {
hasErrors.set(true);
}
}
}
}

View File

@ -13,21 +13,16 @@
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
type="StackPane" FXUtils.limitWidth="160" FXUtils.limitHeight="156"> type="StackPane" FXUtils.limitWidth="160" FXUtils.limitHeight="156">
<VBox fx:id="content"> <VBox fx:id="content">
<StackPane fx:id="header" VBox.vgrow="ALWAYS"> <StackPane fx:id="header" VBox.vgrow="ALWAYS" style="-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;">
<BorderPane> <HBox StackPane.alignment="TOP_CENTER" alignment="CENTER_RIGHT">
<top> <JFXProgressBar fx:id="pgsSkin" visible="false" />
<HBox alignment="CENTER_RIGHT"> </HBox>
<JFXProgressBar fx:id="pgsSkin" visible="false" /> <VBox style="-fx-padding: 8 8 0 8;">
</HBox> <Label fx:id="lblUser" style="-fx-font-size: 15;" wrapText="true" textAlignment="JUSTIFY" />
</top> <Label fx:id="lblEmail" style="-fx-font-size: 11; -fx-text-fill: gray;" />
<center> <Label fx:id="lblServer" style="-fx-font-size: 11; -fx-text-fill: gray;" />
<VBox style="-fx-padding: 20 20 0 20;"> <Label fx:id="lblType" style="-fx-font-size: 11; -fx-text-fill: gray;" />
<Label fx:id="lblUser" style="-fx-font-size: 20;" wrapText="true" textAlignment="JUSTIFY" /> </VBox>
<Label fx:id="lblEmail" style="-fx-font-size: 12;" />
<Label fx:id="lblType" style="-fx-font-size: 12;" />
</VBox>
</center>
</BorderPane>
</StackPane> </StackPane>
<StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40"> <StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40">
<BorderPane> <BorderPane>

View File

@ -7,6 +7,8 @@
<?import javafx.scene.control.ScrollPane?> <?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.*?> <?import javafx.scene.layout.*?>
<?import org.jackhuang.hmcl.ui.FXUtils?> <?import org.jackhuang.hmcl.ui.FXUtils?>
<?import javafx.scene.control.Hyperlink?>
<?import java.lang.String?>
<fx:root xmlns="http://javafx.com/javafx" <fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml" xmlns:fx="http://javafx.com/fxml"
type="StackPane"> type="StackPane">
@ -31,20 +33,28 @@
<body> <body>
<GridPane vgap="15" hgap="15" style="-fx-padding: 15 0 0 0;"> <GridPane vgap="15" hgap="15" style="-fx-padding: 15 0 0 0;">
<columnConstraints> <columnConstraints>
<ColumnConstraints maxWidth="100" /> <ColumnConstraints maxWidth="70" minWidth="70"/>
<ColumnConstraints /> <ColumnConstraints />
<ColumnConstraints minWidth="140" />
</columnConstraints> </columnConstraints>
<Label text="%account.methods" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="0" /> <Label text="%account.methods" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<JFXComboBox fx:id="cboType" GridPane.columnIndex="1" GridPane.rowIndex="0" /> <JFXComboBox fx:id="cboType" GridPane.columnIndex="1" GridPane.rowIndex="0" GridPane.columnSpan="2" />
<JFXTextField fx:id="txtUsername" promptText="%account.username" labelFloat="true" GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2" FXUtils.validateWhileTextChanged="true"> <Label fx:id="lblAddInjectorServer" text="%account.injector.server" GridPane.halignment="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<JFXComboBox fx:id="cboServers" maxHeight="25" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Hyperlink fx:id="linkAddInjectorServer" text="%account.injector.add" onMouseClicked="#onAddInjecterServer" GridPane.columnIndex="2" GridPane.rowIndex="1" />
<JFXTextField fx:id="txtUsername" promptText="%account.username" labelFloat="true" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="3" FXUtils.validateWhileTextChanged="true">
<validators> <validators>
<RequiredFieldValidator message="%input.not_empty"> <RequiredFieldValidator message="%input.not_empty">
</RequiredFieldValidator> </RequiredFieldValidator>
</validators> </validators>
</JFXTextField> </JFXTextField>
<JFXPasswordField fx:id="txtPassword" promptText="%account.password" labelFloat="true" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" FXUtils.validateWhileTextChanged="true"> <JFXPasswordField fx:id="txtPassword" promptText="%account.password" labelFloat="true" GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="3" FXUtils.validateWhileTextChanged="true">
<validators> <validators>
<RequiredFieldValidator message="%input.not_empty"> <RequiredFieldValidator message="%input.not_empty">
</RequiredFieldValidator> </RequiredFieldValidator>

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import com.jfoenix.controls.JFXButton?>
<?import com.jfoenix.controls.JFXSpinner?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import com.jfoenix.controls.JFXDialog?>
<?import com.jfoenix.controls.JFXDialogLayout?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import com.jfoenix.controls.JFXTextField?>
<?import org.jackhuang.hmcl.ui.construct.URLValidator?>
<?import java.lang.String?>
<?import com.jfoenix.controls.JFXProgressBar?>
<fx:root xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
type="StackPane">
<JFXSpinner fx:id="spinner" style="-fx-radius:16" styleClass="materialDesign-purple, first-spinner" />
<StackPane fx:id="contentPane">
<ScrollPane fx:id="scrollPane" fitToWidth="true" fitToHeight="true">
<VBox fx:id="listPane" spacing="10" style="-fx-padding: 20 20 70 20;">
</VBox>
</ScrollPane>
<VBox style="-fx-padding: 15;" spacing="15" pickOnBounds="false" alignment="BOTTOM_RIGHT">
<JFXButton prefWidth="40" prefHeight="40" buttonType="RAISED" onMouseClicked="#onAdd" styleClass="jfx-button-raised-round">
<graphic>
<fx:include source="/assets/svg/plus.fxml"/>
</graphic>
</JFXButton>
</VBox>
</StackPane>
<JFXDialog fx:id="dialog">
<StackPane>
<StackPane fx:id="addServerContainer">
<JFXDialogLayout fx:id="addServerPane">
<heading>
<Label text="%account.injector.add" />
</heading>
<body>
<JFXTextField fx:id="txtServerIp" promptText="%account.injector.server_ip" labelFloat="true">
<validators>
<URLValidator message="%input.url">
<protocols>
<String fx:value="http" />
<String fx:value="https" />
</protocols>
</URLValidator>
</validators>
</JFXTextField>
</body>
<actions>
<Label fx:id="lblCreationWarning" />
<JFXButton onMouseClicked="#onAddCancel" text="%button.ok" styleClass="dialog-cancel" />
<JFXButton fx:id="btnAddNext" onMouseClicked="#onAddNext" text="%wizard.next" styleClass="dialog-accept" />
</actions>
</JFXDialogLayout>
<JFXDialogLayout fx:id="confirmServerPane">
<heading>
<Label text="%account.injector.add" />
</heading>
<body>
<GridPane vgap="15" hgap="15" style="-fx-padding: 15 0 0 0;">
<columnConstraints>
<ColumnConstraints maxWidth="100" />
<ColumnConstraints />
</columnConstraints>
<Label text="%account.injector.server_ip" GridPane.columnIndex="0" GridPane.rowIndex="0" />
<Label text="%account.injector.server_name" GridPane.columnIndex="0" GridPane.rowIndex="1" />
<Label fx:id="lblServerIp" GridPane.columnIndex="1" GridPane.rowIndex="0" />
<Label fx:id="lblServerName" GridPane.columnIndex="1" GridPane.rowIndex="1" />
<Label text="%account.injector.http" style="-fx-text-fill: red;" GridPane.rowIndex="2" GridPane.columnSpan="2" GridPane.columnIndex="0" />
</GridPane>
</body>
<actions>
<JFXButton onMouseClicked="#onAddPrev" text="%wizard.prev" styleClass="dialog-cancel" />
<JFXButton onMouseClicked="#onAddCancel" text="%button.cancel" styleClass="dialog-cancel" />
<JFXButton onMouseClicked="#onAddFinish" text="%wizard.finish" styleClass="dialog-accept" />
</actions>
</JFXDialogLayout>
</StackPane>
<JFXProgressBar fx:id="progressBar" visible="false" StackPane.alignment="TOP_CENTER" />
</StackPane>
</JFXDialog>
</fx:root>

View File

@ -61,7 +61,7 @@
<bottom> <bottom>
<HBox alignment="CENTER_RIGHT"> <HBox alignment="CENTER_RIGHT">
<JFXButton fx:id="btnNext" onMouseClicked="#onNext" prefWidth="100" prefHeight="40" buttonType="RAISED" <JFXButton fx:id="btnNext" onMouseClicked="#onNext" prefWidth="100" prefHeight="40" buttonType="RAISED"
text="%wizard.next_>" styleClass="jfx-button-raised"/> text="%wizard.next" styleClass="jfx-button-raised"/>
</HBox> </HBox>
</bottom> </bottom>
</BorderPane> </BorderPane>

View File

@ -12,19 +12,11 @@
type="StackPane" pickOnBounds="false" FXUtils.limitWidth="160" FXUtils.limitHeight="156"> type="StackPane" pickOnBounds="false" FXUtils.limitWidth="160" FXUtils.limitHeight="156">
<VBox fx:id="content" pickOnBounds="false"> <VBox fx:id="content" pickOnBounds="false">
<StackPane fx:id="header" VBox.vgrow="ALWAYS" pickOnBounds="false" style="-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;"> <StackPane fx:id="header" VBox.vgrow="ALWAYS" pickOnBounds="false" style="-fx-background-radius: 2 2 0 0; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;">
<BorderPane> <VBox style="-fx-padding: 8 8 0 8;">
<top> <Label fx:id="lblVersionName" style="-fx-font-size: 15;" textAlignment="JUSTIFY" wrapText="true" />
<HBox alignment="CENTER_RIGHT"> <Label fx:id="lblGameVersion" style="-fx-font-size: 11;" textAlignment="JUSTIFY" wrapText="true" />
</HBox> <Label fx:id="lblLibraries" style="-fx-font-size: 11; -fx-text-fill: gray;" textAlignment="JUSTIFY" wrapText="true" />
</top> </VBox>
<center>
<VBox style="-fx-padding: 8 8 0 8;">
<Label fx:id="lblVersionName" style="-fx-font-size: 15;" textAlignment="JUSTIFY" wrapText="true" />
<Label fx:id="lblGameVersion" style="-fx-font-size: 10;" textAlignment="JUSTIFY" wrapText="true" />
<Label fx:id="lblLibraries" style="-fx-font-size: 10; -fx-text-fill: gray;" textAlignment="JUSTIFY" wrapText="true" />
</VBox>
</center>
</BorderPane>
</StackPane> </StackPane>
<StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40" pickOnBounds="false"> <StackPane fx:id="body" style="-fx-background-radius: 0 0 2 2; -fx-background-color: rgb(255,255,255,0.87); -fx-padding: 8;" minHeight="40" pickOnBounds="false">
<BorderPane> <BorderPane>

View File

@ -38,7 +38,13 @@ account.failed.invalid_credentials=Incorrect password. Or forbidden to log in te
account.failed.invalid_password=Invalid password account.failed.invalid_password=Invalid password
account.failed.invalid_token=Please log out and re-input your password to log in. account.failed.invalid_token=Please log out and re-input your password to log in.
account.failed.no_charactor=No character in this account. account.failed.no_charactor=No character in this account.
account.injector.add=Add new authentication server
account.injector.http=Warning: This server is using HTTP, which will cause your password be transmitted in clear text.
account.injector.server=Auth Server
account.injector.server_ip=Server URL
account.injector.server_name=Server Name
account.methods=Login Type account.methods=Login Type
account.methods.authlib_injector=authlib-injector
account.methods.no_method=No login method account.methods.no_method=No login method
account.methods.offline=Offline account.methods.offline=Offline
account.methods.yggdrasil=Mojang account.methods.yggdrasil=Mojang
@ -127,6 +133,7 @@ folder.screenshots=Screenshots
input.email=The username must be an e-mail. input.email=The username must be an e-mail.
input.number=Must be a number. input.number=Must be a number.
input.not_empty=Input Requrired! input.not_empty=Input Requrired!
input.url=Must be a valid URL.
install=Install New Game install=Install New Game
install.failed=Failed to install install.failed=Failed to install
@ -406,11 +413,11 @@ version.manage.rename.message=Please enter the new name
version.settings=Settings version.settings=Settings
version.update=Update version.update=Update
wizard.<_prev=< Prev wizard.prev=< Prev
wizard.close=Close wizard.close=Close
wizard.failed=Failed wizard.failed=Failed
wizard.finish=Finish wizard.finish=Finish
wizard.help=Help wizard.help=Help
wizard.next_>=Next > wizard.next=Next >
wizard.steps=Steps wizard.steps=Steps
wizard.summary=Summary wizard.summary=Summary

View File

@ -38,7 +38,13 @@ account.failed.invalid_credentials=您的用户名或密码错误,或者登录
account.failed.invalid_password=无效的密码 account.failed.invalid_password=无效的密码
account.failed.invalid_token=请尝试登出并重新输入密码登录 account.failed.invalid_token=请尝试登出并重新输入密码登录
account.failed.no_charactor=该帐号没有角色 account.failed.no_charactor=该帐号没有角色
account.injector.add=添加登录认证服务器
account.injector.http=警告此服务器使用不安全的http协议您的密码在登录时会被明文传输。
account.injector.server=认证服务器
account.injector.server_ip=服务器地址
account.injector.server_name=服务器名称
account.methods=登录方式 account.methods=登录方式
account.methods.authlib_injector=authlib-injector 登录
account.methods.no_method=没有登入方式... account.methods.no_method=没有登入方式...
account.methods.offline=离线模式 account.methods.offline=离线模式
account.methods.yggdrasil=正版登录 account.methods.yggdrasil=正版登录
@ -127,6 +133,7 @@ folder.screenshots=截图文件夹
input.email=用户名必须是邮箱 input.email=用户名必须是邮箱
input.number=必须是数字 input.number=必须是数字
input.not_empty=必填项 input.not_empty=必填项
input.url=必须是合法的链接
install=添加游戏 install=添加游戏
install.failed=安装失败 install.failed=安装失败
@ -406,11 +413,11 @@ version.manage.rename.message=请输入要改成的名字
version.settings=游戏设置 version.settings=游戏设置
version.update=更新 version.update=更新
wizard.<_prev=< 上一步 wizard.prev=< 上一步
wizard.close=关闭 wizard.close=关闭
wizard.failed=失败 wizard.failed=失败
wizard.finish=完成 wizard.finish=完成
wizard.help=帮助 wizard.help=帮助
wizard.next_>=下一步 > wizard.next=下一步 >
wizard.steps=步骤 wizard.steps=步骤
wizard.summary=概要 wizard.summary=概要

View File

@ -57,6 +57,12 @@ public abstract class Account {
*/ */
public abstract AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException; public abstract AuthInfo logIn(MultiCharacterSelector selector, Proxy proxy) throws AuthenticationException;
public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password) throws AuthenticationException {
return logInWithPassword(selector, password, Proxy.NO_PROXY);
}
public abstract AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException;
public abstract boolean canPlayOffline(); public abstract boolean canPlayOffline();
/** /**

View File

@ -64,6 +64,11 @@ public class OfflineAccount extends Account {
return new AuthInfo(username, uuid, uuid); return new AuthInfo(username, uuid, uuid);
} }
@Override
public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException {
return logIn(selector, proxy);
}
@Override @Override
public void logOut() { public void logOut() {
// Offline account need not log out. // Offline account need not log out.

View File

@ -32,7 +32,7 @@ public class OfflineAccountFactory extends AccountFactory<OfflineAccount> {
} }
@Override @Override
public OfflineAccount fromUsername(String username, String password) { public OfflineAccount fromUsername(String username, String password, Object additionalData) {
return new OfflineAccount(username, getUUIDFromUserName(username)); return new OfflineAccount(username, getUUIDFromUserName(username));
} }

View File

@ -29,13 +29,7 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
// Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time. // Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time.
GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL)); GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL));
AtomicBoolean flag = new AtomicBoolean(true); AtomicBoolean flag = new AtomicBoolean(true);
Thread thread = Lang.thread(() -> { Thread thread = Lang.thread(() -> flag.set(getTask.test()));
try {
getTask.run();
} catch (Exception ignore) {
flag.set(false);
}
});
AuthInfo info = super.logIn(selector, proxy); AuthInfo info = super.logIn(selector, proxy);
try { try {
@ -53,6 +47,29 @@ public class AuthlibInjectorAccount extends YggdrasilAccount {
} }
} }
@Override
public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException {
// Authlib Injector recommends launchers to pre-fetch the server basic information before launched the game to save time.
GetTask getTask = new GetTask(NetworkUtils.toURL(serverBaseURL));
AtomicBoolean flag = new AtomicBoolean(true);
Thread thread = Lang.thread(() -> flag.set(getTask.test()));
AuthInfo info = super.logInWithPassword(selector, password, proxy);
try {
thread.join();
String arg = "-javaagent:" + injectorJarPath.get() + "=" + serverBaseURL;
Arguments arguments = Arguments.addJVMArguments(null, arg);
//if (flag.get())
// arguments = Arguments.addJVMArguments(arguments, "-Dorg.to2mbn.authlibinjector.config.prefetched=" + getTask.getResult());
return info.setArguments(arguments);
} catch (Exception e) {
throw new AuthenticationException("Unable to get authlib injector jar path", e);
}
}
@Override @Override
public Map<Object, Object> toStorageImpl() { public Map<Object, Object> toStorageImpl() {
Map<Object, Object> map = super.toStorageImpl(); Map<Object, Object> map = super.toStorageImpl();

View File

@ -0,0 +1,19 @@
package org.jackhuang.hmcl.auth.yggdrasil;
public class AuthlibInjectorServerInfo {
private final String serverIp;
private final String serverName;
public AuthlibInjectorServerInfo(String serverIp, String serverName) {
this.serverIp = serverIp;
this.serverName = serverName;
}
public String getServerIp() {
return serverIp;
}
public String getServerName() {
return serverName;
}
}

View File

@ -0,0 +1,34 @@
package org.jackhuang.hmcl.auth.yggdrasil;
public class AuthlibInjectorServerResponse {
private final Meta meta;
public AuthlibInjectorServerResponse() {
this(new Meta());
}
public AuthlibInjectorServerResponse(Meta meta) {
this.meta = meta;
}
public Meta getMeta() {
return meta;
}
public static class Meta {
private final String serverName;
public Meta() {
this("");
}
public Meta(String serverName) {
this.serverName = serverName;
}
public String getServerName() {
return serverName;
}
}
}

View File

@ -132,24 +132,47 @@ public class YggdrasilAccount extends Account {
} }
} }
@Override
public AuthInfo logInWithPassword(MultiCharacterSelector selector, String password, Proxy proxy) throws AuthenticationException {
logInWithPassword0(password, proxy);
if (!isLoggedIn())
throw new AuthenticationException("Wrong password for account " + username);
if (selectedProfile == null) {
if (profiles == null || profiles.length <= 0)
throw new NoCharacterException(this);
selectedProfile = selector.select(this, Arrays.asList(profiles));
}
return new AuthInfo(selectedProfile, accessToken, userType, GSON.toJson(userProperties));
}
private void logIn0(Proxy proxy) throws AuthenticationException { private void logIn0(Proxy proxy) throws AuthenticationException {
if (StringUtils.isNotBlank(accessToken)) { if (StringUtils.isNotBlank(accessToken)) {
if (StringUtils.isBlank(userId)) logInWithToken(proxy);
if (StringUtils.isNotBlank(username))
userId = username;
else
throw new AuthenticationException("Invalid uuid and username");
if (checkTokenValidity(proxy)) {
isOnline = true;
return;
}
logIn1(routeRefresh, new RefreshRequest(accessToken, clientToken), proxy);
} else if (StringUtils.isNotBlank(password)) } else if (StringUtils.isNotBlank(password))
logIn1(routeAuthenticate, new AuthenticationRequest(username, password, clientToken), proxy); logInWithPassword0(password, proxy);
else else
throw new AuthenticationException("Password cannot be blank"); throw new AuthenticationException("Password cannot be blank");
} }
private void logInWithToken(Proxy proxy) throws AuthenticationException {
if (StringUtils.isBlank(userId))
if (StringUtils.isNotBlank(username))
userId = username;
else
throw new AuthenticationException("Invalid uuid and username");
if (checkTokenValidity(proxy)) {
isOnline = true;
return;
}
logIn1(routeRefresh, new RefreshRequest(accessToken, clientToken), proxy);
}
public void logInWithPassword0(String password, Proxy proxy) throws AuthenticationException {
logIn1(routeAuthenticate, new AuthenticationRequest(username, password, clientToken), proxy);
}
private void logIn1(URL url, Object input, Proxy proxy) throws AuthenticationException { private void logIn1(URL url, Object input, Proxy proxy) throws AuthenticationException {
AuthenticationResponse response = makeRequest(url, input, proxy) AuthenticationResponse response = makeRequest(url, input, proxy)
.orElseThrow(() -> new AuthenticationException("Server response empty")); .orElseThrow(() -> new AuthenticationException("Server response empty"));

View File

@ -138,10 +138,16 @@ public class DefaultLauncher extends Launcher {
configuration.put("${assets_root}", gameAssets.getAbsolutePath()); configuration.put("${assets_root}", gameAssets.getAbsolutePath());
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration)); res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getJvm).orElseGet(this::getDefaultJVMArguments), configuration));
if (authInfo.getArguments() != null && authInfo.getArguments().getJvm() != null && !authInfo.getArguments().getJvm().isEmpty())
res.addAll(Arguments.parseArguments(authInfo.getArguments().getJvm(), configuration));
res.add(version.getMainClass()); res.add(version.getMainClass());
Map<String, Boolean> features = getFeatures(); Map<String, Boolean> features = getFeatures();
res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getGame).orElseGet(this::getDefaultGameArguments), configuration, features)); res.addAll(Arguments.parseArguments(version.getArguments().map(Arguments::getGame).orElseGet(this::getDefaultGameArguments), configuration, features));
if (authInfo.getArguments() != null && authInfo.getArguments().getGame() != null && !authInfo.getArguments().getGame().isEmpty())
res.addAll(Arguments.parseArguments(authInfo.getArguments().getGame(), configuration, features));
res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(LinkedList::new), configuration)); res.addAll(Arguments.parseStringArguments(version.getMinecraftArguments().map(StringUtils::tokenize).orElseGet(LinkedList::new), configuration));
// Optional Minecraft arguments // Optional Minecraft arguments

View File

@ -192,6 +192,7 @@ public final class TaskExecutor {
// do nothing // do nothing
} catch (Exception e) { } catch (Exception e) {
lastException = e; lastException = e;
variables.set("lastException", e);
Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e); Logging.LOG.log(Level.FINE, "Task failed: " + task.getName(), e);
task.onDone().fireEvent(new TaskEvent(this, task, true)); task.onDone().fireEvent(new TaskEvent(this, task, true));
taskListeners.forEach(it -> it.onFailed(task, e)); taskListeners.forEach(it -> it.onFailed(task, e));

View File

@ -35,15 +35,15 @@ public final class AutoTypingMap<K> {
this.impl = impl; this.impl = impl;
} }
public <V> V get(K key) { public synchronized <V> V get(K key) {
return (V) impl.get(key); return (V) impl.get(key);
} }
public <V> Optional<V> getOptional(K key) { public synchronized <V> Optional<V> getOptional(K key) {
return Optional.ofNullable(get(key)); return Optional.ofNullable(get(key));
} }
public void set(K key, Object value) { public synchronized void set(K key, Object value) {
if (value != null) if (value != null)
impl.put(key, value); impl.put(key, value);
} }

View File

@ -93,7 +93,7 @@ public final class NetworkUtils {
public static HttpURLConnection createConnection(URL url, Proxy proxy) throws IOException { public static HttpURLConnection createConnection(URL url, Proxy proxy) throws IOException {
initHttps(); initHttps();
HttpURLConnection connection = (HttpURLConnection) url.openConnection(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(proxy);
connection.setDoInput(true); connection.setDoInput(true);
connection.setUseCaches(false); connection.setUseCaches(false);
connection.setConnectTimeout(15000); connection.setConnectTimeout(15000);