fix: cannot display all game versions in some cases of DownloadPage. Closes #1007.

This commit is contained in:
huanghongxun 2021-09-12 01:02:16 +08:00
parent 97f08bf74e
commit d7f5109a65
13 changed files with 320 additions and 42 deletions

View File

@ -272,9 +272,9 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
});
HBox box = new HBox(8);
Hyperlink birthLink = new Hyperlink(i18n("account.methods.microsoft.birth"));
JFXHyperlink birthLink = new JFXHyperlink(i18n("account.methods.microsoft.birth"));
birthLink.setOnAction(e -> FXUtils.openLink("https://support.microsoft.com/zh-cn/account-billing/如何更改-microsoft-帐户上的出生日期-837badbc-999e-54d2-2617-d19206b9540a"));
Hyperlink profileLink = new Hyperlink(i18n("account.methods.microsoft.profile"));
JFXHyperlink profileLink = new JFXHyperlink(i18n("account.methods.microsoft.profile"));
profileLink.setOnAction(e -> FXUtils.openLink("https://account.live.com/editprof.aspx"));
box.getChildren().setAll(profileLink, birthLink);
GridPane.setColumnSpan(box, 2);
@ -415,7 +415,7 @@ public class CreateAccountPane extends JFXDialogLayout implements DialogAware {
if (factory instanceof YggdrasilAccountFactory) {
HBox box = new HBox();
Hyperlink migrationLink = new Hyperlink(i18n("account.methods.yggdrasil.migration"));
JFXHyperlink migrationLink = new JFXHyperlink(i18n("account.methods.yggdrasil.migration"));
migrationLink.setOnAction(e -> FXUtils.openLink("https://help.minecraft.net/hc/en-us/articles/360050865492-JAVA-Account-Migration-FAQ"));
GridPane.setColumnSpan(box, 2);
box.getChildren().setAll(migrationLink);

View File

@ -0,0 +1,31 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 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.ui.construct;
import javafx.scene.control.Hyperlink;
import org.jackhuang.hmcl.setting.Theme;
import org.jackhuang.hmcl.ui.SVG;
public class JFXHyperlink extends Hyperlink {
public JFXHyperlink(String text) {
super(text);
setGraphic(SVG.launchOutline(Theme.blackFillBinding(), 16, 16));
}
}

View File

@ -22,10 +22,14 @@ import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.util.AggregatedObservableList;
import org.jackhuang.hmcl.util.javafx.MappedObservableList;
public class TwoLineListItem extends VBox {
@ -35,7 +39,8 @@ public class TwoLineListItem extends VBox {
private final ObservableList<String> tags = FXCollections.observableArrayList();
private final StringProperty subtitle = new SimpleStringProperty(this, "subtitle");
private final ObservableList<Label> tagLabels;
private final ObservableList<Node> tagLabels;
private final AggregatedObservableList<Node> firstLineChildren;
public TwoLineListItem(String titleString, String subtitleString) {
this();
@ -47,23 +52,24 @@ public class TwoLineListItem extends VBox {
public TwoLineListItem() {
setMouseTransparent(true);
HBox firstLine = new HBox();
FlowPane firstLine = new FlowPane();
firstLine.setMaxWidth(Double.MAX_VALUE);
Label lblTitle = new Label();
lblTitle.getStyleClass().add("title");
lblTitle.textProperty().bind(title);
HBox tagContainer = new HBox();
tagLabels = MappedObservableList.create(tags, tag -> {
Label tagLabel = new Label();
tagLabel.getStyleClass().add("tag");
tagLabel.setText(tag);
FlowPane.setMargin(tagLabel, new Insets(0, 8, 0, 0));
return tagLabel;
});
Bindings.bindContent(tagContainer.getChildren(), tagLabels);
firstLine.getChildren().addAll(lblTitle, tagContainer);
firstLineChildren = new AggregatedObservableList<>();
firstLineChildren.appendList(FXCollections.singletonObservableList(lblTitle));
firstLineChildren.appendList(tagLabels);
Bindings.bindContent(firstLine.getChildren(), firstLineChildren.getAggregatedList());
Label lblSubtitle = new Label();
lblSubtitle.getStyleClass().add("subtitle");

View File

@ -17,17 +17,22 @@
*/
package org.jackhuang.hmcl.ui.versions;
import com.jfoenix.controls.*;
import com.jfoenix.controls.JFXButton;
import com.jfoenix.controls.JFXComboBox;
import com.jfoenix.controls.JFXListView;
import com.jfoenix.controls.JFXTextField;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.Control;
import javafx.scene.control.Label;
import javafx.scene.control.Skin;
import javafx.scene.control.SkinBase;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
@ -47,7 +52,9 @@ import org.jackhuang.hmcl.util.StringUtils;
import org.jackhuang.hmcl.util.javafx.BindingMapping;
import java.io.File;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static org.jackhuang.hmcl.util.i18n.I18n.i18n;
@ -61,6 +68,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
private final ListProperty<CurseAddon> items = new SimpleListProperty<>(this, "items", FXCollections.observableArrayList());
private final DownloadPage.DownloadCallback callback;
private boolean searchInitialized = false;
protected final BooleanProperty supportChinese = new SimpleBooleanProperty();
/**
* @see org.jackhuang.hmcl.mod.curse.CurseModManager#SECTION_MODPACK
@ -121,6 +129,7 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
public void search(String userGameVersion, int category, int pageOffset, String searchFilter, int sort) {
setLoading(true);
setFailed(false);
File versionJar = StringUtils.isNotBlank(version.get().getVersion())
? version.get().getProfile().getRepository().getVersionJar(version.get().getVersion())
: null;
@ -194,11 +203,10 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
}
JFXTextField nameField = new JFXTextField();
nameField.setPromptText(i18n("mods.name"));
nameField.setPromptText(getSkinnable().supportChinese.get() ? i18n("search.hint.chinese") : i18n("search.hint.english"));
JFXTextField gameVersionField = new JFXTextField();
Label lblGameVersion = new Label(i18n("world.game_version"));
gameVersionField.setPromptText(i18n("world.game_version"));
searchPane.addRow(rowIndex++, new Label(i18n("mods.name")), nameField, lblGameVersion, gameVersionField);
ObjectBinding<Boolean> hasVersion = BindingMapping.of(getSkinnable().version)
@ -239,7 +247,6 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
sortStackPane.getChildren().setAll(sortComboBox);
sortComboBox.prefWidthProperty().bind(sortStackPane.widthProperty());
sortComboBox.getStyleClass().add("fit-width");
sortComboBox.setPromptText(i18n("search.sort"));
sortComboBox.getItems().setAll(
i18n("curse.sort.date_created"),
i18n("curse.sort.popularity"),
@ -252,8 +259,12 @@ public class DownloadListPage extends Control implements DecoratorPage, VersionP
JFXButton searchButton = new JFXButton();
searchButton.setText(i18n("search"));
GridPane.setHalignment(searchButton, HPos.LEFT);
searchPane.addRow(rowIndex++, searchButton);
searchButton.getStyleClass().add("jfx-button-raised");
searchButton.setButtonType(JFXButton.ButtonType.RAISED);
HBox searchBox = new HBox(searchButton);
GridPane.setColumnSpan(searchBox, 4);
searchBox.setAlignment(Pos.CENTER_RIGHT);
searchPane.addRow(rowIndex++, searchBox);
EventHandler<ActionEvent> searchAction = e -> getSkinnable()
.search(gameVersionField.getText(),

View File

@ -35,6 +35,7 @@ import javafx.scene.layout.Priority;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import org.jackhuang.hmcl.game.GameVersion;
import org.jackhuang.hmcl.mod.ModManager;
import org.jackhuang.hmcl.mod.curse.CurseAddon;
import org.jackhuang.hmcl.mod.curse.CurseModManager;
import org.jackhuang.hmcl.setting.Profile;
@ -45,6 +46,7 @@ import org.jackhuang.hmcl.ui.Controllers;
import org.jackhuang.hmcl.ui.FXUtils;
import org.jackhuang.hmcl.ui.SVG;
import org.jackhuang.hmcl.ui.construct.FloatListCell;
import org.jackhuang.hmcl.ui.construct.JFXHyperlink;
import org.jackhuang.hmcl.ui.construct.SpinnerPane;
import org.jackhuang.hmcl.ui.construct.TwoLineListItem;
import org.jackhuang.hmcl.ui.decorator.DecoratorPage;
@ -70,11 +72,13 @@ public class DownloadPage extends Control implements DecoratorPage {
private final BooleanProperty loading = new SimpleBooleanProperty(false);
private final BooleanProperty failed = new SimpleBooleanProperty(false);
private final CurseAddon addon;
private final ModTranslations.Mod mod;
private final Profile.ProfileVersion version;
private final DownloadCallback callback;
public DownloadPage(CurseAddon addon, Profile.ProfileVersion version, @Nullable DownloadCallback callback) {
this.addon = addon;
this.mod = ModTranslations.getModByCurseForgeId(addon.getSlug());
this.version = version;
this.callback = callback;
@ -183,6 +187,14 @@ public class DownloadPage extends Control implements DecoratorPage {
descriptionPane.getStyleClass().add("card");
BorderPane.setMargin(descriptionPane, new Insets(11, 11, 0, 11));
ImageView imageView = new ImageView();
for (CurseAddon.Attachment attachment : getSkinnable().addon.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
}
}
descriptionPane.getChildren().add(FXUtils.limitingSize(imageView, 40, 40));
TwoLineListItem content = new TwoLineListItem();
HBox.setHgrow(content, Priority.ALWAYS);
ModTranslations.Mod mod = ModTranslations.getModByCurseForgeId(getSkinnable().addon.getSlug());
@ -191,20 +203,23 @@ public class DownloadPage extends Control implements DecoratorPage {
content.getTags().setAll(getSkinnable().addon.getCategories().stream()
.map(category -> i18n("curse.category." + category.getCategoryId()))
.collect(Collectors.toList()));
descriptionPane.getChildren().add(content);
ImageView imageView = new ImageView();
for (CurseAddon.Attachment attachment : getSkinnable().addon.getAttachments()) {
if (attachment.isDefault()) {
imageView.setImage(new Image(attachment.getThumbnailUrl(), 40, 40, true, true, true));
if (getSkinnable().mod != null) {
JFXHyperlink openMcmodButton = new JFXHyperlink(i18n("mods.mcmod"));
openMcmodButton.setOnAction(e -> FXUtils.openLink(ModManager.getMcmodUrl(getSkinnable().mod.getMcmod())));
descriptionPane.getChildren().add(openMcmodButton);
if (StringUtils.isNotBlank(getSkinnable().mod.getMcbbs())) {
JFXHyperlink openMcbbsButton = new JFXHyperlink(i18n("mods.mcbbs"));
openMcbbsButton.setOnAction(e -> FXUtils.openLink(ModManager.getMcbbsUrl(getSkinnable().mod.getMcbbs())));
descriptionPane.getChildren().add(openMcbbsButton);
}
}
JFXButton openUrlButton = new JFXButton();
openUrlButton.getStyleClass().add("toggle-icon4");
openUrlButton.setGraphic(SVG.launchOutline(Theme.blackFillBinding(), -1, -1));
JFXHyperlink openUrlButton = new JFXHyperlink(i18n("mods.curseforge"));
openUrlButton.setOnAction(e -> FXUtils.openLink(getSkinnable().addon.getWebsiteUrl()));
descriptionPane.getChildren().setAll(FXUtils.limitingSize(imageView, 40, 40), content, openUrlButton);
descriptionPane.getChildren().add(openUrlButton);
SpinnerPane spinnerPane = new SpinnerPane();

View File

@ -26,6 +26,8 @@ import java.util.List;
public class ModDownloadListPage extends DownloadListPage {
public ModDownloadListPage(int section, DownloadPage.DownloadCallback callback, boolean versionSelection) {
super(section, callback, versionSelection);
supportChinese.set(true);
}
@Override

View File

@ -190,7 +190,7 @@ public final class ModTranslations {
public String getDisplayName() {
StringBuilder builder = new StringBuilder();
if (StringUtils.isNotBlank(abbr)) {
builder.append("[").append(abbr).append("]");
builder.append("[").append(abbr).append("] ");
}
builder.append(name);
if (StringUtils.isNotBlank(subname)) {

View File

@ -0,0 +1,199 @@
/*
* Hello Minecraft! Launcher
* Copyright (C) 2021 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.util;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
public class AggregatedObservableList<T> {
protected final List<ObservableList<T>> lists = new ArrayList<>();
final private List<Integer> sizes = new ArrayList<>();
final private List<InternalListModificationListener> listeners = new ArrayList<>();
final protected ObservableList<T> aggregatedList = FXCollections.observableArrayList();
public AggregatedObservableList() {
}
/**
* The Aggregated Observable List. This list is unmodifiable, because sorting this list would mess up the entire bookkeeping we do here.
*
* @return an unmodifiable view of the aggregatedList
*/
public ObservableList<T> getAggregatedList() {
return FXCollections.unmodifiableObservableList(aggregatedList);
}
public void appendList(@NotNull ObservableList<T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
list.addListener(listener);
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.add(list.size());
aggregatedList.addAll(list);
listeners.add(listener);
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void prependList(@NotNull ObservableList<T> list) {
assert !lists.contains(list) : "List is already contained: " + list;
lists.add(0, list);
final InternalListModificationListener listener = new InternalListModificationListener(list);
list.addListener(listener);
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.add(0, list.size());
aggregatedList.addAll(0, list);
listeners.add(0, listener);
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
public void removeList(@NotNull ObservableList<T> list) {
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
final int index = lists.indexOf(list);
if (index < 0) {
throw new IllegalArgumentException("Cannot remove a list that is not contained: " + list + " lists=" + lists);
}
final int startIndex = getStartIndex(list);
final int endIndex = getEndIndex(list, startIndex);
// we want to find the start index of this list inside the aggregated List. End index will be start + size - 1.
lists.remove(list);
sizes.remove(index);
final InternalListModificationListener listener = listeners.remove(index);
list.removeListener(listener);
aggregatedList.remove(startIndex, endIndex + 1); // end + 1 because end is exclusive
assert lists.size() == sizes.size() && lists.size() == listeners.size() :
"lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size() + " or not equal to listeners.size=" + listeners.size();
}
/**
* Get the start index of this list inside the aggregated List.
* This is a private function. we can safely asume, that the list is in the map.
*
* @param list the list in question
* @return the start index of this list in the aggregated List
*/
private int getStartIndex(@NotNull ObservableList<T> list) {
int startIndex = 0;
//System.out.println("=== searching startIndex of " + list);
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
final int listIndex = lists.indexOf(list);
for (int i = 0; i < listIndex; i++) {
final Integer size = sizes.get(i);
startIndex += size;
//System.out.println(" startIndex = " + startIndex + " added=" + size);
}
//System.out.println("startIndex = " + startIndex);
return startIndex;
}
/**
* Get the end index of this list inside the aggregated List.
* This is a private function. we can safely asume, that the list is in the map.
*
* @param list the list in question
* @param startIndex the start of the list (retrieve with {@link #getStartIndex(ObservableList)}
* @return the end index of this list in the aggregated List
*/
private int getEndIndex(@NotNull ObservableList<T> list, int startIndex) {
assert lists.size() == sizes.size() : "lists.size=" + lists.size() + " not equal to sizes.size=" + sizes.size();
final int index = lists.indexOf(list);
return startIndex + sizes.get(index) - 1;
}
private class InternalListModificationListener implements ListChangeListener<T> {
@NotNull
private final ObservableList<T> list;
public InternalListModificationListener(@NotNull ObservableList<T> list) {
this.list = list;
}
/**
* Called after a change has been made to an ObservableList.
*
* @param change an object representing the change that was done
* @see Change
*/
@Override
public void onChanged(Change<? extends T> change) {
final ObservableList<? extends T> changedList = change.getList();
final int startIndex = getStartIndex(list);
final int index = lists.indexOf(list);
final int newSize = changedList.size();
//System.out.println("onChanged for list=" + list + " aggregate=" + aggregatedList);
while (change.next()) {
final int from = change.getFrom();
final int to = change.getTo();
//System.out.println(" startIndex=" + startIndex + " from=" + from + " to=" + to);
if (change.wasPermutated()) {
final ArrayList<T> copy = new ArrayList<>(aggregatedList.subList(startIndex + from, startIndex + to));
//System.out.println(" permutating sublist=" + copy);
for (int oldIndex = from; oldIndex < to; oldIndex++) {
int newIndex = change.getPermutation(oldIndex);
copy.set(newIndex - from, aggregatedList.get(startIndex + oldIndex));
}
//System.out.println(" permutating done sublist=" + copy);
aggregatedList.subList(startIndex + from, startIndex + to).clear();
aggregatedList.addAll(startIndex + from, copy);
} else if (change.wasUpdated()) {
// do nothing
} else {
if (change.wasRemoved()) {
List<? extends T> removed = change.getRemoved();
//System.out.println(" removed= " + removed);
// IMPORTANT! FROM == TO when removing items.
aggregatedList.remove(startIndex + from, startIndex + from + removed.size());
}
if (change.wasAdded()) {
List<? extends T> added = change.getAddedSubList();
//System.out.println(" added= " + added);
//add those elements to your data
aggregatedList.addAll(startIndex + from, added);
}
}
}
// update the size of the list in the map
//System.out.println("list = " + list + " puttingInMap=" + list.hashCode());
sizes.set(index, newSize);
//System.out.println("listSizesMap = " + sizes);
}
}
public String dump(Function<T, Object> function) {
StringBuilder sb = new StringBuilder();
sb.append("[");
aggregatedList.forEach(el -> sb.append(function.apply(el)).append(","));
final int length = sb.length();
sb.replace(length - 1, length, "");
sb.append("]");
return sb.toString();
}
}

View File

@ -94,7 +94,7 @@
-fx-padding: 0 0 0 10;
}
.advanced-list-item > .rippler-container > .container > .two-line-list-item > HBox > .title {
.advanced-list-item > .rippler-container > .container > .two-line-list-item > FlowPane > .title {
-fx-font-size: 13;
-fx-text-alignment: justify;
}
@ -108,7 +108,7 @@
-fx-background-color: -fx-base-rippler-color;
}
.advanced-list-item:selected > .rippler-container > .container > .two-line-list-item > HBox > .title {
.advanced-list-item:selected > .rippler-container > .container > .two-line-list-item > FlowPane > .title {
-fx-text-fill: -fx-base-color;
-fx-font-weight: bold;
}
@ -121,7 +121,7 @@
-fx-padding: 0 0 0 0;
}
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > HBox > .title {
.profile-list-item > .rippler-container > BorderPane > .two-line-list-item > FlowPane > .title {
-fx-font-size: 13;
}
@ -137,7 +137,7 @@
-fx-background-color: -fx-base-rippler-color;
}
.profile-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > HBox .title {
.profile-list-item:selected > .rippler-container > BorderPane > .two-line-list-item > FlowPane > .title {
-fx-text-fill: -fx-base-color;
-fx-font-weight: bold;
}
@ -205,9 +205,10 @@
-fx-alignment: center-left;
}
.two-line-list-item > HBox > .title {
.two-line-list-item > FlowPane > .title {
-fx-text-fill: #292929;
-fx-font-size: 15px;
-fx-padding: 0 8 0 0;
}
.two-line-list-item > HBox > .subtitle {
@ -216,11 +217,7 @@
-fx-font-size: 12px;
}
.two-line-list-item > HBox > HBox {
-fx-spacing: 8;
}
.two-line-list-item > HBox > HBox > .tag {
.two-line-list-item > FlowPane > .tag {
-fx-text-fill: -fx-base-color;
-fx-background-color: -fx-base-rippler-color;
-fx-padding: 2;
@ -234,7 +231,7 @@
-fx-text-fill: white;
}
.bubble > HBox > .two-line-list-item > HBox > .title,
.bubble > HBox > .two-line-list-item > FlowPane > .title,
.bubble > HBox > .two-line-list-item > HBox > .subtitle {
-fx-text-fill: white;
}

View File

@ -491,13 +491,16 @@ mods.add.failed=Failed to install mods %s.
mods.add.success=Successfully installed mods %s.
mods.category=Category
mods.choose_mod=Choose your mods
mods.curseforge=CurseForge
mods.disable=Disable
mods.download=Mod Downloads
mods.download.title=Mod Downloads - %1s
mods.enable=Enable
mods.manage=Mods
mods.mcmod.page=MCWiki
mods.mcmod.search=Search in MCWiki
mods.mcbbs=MCBBS
mods.mcmod=MCMOD
mods.mcmod.page=MCMOD
mods.mcmod.search=Search in MCMOD
mods.name=Name
mods.not_modded=You should install a modloader first (Fabric, Forge or LiteLoader)
mods.url=Official Page

View File

@ -498,11 +498,14 @@ mods.add.failed=新增模組 %s 失敗。
mods.add.success=成功新增模組 %s。
mods.category=類別
mods.choose_mod=選擇模組
mods.curseforge=CurseForge
mods.disable=停用
mods.download=模組下載
mods.download.title=模組下載 - %1s
mods.enable=啟用
mods.manage=模組管理
mods.mcbbs=MCBBS
mods.mcmod=MC 百科
mods.mcmod.page=MC 百科頁面
mods.mcmod.search=MC 百科蒐索
mods.name=名稱

View File

@ -494,11 +494,14 @@ mods.add.failed=添加模组 %s 失败。
mods.add.success=成功添加模组 %s。
mods.category=类别
mods.choose_mod=选择模组
mods.curseforge=CurseForge
mods.disable=禁用
mods.download=模组下载
mods.download.title=模组下载 - %1s
mods.enable=启用
mods.manage=模组管理
mods.mcbbs=MCBBS
mods.mcmod=MC 百科
mods.mcmod.page=MC 百科页面
mods.mcmod.search=MC 百科搜索
mods.name=名称

View File

@ -192,5 +192,13 @@ public final class ModManager {
return getModsDirectory().resolve(fileName);
}
public static String getMcmodUrl(String mcmodId) {
return String.format("https://www.mcmod.cn/class/%s.html", mcmodId);
}
public static String getMcbbsUrl(String mcbbsId) {
return String.format("https://www.mcbbs.net/thread-%s-1-1.html", mcbbsId);
}
public static final String DISABLED_EXTENSION = ".disabled";
}