mirror of
https://github.com/HMCL-dev/HMCL.git
synced 2025-09-23 11:14:44 -04:00
Merge branch 'main' into main
This commit is contained in:
commit
80c92904ad
@ -236,7 +236,11 @@ public final class ModpackHelper {
|
||||
if (provider == null) {
|
||||
throw new UnsupportedModpackException();
|
||||
}
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
if (modpack.getManifest() instanceof MultiMCInstanceConfiguration)
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack)
|
||||
.thenComposeAsync(() -> createMultiMCPostUpdateTask(profile, (MultiMCInstanceConfiguration) modpack.getManifest(), name));
|
||||
else
|
||||
return provider.createUpdateTask(profile.getDependency(), name, zipFile, modpack);
|
||||
}
|
||||
|
||||
public static void toVersionSetting(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
@ -276,6 +280,24 @@ public final class ModpackHelper {
|
||||
}
|
||||
}
|
||||
|
||||
private static void applyCommandAndJvmSettings(MultiMCInstanceConfiguration c, VersionSetting vs) {
|
||||
if (c.isOverrideCommands()) {
|
||||
vs.setWrapper(Lang.nonNull(c.getWrapperCommand(), ""));
|
||||
vs.setPreLaunchCommand(Lang.nonNull(c.getPreLaunchCommand(), ""));
|
||||
}
|
||||
|
||||
if (c.isOverrideJavaArgs()) {
|
||||
vs.setJavaArgs(Lang.nonNull(c.getJvmArgs(), ""));
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostUpdateTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
ModpackHelper.applyCommandAndJvmSettings(manifest, vs);
|
||||
});
|
||||
}
|
||||
|
||||
private static Task<Void> createMultiMCPostInstallTask(Profile profile, MultiMCInstanceConfiguration manifest, String version) {
|
||||
return Task.runAsync(Schedulers.javafx(), () -> {
|
||||
VersionSetting vs = Objects.requireNonNull(profile.getRepository().specializeVersionSetting(version));
|
||||
|
@ -52,6 +52,7 @@ import javafx.stage.Stage;
|
||||
import javafx.util.Callback;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.task.CacheFileTask;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@ -1159,6 +1160,13 @@ public final class FXUtils {
|
||||
return button;
|
||||
}
|
||||
|
||||
public static JFXButton newToggleButton4(SVG icon) {
|
||||
JFXButton button = new JFXButton();
|
||||
button.getStyleClass().add("toggle-icon4");
|
||||
button.setGraphic(icon.createIcon(Theme.blackFill(), -1));
|
||||
return button;
|
||||
}
|
||||
|
||||
public static Label truncatedLabel(String text, int limit) {
|
||||
Label label = new Label();
|
||||
if (text.length() <= limit) {
|
||||
|
@ -19,12 +19,13 @@ package org.jackhuang.hmcl.ui.download;
|
||||
|
||||
import com.jfoenix.controls.*;
|
||||
import javafx.beans.InvalidationListener;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.*;
|
||||
@ -41,7 +42,6 @@ import org.jackhuang.hmcl.download.neoforge.NeoForgeRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.optifine.OptiFineRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltAPIRemoteVersion;
|
||||
import org.jackhuang.hmcl.download.quilt.QuiltRemoteVersion;
|
||||
import org.jackhuang.hmcl.setting.Theme;
|
||||
import org.jackhuang.hmcl.setting.VersionIconType;
|
||||
import org.jackhuang.hmcl.task.Schedulers;
|
||||
import org.jackhuang.hmcl.task.Task;
|
||||
@ -73,17 +73,23 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
private final String libraryId;
|
||||
private final String title;
|
||||
private final Navigation navigation;
|
||||
private final DownloadProvider downloadProvider;
|
||||
private final VersionList<?> versionList;
|
||||
private final Runnable callback;
|
||||
|
||||
private final ObservableList<RemoteVersion> versions = FXCollections.observableArrayList();
|
||||
private final ObjectProperty<Status> status = new SimpleObjectProperty<>(Status.LOADING);
|
||||
|
||||
public VersionsPage(Navigation navigation, String title, String gameVersion, DownloadProvider downloadProvider, String libraryId, Runnable callback) {
|
||||
public VersionsPage(Navigation navigation,
|
||||
String title, String gameVersion,
|
||||
DownloadProvider downloadProvider,
|
||||
String libraryId,
|
||||
Runnable callback) {
|
||||
this.title = title;
|
||||
this.gameVersion = gameVersion;
|
||||
this.libraryId = libraryId;
|
||||
this.navigation = navigation;
|
||||
this.downloadProvider = downloadProvider;
|
||||
this.versionList = downloadProvider.getVersionListById(libraryId);
|
||||
this.callback = callback;
|
||||
|
||||
@ -130,10 +136,6 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
navigation.onPrev(true);
|
||||
}
|
||||
|
||||
private void onSponsor() {
|
||||
FXUtils.openLink("https://bmclapidoc.bangbang93.com");
|
||||
}
|
||||
|
||||
private enum Status {
|
||||
LOADING,
|
||||
FAILED,
|
||||
@ -149,22 +151,61 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
|
||||
private static class RemoteVersionListCell extends ListCell<RemoteVersion> {
|
||||
private final IconedTwoLineListItem content = new IconedTwoLineListItem();
|
||||
private final RipplerContainer ripplerContainer = new RipplerContainer(content);
|
||||
private final VersionsPage control;
|
||||
|
||||
private final TwoLineListItem twoLineListItem = new TwoLineListItem();
|
||||
private final ImageView imageView = new ImageView();
|
||||
private final StackPane pane = new StackPane();
|
||||
|
||||
private final Holder<RemoteVersionListCell> lastCell;
|
||||
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, String libraryId) {
|
||||
RemoteVersionListCell(Holder<RemoteVersionListCell> lastCell, VersionsPage control) {
|
||||
this.lastCell = lastCell;
|
||||
if ("game".equals(libraryId)) {
|
||||
content.getExternalLinkButton().setGraphic(SVG.GLOBE_BOOK.createIcon(Theme.blackFill(), -1));
|
||||
FXUtils.installFastTooltip(content.getExternalLinkButton(), i18n("wiki.tooltip"));
|
||||
this.control = control;
|
||||
|
||||
HBox hbox = new HBox(16);
|
||||
HBox.setHgrow(twoLineListItem, Priority.ALWAYS);
|
||||
hbox.setAlignment(Pos.CENTER);
|
||||
|
||||
HBox actions = new HBox(8);
|
||||
actions.setAlignment(Pos.CENTER);
|
||||
{
|
||||
if ("game".equals(control.libraryId)) {
|
||||
JFXButton wikiButton = newToggleButton4(SVG.GLOBE_BOOK);
|
||||
wikiButton.setOnAction(event -> onOpenWiki());
|
||||
FXUtils.installFastTooltip(wikiButton, i18n("wiki.tooltip"));
|
||||
actions.getChildren().add(wikiButton);
|
||||
}
|
||||
|
||||
JFXButton actionButton = newToggleButton4(SVG.ARROW_FORWARD);
|
||||
actionButton.setOnAction(e -> onAction());
|
||||
actions.getChildren().add(actionButton);
|
||||
}
|
||||
|
||||
hbox.getChildren().setAll(imageView, twoLineListItem, actions);
|
||||
|
||||
pane.getStyleClass().add("md-list-cell");
|
||||
StackPane.setMargin(content, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(ripplerContainer);
|
||||
StackPane.setMargin(hbox, new Insets(10, 16, 10, 16));
|
||||
pane.getChildren().setAll(new RipplerContainer(hbox));
|
||||
|
||||
FXUtils.onClicked(this, this::onAction);
|
||||
}
|
||||
|
||||
private void onAction() {
|
||||
RemoteVersion item = getItem();
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
control.navigation.getSettings().put(control.libraryId, item);
|
||||
control.callback.run();
|
||||
}
|
||||
|
||||
private void onOpenWiki() {
|
||||
RemoteVersion item = getItem();
|
||||
if (!(item instanceof GameRemoteVersion))
|
||||
return;
|
||||
|
||||
FXUtils.openLink(I18n.getWikiLink((GameRemoteVersion) item));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -182,37 +223,36 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
}
|
||||
setGraphic(pane);
|
||||
|
||||
content.setTitle(I18n.getDisplaySelfVersion(remoteVersion));
|
||||
twoLineListItem.setTitle(I18n.getDisplaySelfVersion(remoteVersion));
|
||||
if (remoteVersion.getReleaseDate() != null) {
|
||||
content.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
twoLineListItem.setSubtitle(I18n.formatDateTime(remoteVersion.getReleaseDate()));
|
||||
} else {
|
||||
content.setSubtitle(null);
|
||||
twoLineListItem.setSubtitle(null);
|
||||
}
|
||||
|
||||
if (remoteVersion instanceof GameRemoteVersion) {
|
||||
RemoteVersion.Type versionType = remoteVersion.getVersionType();
|
||||
switch (versionType) {
|
||||
case RELEASE:
|
||||
content.getTags().setAll(i18n("version.game.release"));
|
||||
content.setImage(VersionIconType.GRASS.getIcon());
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.release"));
|
||||
imageView.setImage(VersionIconType.GRASS.getIcon());
|
||||
break;
|
||||
case PENDING:
|
||||
case SNAPSHOT:
|
||||
if (versionType == RemoteVersion.Type.SNAPSHOT
|
||||
&& GameVersionNumber.asGameVersion(remoteVersion.getGameVersion()).isAprilFools()) {
|
||||
content.getTags().setAll(i18n("version.game.april_fools"));
|
||||
content.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.april_fools"));
|
||||
imageView.setImage(VersionIconType.APRIL_FOOLS.getIcon());
|
||||
} else {
|
||||
content.getTags().setAll(i18n("version.game.snapshot"));
|
||||
content.setImage(VersionIconType.COMMAND.getIcon());
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.snapshot"));
|
||||
imageView.setImage(VersionIconType.COMMAND.getIcon());
|
||||
}
|
||||
break;
|
||||
default:
|
||||
content.getTags().setAll(i18n("version.game.old"));
|
||||
content.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
twoLineListItem.getTags().setAll(i18n("version.game.old"));
|
||||
imageView.setImage(VersionIconType.CRAFT_TABLE.getIcon());
|
||||
break;
|
||||
}
|
||||
content.setExternalLink(I18n.getWikiLink((GameRemoteVersion) remoteVersion));
|
||||
} else {
|
||||
VersionIconType iconType;
|
||||
if (remoteVersion instanceof LiteLoaderRemoteVersion)
|
||||
@ -230,14 +270,13 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
else if (remoteVersion instanceof QuiltRemoteVersion || remoteVersion instanceof QuiltAPIRemoteVersion)
|
||||
iconType = VersionIconType.QUILT;
|
||||
else
|
||||
iconType = null;
|
||||
iconType = VersionIconType.COMMAND;
|
||||
|
||||
content.setImage(iconType != null ? iconType.getIcon() : null);
|
||||
if (content.getSubtitle() == null)
|
||||
content.setSubtitle(remoteVersion.getGameVersion());
|
||||
imageView.setImage(iconType.getIcon());
|
||||
if (twoLineListItem.getSubtitle() == null)
|
||||
twoLineListItem.setSubtitle(remoteVersion.getGameVersion());
|
||||
else
|
||||
content.getTags().setAll(remoteVersion.getGameVersion());
|
||||
content.setExternalLink(null);
|
||||
twoLineListItem.getTags().setAll(remoteVersion.getGameVersion());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -355,14 +394,7 @@ public final class VersionsPage extends Control implements WizardPage, Refreshab
|
||||
control.versions.addListener((InvalidationListener) o -> updateList());
|
||||
|
||||
Holder<RemoteVersionListCell> lastCell = new Holder<>();
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control.libraryId));
|
||||
|
||||
FXUtils.onClicked(list, () -> {
|
||||
if (list.getSelectionModel().getSelectedIndex() < 0)
|
||||
return;
|
||||
control.navigation.getSettings().put(control.libraryId, list.getSelectionModel().getSelectedItem());
|
||||
control.callback.run();
|
||||
});
|
||||
list.setCellFactory(listView -> new RemoteVersionListCell(lastCell, control));
|
||||
|
||||
ComponentList.setVgrow(list, Priority.ALWAYS);
|
||||
|
||||
|
@ -37,6 +37,7 @@ import org.jackhuang.hmcl.util.Restarter;
|
||||
import org.jackhuang.hmcl.util.StringUtils;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.io.FileUtils;
|
||||
import org.jackhuang.hmcl.util.io.IOUtils;
|
||||
import org.jackhuang.hmcl.util.logging.Level;
|
||||
import org.tukaani.xz.XZInputStream;
|
||||
|
||||
@ -44,11 +45,8 @@ import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.channels.Channels;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
@ -170,6 +168,42 @@ public final class SettingsPage extends SettingsView {
|
||||
UpdateHandler.updateFrom(target);
|
||||
}
|
||||
|
||||
/// This method guarantees to close both `input` and the current zip entry.
|
||||
///
|
||||
/// If no exception occurs, this method returns `true`;
|
||||
/// If an exception occurs while reading from `input`, this method returns `false`;
|
||||
/// If an exception occurs while writing to `output`, this method will throw it as is.
|
||||
private static boolean exportLogFile(ZipOutputStream output,
|
||||
Path file, // For logging
|
||||
String entryName,
|
||||
InputStream input,
|
||||
byte[] buffer) throws IOException {
|
||||
//noinspection TryFinallyCanBeTryWithResources
|
||||
try {
|
||||
output.putNextEntry(new ZipEntry(entryName));
|
||||
int read;
|
||||
while (true) {
|
||||
try {
|
||||
read = input.read(buffer);
|
||||
if (read <= 0)
|
||||
return true;
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to decompress log file " + file, ex);
|
||||
return false;
|
||||
}
|
||||
|
||||
output.write(buffer, 0, read);
|
||||
}
|
||||
} finally {
|
||||
try {
|
||||
input.close();
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to close log file " + file, ex);
|
||||
}
|
||||
output.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onExportLogs() {
|
||||
thread(() -> {
|
||||
@ -190,45 +224,54 @@ public final class SettingsPage extends SettingsView {
|
||||
|
||||
LOG.info("Exporting latest logs to " + outputFile);
|
||||
|
||||
Path tempFile = Files.createTempFile("hmcl-decompress-log-", ".txt");
|
||||
try (var tempChannel = FileChannel.open(tempFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
||||
var os = Files.newOutputStream(outputFile);
|
||||
byte[] buffer = new byte[IOUtils.DEFAULT_BUFFER_SIZE];
|
||||
try (var os = Files.newOutputStream(outputFile);
|
||||
var zos = new ZipOutputStream(os)) {
|
||||
|
||||
for (Path path : recentLogFiles) {
|
||||
String extension = FileUtils.getExtension(path);
|
||||
decompress:
|
||||
if ("gz".equalsIgnoreCase(extension) || "xz".equalsIgnoreCase(extension)) {
|
||||
try (InputStream fis = Files.newInputStream(path);
|
||||
InputStream uncompressed = "gz".equalsIgnoreCase(extension)
|
||||
? new GZIPInputStream(fis)
|
||||
: new XZInputStream(fis)) {
|
||||
uncompressed.transferTo(Channels.newOutputStream(tempChannel));
|
||||
} catch (IOException e) {
|
||||
LOG.warning("Failed to decompress log: " + path, e);
|
||||
break decompress;
|
||||
String fileName = FileUtils.getName(path);
|
||||
String extension = StringUtils.substringAfterLast(fileName, '.');
|
||||
|
||||
if ("gz".equals(extension) || "xz".equals(extension)) {
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed,
|
||||
// then copy the compressed file content as-is into a new entry in the zip file.
|
||||
|
||||
InputStream input = null;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
input = "gz".equals(extension)
|
||||
? new GZIPInputStream(input)
|
||||
: new XZInputStream(input);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
IOUtils.closeQuietly(input, ex);
|
||||
input = null;
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(StringUtils.substringBeforeLast(FileUtils.getName(path), '.')));
|
||||
Channels.newInputStream(tempChannel).transferTo(zos);
|
||||
zos.closeEntry();
|
||||
tempChannel.truncate(0);
|
||||
String entryName = StringUtils.substringBeforeLast(fileName, ".");
|
||||
if (input != null && exportLogFile(zos, path, entryName, input, buffer))
|
||||
continue;
|
||||
}
|
||||
|
||||
// Copy the log file content as-is into a new entry in the zip file.
|
||||
// If an exception occurs while decompressing the input file, we should
|
||||
// ensure the input file and the current zip entry are closed.
|
||||
|
||||
InputStream input;
|
||||
try {
|
||||
input = Files.newInputStream(path);
|
||||
} catch (Throwable ex) {
|
||||
LOG.warning("Failed to open log file " + path, ex);
|
||||
continue;
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry(FileUtils.getName(path)));
|
||||
Files.copy(path, zos);
|
||||
zos.closeEntry();
|
||||
exportLogFile(zos, path, fileName, input, buffer);
|
||||
}
|
||||
|
||||
zos.putNextEntry(new ZipEntry("hmcl-latest.log"));
|
||||
LOG.exportLogs(zos);
|
||||
zos.closeEntry();
|
||||
} finally {
|
||||
try {
|
||||
Files.deleteIfExists(tempFile);
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
|
@ -39,7 +39,6 @@ import org.jackhuang.hmcl.ui.construct.ComponentSublist;
|
||||
import org.jackhuang.hmcl.ui.construct.MultiFileItem;
|
||||
import org.jackhuang.hmcl.ui.construct.OptionToggleButton;
|
||||
import org.jackhuang.hmcl.util.i18n.I18n;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales;
|
||||
import org.jackhuang.hmcl.util.i18n.Locales.SupportedLocale;
|
||||
|
||||
import java.util.Arrays;
|
||||
@ -183,8 +182,10 @@ public abstract class SettingsView extends StackPane {
|
||||
SupportedLocale currentLocale = I18n.getLocale();
|
||||
cboLanguage = new JFXComboBox<>();
|
||||
cboLanguage.setConverter(stringConverter(locale -> {
|
||||
if (locale.isSameLanguage(currentLocale) || locale == Locales.DEFAULT)
|
||||
if (locale.isDefault())
|
||||
return locale.getDisplayName(currentLocale);
|
||||
else if (locale.isSameLanguage(currentLocale))
|
||||
return locale.getDisplayName(locale);
|
||||
else
|
||||
return locale.getDisplayName(currentLocale) + " - " + locale.getDisplayName(locale);
|
||||
}));
|
||||
|
@ -44,13 +44,8 @@ public final class Locales {
|
||||
|
||||
public static final SupportedLocale DEFAULT = new SupportedLocale("def", getDefaultLocale()) {
|
||||
@Override
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
try {
|
||||
return inLocale.getResourceBundle().getString("lang.default");
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get localized name for default locale", e);
|
||||
return "Default";
|
||||
}
|
||||
public boolean isDefault() {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -92,19 +87,7 @@ public final class Locales {
|
||||
/**
|
||||
* Wenyan (Classical Chinese)
|
||||
*/
|
||||
public static final SupportedLocale WENYAN = new SupportedLocale("lzh", Locale.forLanguageTag("lzh")) {
|
||||
|
||||
@Override
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
if (LocaleUtils.isChinese(inLocale.locale))
|
||||
return "文言";
|
||||
|
||||
String name = super.getDisplayName(inLocale);
|
||||
return name.equals("lzh") || name.equals("Literary Chinese")
|
||||
? "Chinese (Classical)"
|
||||
: name;
|
||||
}
|
||||
};
|
||||
public static final SupportedLocale WENYAN = new SupportedLocale("lzh", Locale.forLanguageTag("lzh"));
|
||||
|
||||
public static final List<SupportedLocale> LOCALES = List.of(DEFAULT, EN, ES, JA, RU, UK, ZH_HANS, ZH_HANT, WENYAN);
|
||||
|
||||
@ -132,6 +115,10 @@ public final class Locales {
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
public boolean isDefault() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
@ -143,6 +130,26 @@ public final class Locales {
|
||||
public String getDisplayName(SupportedLocale inLocale) {
|
||||
if (inLocale.locale.getLanguage().equals("lzh"))
|
||||
inLocale = ZH_HANT;
|
||||
|
||||
if (isDefault()) {
|
||||
try {
|
||||
return inLocale.getResourceBundle().getString("lang.default");
|
||||
} catch (Throwable e) {
|
||||
LOG.warning("Failed to get localized name for default locale", e);
|
||||
return "Default";
|
||||
}
|
||||
}
|
||||
|
||||
if (this.locale.getLanguage().equals("lzh")) {
|
||||
if (LocaleUtils.isChinese(inLocale.locale))
|
||||
return "文言";
|
||||
|
||||
String name = locale.getDisplayName(inLocale.getLocale());
|
||||
return name.equals("lzh") || name.equals("Literary Chinese")
|
||||
? "Chinese (Classical)"
|
||||
: name;
|
||||
}
|
||||
|
||||
return locale.getDisplayName(inLocale.getLocale());
|
||||
}
|
||||
|
||||
|
@ -44,7 +44,7 @@ public final class WenyanUtils {
|
||||
};
|
||||
|
||||
private static final char[] TIAN_GAN = {'甲', '乙', '丙', '丁', '戊', '己', '庚', '辛', '壬', '癸'};
|
||||
private static final char[] DI_ZHI = {'子', '醜', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'};
|
||||
private static final char[] DI_ZHI = {'子', '丑', '寅', '卯', '辰', '巳', '午', '未', '申', '酉', '戌', '亥'};
|
||||
|
||||
private static String digitToString(char digit) {
|
||||
return digit >= '0' && digit <= '9'
|
||||
|
@ -1,3 +1,5 @@
|
||||
import org.jackhuang.hmcl.gradle.docs.UpdateDocuments
|
||||
|
||||
plugins {
|
||||
id("checkstyle")
|
||||
}
|
||||
@ -60,3 +62,8 @@ subprojects {
|
||||
org.jackhuang.hmcl.gradle.javafx.JavaFXUtils.register(rootProject)
|
||||
|
||||
defaultTasks("clean", "build")
|
||||
|
||||
|
||||
tasks.register<UpdateDocuments>("updateDocuments") {
|
||||
documentsDir.set(layout.projectDirectory.dir("docs"))
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.docs;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/// @author Glavo
|
||||
public record Document(DocumentFileTree directory,
|
||||
Path file,
|
||||
String name, DocumentLocale locale,
|
||||
List<Item> items) {
|
||||
|
||||
private static final Pattern MACRO_BEGIN = Pattern.compile(
|
||||
"<!-- #BEGIN (?<name>\\w+) -->"
|
||||
);
|
||||
|
||||
private static final Pattern MACRO_PROPERTY_LINE = Pattern.compile(
|
||||
"<!-- #PROPERTY (?<name>\\w+)=(?<value>.*) -->"
|
||||
);
|
||||
|
||||
private static String parsePropertyValue(String value) {
|
||||
int i = 0;
|
||||
while (i < value.length()) {
|
||||
char ch = value.charAt(i);
|
||||
if (ch == '\\')
|
||||
break;
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i == value.length())
|
||||
return value;
|
||||
|
||||
StringBuilder builder = new StringBuilder(value.length());
|
||||
builder.append(value, 0, i);
|
||||
for (; i < value.length(); i++) {
|
||||
char ch = value.charAt(i);
|
||||
if (ch == '\\' && i < value.length() - 1) {
|
||||
char next = value.charAt(++i);
|
||||
switch (next) {
|
||||
case 'n' -> builder.append('\n');
|
||||
case 'r' -> builder.append('\r');
|
||||
case '\\' -> builder.append('\\');
|
||||
default -> builder.append(next);
|
||||
}
|
||||
} else {
|
||||
builder.append(ch);
|
||||
}
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
static void writePropertyValue(StringBuilder builder, String value) {
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
char ch = value.charAt(i);
|
||||
|
||||
switch (ch) {
|
||||
case '\\' -> builder.append("\\\\");
|
||||
case '\r' -> builder.append("\\r");
|
||||
case '\n' -> builder.append("\\n");
|
||||
default -> builder.append(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Document load(DocumentFileTree directory, Path file, String name, DocumentLocale locale) throws IOException {
|
||||
var items = new ArrayList<Item>();
|
||||
try (var reader = Files.newBufferedReader(file)) {
|
||||
String line;
|
||||
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!line.startsWith("<!-- #")) {
|
||||
items.add(new Line(line));
|
||||
} else {
|
||||
Matcher matcher = MACRO_BEGIN.matcher(line);
|
||||
if (!matcher.matches())
|
||||
throw new IOException("Invalid macro begin line: " + line);
|
||||
|
||||
String macroName = matcher.group("name");
|
||||
String endLine = "<!-- #END " + macroName + " -->";
|
||||
var properties = new LinkedHashMap<String, List<String>>();
|
||||
var lines = new ArrayList<String>();
|
||||
|
||||
// Handle properties
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (!line.startsWith("<!-- #") || line.equals(endLine))
|
||||
break;
|
||||
|
||||
Matcher propertyMatcher = MACRO_PROPERTY_LINE.matcher(line);
|
||||
if (propertyMatcher.matches()) {
|
||||
String propertyName = propertyMatcher.group("name");
|
||||
String propertyValue = parsePropertyValue(propertyMatcher.group("value"));
|
||||
|
||||
properties.computeIfAbsent(propertyName, k -> new ArrayList<>(1))
|
||||
.add(propertyValue);
|
||||
} else {
|
||||
throw new IOException("Invalid macro property line: " + line);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle lines
|
||||
if (line != null && !line.equals(endLine)) {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("<!-- #"))
|
||||
break;
|
||||
|
||||
lines.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (line == null || !line.equals(endLine))
|
||||
throw new IOException("Invalid macro end line: " + line);
|
||||
|
||||
items.add(new MacroBlock(macroName,
|
||||
Collections.unmodifiableMap(properties),
|
||||
Collections.unmodifiableList(lines)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return new Document(directory, file, name, locale, items);
|
||||
}
|
||||
|
||||
public sealed interface Item {
|
||||
}
|
||||
|
||||
public record MacroBlock(String name, Map<String, List<String>> properties,
|
||||
List<String> contentLines) implements Item {
|
||||
}
|
||||
|
||||
public record Line(String content) implements Item {
|
||||
}
|
||||
}
|
@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.docs;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.TreeMap;
|
||||
|
||||
/// @author Glavo
|
||||
public final class DocumentFileTree {
|
||||
|
||||
public static DocumentFileTree load(Path dir) throws IOException {
|
||||
Path documentsDir = dir.toRealPath();
|
||||
DocumentFileTree rootTree = new DocumentFileTree();
|
||||
|
||||
Files.walkFileTree(documentsDir, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public @NotNull FileVisitResult visitFile(@NotNull Path file, @NotNull BasicFileAttributes attrs) throws IOException {
|
||||
String fileName = file.getFileName().toString();
|
||||
if (fileName.endsWith(".md")) {
|
||||
DocumentFileTree tree = rootTree.getFileTree(documentsDir.relativize(file.getParent()));
|
||||
if (tree == null)
|
||||
throw new AssertionError();
|
||||
|
||||
var result = DocumentLocale.parseFileName(fileName.substring(0, fileName.length() - ".md".length()));
|
||||
tree.getFiles().computeIfAbsent(result.name(), name -> new LocalizedDocument(tree, name))
|
||||
.getDocuments()
|
||||
.put(result.locale(), Document.load(tree, file, result.name(), result.locale()));
|
||||
}
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
|
||||
return rootTree;
|
||||
}
|
||||
|
||||
private final @Nullable DocumentFileTree parent;
|
||||
private final TreeMap<String, DocumentFileTree> children = new TreeMap<>();
|
||||
private final TreeMap<String, LocalizedDocument> files = new TreeMap<>();
|
||||
|
||||
public DocumentFileTree() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DocumentFileTree(@Nullable DocumentFileTree parent) {
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Nullable DocumentFileTree getFileTree(Path relativePath) {
|
||||
if (relativePath.isAbsolute())
|
||||
throw new IllegalArgumentException(relativePath + " is absolute");
|
||||
|
||||
if (relativePath.getNameCount() == 0)
|
||||
throw new IllegalArgumentException(relativePath + " is empty");
|
||||
|
||||
if (relativePath.getNameCount() == 1 && relativePath.getName(0).toString().isEmpty())
|
||||
return this;
|
||||
|
||||
DocumentFileTree current = this;
|
||||
for (int i = 0; i < relativePath.getNameCount(); i++) {
|
||||
String name = relativePath.getName(i).toString();
|
||||
if (name.isEmpty())
|
||||
throw new IllegalStateException(name + " is empty");
|
||||
else if (name.equals("."))
|
||||
continue;
|
||||
else if (name.equals("..")) {
|
||||
current = current.parent;
|
||||
if (current == null)
|
||||
return null;
|
||||
} else {
|
||||
DocumentFileTree finalCurrent = current;
|
||||
current = current.children.computeIfAbsent(name, ignored -> new DocumentFileTree(finalCurrent));
|
||||
}
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
public @Nullable DocumentFileTree getParent() {
|
||||
return parent;
|
||||
}
|
||||
|
||||
public TreeMap<String, DocumentFileTree> getChildren() {
|
||||
return children;
|
||||
}
|
||||
|
||||
public TreeMap<String, LocalizedDocument> getFiles() {
|
||||
return files;
|
||||
}
|
||||
}
|
@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.docs;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/// @author Glavo
|
||||
public enum DocumentLocale {
|
||||
ENGLISH(Locale.ENGLISH, "") {
|
||||
@Override
|
||||
public List<DocumentLocale> getCandidates() {
|
||||
return List.of(ENGLISH);
|
||||
}
|
||||
},
|
||||
SIMPLIFIED_CHINESE(Locale.forLanguageTag("zh-Hans"), "zh"),
|
||||
TRADITIONAL_CHINESE("zh-Hant") {
|
||||
@Override
|
||||
public List<DocumentLocale> getCandidates() {
|
||||
return List.of(TRADITIONAL_CHINESE, SIMPLIFIED_CHINESE, ENGLISH);
|
||||
}
|
||||
},
|
||||
WENYAN("lzh") {
|
||||
@Override
|
||||
public String getLanguageDisplayName() {
|
||||
return TRADITIONAL_CHINESE.getLanguageDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSubLanguageDisplayName() {
|
||||
return "文言";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<DocumentLocale> getCandidates() {
|
||||
return List.of(WENYAN, TRADITIONAL_CHINESE, SIMPLIFIED_CHINESE, ENGLISH);
|
||||
}
|
||||
},
|
||||
JAPANESE("ja"),
|
||||
SPANISH("es"),
|
||||
RUSSIAN("ru"),
|
||||
UKRAINIAN("uk"),
|
||||
;
|
||||
|
||||
public record LocaleAndName(DocumentLocale locale, String name) {
|
||||
}
|
||||
|
||||
public static LocaleAndName parseFileName(String fileNameWithoutExtension) {
|
||||
for (DocumentLocale locale : values()) {
|
||||
String suffix = locale.getFileNameSuffix();
|
||||
if (suffix.isEmpty())
|
||||
continue;
|
||||
|
||||
if (fileNameWithoutExtension.endsWith(suffix))
|
||||
return new LocaleAndName(locale, fileNameWithoutExtension.substring(0, fileNameWithoutExtension.length() - locale.getFileNameSuffix().length()));
|
||||
}
|
||||
return new LocaleAndName(ENGLISH, fileNameWithoutExtension);
|
||||
}
|
||||
|
||||
private final Locale locale;
|
||||
private final String languageTag;
|
||||
private final String fileNameSuffix;
|
||||
|
||||
DocumentLocale(String languageTag) {
|
||||
this(Locale.forLanguageTag(languageTag), languageTag);
|
||||
}
|
||||
|
||||
DocumentLocale(Locale locale, String languageTag) {
|
||||
this.locale = locale;
|
||||
this.languageTag = languageTag;
|
||||
this.fileNameSuffix = languageTag.isEmpty() ? "" : "_" + languageTag.replace('-', '_');
|
||||
}
|
||||
|
||||
public String getLanguageDisplayName() {
|
||||
return locale.getDisplayLanguage(locale);
|
||||
}
|
||||
|
||||
public String getSubLanguageDisplayName() {
|
||||
boolean hasScript = !locale.getScript().isEmpty();
|
||||
boolean hasRegion = !locale.getCountry().isEmpty();
|
||||
|
||||
if (hasScript && hasRegion)
|
||||
throw new AssertionError("Unsupported locale: " + locale);
|
||||
|
||||
if (hasScript)
|
||||
return locale.getDisplayScript(locale);
|
||||
if (hasRegion)
|
||||
return locale.getDisplayCountry(locale);
|
||||
return "";
|
||||
}
|
||||
|
||||
public Locale getLocale() {
|
||||
return locale;
|
||||
}
|
||||
|
||||
public String getFileNameSuffix() {
|
||||
return fileNameSuffix;
|
||||
}
|
||||
|
||||
public List<DocumentLocale> getCandidates() {
|
||||
return List.of(this, ENGLISH);
|
||||
}
|
||||
}
|
@ -0,0 +1,62 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.docs;
|
||||
|
||||
import java.util.EnumMap;
|
||||
|
||||
/// @author Glavo
|
||||
public final class LocalizedDocument {
|
||||
private final DocumentFileTree directory;
|
||||
private final String name;
|
||||
private final EnumMap<DocumentLocale, Document> documents = new EnumMap<>(DocumentLocale.class);
|
||||
|
||||
public LocalizedDocument(DocumentFileTree directory, String name) {
|
||||
this.directory = directory;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public DocumentFileTree getDirectory() {
|
||||
return directory;
|
||||
}
|
||||
|
||||
public EnumMap<DocumentLocale, Document> getDocuments() {
|
||||
return documents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
return obj instanceof LocalizedDocument that
|
||||
&& this.documents.equals(that.documents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return documents.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LocalizedDocument[" +
|
||||
"files=" + documents + ']';
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,144 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.docs;
|
||||
|
||||
import javax.print.Doc;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
/// @author Glavo
|
||||
public enum MacroProcessor {
|
||||
LANGUAGE_SWITCHER {
|
||||
private static <T> boolean containsIdentity(List<T> list, T element) {
|
||||
for (T t : list) {
|
||||
if (t == element)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void apply(Document document,
|
||||
Document.MacroBlock macroBlock,
|
||||
StringBuilder outputBuilder) throws IOException {
|
||||
LocalizedDocument localized = document.directory().getFiles().get(document.name());
|
||||
if (localized == null || localized.getDocuments().isEmpty())
|
||||
throw new AssertionError("Document " + document.name() + " does not exist");
|
||||
|
||||
MacroProcessor.writeBegin(outputBuilder, macroBlock);
|
||||
if (localized.getDocuments().size() > 1) {
|
||||
var languageToDocs = new LinkedHashMap<String, List<Document>>();
|
||||
for (DocumentLocale locale : DocumentLocale.values()) {
|
||||
Document targetDoc = localized.getDocuments().get(locale);
|
||||
if (targetDoc != null) {
|
||||
languageToDocs.computeIfAbsent(locale.getLanguageDisplayName(), name -> new ArrayList<>(1))
|
||||
.add(targetDoc);
|
||||
}
|
||||
}
|
||||
|
||||
boolean firstLanguage = true;
|
||||
|
||||
for (var entry : languageToDocs.entrySet()) {
|
||||
if (firstLanguage)
|
||||
firstLanguage = false;
|
||||
else
|
||||
outputBuilder.append(" | ");
|
||||
|
||||
String languageName = entry.getKey();
|
||||
List<Document> targetDocs = entry.getValue();
|
||||
|
||||
boolean containsCurrent = containsIdentity(targetDocs, document);
|
||||
if (targetDocs.size() == 1) {
|
||||
if (containsCurrent)
|
||||
outputBuilder.append("**").append(languageName).append("**");
|
||||
else
|
||||
outputBuilder.append("[").append(languageName).append("](").append(targetDocs.get(0).file().getFileName()).append(")");
|
||||
} else {
|
||||
if (containsCurrent)
|
||||
outputBuilder.append("**").append(languageName).append("**");
|
||||
else
|
||||
outputBuilder.append(languageName);
|
||||
|
||||
outputBuilder.append(" (");
|
||||
|
||||
boolean isFirst = true;
|
||||
for (Document targetDoc : targetDocs) {
|
||||
if (isFirst)
|
||||
isFirst = false;
|
||||
else
|
||||
outputBuilder.append(", ");
|
||||
|
||||
String subLanguage = targetDoc.locale().getSubLanguageDisplayName();
|
||||
|
||||
if (targetDoc == document) {
|
||||
outputBuilder.append("**").append(subLanguage).append("**");
|
||||
} else {
|
||||
outputBuilder.append('[').append(subLanguage).append("](").append(targetDoc.file().getFileName()).append(")");
|
||||
}
|
||||
}
|
||||
|
||||
outputBuilder.append(")");
|
||||
}
|
||||
}
|
||||
|
||||
outputBuilder.append('\n');
|
||||
}
|
||||
MacroProcessor.writeEnd(outputBuilder, macroBlock);
|
||||
}
|
||||
},
|
||||
BLOCK {
|
||||
@Override
|
||||
public void apply(Document document, Document.MacroBlock macroBlock, StringBuilder outputBuilder) throws IOException {
|
||||
MacroProcessor.writeBegin(outputBuilder, macroBlock);
|
||||
MacroProcessor.writeProperties(outputBuilder, macroBlock);
|
||||
for (String line : macroBlock.contentLines()) {
|
||||
outputBuilder.append(line).append('\n');
|
||||
}
|
||||
MacroProcessor.writeEnd(outputBuilder, macroBlock);
|
||||
}
|
||||
};
|
||||
|
||||
private static void writeBegin(StringBuilder builder, Document.MacroBlock macroBlock) throws IOException {
|
||||
builder.append("<!-- #BEGIN ");
|
||||
builder.append(macroBlock.name());
|
||||
builder.append(" -->\n");
|
||||
}
|
||||
|
||||
private static void writeEnd(StringBuilder builder, Document.MacroBlock macroBlock) throws IOException {
|
||||
builder.append("<!-- #END ");
|
||||
builder.append(macroBlock.name());
|
||||
builder.append(" -->\n");
|
||||
}
|
||||
|
||||
private static void writeProperties(StringBuilder builder, Document.MacroBlock macroBlock) throws IOException {
|
||||
macroBlock.properties().forEach((key, values) -> {
|
||||
for (String value : values) {
|
||||
builder.append("<!-- #PROPERTY ").append(key).append('=');
|
||||
Document.writePropertyValue(builder, value);
|
||||
builder.append(" -->\n");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public abstract void apply(Document document,
|
||||
Document.MacroBlock macroBlock,
|
||||
StringBuilder outputBuilder) throws IOException;
|
||||
}
|
@ -0,0 +1,131 @@
|
||||
/*
|
||||
* Hello Minecraft! Launcher
|
||||
* Copyright (C) 2025 huangyuhui <huanghongxun2008@126.com> and contributors
|
||||
*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.jackhuang.hmcl.gradle.docs;
|
||||
|
||||
import org.gradle.api.DefaultTask;
|
||||
import org.gradle.api.file.DirectoryProperty;
|
||||
import org.gradle.api.tasks.InputDirectory;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/// @author Glavo
|
||||
public abstract class UpdateDocuments extends DefaultTask {
|
||||
|
||||
@InputDirectory
|
||||
public abstract DirectoryProperty getDocumentsDir();
|
||||
|
||||
// ---
|
||||
|
||||
private static final Pattern LINK_PATTERN = Pattern.compile(
|
||||
"(?<=]\\()[a-zA-Z0-9_\\-./]+\\.md(?=\\))"
|
||||
);
|
||||
|
||||
private void processLine(StringBuilder outputBuilder, String line, Document document) {
|
||||
outputBuilder.append(LINK_PATTERN.matcher(line).replaceAll(matchResult -> {
|
||||
String rawLink = matchResult.group();
|
||||
String[] splitPath = rawLink.split("/");
|
||||
|
||||
if (splitPath.length == 0)
|
||||
return rawLink;
|
||||
|
||||
String fileName = splitPath[splitPath.length - 1];
|
||||
if (!fileName.endsWith(".md"))
|
||||
return rawLink;
|
||||
|
||||
DocumentFileTree current = document.directory();
|
||||
for (int i = 0; i < splitPath.length - 1; i++) {
|
||||
String name = splitPath[i];
|
||||
switch (name) {
|
||||
case "" -> {
|
||||
return rawLink;
|
||||
}
|
||||
case "." -> {
|
||||
continue;
|
||||
}
|
||||
case ".." -> {
|
||||
current = current.getParent();
|
||||
if (current == null)
|
||||
return rawLink;
|
||||
}
|
||||
default -> {
|
||||
current = current.getChildren().get(name);
|
||||
if (current == null)
|
||||
return rawLink;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DocumentLocale.LocaleAndName currentLocaleAndName = DocumentLocale.parseFileName(fileName.substring(0, fileName.length() - ".md".length()));
|
||||
LocalizedDocument localizedDocument = current.getFiles().get(currentLocaleAndName.name());
|
||||
if (localizedDocument != null) {
|
||||
List<DocumentLocale> candidateLocales = document.locale().getCandidates();
|
||||
for (DocumentLocale candidateLocale : candidateLocales) {
|
||||
if (candidateLocale == currentLocaleAndName.locale())
|
||||
return rawLink;
|
||||
|
||||
Document targetDoc = localizedDocument.getDocuments().get(candidateLocale);
|
||||
if (targetDoc != null) {
|
||||
splitPath[splitPath.length - 1] = targetDoc.file().getFileName().toString();
|
||||
return String.join("/", splitPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rawLink;
|
||||
})).append('\n');
|
||||
}
|
||||
|
||||
private void updateDocument(Document document) throws IOException {
|
||||
StringBuilder outputBuilder = new StringBuilder(8192);
|
||||
|
||||
for (Document.Item item : document.items()) {
|
||||
if (item instanceof Document.Line line) {
|
||||
processLine(outputBuilder, line.content(), document);
|
||||
} else if (item instanceof Document.MacroBlock macro) {
|
||||
var processor = MacroProcessor.valueOf(macro.name());
|
||||
processor.apply(document, macro, outputBuilder);
|
||||
} else
|
||||
throw new IllegalArgumentException("Unknown item type: " + item.getClass());
|
||||
}
|
||||
|
||||
Files.writeString(document.file(), outputBuilder.toString());
|
||||
}
|
||||
|
||||
private void processDocuments(DocumentFileTree tree) throws IOException {
|
||||
for (LocalizedDocument localizedDocument : tree.getFiles().values()) {
|
||||
for (Document document : localizedDocument.getDocuments().values()) {
|
||||
updateDocument(document);
|
||||
}
|
||||
}
|
||||
|
||||
for (DocumentFileTree subTree : tree.getChildren().values()) {
|
||||
processDocuments(subTree);
|
||||
}
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
public void run() throws IOException {
|
||||
Path rootDir = getDocumentsDir().get().getAsFile().toPath();
|
||||
processDocuments(DocumentFileTree.load(rootDir));
|
||||
}
|
||||
}
|
@ -1,6 +1,8 @@
|
||||
# Platform Support Status
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
**English** | 中文 ([简体](PLATFORM_zh.md), [繁體](PLATFORM_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:------------------------------------------------------------------------|:---------------------------|
|
||||
@ -35,4 +37,4 @@ Legend:
|
||||
* `/`: Not applicable.
|
||||
|
||||
We have no plans to support these platforms at this time, mainly because we do not have the equipment to test them.
|
||||
If you can help us adapt, please file a support request via GitHub Issue.
|
||||
If you can help us adapt, please file a support request via GitHub Issue.
|
||||
|
@ -1,6 +1,8 @@
|
||||
# 平台支持状态
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](PLATFORM.md) | **中文** (**简体**, [繁體](PLATFORM_zh_Hant.md))
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
|
||||
@ -35,4 +37,4 @@
|
||||
* `/`: 不支持的平台
|
||||
|
||||
我们目前还没有打算支持这些平台,主要是因为我们没有测试这些平台的设备。
|
||||
如果你能帮助我们进行测试,请通过提交 Issue 提出支持请求。
|
||||
如果你能帮助我们进行测试,请通过提交 Issue 提出支持请求。
|
||||
|
@ -1,6 +1,8 @@
|
||||
# 平臺支援狀態
|
||||
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](PLATFORM.md) | **中文** ([简体](PLATFORM_zh.md), **繁體**)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
| | Windows | Linux | macOS | FreeBSD |
|
||||
|----------------------------|:--------------------------------------------------|:---------------------------|:-----------------------------------------------------------------------|:--------------------------|
|
||||
@ -35,4 +37,4 @@
|
||||
* `/`: 不支援的平臺
|
||||
|
||||
我們目前還沒有打算支援這些平臺,主要是因為我們沒有測試這些平臺的裝置。
|
||||
如果你能幫助我們進行測試,請透過提交 Issue 提出支援請求。
|
||||
如果你能幫助我們進行測試,請透過提交 Issue 提出支援請求。
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
**English** | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) |
|
||||
[español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
**English** | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Introduction
|
||||
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) |
|
||||
**español** | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | **español** | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Introducción
|
||||
|
||||
@ -61,4 +62,4 @@ Asegúrate de tener instalado Java 17 o una versión posterior.
|
||||
| `-Dhmcl.native.encoding=<codificación>` | Sobrescribe la codificación nativa |
|
||||
| `-Dhmcl.microsoft.auth.id=<ID de App>` | Sobrescribe el ID de la App OAuth de Microsoft |
|
||||
| `-Dhmcl.microsoft.auth.secret=<Secreto de App>` | Sobrescribe el secreto de la App OAuth de Microsoft |
|
||||
| `-Dhmcl.curseforge.apikey=<Clave API>` | Sobrescribe la clave API de CurseForge |
|
||||
| `-Dhmcl.curseforge.apikey=<Clave API>` | Sobrescribe la clave API de CurseForge |
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** |
|
||||
[español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | **日本語** | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 紹介
|
||||
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) |
|
||||
[español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | **中文** ([简体](README_zh.md), [繁體](README_zh_Hant.md), **文言**) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
### 概說
|
||||
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) |
|
||||
[español](README_es.md) | **русский** | [українська](README_uk.md)
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | **русский** | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Введение
|
||||
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) |
|
||||
[español](README_es.md) | [русский](README_ru.md) | **українська**
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | 中文 ([简体](README_zh.md), [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | **українська**
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## Вступ
|
||||
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) |
|
||||
[español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | **中文** (**简体**, [繁體](README_zh_Hant.md), [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 简介
|
||||
|
||||
|
@ -6,8 +6,9 @@
|
||||
[](https://discord.gg/jVvC7HfM6U)
|
||||
[](https://docs.hmcl.net/groups.html)
|
||||
|
||||
[English](README.md) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) |
|
||||
[español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #BEGIN LANGUAGE_SWITCHER -->
|
||||
[English](README.md) | **中文** ([简体](README_zh.md), **繁體**, [文言](README_lzh.md)) | [日本語](README_ja.md) | [español](README_es.md) | [русский](README_ru.md) | [українська](README_uk.md)
|
||||
<!-- #END LANGUAGE_SWITCHER -->
|
||||
|
||||
## 簡介
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user