Initial project
138
.gitignore
vendored
@ -1,98 +1,48 @@
|
||||
# ---> JetBrains
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
|
||||
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
|
||||
|
||||
# User-specific stuff
|
||||
.idea/**/workspace.xml
|
||||
.idea/**/tasks.xml
|
||||
.idea/**/usage.statistics.xml
|
||||
.idea/**/dictionaries
|
||||
.idea/**/shelf
|
||||
|
||||
# AWS User-specific
|
||||
.idea/**/aws.xml
|
||||
|
||||
# Generated files
|
||||
.idea/**/contentModel.xml
|
||||
|
||||
# Sensitive or high-churn files
|
||||
.idea/**/dataSources/
|
||||
.idea/**/dataSources.ids
|
||||
.idea/**/dataSources.local.xml
|
||||
.idea/**/sqlDataSources.xml
|
||||
.idea/**/dynamic.xml
|
||||
.idea/**/uiDesigner.xml
|
||||
.idea/**/dbnavigator.xml
|
||||
|
||||
# Gradle
|
||||
.idea/**/gradle.xml
|
||||
.idea/**/libraries
|
||||
|
||||
# Gradle and Maven with auto-import
|
||||
# When using Gradle or Maven with auto-import, you should exclude module files,
|
||||
# since they will be recreated, and may cause churn. Uncomment if using
|
||||
# auto-import.
|
||||
# .idea/artifacts
|
||||
# .idea/compiler.xml
|
||||
# .idea/jarRepositories.xml
|
||||
# .idea/modules.xml
|
||||
# .idea/*.iml
|
||||
# .idea/modules
|
||||
# *.iml
|
||||
# *.ipr
|
||||
|
||||
# CMake
|
||||
cmake-build-*/
|
||||
|
||||
# Mongo Explorer plugin
|
||||
.idea/**/mongoSettings.xml
|
||||
|
||||
# File-based project format
|
||||
*.iws
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
|
||||
# mpeltonen/sbt-idea plugin
|
||||
.idea_modules/
|
||||
|
||||
# JIRA plugin
|
||||
atlassian-ide-plugin.xml
|
||||
|
||||
# Cursive Clojure plugin
|
||||
.idea/replstate.xml
|
||||
|
||||
# SonarLint plugin
|
||||
.idea/sonarlint/
|
||||
|
||||
# Crashlytics plugin (for Android Studio and IntelliJ)
|
||||
com_crashlytics_export_strings.xml
|
||||
crashlytics.properties
|
||||
crashlytics-build.properties
|
||||
fabric.properties
|
||||
|
||||
# Editor-based Rest Client
|
||||
.idea/httpRequests
|
||||
|
||||
# Android studio 3.1+ serialized cache file
|
||||
.idea/caches/build_file_checksums.ser
|
||||
|
||||
# ---> Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
# https://github.com/takari/maven-wrapper#usage-without-binary-jar
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
!.mvn/wrapper/maven-wrapper.jar
|
||||
!**/src/main/**/target/
|
||||
!**/src/test/**/target/
|
||||
|
||||
# Eclipse m2e generated files
|
||||
# Eclipse Core
|
||||
.project
|
||||
# JDT-specific (Eclipse Java Development Tools)
|
||||
### IntelliJ IDEA ###
|
||||
.idea/modules.xml
|
||||
.idea/jarRepositories.xml
|
||||
.idea/compiler.xml
|
||||
.idea/libraries/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
|
||||
### Eclipse ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
build/
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
/.minecraft
|
||||
/.physics_mod_cache
|
||||
/config
|
||||
/CustomSkinLoader
|
||||
/etc
|
||||
/run
|
||||
/logs
|
||||
/jre*
|
||||
/ForeverMC.yaml
|
||||
|
||||
4
.idea/.gitignore
generated
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
/developer-tools.xml
|
||||
7
.idea/encodings.xml
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Encoding" defaultCharsetForPropertiesFiles="UTF-8">
|
||||
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
|
||||
</component>
|
||||
</project>
|
||||
9
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
@ -0,0 +1,9 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="ForCanBeForeach" enabled="false" level="WARNING" enabled_by_default="false">
|
||||
<option name="REPORT_INDEXED_LOOP" value="true" />
|
||||
<option name="ignoreUntypedCollections" value="false" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
||||
14
.idea/misc.xml
generated
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="MavenProjectsManager">
|
||||
<option name="originalFiles">
|
||||
<list>
|
||||
<option value="$PROJECT_DIR$/pom.xml" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" project-jdk-name="21" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
</project>
|
||||
124
.idea/uiDesigner.xml
generated
Normal file
@ -0,0 +1,124 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Palette2">
|
||||
<group name="Swing">
|
||||
<item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
|
||||
</item>
|
||||
<item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.svg" removable="false" auto-create-binding="false" can-attach-label="true">
|
||||
<default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
|
||||
<initial-values>
|
||||
<property name="text" value="Button" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="RadioButton" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="CheckBox" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
|
||||
<initial-values>
|
||||
<property name="text" value="Label" />
|
||||
</initial-values>
|
||||
</item>
|
||||
<item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
|
||||
<preferred-size width="150" height="-1" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
|
||||
<preferred-size width="150" height="50" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
|
||||
<preferred-size width="200" height="200" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.svg" removable="false" auto-create-binding="true" can-attach-label="true">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
|
||||
</item>
|
||||
<item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
|
||||
<preferred-size width="-1" height="20" />
|
||||
</default-constraints>
|
||||
</item>
|
||||
<item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.svg" removable="false" auto-create-binding="false" can-attach-label="false">
|
||||
<default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
|
||||
</item>
|
||||
<item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.svg" removable="false" auto-create-binding="true" can-attach-label="false">
|
||||
<default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
|
||||
</item>
|
||||
</group>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
99
pom.xml
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>cn.forevermc.launcher</groupId>
|
||||
<artifactId>ForeverMC</artifactId>
|
||||
<version>2.3.0</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<version>3.7.1</version>
|
||||
<configuration>
|
||||
<archive>
|
||||
<manifest>
|
||||
<mainClass>cn.forevermc.launcher.ForeverMC</mainClass>
|
||||
</manifest>
|
||||
</archive>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>single</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>jitpack.io</id>
|
||||
<url>https://jitpack.io</url>
|
||||
</repository>
|
||||
<repository>
|
||||
<id>maven2</id>
|
||||
<url>https://repo.maven.apache.org/maven2/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.fx.ui</groupId>
|
||||
<artifactId>timi-fx-ui</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.inject</groupId>
|
||||
<artifactId>timi-inject</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.openjfx</groupId>
|
||||
<artifactId>javafx-media</artifactId>
|
||||
<version>21.0.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.compress</groupId>
|
||||
<artifactId>timi-compress</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.imyeyu.network</groupId>
|
||||
<artifactId>timi-network</artifactId>
|
||||
<version>0.0.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.34</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>5.10.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
63
src/main/java/cn/forevermc/launcher/ForeverMC.java
Normal file
@ -0,0 +1,63 @@
|
||||
package cn.forevermc.launcher;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.ctrl.Main;
|
||||
import javafx.application.Application;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.config.ConfigLoader;
|
||||
import com.imyeyu.fx.config.BindingsConfig;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.InjectApp;
|
||||
import com.imyeyu.inject.annotation.TimiInjectApplication;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.lang.multi.ResourcesMultilingual;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Forever Minecraft 启动器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-18 15:36
|
||||
*/
|
||||
@Slf4j
|
||||
@TimiInjectApplication
|
||||
public class ForeverMC {
|
||||
|
||||
public static final String VERSION = "2.2.3";
|
||||
|
||||
@Getter
|
||||
private static Config config;
|
||||
|
||||
@Getter
|
||||
private static ConfigLoader<Config> configLoader;
|
||||
|
||||
@Getter
|
||||
private static InjectApp injectApp;
|
||||
|
||||
public static void main(String[] args) {
|
||||
injectApp = new InjectApp(ForeverMC.class);
|
||||
try {
|
||||
{
|
||||
configLoader = BindingsConfig.build("ForeverMC.yaml", Config.class);
|
||||
config = configLoader.load();
|
||||
if (TimiJava.isEmpty(config.getLauncher().getClientId().get())) {
|
||||
config.getLauncher().getClientId().set(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
ResourcesMultilingual multilingual = TimiFXUI.MULTILINGUAL;
|
||||
multilingual.addAll("lang/timi-fx-ui/%s.lang");
|
||||
multilingual.addAll("lang/%s.lang");
|
||||
multilingual.setActivated(config.getMain().getLanguage().get());
|
||||
|
||||
// 禁止系统 DPI 缩放
|
||||
System.setProperty("prism.allowhidpi", "false");
|
||||
System.setProperty("glass.win.minHiDPI", "1");
|
||||
}
|
||||
Application.launch(Main.class);
|
||||
} catch (Exception e) {
|
||||
log.error("fatal error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
45
src/main/java/cn/forevermc/launcher/bean/APISetting.java
Normal file
@ -0,0 +1,45 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2024-04-29 20:05
|
||||
*/
|
||||
@Data
|
||||
public class APISetting {
|
||||
|
||||
public enum Key {
|
||||
|
||||
/** 闪烁标语 */
|
||||
FMC_SPLASHES,
|
||||
|
||||
/** 启动器背景 */
|
||||
FMC_BG,
|
||||
|
||||
FMC_BGM,
|
||||
}
|
||||
|
||||
private DynamicList bg;
|
||||
|
||||
private DynamicList bgm;
|
||||
|
||||
private DynamicList splashes;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-01 15:59
|
||||
*/
|
||||
@Data
|
||||
public static class DynamicList {
|
||||
|
||||
/** 激活的标语 */
|
||||
private String active;
|
||||
|
||||
/** 标语列表 */
|
||||
private List<String> list;
|
||||
}
|
||||
}
|
||||
22
src/main/java/cn/forevermc/launcher/bean/ComponentSize.java
Normal file
@ -0,0 +1,22 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
/**
|
||||
* 组件大小
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-09 16:43
|
||||
*/
|
||||
public class ComponentSize {
|
||||
|
||||
/** 小号 */
|
||||
public static final double SMALL = 150;
|
||||
|
||||
/** 正常 */
|
||||
public static final double NORMAL = 200;
|
||||
|
||||
/** 中等 */
|
||||
public static final double MEDIUM = 300;
|
||||
|
||||
/** 大号 */
|
||||
public static final double LARGE = 400;
|
||||
}
|
||||
94
src/main/java/cn/forevermc/launcher/bean/Config.java
Normal file
@ -0,0 +1,94 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import lombok.Data;
|
||||
import com.imyeyu.java.bean.Language;
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-04-18 17:55
|
||||
*/
|
||||
@Data
|
||||
public class Config {
|
||||
|
||||
/** 主配置 */
|
||||
private Main main;
|
||||
|
||||
/** 玩家配置 */
|
||||
private Player player;
|
||||
|
||||
/** 启动配置 */
|
||||
private Launcher launcher;
|
||||
|
||||
/**
|
||||
* 主配置
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-04-18 18:00
|
||||
*/
|
||||
@Data
|
||||
public static class Main {
|
||||
|
||||
/** 启动器语言 */
|
||||
private ObjectProperty<Language> language;
|
||||
|
||||
/** 背景音乐音量,[0, 100] */
|
||||
private DoubleProperty bgmVolume;
|
||||
|
||||
/** 音效音量,[0, 100] */
|
||||
private DoubleProperty soundVolume;
|
||||
|
||||
/** 游戏下载源 */
|
||||
private ObjectProperty<GameDownloadSource> gameDownloadSource;
|
||||
|
||||
/** 运行时下载源 */
|
||||
private ObjectProperty<RuntimeDownloadSource> runtimeDownloadSource;
|
||||
}
|
||||
|
||||
/**
|
||||
* 玩家配置
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-04-18 18:00
|
||||
*/
|
||||
@Data
|
||||
public static class Player {
|
||||
|
||||
/** 登录名 */
|
||||
private StringProperty name;
|
||||
|
||||
/** 密码 */
|
||||
private StringProperty password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动器配置
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-04-18 17:59
|
||||
*/
|
||||
@Data
|
||||
public static class Launcher {
|
||||
|
||||
/** 最大内存 */
|
||||
private DoubleProperty memory;
|
||||
|
||||
/** 启动游戏 */
|
||||
private StringProperty game;
|
||||
|
||||
/** 客户端 ID */
|
||||
private StringProperty clientId;
|
||||
|
||||
/** true 为自动启动 */
|
||||
private BooleanProperty autoStartup;
|
||||
|
||||
/** 多线程下载游戏 */
|
||||
private IntegerProperty multiDownload;
|
||||
}
|
||||
}
|
||||
21
src/main/java/cn/forevermc/launcher/bean/FabricAPI.java
Normal file
@ -0,0 +1,21 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-06-11 19:29
|
||||
*/
|
||||
@Data
|
||||
public class FabricAPI {
|
||||
|
||||
private String name;
|
||||
|
||||
private String fabricVer;
|
||||
|
||||
private String minecraftVer;
|
||||
|
||||
private String mongoId;
|
||||
}
|
||||
80
src/main/java/cn/forevermc/launcher/bean/FileDownload.java
Normal file
@ -0,0 +1,80 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import cn.forevermc.launcher.util.Path;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ReadOnlyBooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import lombok.Data;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.utils.Digest;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 文件下载对象
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 11:22
|
||||
*/
|
||||
@Data
|
||||
public class FileDownload {
|
||||
|
||||
/** 本地文件对象 */
|
||||
private File file;
|
||||
|
||||
/** 大小 */
|
||||
private Long size;
|
||||
|
||||
/** 请求 URL */
|
||||
private String url;
|
||||
|
||||
/** 哈希 */
|
||||
private String hash;
|
||||
|
||||
/** 显示名称 */
|
||||
private String displayName;
|
||||
|
||||
/** 进度 */
|
||||
private DoubleProperty progress;
|
||||
|
||||
/** true 为下载错误 */
|
||||
private BooleanProperty isError;
|
||||
|
||||
/** 重试次数 */
|
||||
private int retry = 0;
|
||||
|
||||
public FileDownload(File file) {
|
||||
this.file = file;
|
||||
this.displayName = file.getAbsolutePath().substring(file.getAbsolutePath().indexOf(Path.P_ROOT));
|
||||
|
||||
progress = new SimpleDoubleProperty(0);
|
||||
isError = new SimpleBooleanProperty(false);
|
||||
}
|
||||
|
||||
/** @return true 为存在且 HASH 匹配 */
|
||||
public boolean exist() throws Exception {
|
||||
return file.exists() && hash.equals(Digest.sha1(IO.toBytes(file)));
|
||||
}
|
||||
|
||||
/** @return 文件名 */
|
||||
public String getFileName() {
|
||||
return file.getName();
|
||||
}
|
||||
|
||||
/** @return 进度监听 */
|
||||
public DoubleProperty progressProperty() {
|
||||
return progress;
|
||||
}
|
||||
|
||||
/** @param isError true 为下载错误 */
|
||||
public void setError(boolean isError) {
|
||||
this.isError.set(isError);
|
||||
}
|
||||
|
||||
/** @return 错误监听,只读 */
|
||||
public ReadOnlyBooleanProperty errorProperty() {
|
||||
return isError;
|
||||
}
|
||||
}
|
||||
350
src/main/java/cn/forevermc/launcher/bean/Game.java
Normal file
@ -0,0 +1,350 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 游戏
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-11-26 11:13
|
||||
*/
|
||||
@Data
|
||||
public class Game {
|
||||
|
||||
/**
|
||||
* 游戏版本类型
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 14:54
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
/** 快照 */
|
||||
SNAPSHOT,
|
||||
|
||||
/** 正式版 */
|
||||
RELEASE,
|
||||
|
||||
/** 旧的测试版 */
|
||||
OLD_BETA,
|
||||
|
||||
/** 旧的实验版 */
|
||||
OLD_ALPHA
|
||||
}
|
||||
|
||||
// ---------- 版本属性,这些属性在解析前就有效 ----------
|
||||
|
||||
/** 显示名称 */
|
||||
private String name;
|
||||
|
||||
/** 版本路径(如:.minecraft/versions/1.20) */
|
||||
private File path;
|
||||
|
||||
/** JAR 文件 */
|
||||
private File jar;
|
||||
|
||||
/** JSON 文件 */
|
||||
private File json;
|
||||
|
||||
/** yaml 文件(ForeverMC 针对此整合包的配置) */
|
||||
private File yaml;
|
||||
|
||||
/** JSON 文件的根节点 */
|
||||
private JsonObject jsonRoot;
|
||||
|
||||
/** 发布时间 */
|
||||
private Long releaseAt;
|
||||
|
||||
/** true 为原版 */
|
||||
private boolean isOriginal;
|
||||
|
||||
/** true 为 Fabric 版本 */
|
||||
private boolean isFabric;
|
||||
|
||||
/** true 为上古版本 */
|
||||
private boolean isLegacy;
|
||||
|
||||
/** 版本设置 */
|
||||
private Option option;
|
||||
|
||||
// ---------- 版本 JSON 数据,这些属性在解析后才有效 ----------
|
||||
|
||||
/** ID(版本号,可能会被修改) */
|
||||
private String id;
|
||||
|
||||
/** 游戏版本类型 */
|
||||
private Type type;
|
||||
|
||||
/** 元数据 URL */
|
||||
private String url;
|
||||
|
||||
/** 版本元数据 */
|
||||
private MetaData metaData;
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
if (this == object) return true;
|
||||
if (object == null || getClass() != object.getClass()) return false;
|
||||
|
||||
Game game = (Game) object;
|
||||
return Objects.equals(path, game.path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = name != null ? name.hashCode() : 0;
|
||||
result = 31 * result + (path != null ? path.hashCode() : 0);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本元数据,通过 {@link cn.forevermc.launcher.service.GameService#readMetaData(Game)} 解析后有效
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 11:28
|
||||
*/
|
||||
@Data
|
||||
public static class MetaData {
|
||||
|
||||
/**
|
||||
* 真实版本号,目前仅在正式版有效
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-17 16:11
|
||||
*/
|
||||
@Getter
|
||||
public static class RealVersion {
|
||||
|
||||
/** 版本号匹配正则 */
|
||||
private static final Pattern PATTERN = Pattern.compile("(\\d)\\.(\\d+)\\.?(\\d+)?");
|
||||
|
||||
/** 版本号 */
|
||||
private String value;
|
||||
|
||||
/** 根版本 */
|
||||
private int root = 0;
|
||||
|
||||
/** 主版本 */
|
||||
private int main = 0;
|
||||
|
||||
/** BUG 修正版本 */
|
||||
private int bug = 0;
|
||||
|
||||
/**
|
||||
* 设置版本号,通过此方法自动解析详细版本号
|
||||
*
|
||||
* @param value 版本号
|
||||
*/
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
Matcher matcher = PATTERN.matcher(value);
|
||||
if (matcher.find()) {
|
||||
root = Integer.parseInt(matcher.group(1));
|
||||
main = Integer.parseInt(matcher.group(2));
|
||||
if (TimiJava.isNotEmpty(matcher.group(3))) {
|
||||
bug = Integer.parseInt(matcher.group(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return true 为支持 Fabric */
|
||||
public boolean isSupportFabric() {
|
||||
return main < 14;
|
||||
}
|
||||
|
||||
/** @return true 为更现代的样式 */
|
||||
public boolean isModernStyle() {
|
||||
return 19 < main;
|
||||
}
|
||||
}
|
||||
|
||||
/** 版本号(可能因模组加载器而改变) */
|
||||
private String version;
|
||||
|
||||
/** 真实的 Minecraft 版本号 */
|
||||
private RealVersion realVersion;
|
||||
|
||||
/** 标题 */
|
||||
private String title;
|
||||
|
||||
/** 启动类 */
|
||||
private String mainClass;
|
||||
|
||||
/** 需要 Java 版本 */
|
||||
private String javaVersion;
|
||||
|
||||
/** 游戏参数模板 */
|
||||
private String argsGame;
|
||||
|
||||
/** JVM 参数模板 */
|
||||
private String argsJVM;
|
||||
|
||||
/** 启动核心地址 */
|
||||
private String coreURL;
|
||||
|
||||
/** 启动核心 SHA1 */
|
||||
private String coreSHA1;
|
||||
|
||||
/** 核心大小 */
|
||||
private Long coreSize;
|
||||
|
||||
/** 资源版本 */
|
||||
private String assetsVersion;
|
||||
|
||||
/** 资源列表 */
|
||||
private List<Assets> assetList;
|
||||
|
||||
/** 依赖列表 */
|
||||
private List<Libraries> librarieList;
|
||||
|
||||
/** 需下载的文件列表(解析结果,包括资源和依赖文件) */
|
||||
private List<FileDownload> downloadList;
|
||||
|
||||
/**
|
||||
* 资源
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 11:04
|
||||
*/
|
||||
@Data
|
||||
public static class Assets {
|
||||
|
||||
/** 哈希值 */
|
||||
private String hash;
|
||||
|
||||
/** 下载 URL */
|
||||
private String url;
|
||||
|
||||
/** 大小 */
|
||||
private Long size;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依赖
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 11:04
|
||||
*/
|
||||
@Data
|
||||
public static class Libraries {
|
||||
|
||||
/** 哈希值 */
|
||||
private String sha1;
|
||||
|
||||
/** 相对路径 */
|
||||
private String path;
|
||||
|
||||
/** 下载 URL */
|
||||
private String url;
|
||||
|
||||
/** 文件大小 */
|
||||
private Long size;
|
||||
|
||||
/** true 为 JNA 依赖 */
|
||||
private boolean isNatives;
|
||||
|
||||
/** @return true 为非 JNA 依赖 */
|
||||
public boolean isNotNatives() {
|
||||
return !isNatives;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 版本设置
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-09 20:31
|
||||
*/
|
||||
@Data
|
||||
public static class Option implements Serializable {
|
||||
|
||||
/**
|
||||
* 登录方式
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-05 23:05
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum LoginType implements Serializable {
|
||||
|
||||
/** 离线 */
|
||||
OFFLINE(TimiFXUI.MULTILINGUAL.text("offline"), TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.account.type", TimiFXUI.MULTILINGUAL.text("offline"))),
|
||||
|
||||
/** ForeverMC */
|
||||
FOREVER_MC("ForeverMC", TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.account.type", TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.account.fmc"))),
|
||||
|
||||
/** 官方正版验证(未支持) */
|
||||
MOJANG("Mojang", TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.account.type", TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.account.mojang")));
|
||||
|
||||
/** 显示名称 */
|
||||
final String name;
|
||||
|
||||
/** 提示 */
|
||||
final String tips;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可选规则
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-17 11:34
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum Rules {
|
||||
|
||||
/** 体验模式 */
|
||||
DEMO("is_demo_user"),
|
||||
|
||||
/** 自定义尺寸 */
|
||||
CUSTOM_RESOLUTION("has_custom_resolution"),
|
||||
|
||||
/** 快速游戏日志 */
|
||||
QUICK_PLAYS_SUPPORT("has_quick_plays_support"),
|
||||
|
||||
/** 快速单人游戏 */
|
||||
QUICK_PLAY_SINGLEPLAYER("is_quick_play_singleplayer"),
|
||||
|
||||
/** 快速多人游戏 */
|
||||
QUICK_PLAY_MULTIPLAYER("is_quick_play_multiplayer"),
|
||||
|
||||
/** 快速领域游戏 */
|
||||
QUICK_PLAY_REALMS("is_quick_play_realms");
|
||||
|
||||
/** 名称 */
|
||||
final String name;
|
||||
}
|
||||
|
||||
/** 名称,从整合版版本包获取并储存 */
|
||||
private StringProperty name;
|
||||
|
||||
/** 支持的登录方式 */
|
||||
private ObjectProperty<LoginType> loginType;
|
||||
|
||||
/** 启动 Java */
|
||||
private StringProperty java;
|
||||
|
||||
/** 服务器 */
|
||||
private StringProperty server;
|
||||
|
||||
/** 是否自动连接 */
|
||||
private BooleanProperty autoConnect;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2023-06-20 15:21
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum GameDownloadSource {
|
||||
|
||||
/** 官方源 */
|
||||
MOJANG("https://libraries.minecraft.net/", "https://resources.download.minecraft.net/"),
|
||||
|
||||
/** BMCLAPI */
|
||||
BMCLAPI("https://bmclapi2.bangbang93.com/maven/", "https://bmclapi2.bangbang93.com/assets/");
|
||||
|
||||
/** 依赖 */
|
||||
final String libraries;
|
||||
|
||||
/** 资源 */
|
||||
final String resources;
|
||||
}
|
||||
167
src/main/java/cn/forevermc/launcher/bean/LaunchGame.java
Normal file
@ -0,0 +1,167 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import cn.forevermc.launcher.service.FMCLoginService;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import com.imyeyu.inject.annotation.Bean;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
|
||||
/**
|
||||
* 启动游戏对象
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-07-20 19:24
|
||||
*/
|
||||
@Bean
|
||||
public class LaunchGame extends SimpleObjectProperty<Game> {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private FMCLoginService fmcLoginService;
|
||||
|
||||
/** true 为使用新标题 */
|
||||
private final BooleanBinding newTitle;
|
||||
|
||||
/** true 为使用新 UI */
|
||||
private final BooleanBinding newUI;
|
||||
|
||||
/** true 为远古版本 */
|
||||
private final BooleanBinding legacy;
|
||||
|
||||
/** true 为官方原版 */
|
||||
private final BooleanBinding original;
|
||||
|
||||
/** true 为使用 Fabric */
|
||||
private final BooleanBinding fabric;
|
||||
|
||||
/** true 为支持 ForeverMC 登录 */
|
||||
private final BooleanBinding supportFMCLogin;
|
||||
|
||||
public LaunchGame() {
|
||||
newTitle = Bindings.createBooleanBinding(() -> {
|
||||
if (get() == null) {
|
||||
return false;
|
||||
}
|
||||
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(get());
|
||||
return 1 <= realVersion.getRoot() && 20 <= realVersion.getMain(); // 1.20 <= 使用新标题
|
||||
}, this);
|
||||
newUI = Bindings.createBooleanBinding(() -> {
|
||||
if (get() == null) {
|
||||
return false;
|
||||
}
|
||||
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(get());
|
||||
return 1 <= realVersion.getRoot() && 21 <= realVersion.getMain(); // 1.21 <= 使用新 UI
|
||||
}, this);
|
||||
legacy = Bindings.createBooleanBinding(() -> {
|
||||
if (get() == null) {
|
||||
return false;
|
||||
}
|
||||
return get().isLegacy();
|
||||
}, this);
|
||||
original = Bindings.createBooleanBinding(() -> {
|
||||
if (get() == null) {
|
||||
return false;
|
||||
}
|
||||
return get().isOriginal();
|
||||
}, this);
|
||||
fabric = Bindings.createBooleanBinding(() -> {
|
||||
if (get() == null) {
|
||||
return false;
|
||||
}
|
||||
return get().isFabric();
|
||||
}, this);
|
||||
supportFMCLogin = Bindings.createBooleanBinding(() -> {
|
||||
if (fmcLoginService == null) {
|
||||
return false;
|
||||
}
|
||||
return fmcLoginService.isSupportFMCLogin(get());
|
||||
}, this, FMCLoginService.loginModMapProperty(), FMCLoginService.fabricAPIMapProperty());
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
String defLaunch = config.getLauncher().getGame().get();
|
||||
config.getLauncher().getGame().bind(Bindings.createStringBinding(() -> {
|
||||
if (get() == null) {
|
||||
return defLaunch;
|
||||
}
|
||||
return get().getName();
|
||||
}, this));
|
||||
}
|
||||
|
||||
public boolean isOffline() {
|
||||
return get().getOption().getLoginType().get() == Game.Option.LoginType.OFFLINE;
|
||||
}
|
||||
|
||||
public boolean isOnline() {
|
||||
return !isOffline();
|
||||
}
|
||||
|
||||
public boolean isFMCLogin() {
|
||||
return get().getOption().getLoginType().get() == Game.Option.LoginType.FOREVER_MC;
|
||||
}
|
||||
|
||||
public boolean isMojangLogin() {
|
||||
return get().getOption().getLoginType().get() == Game.Option.LoginType.MOJANG;
|
||||
}
|
||||
|
||||
public boolean isNewTitle() {
|
||||
return newTitle.get();
|
||||
}
|
||||
|
||||
public BooleanBinding newTitleProperty() {
|
||||
return newTitle;
|
||||
}
|
||||
|
||||
public boolean isNewUI() {
|
||||
return newUI.get();
|
||||
}
|
||||
|
||||
public BooleanBinding newUIProperty() {
|
||||
return newUI;
|
||||
}
|
||||
|
||||
public boolean isLegacy() {
|
||||
return legacy.get();
|
||||
}
|
||||
|
||||
public BooleanBinding legacyProperty() {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
public boolean isOriginal() {
|
||||
return original.get();
|
||||
}
|
||||
|
||||
public BooleanBinding originalProperty() {
|
||||
return original;
|
||||
}
|
||||
|
||||
public boolean isFabric() {
|
||||
return fabric.get();
|
||||
}
|
||||
|
||||
public BooleanBinding fabricProperty() {
|
||||
return fabric;
|
||||
}
|
||||
|
||||
public boolean isSupportFMCLogin() {
|
||||
return supportFMCLogin.get();
|
||||
}
|
||||
|
||||
public boolean isNotSupportFMCLogin() {
|
||||
return !supportFMCLogin.get();
|
||||
}
|
||||
|
||||
public BooleanBinding supportFMCLoginProperty() {
|
||||
return supportFMCLogin;
|
||||
}
|
||||
}
|
||||
32
src/main/java/cn/forevermc/launcher/bean/MCPopupTips.java
Normal file
@ -0,0 +1,32 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.Border;
|
||||
import com.imyeyu.fx.ui.components.popup.tips.AbstractPopupTips;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
|
||||
/**
|
||||
* 弹出提示组件
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-08 17:12
|
||||
*/
|
||||
public class MCPopupTips extends AbstractPopupTips<Label> {
|
||||
|
||||
private static final Border BORDER = new BorderStroke("#25025C").width(3).build();
|
||||
private static final Insets PADDING = new Insets(4);
|
||||
|
||||
public MCPopupTips() {
|
||||
this("");
|
||||
}
|
||||
|
||||
public MCPopupTips(String text) {
|
||||
setNode(new Label(text));
|
||||
getNode().setBorder(BORDER);
|
||||
getNode().setPadding(PADDING);
|
||||
getNode().setTextFill(Colorful.WHITE);
|
||||
getNode().setBackground(Background.EMPTY);
|
||||
}
|
||||
}
|
||||
44
src/main/java/cn/forevermc/launcher/bean/OpenJDK.java
Normal file
@ -0,0 +1,44 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import lombok.Data;
|
||||
import com.imyeyu.utils.OS;
|
||||
|
||||
/**
|
||||
* OpenJDK
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-06-10 10:35
|
||||
*/
|
||||
@Data
|
||||
public class OpenJDK {
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-06-10 10:35
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
/** 集成开发工具 */
|
||||
JDK,
|
||||
|
||||
/** 运行时 */
|
||||
JRE
|
||||
}
|
||||
|
||||
/** 类型 */
|
||||
private Type type;
|
||||
|
||||
/** 平台 */
|
||||
private OS.Platform platform;
|
||||
|
||||
/** 版本 */
|
||||
private String version;
|
||||
|
||||
/** 名称 */
|
||||
private String name;
|
||||
|
||||
/** 数据(可能是下载链接,可能是 mongoId) */
|
||||
private String data;
|
||||
}
|
||||
85
src/main/java/cn/forevermc/launcher/bean/Pack.java
Normal file
@ -0,0 +1,85 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* ForeverMC 版本包
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-13 17:17
|
||||
*/
|
||||
@Data
|
||||
public class Pack {
|
||||
|
||||
/** 名称 */
|
||||
private String name;
|
||||
|
||||
/** 版本 */
|
||||
private String ver;
|
||||
|
||||
/** 标题 */
|
||||
private String title;
|
||||
|
||||
/** 简介 */
|
||||
private String description;
|
||||
|
||||
/** 游戏版本 */
|
||||
private String gameVer;
|
||||
|
||||
/** 默认配置 */
|
||||
private String defOption;
|
||||
|
||||
/** 文件大小 */
|
||||
private long size;
|
||||
|
||||
/** true 为过时 */
|
||||
private boolean isDeprecated;
|
||||
|
||||
/** 创建时间 */
|
||||
private Long createdAt;
|
||||
|
||||
/** 更新时间 */
|
||||
private Long updatedAt;
|
||||
|
||||
/** 下载源列表 */
|
||||
private List<Source> sourceList;
|
||||
|
||||
/**
|
||||
* 下载源
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-13 17:32
|
||||
*/
|
||||
@Data
|
||||
public static class Source {
|
||||
|
||||
/**
|
||||
* 数据类型
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-06-18 20:11
|
||||
*/
|
||||
public enum Type {
|
||||
|
||||
/** 附件 */
|
||||
ATTACH,
|
||||
|
||||
/** 地址 */
|
||||
URL
|
||||
}
|
||||
|
||||
/** 名称 */
|
||||
private String name;
|
||||
|
||||
/** 数据类型 */
|
||||
private Type type;
|
||||
|
||||
/** 数据 */
|
||||
private String data;
|
||||
|
||||
/** true 为默认选择此下载源 */
|
||||
private boolean isDefault;
|
||||
}
|
||||
}
|
||||
88
src/main/java/cn/forevermc/launcher/bean/Page.java
Normal file
@ -0,0 +1,88 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import cn.forevermc.launcher.ctrl.pane.About;
|
||||
import cn.forevermc.launcher.ctrl.pane.CanvasProgress;
|
||||
import cn.forevermc.launcher.ctrl.pane.Dialog;
|
||||
import cn.forevermc.launcher.ctrl.pane.GameOption;
|
||||
import cn.forevermc.launcher.ctrl.pane.GameSelect;
|
||||
import cn.forevermc.launcher.ctrl.pane.LangSelect;
|
||||
import cn.forevermc.launcher.ctrl.pane.Launching;
|
||||
import cn.forevermc.launcher.ctrl.pane.Menu;
|
||||
import cn.forevermc.launcher.ctrl.pane.Option;
|
||||
import cn.forevermc.launcher.ctrl.pane.OriginalSelect;
|
||||
import cn.forevermc.launcher.ctrl.pane.PackSelect;
|
||||
import cn.forevermc.launcher.ctrl.pane.PlayerSelect;
|
||||
import cn.forevermc.launcher.ctrl.pane.ResourceDownload;
|
||||
import cn.forevermc.launcher.view.components.AbstractPane;
|
||||
import com.imyeyu.inject.TimiInject;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.StaticInject;
|
||||
|
||||
/**
|
||||
* 页面
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 16:38
|
||||
*/
|
||||
@StaticInject
|
||||
public enum Page {
|
||||
|
||||
/** 会话 */
|
||||
DIALOG(Dialog.class),
|
||||
|
||||
/** 画板进度 */
|
||||
CANVAS_PROGRESS(CanvasProgress.class),
|
||||
|
||||
/** 菜单 */
|
||||
MENU(Menu.class),
|
||||
|
||||
/** 选项 */
|
||||
OPTION(Option.class),
|
||||
|
||||
/** 原版下载 */
|
||||
ORIGINAL_SELECT(OriginalSelect.class),
|
||||
|
||||
/** 整合版选择 */
|
||||
PACK_SELECT(PackSelect.class),
|
||||
|
||||
/** 版本配置 */
|
||||
PACK_OPTION(GameOption.class),
|
||||
|
||||
/** 启动版本选择 */
|
||||
GAME_SELECT(GameSelect.class),
|
||||
|
||||
/** 玩家选择 */
|
||||
PLAYER_SELECT(PlayerSelect.class),
|
||||
|
||||
/** 资源下载 */
|
||||
RESOURCE_DOWNLOAD(ResourceDownload.class),
|
||||
|
||||
/** 启动过渡页 */
|
||||
LAUNCHING(Launching.class),
|
||||
|
||||
/** 语言选择 */
|
||||
LANG_SELECT(LangSelect.class),
|
||||
|
||||
/** 关于 */
|
||||
ABOUT(About.class);
|
||||
|
||||
@Inject
|
||||
private static TimiInject injector;
|
||||
|
||||
/** 页面类 */
|
||||
final Class<? extends AbstractPane> page;
|
||||
|
||||
Page(Class<? extends AbstractPane> page) {
|
||||
this.page = page;
|
||||
}
|
||||
|
||||
/** @return 页面类 */
|
||||
public Class<?> getClazz() {
|
||||
return page;
|
||||
}
|
||||
|
||||
/** @return 从 TimiInject 控制反转对象获取该页面 */
|
||||
public AbstractPane getIOCPage() {
|
||||
return injector.di(page);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
package cn.forevermc.launcher.bean;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 运行时下载源
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-06-07 00:13
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum RuntimeDownloadSource {
|
||||
|
||||
/** ForeverMC */
|
||||
FOREVER_MC("ForeverMC", "OpenJDKMirror"),
|
||||
|
||||
/** 清华大学 */
|
||||
TUNA("Tsinghua Tuna", "OpenJDKTunaMirror"),
|
||||
|
||||
/** Github */
|
||||
GITHUB("Github", "OpenJDKGithubMirror");
|
||||
|
||||
/** 名称 */
|
||||
final String name;
|
||||
|
||||
final String mirror;
|
||||
}
|
||||
165
src/main/java/cn/forevermc/launcher/core/DownloadThread.java
Normal file
@ -0,0 +1,165 @@
|
||||
package cn.forevermc.launcher.core;
|
||||
|
||||
import cn.forevermc.launcher.bean.FileDownload;
|
||||
import cn.forevermc.launcher.ctrl.pane.ResourceDownload;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.StaticInject;
|
||||
import com.imyeyu.io.IOSpeedService;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.java.bean.CallbackReturn;
|
||||
import com.imyeyu.network.ProgressiveRequest;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.fluent.Request;
|
||||
import org.apache.hc.core5.util.Timeout;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 下载线程,返回为当前下载文件,内部调度
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 13:27
|
||||
*/
|
||||
@Slf4j
|
||||
@StaticInject
|
||||
public class DownloadThread extends RunAsync<FileDownload> {
|
||||
|
||||
@Inject
|
||||
private static ResourceDownload resourceDownload;
|
||||
|
||||
@Inject
|
||||
private static IOSpeedService.Item ioSpeedItem;
|
||||
|
||||
/** 线程 ID */
|
||||
private static int id = 0;
|
||||
|
||||
/** 完成事件 */
|
||||
@Setter
|
||||
private CallbackArg<FileDownload> onFinishEvent;
|
||||
|
||||
/** 错误事件 */
|
||||
@Setter
|
||||
private CallbackArg<FileDownload> onErrorEvent;
|
||||
|
||||
/** 结束事件 */
|
||||
@Setter
|
||||
private Callback onFinallyEvent;
|
||||
|
||||
private final String name;
|
||||
private final CallbackReturn<FileDownload> fetchFileHandler;
|
||||
|
||||
/** true 为中断 */
|
||||
private boolean isInterrupted = false;
|
||||
|
||||
/** 当前下载对象 */
|
||||
private FileDownload download;
|
||||
|
||||
/**
|
||||
* 默认构造器
|
||||
*
|
||||
* @param fetchFileHandler 任务获取回调,此接口不返回空时将持续进行下载文件,返回空时自然结束,非 FX 线程触发
|
||||
*/
|
||||
public DownloadThread(CallbackReturn<FileDownload> fetchFileHandler) {
|
||||
this.fetchFileHandler = fetchFileHandler;
|
||||
name = "[Download Service-%s] ".formatted(id++);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected FileDownload call() throws Exception {
|
||||
if (isInterrupted) {
|
||||
return null;
|
||||
}
|
||||
progress(0);
|
||||
if (download != null) {
|
||||
log.info("{}{}", name, download.getUrl());
|
||||
|
||||
long size;
|
||||
if (download.getSize() == null) {
|
||||
size = ProgressiveRequest.wrap(Request.head(download.getUrl()).connectTimeout(Timeout.ofSeconds(6))).length();
|
||||
} else {
|
||||
size = download.getSize();
|
||||
}
|
||||
// 下载文件
|
||||
ProgressiveRequest.get(download.getUrl(), (total, read, now) -> {
|
||||
ioSpeedItem.push(now);
|
||||
progress(1D * read / size);
|
||||
return !isInterrupted;
|
||||
}).toFile(download.getFile());
|
||||
}
|
||||
progress(1);
|
||||
return download;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(FileDownload file) {
|
||||
if (file != null) {
|
||||
onFinishEvent.handler(file);
|
||||
}
|
||||
if (isInterrupted) {
|
||||
log.info(name + " died");
|
||||
return;
|
||||
}
|
||||
// 继续从回调接口获取并重新开始
|
||||
download = fetchFileHandler.handler();
|
||||
if (download != null) {
|
||||
download.progressProperty().bind(progressProperty());
|
||||
restart();
|
||||
} else {
|
||||
log.info(name + " died");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
if (isInterrupted) {
|
||||
log.info(name + " died");
|
||||
return;
|
||||
}
|
||||
if (download.getRetry() < 8) {
|
||||
// 重试
|
||||
log.warn("download error " + e.getMessage() + " and retry: " + download.getUrl());
|
||||
download.setRetry(download.getRetry() + 1);
|
||||
resourceDownload.putDownloadDeque(List.of(download));
|
||||
} else {
|
||||
log.error("download fail: " + download.getUrl(), e);
|
||||
download.setError(true);
|
||||
}
|
||||
// TODO 最终错误常驻列表并说明下载失败
|
||||
onErrorEvent.handler(download);
|
||||
|
||||
// 任务异常也需继续抽取队列继续下载
|
||||
download = fetchFileHandler.handler();
|
||||
if (download != null) {
|
||||
download.progressProperty().bind(progressProperty());
|
||||
restart();
|
||||
} else {
|
||||
log.info(name + " died");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinally() {
|
||||
onFinallyEvent.handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
isInterrupted = false;
|
||||
super.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restart() {
|
||||
isInterrupted = false;
|
||||
super.restart();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean cancel() {
|
||||
isInterrupted = true;
|
||||
return super.cancel();
|
||||
}
|
||||
}
|
||||
750
src/main/java/cn/forevermc/launcher/core/Launcher.java
Normal file
@ -0,0 +1,750 @@
|
||||
package cn.forevermc.launcher.core;
|
||||
|
||||
import cn.forevermc.launcher.ForeverMC;
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.bean.OpenJDK;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.bean.RuntimeDownloadSource;
|
||||
import cn.forevermc.launcher.ctrl.pane.CanvasProgress;
|
||||
import cn.forevermc.launcher.ctrl.pane.Dialog;
|
||||
import cn.forevermc.launcher.ctrl.pane.GameOption;
|
||||
import cn.forevermc.launcher.ctrl.pane.PlayerSelect;
|
||||
import cn.forevermc.launcher.ctrl.pane.ResourceDownload;
|
||||
import cn.forevermc.launcher.service.APISettingService;
|
||||
import cn.forevermc.launcher.service.FMCLoginService;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.Path;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import com.google.gson.Gson;
|
||||
import com.imyeyu.compress.CompressType;
|
||||
import com.imyeyu.compress.Decompressor;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.io.IOSize;
|
||||
import com.imyeyu.io.IOSpeedService;
|
||||
import com.imyeyu.io.JarReader;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||
import com.imyeyu.java.bean.CallbackReturn;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.network.FileRequest;
|
||||
import com.imyeyu.network.Network;
|
||||
import com.imyeyu.network.ProgressiveRequest;
|
||||
import com.imyeyu.network.TimiRequest;
|
||||
import com.imyeyu.utils.Digest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import com.imyeyu.utils.StringInterpolator;
|
||||
import com.imyeyu.utils.Text;
|
||||
import com.imyeyu.utils.Time;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.paint.Paint;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.Cleanup;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.fluent.Request;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 启动器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 13:20
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class Launcher implements OS.FileSystem {
|
||||
|
||||
/**
|
||||
* 状态,目前仅内部使用
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-16 15:29
|
||||
*/
|
||||
private enum Status {
|
||||
|
||||
/** 空闲 */
|
||||
IDLE,
|
||||
|
||||
/** 初始化 */
|
||||
INITIALIZING,
|
||||
|
||||
/** 基础检查 */
|
||||
BASE_CHECKING,
|
||||
|
||||
/** 解析版本包 */
|
||||
PARSING_PACK,
|
||||
|
||||
/** 账号检查 */
|
||||
ACCOUNT_AUTH,
|
||||
|
||||
/** 启动玩家选择 */
|
||||
PLAYER_SELECT,
|
||||
|
||||
/** 资源下载 */
|
||||
RESOURCE_DOWNLOAD,
|
||||
|
||||
/** Java 下载 */
|
||||
JAVA_DOWNLOAD,
|
||||
|
||||
/** JNA 检查 */
|
||||
JNA_CHECK,
|
||||
|
||||
/** 构建启动命令 */
|
||||
BUILD_COMMAND,
|
||||
|
||||
/** 执行启动 */
|
||||
LAUNCHING,
|
||||
|
||||
/** 等待游戏 */
|
||||
WAITING_GAME,
|
||||
|
||||
/** 超时 */
|
||||
TIMEOUT,
|
||||
|
||||
/** 中断 */
|
||||
INTERRUPT,
|
||||
|
||||
/** 成功 */
|
||||
SUCCEED
|
||||
}
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
@Inject
|
||||
private Stage stage;
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private GameOption gameOption;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private PlayerSelect playerSelect;
|
||||
|
||||
@Inject
|
||||
private CanvasProgress canvasProgress;
|
||||
|
||||
@Inject
|
||||
private FMCLoginService fmcLoginService;
|
||||
|
||||
@Inject
|
||||
private ResourceDownload resourceDownload;
|
||||
|
||||
@Inject
|
||||
private APISettingService apiSettingService;
|
||||
|
||||
@Inject
|
||||
private IOSpeedService.Item ioSpeedItem;
|
||||
|
||||
private final ObjectProperty<Status> status;
|
||||
|
||||
/** 线程锁 */
|
||||
private final Object locker;
|
||||
|
||||
public Launcher() {
|
||||
status = new SimpleObjectProperty<>(Status.IDLE);
|
||||
locker = new Object();
|
||||
|
||||
// 根据状态切换页面
|
||||
status.addListener((obs, o, newStatus) -> {
|
||||
if (newStatus != null) {
|
||||
switch (newStatus) {
|
||||
case IDLE -> pageService.to(Page.MENU);
|
||||
case INITIALIZING -> {
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("loading"));
|
||||
dialog.eject();
|
||||
}
|
||||
case ACCOUNT_AUTH -> dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.account.checking"));
|
||||
case PLAYER_SELECT -> pageService.to(Page.PLAYER_SELECT);
|
||||
case RESOURCE_DOWNLOAD -> pageService.to(Page.RESOURCE_DOWNLOAD);
|
||||
case JAVA_DOWNLOAD -> {
|
||||
RuntimeDownloadSource source = config.getMain().getRuntimeDownloadSource().get();
|
||||
String requiredRTVer = gameService.getRequiredRTVer(launchGame.get());
|
||||
log.info("required jre runtime ver.{} and download from {}", source, requiredRTVer);
|
||||
canvasProgress.setTask(new RunAsync<String>() {
|
||||
|
||||
/** 镜像地址 */
|
||||
static final String MIRROR_API = Resources.TIMI_SERVER_API + "/mirror/%s";
|
||||
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
progress(0);
|
||||
OpenJDK jre = getDownloadJRE(source, requiredRTVer);
|
||||
String url = getDownloadURL(source, jre);
|
||||
long jreLength = FileRequest.wrap(Request.head(url)).length();
|
||||
String fileName = "jre" + jre.getVersion() + "." + Network.uriFileExtension(jre.getName());
|
||||
File compressedFile = new File("etc" + SEP + fileName);
|
||||
if (!compressedFile.exists() || compressedFile.length() != jreLength) {
|
||||
log.info("downloading jre: {}", url);
|
||||
update("DOWNLOAD");
|
||||
ProgressiveRequest.get(url, (total, read, now) -> {
|
||||
ioSpeedItem.push(now);
|
||||
progress(1D * read / jreLength);
|
||||
return !isInterrupted;
|
||||
}).toFile(compressedFile);
|
||||
}
|
||||
if (!isInterrupted && compressedFile.exists()) {
|
||||
update("DECOMPRESS");
|
||||
log.info("decompressing jre: {}", compressedFile.getAbsolutePath());
|
||||
{
|
||||
File jreDir = IO.dir("jre" + jre.getVersion());
|
||||
Decompressor decompressor = CompressType.fromFile(compressedFile);
|
||||
decompressor.setFileCallback(file -> message(file.getName()));
|
||||
decompressor.setProgressCallback(this::progress);
|
||||
decompressor.run(compressedFile, jreDir.getAbsolutePath());
|
||||
IO.destroy(compressedFile);
|
||||
|
||||
File[] jreFiles = jreDir.listFiles();
|
||||
if (jreFiles != null && jreFiles.length == 1) {
|
||||
// 存在根文件夹
|
||||
File[] files = jreFiles[0].listFiles();
|
||||
if (TimiJava.isEmpty(files)) {
|
||||
throw new TimiException(TimiCode.ERROR, "TODO empty jre files");
|
||||
}
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
IO.move(files[i], jreDir.getAbsolutePath());
|
||||
}
|
||||
IO.destroy(jreFiles[0]);
|
||||
}
|
||||
}
|
||||
log.info("decompress done");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdate(String status) {
|
||||
if (TimiJava.isNotEmpty(status)) {
|
||||
CallbackArg<Double> dlSpeed = d -> Platform.runLater(() -> canvasProgress.getSubLabel().setText(IOSize.format(d, IOSize.Unit.MB) + "/s"));
|
||||
switch (status) {
|
||||
case "DOWNLOAD" -> {
|
||||
ioSpeedItem.addBufferListener(dlSpeed);
|
||||
|
||||
canvasProgress.setCanCancel(true);
|
||||
canvasProgress.setBackgroundColor(TimiFXUI.Colorful.BLACK);
|
||||
canvasProgress.setColor(Paint.valueOf("#AAA"));
|
||||
canvasProgress.reset();
|
||||
}
|
||||
case "DECOMPRESS" -> {
|
||||
ioSpeedItem.removeBufferListener(dlSpeed);
|
||||
|
||||
canvasProgress.setCanCancel(false);
|
||||
canvasProgress.setBackgroundColor(Paint.valueOf("#AAA"));
|
||||
canvasProgress.setColor(Paint.valueOf("#0A0"));
|
||||
canvasProgress.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
// TODO 失败处理
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinally() {
|
||||
// 释放线程
|
||||
log.info("runtime download finished and unlocked launcher thread");
|
||||
synchronized (locker) {
|
||||
locker.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下载链接
|
||||
*
|
||||
* @param source 下载源
|
||||
* @param jreVer 需求运行时版本
|
||||
* @return 下载链接
|
||||
* @throws Exception 获取异常
|
||||
*/
|
||||
private OpenJDK getDownloadJRE(RuntimeDownloadSource source, String jreVer) throws Exception {
|
||||
List<OpenJDK> jdkList = TimiRequest.<List<OpenJDK>>get(MIRROR_API.formatted(source.getMirror())).result();
|
||||
for (int i = 0; i < jdkList.size(); i++) {
|
||||
OpenJDK jdk = jdkList.get(i);
|
||||
if (jdk.getPlatform() == OS.PLATFORM && jdk.getType() == OpenJDK.Type.JRE && jdk.getVersion().equals(jreVer)) {
|
||||
return jdk;
|
||||
}
|
||||
}
|
||||
throw new TimiException(TimiCode.ERROR).msgKey("fmc.launcher.rt.not_support");
|
||||
}
|
||||
|
||||
private String getDownloadURL(RuntimeDownloadSource source, OpenJDK jre) {
|
||||
return switch (source) {
|
||||
case FOREVER_MC -> Resources.TIMI_SERVER_API + "/attachment/download/" + jre.getData();
|
||||
case TUNA, GITHUB -> jre.getData();
|
||||
};
|
||||
}
|
||||
});
|
||||
canvasProgress.setCanCancel(true);
|
||||
canvasProgress.setBackgroundColor(TimiFXUI.Colorful.BLACK);
|
||||
canvasProgress.setColor(Paint.valueOf("#AAA"));
|
||||
canvasProgress.getLabel().setText(TimiFXUI.MULTILINGUAL.text("fmc.launcher.rt.download.tips", source.getName()));
|
||||
pageService.to(Page.CANVAS_PROGRESS);
|
||||
}
|
||||
case JNA_CHECK -> {
|
||||
dialog.setTitle("");
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.launching"));
|
||||
dialog.eject();
|
||||
}
|
||||
case WAITING_GAME -> {
|
||||
if (launchGame.get().isLegacy()) {
|
||||
stage.getIcons().setAll(Resources.ICON_MC_ANCIENT);
|
||||
} else {
|
||||
stage.getIcons().setAll(Resources.ICON_MC);
|
||||
}
|
||||
pageService.to(Page.LAUNCHING);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 执行启动 */
|
||||
public void launch() {
|
||||
if (status.get() != Status.IDLE) {
|
||||
throw new TimiException(TimiCode.ERROR_SERVICE_BUSY, "launcher not in idle");
|
||||
}
|
||||
log.info("initializing launcher..");
|
||||
|
||||
if (status.isBound()) {
|
||||
status.unbind();
|
||||
}
|
||||
status.set(Status.INITIALIZING);
|
||||
RunAsync<Status> task = new RunAsync<>() {
|
||||
|
||||
@Override
|
||||
protected Status call() throws Exception {
|
||||
// 基本数据
|
||||
Game game = launchGame.get();
|
||||
|
||||
// 基本校验
|
||||
update(Status.BASE_CHECKING);
|
||||
if (game == null) {
|
||||
log.warn("not select launch game");
|
||||
throw new LauncherException(Page.GAME_SELECT, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_game"));
|
||||
}
|
||||
log.info("do launch game: {}", game.getName());
|
||||
|
||||
Game.Option packOption = game.getOption();
|
||||
if (TimiJava.isEmpty(config.getPlayer().getName())) {
|
||||
log.warn("empty player name");
|
||||
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_name"));
|
||||
}
|
||||
if (launchGame.isOnline() && TimiJava.isEmpty(config.getPlayer().getPassword())) {
|
||||
log.warn("empty player password");
|
||||
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.empty_password"));
|
||||
}
|
||||
|
||||
// 解码版本包
|
||||
log.info("parsing game");
|
||||
update(Status.PARSING_PACK);
|
||||
gameService.readMetaData(game);
|
||||
|
||||
// 验证账号
|
||||
String token = null;
|
||||
String playerName;
|
||||
FMCLoginService.MinecraftPlayer player = null;
|
||||
{
|
||||
if (launchGame.isOffline()) {
|
||||
// 离线
|
||||
log.info("skip account check for offline mode");
|
||||
playerName = config.getPlayer().getName().get();
|
||||
} else {
|
||||
fmcLoginService.checkLoginMod(game);
|
||||
// 账号验证
|
||||
log.info("{}: account checking..", packOption.getLoginType());
|
||||
update(Status.ACCOUNT_AUTH);
|
||||
String user = config.getPlayer().getName().get();
|
||||
String password = config.getPlayer().getPassword().get();
|
||||
|
||||
FMCLoginService.TokenResponse resp = fmcLoginService.genLoginToken(user, password);
|
||||
List<FMCLoginService.MinecraftPlayer> playerList = fmcLoginService.listBoundPlayer(resp.getToken());
|
||||
if (playerList.isEmpty()) {
|
||||
log.warn("not bind minecraft player");
|
||||
throw new LauncherException(Page.MENU, "not bind minecraft player");
|
||||
}
|
||||
if (1 < playerList.size()) {
|
||||
FMCLoginService.MinecraftPlayer playerByName = null;
|
||||
for (int i = 0; i < playerList.size(); i++) {
|
||||
if (playerList.get(i).getName().equals(user)) {
|
||||
playerByName = playerList.get(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (playerByName != null) {
|
||||
player = playerByName;
|
||||
} else {
|
||||
// 选择启动账号
|
||||
playerSelect.getItems().setAll(playerList);
|
||||
playerSelect.setOnCloseEvent(() -> {
|
||||
synchronized (locker) {
|
||||
log.info("selected player and unlocked launcher thread");
|
||||
locker.notifyAll();
|
||||
}
|
||||
});
|
||||
update(Status.PLAYER_SELECT);
|
||||
// 锁定线程
|
||||
log.info("locked launcher thread before selected player");
|
||||
synchronized (locker) {
|
||||
locker.wait();
|
||||
}
|
||||
switch (playerSelect.getAction()) {
|
||||
case CONFIRM -> player = playerSelect.getSelected();
|
||||
case CANCEL -> {
|
||||
return Status.INTERRUPT;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 仅有一个启动账号
|
||||
player = playerList.getFirst();
|
||||
}
|
||||
// 登录
|
||||
FMCLoginService.TokenResponse loginResp = fmcLoginService.doLogin(player, resp.getToken());
|
||||
playerName = player.getName();
|
||||
token = loginResp.getToken();
|
||||
}
|
||||
}
|
||||
|
||||
// 下载资源
|
||||
{
|
||||
if (TimiJava.isNotEmpty(game.getMetaData().getDownloadList())) {
|
||||
log.info("starting resources download");
|
||||
update(Status.RESOURCE_DOWNLOAD);
|
||||
resourceDownload.setDownloadDeque(game.getMetaData().getDownloadList());
|
||||
resourceDownload.setOnFinallyEvent(() -> {
|
||||
log.info("resources download finished and unlocked launcher thread");
|
||||
synchronized (locker) {
|
||||
locker.notifyAll();
|
||||
}
|
||||
});
|
||||
resourceDownload.start();
|
||||
log.info("locked launcher thread before resources download finish");
|
||||
// 锁定线程
|
||||
synchronized (locker) {
|
||||
locker.wait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Java 下载
|
||||
File validJRE;
|
||||
CallbackReturn<File> installJava = () -> {
|
||||
try {
|
||||
log.info("finding java..");
|
||||
String jreVer = gameService.getRequiredRTVer(game);
|
||||
File javaw = new File("jre" + jreVer + SEP + "bin" + SEP + (OS.IS_WINDOWS ? "javaw.exe" : "java"));
|
||||
if (javaw.exists() && javaw.isFile() && 0 < javaw.length()) {
|
||||
log.info("found at {}", javaw.getAbsolutePath());
|
||||
return javaw;
|
||||
}
|
||||
log.info("fetch jre list from api..");
|
||||
// 下载
|
||||
update(Status.JAVA_DOWNLOAD);
|
||||
// 锁定线程
|
||||
log.info("locked launcher thread before runtime download finish");
|
||||
synchronized (locker) {
|
||||
locker.wait();
|
||||
}
|
||||
return javaw;
|
||||
} catch (Exception e) {
|
||||
log.error("download java error", e);
|
||||
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.java.error"));
|
||||
}
|
||||
};
|
||||
if (TimiJava.isEmpty(game.getOption().getJava().get())) {
|
||||
validJRE = installJava.handler();
|
||||
} else {
|
||||
File java = new File(game.getOption().getJava().get());
|
||||
if (java.exists()) {
|
||||
validJRE = java;
|
||||
} else {
|
||||
validJRE = installJava.handler();
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 JNA
|
||||
update(Status.JNA_CHECK);
|
||||
log.info("checking JNA..");
|
||||
{
|
||||
File natives = IO.dir(Path.P_PACK + game.getMetaData().getVersion() + SEP + "natives");
|
||||
final String jnaFormat = OS.IS_WINDOWS ? ".dll" : ".so";
|
||||
JarReader jarReader;
|
||||
Map<String, InputStream> jarFiles;
|
||||
List<Game.MetaData.Libraries> jnaList = game.getMetaData().getLibrarieList().stream().filter(Game.MetaData.Libraries::isNatives).toList();
|
||||
for (int i = 0; i < jnaList.size(); i++) {
|
||||
// 读取 JNA jar
|
||||
File file = new File(Path.P_LIB + jnaList.get(i).getPath());
|
||||
jarReader = new JarReader(file);
|
||||
jarFiles = jarReader.getFiles();
|
||||
// 获取所有文件
|
||||
for (Map.Entry<String, InputStream> jarFile : jarFiles.entrySet()) {
|
||||
String path = jarFile.getKey();
|
||||
if (path.endsWith(jnaFormat)) {
|
||||
// 获取 JNA
|
||||
String name;
|
||||
if (path.contains(SEP)) {
|
||||
name = path.substring(path.indexOf("/"));
|
||||
} else {
|
||||
name = path;
|
||||
}
|
||||
// 校验 SHA1
|
||||
byte[] bytes = jarFile.getValue().readAllBytes();
|
||||
String jarSHA1 = Digest.sha1(bytes);
|
||||
File jnaFile = new File(natives.getAbsolutePath() + SEP + name);
|
||||
if (!jnaFile.exists() || !Digest.sha1(IO.toBytes(jnaFile)).equals(jarSHA1)) {
|
||||
// JNA 异常,重新导出
|
||||
IO.toFile(jnaFile, new ByteArrayInputStream(bytes));
|
||||
}
|
||||
}
|
||||
}
|
||||
jarReader.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 构建启动命令
|
||||
update(Status.BUILD_COMMAND);
|
||||
log.info("building launch command..");
|
||||
StringBuilder command = new StringBuilder();
|
||||
{
|
||||
// 依赖 classpath
|
||||
String classpath;
|
||||
{
|
||||
StringBuilder classpathBuilder = new StringBuilder();
|
||||
// 一般依赖
|
||||
List<Game.MetaData.Libraries> classpathList = game.getMetaData().getLibrarieList().stream().filter(Game.MetaData.Libraries::isNotNatives).distinct().toList();
|
||||
for (int i = 0; i < classpathList.size(); i++) {
|
||||
classpathBuilder.append(Path.ABS_LIB).append(classpathList.get(i).getPath()).append(';');
|
||||
}
|
||||
// 上古依赖
|
||||
File libDir = new File(IO.fitPath(game.getPath().getAbsolutePath()) + "lib");
|
||||
if (libDir.exists() && libDir.isDirectory()) {
|
||||
File[] libs = libDir.listFiles();
|
||||
if (libs != null) {
|
||||
for (int i = 0; i < libs.length; i++) {
|
||||
if (libs[i].getName().endsWith(".jar")) {
|
||||
classpathBuilder.append(libs[i].getAbsolutePath()).append(';');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 启动核心
|
||||
classpathBuilder.append(Path.ABS_PACK).append(game.getMetaData().getVersion()).append(SEP).append(game.getMetaData().getVersion()).append(".jar");
|
||||
classpath = classpathBuilder.toString();
|
||||
}
|
||||
CallbackArgReturn<String, String> escapePath = p -> {
|
||||
if (p.endsWith("\\")) {
|
||||
return Text.quote(p.substring(0, p.length() - 1)).replace("\\", "\\\\");
|
||||
} else {
|
||||
return Text.quote(p).replace("\\", "\\\\");
|
||||
}
|
||||
};
|
||||
// 启动参数
|
||||
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("classpath", escapePath.handler(classpath));
|
||||
args.put("auth_player_name", Text.quote(playerName));
|
||||
args.put("version_name", Text.quote(game.getMetaData().getVersion()));
|
||||
args.put("game_directory", escapePath.handler(Path.ABS_PACK + game.getName())); // 版本隔离,指向详细版本目录
|
||||
args.put("assets_root", escapePath.handler(Path.ABS_ASSETS));
|
||||
args.put("game_assets", escapePath.handler(Path.ABS_ASSETS)); // 1.6 以下兼容
|
||||
args.put("assets_index_name", game.getMetaData().getAssetsVersion());
|
||||
args.put("auth_uuid", uuid);
|
||||
args.put("auth_access_token", uuid);
|
||||
args.put("auth_session", uuid); // 1.6 以下兼容
|
||||
args.put("clientid", config.getLauncher().getClientId().get());
|
||||
args.put("auth_xuid", 0);
|
||||
args.put("user_type", "Legacy");
|
||||
args.put("version_type", Text.quote("Forever MC"));
|
||||
args.put("natives_directory", escapePath.handler(Path.ABS_PACK + game.getMetaData().getVersion() + SEP + "natives"));
|
||||
args.put("launcher_name", Text.quote("Forever MC"));
|
||||
args.put("launcher_version", ForeverMC.VERSION);
|
||||
args.put("quickPlayMultiplayer", Text.quote(game.getOption().getServer().get()));
|
||||
// JRE
|
||||
command.append('"').append(validJRE.getAbsolutePath()).append('"');
|
||||
{
|
||||
// 参数插值
|
||||
StringInterpolator interpolator = new StringInterpolator(StringInterpolator.DOLLAR_OBJ);
|
||||
// 内存参数
|
||||
command.append(" -Xmn256m -Xmx").append(config.getLauncher().getMemory().intValue()).append('M');
|
||||
if (TimiJava.isNotEmpty(game.getMetaData().getArgsJVM())) {
|
||||
// JVM 参数
|
||||
command.append(' ').append(interpolator.inject(game.getMetaData().getArgsJVM(), args));
|
||||
}
|
||||
// 主类
|
||||
command.append(' ').append(game.getMetaData().getMainClass());
|
||||
// 游戏参数
|
||||
command.append(' ').append(interpolator.inject(game.getMetaData().getArgsGame(), args));
|
||||
}
|
||||
// 令牌
|
||||
if (TimiJava.isNotEmpty(token)) {
|
||||
command.append(" --token ").append(Text.quote(token));
|
||||
}
|
||||
// 自动进入服务器(上古版本的方式,新版本会在 quickPlayMultiplayer 参数生效)
|
||||
String server = game.getOption().getServer().get();
|
||||
if (game.getOption().getAutoConnect().get() && TimiJava.isNotEmpty(server)) {
|
||||
command.append(" --server ");
|
||||
int addressSplit = server.indexOf(":");
|
||||
if (addressSplit == -1) {
|
||||
command.append(Text.quote(server)).append(" --port \"25565\"");
|
||||
} else {
|
||||
String[] address = server.split(":");
|
||||
command.append(address[0]).append(" --port ").append(Text.quote(address[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行启动命令
|
||||
update(Status.LAUNCHING);
|
||||
StringBuilder infoLog = new StringBuilder(); // 辅助检测启动
|
||||
log.info("do launch: {}", command);
|
||||
{
|
||||
IO.toFile(IO.file("etc" + SEP + "latest-script.bat"), command.toString());
|
||||
Runtime runtime = Runtime.getRuntime();
|
||||
Process process = runtime.exec(command.toString());
|
||||
// 必须把数据流读出来,否则填满缓冲区会导致阻塞,以至超长时间无启动
|
||||
new Thread(() -> {
|
||||
try {
|
||||
@Cleanup InputStream infoIS = process.getInputStream();
|
||||
@Cleanup InputStreamReader infoISR = new InputStreamReader(infoIS);
|
||||
@Cleanup BufferedReader infoBR = new BufferedReader(infoISR);
|
||||
|
||||
String line;
|
||||
infoLog.setLength(0);
|
||||
while ((line = infoBR.readLine()) != null) {
|
||||
infoLog.append(line).append(System.lineSeparator());
|
||||
}
|
||||
log.info("process info log: \n{}", infoLog);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}).start();
|
||||
new Thread(() -> {
|
||||
try {
|
||||
@Cleanup InputStream errorIS = process.getErrorStream();
|
||||
@Cleanup InputStreamReader errorISR = new InputStreamReader(errorIS);
|
||||
@Cleanup BufferedReader errorBR = new BufferedReader(errorISR);
|
||||
|
||||
String line;
|
||||
StringBuilder errorLog = new StringBuilder();
|
||||
while ((line = errorBR.readLine()) != null) {
|
||||
errorLog.append(line).append(System.lineSeparator());
|
||||
}
|
||||
// TODO 启动失败结束线程
|
||||
log.error("process error log: \n{}", errorLog);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
// 等待游戏启动
|
||||
update(Status.WAITING_GAME);
|
||||
{
|
||||
long startAt = Time.now();
|
||||
String title = "Launching.. " + game.getMetaData().getTitle();
|
||||
Platform.runLater(() -> stage.setTitle(title));
|
||||
|
||||
String logFlag = "Setting user: " + playerName;
|
||||
if (game.isLegacy()) {
|
||||
logFlag = "OpenAL initialized";
|
||||
}
|
||||
for (int i = 0; i < 64; i++) {
|
||||
Thread.sleep(1000);
|
||||
// 通过窗体检测
|
||||
if (OS.IS_WINDOWS && OS.findProcess4Similarity("javaw.exe", game.getMetaData().getTitle(), title)) {
|
||||
log.info("found the game window in {} ms", Time.now() - startAt);
|
||||
return Status.SUCCEED;
|
||||
}
|
||||
// 通过日志检测
|
||||
if (infoLog.indexOf(logFlag) != -1) {
|
||||
log.info("\"{}\" found in the log indicates successful launch in {} ms", logFlag, Time.now() - startAt);
|
||||
return Status.SUCCEED;
|
||||
}
|
||||
}
|
||||
}
|
||||
log.warn("launcher timeout, not found window of title: {}", game.getMetaData().getTitle());
|
||||
return Status.TIMEOUT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(Status status) {
|
||||
switch (status) {
|
||||
case INTERRUPT -> update(Status.IDLE);
|
||||
case SUCCEED -> stage.close();
|
||||
case TIMEOUT -> {
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.timeout"));
|
||||
dialog.setConfirmEvent(() -> pageService.to(Page.MENU));
|
||||
dialog.eject();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
dialog.setContent(e.getMessage());
|
||||
dialog.setCloseEvent(() -> update(Status.IDLE));
|
||||
if (e instanceof LauncherException le) {
|
||||
log.info("launch exception: {}", e.getMessage());
|
||||
dialog.setCloseEvent(() -> {
|
||||
update(Status.IDLE);
|
||||
pageService.to(le.toPage);
|
||||
});
|
||||
if (le.toPageEvent != null) {
|
||||
le.toPageEvent.handler();
|
||||
}
|
||||
} else {
|
||||
log.info("launch error", e);
|
||||
}
|
||||
dialog.eject();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinally() {
|
||||
log.info("launcher on finally");
|
||||
synchronized (locker) {
|
||||
locker.notifyAll();
|
||||
}
|
||||
}
|
||||
};
|
||||
status.bind(task.valueProperty());
|
||||
task.messageProperty().addListener((obs, o, newMsg) -> dialog.setContent(newMsg));
|
||||
task.start();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package cn.forevermc.launcher.core;
|
||||
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
|
||||
/**
|
||||
* 启动异常
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-06 20:01
|
||||
*/
|
||||
public class LauncherException extends TimiException {
|
||||
|
||||
/** 异常跳转页面 */
|
||||
Page toPage;
|
||||
|
||||
/** 跳转页面事件 */
|
||||
Callback toPageEvent;
|
||||
|
||||
/**
|
||||
* 简单构造器
|
||||
*
|
||||
* @param toPage 异常跳转页面
|
||||
* @param message 提示消息
|
||||
*/
|
||||
public LauncherException(Page toPage, String message) {
|
||||
this(null, toPage, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认构造器
|
||||
*
|
||||
* @param toPageEvent 跳转前回调
|
||||
* @param toPage 异常跳转页面
|
||||
* @param message 提示消息
|
||||
*/
|
||||
public LauncherException(Callback toPageEvent, Page toPage, String message) {
|
||||
super(TimiCode.ERROR, message);
|
||||
this.toPage = toPage;
|
||||
this.toPageEvent = toPageEvent;
|
||||
}
|
||||
}
|
||||
94
src/main/java/cn/forevermc/launcher/ctrl/Main.java
Normal file
@ -0,0 +1,94 @@
|
||||
package cn.forevermc.launcher.ctrl;
|
||||
|
||||
import cn.forevermc.launcher.ForeverMC;
|
||||
import cn.forevermc.launcher.bean.APISetting;
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.Sound;
|
||||
import cn.forevermc.launcher.view.ViewMain;
|
||||
import com.google.gson.Gson;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.stage.Stage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.inject.TimiInject;
|
||||
import com.imyeyu.inject.annotation.IOCReturn;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.io.IOSpeedService;
|
||||
|
||||
import java.awt.SplashScreen;
|
||||
|
||||
/**
|
||||
* 主控
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-18 15:37
|
||||
*/
|
||||
@Slf4j
|
||||
@SuperInject
|
||||
public class Main extends ViewMain {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
@Inject
|
||||
private APISetting apiSetting;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
@Inject
|
||||
private ObjectProperty<MediaPlayer> bgmPlayer;
|
||||
|
||||
private Stage stage;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
this.stage = stage;
|
||||
|
||||
|
||||
TimiInject.run(ForeverMC.getInjectApp()).ioc(this);
|
||||
|
||||
pageService.to(Page.MENU);
|
||||
super.start(stage);
|
||||
|
||||
stage.show();
|
||||
|
||||
if (SplashScreen.getSplashScreen() != null) {
|
||||
SplashScreen.getSplashScreen().close();
|
||||
}
|
||||
|
||||
// 音效音量
|
||||
Sound.CLICK.volumeProperty().bind(config.getMain().getSoundVolume().divide(100));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
log.info("doing shutdown..");
|
||||
stage.close();
|
||||
if (launchGame.get() != null) {
|
||||
gameService.storeOption(launchGame.get());
|
||||
}
|
||||
ForeverMC.getConfigLoader().dump();
|
||||
IOSpeedService.getInstance().shutdown();
|
||||
}
|
||||
|
||||
/** 主窗体 */
|
||||
@IOCReturn
|
||||
public Stage stage() {
|
||||
return stage;
|
||||
}
|
||||
}
|
||||
53
src/main/java/cn/forevermc/launcher/ctrl/pane/About.java
Normal file
@ -0,0 +1,53 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.pages.AboutPane;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
|
||||
/**
|
||||
* 关于
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-25 15:02
|
||||
*/
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class About extends AboutPane {
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
public About() {
|
||||
close.setOnAction(e -> pageService.to(Page.MENU));
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
launchGame.addListener((obs, o, newLaunchGame) -> {
|
||||
if (newLaunchGame == null) {
|
||||
title.setImage(Resources.TITLE);
|
||||
} else {
|
||||
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(newLaunchGame);
|
||||
if (realVersion.isModernStyle()) {
|
||||
title.setImage(Resources.TITLE_NEW);
|
||||
} else {
|
||||
title.setImage(Resources.TITLE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,219 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.ctrl.Main;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.view.pages.CanvasProgressPane;
|
||||
import com.google.gson.Gson;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.paint.Paint;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.utils.AnimationRenderer;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.utils.OS;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 画板进度
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-15 17:49
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class CanvasProgress extends CanvasProgressPane implements OS.FileSystem {
|
||||
|
||||
private static final String PACK_CACHE_PATH = "etc" + SEP + "pack";
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Main main;
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private ObjectProperty<MediaPlayer> bgmPlayer;
|
||||
|
||||
private final List<Pixel> pixelList;
|
||||
private final DoubleProperty progress;
|
||||
private final AnimationRenderer renderer;
|
||||
private final ObjectProperty<Paint> color;
|
||||
private final ObjectProperty<Paint> backgroundColor;
|
||||
|
||||
@Getter
|
||||
private RunAsync<?> task;
|
||||
|
||||
private int i = 0;
|
||||
|
||||
public CanvasProgress() {
|
||||
pixelList = new ArrayList<>();
|
||||
progress = new SimpleDoubleProperty(0);
|
||||
color = new SimpleObjectProperty<>(Colorful.PINK);
|
||||
backgroundColor = new SimpleObjectProperty<>(Colorful.BLACK);
|
||||
|
||||
// 图形进度
|
||||
canvas.visibleProperty().bind(progress.isNotEqualTo(-1));
|
||||
canvas.managedProperty().bind(canvas.visibleProperty());
|
||||
|
||||
int unitSize = 4;
|
||||
int center = (SIZE - 4) / 2;
|
||||
// 层级
|
||||
int layerMax = center / unitSize;
|
||||
for (int layer = 0; layer <= layerMax; layer++) {
|
||||
// 上
|
||||
for (int i = -layer + 1; i <= layer; i++) {
|
||||
int x = center + i * unitSize;
|
||||
int y = center - layer * unitSize;
|
||||
pixelList.add(new Pixel(x, y));
|
||||
}
|
||||
// 右
|
||||
for (int i = -layer; i <= layer; i++) {
|
||||
int x = center + layer * unitSize;
|
||||
int y = center + i * unitSize;
|
||||
pixelList.add(new Pixel(x, y));
|
||||
}
|
||||
// 下
|
||||
for (int i = -layer; i <= layer; i++) {
|
||||
int x = center - i * unitSize;
|
||||
int y = center + layer * unitSize;
|
||||
pixelList.add(new Pixel(x, y));
|
||||
}
|
||||
// 左
|
||||
for (int i = -layer; i <= layer + 1; i++) {
|
||||
int x = center - layer * unitSize;
|
||||
int y = center - i * unitSize;
|
||||
pixelList.add(new Pixel(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
// 图形进度渲染
|
||||
renderer = new AnimationRenderer();
|
||||
renderer.addRenderCallback(aDouble -> {
|
||||
while (1D * i / pixelList.size() < progress.get() && i < pixelList.size()) {
|
||||
Pixel pixel = pixelList.get(i);
|
||||
g.setFill(color.get());
|
||||
g.fillRect(pixel.x, pixel.y, 4, 4);
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
// 背景音乐
|
||||
bgmToggle.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
if (bgmPlayer.get() == null) {
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
bgmToggle.selectedProperty().addListener((obs, o, isSelected) -> {
|
||||
MediaPlayer player = bgmPlayer.get();
|
||||
if (player != null) {
|
||||
if (isSelected) {
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 取消
|
||||
cancel.setOnAction(e -> task.interrupt());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
reset();
|
||||
bgmToggle.visibleProperty().bind(config.getMain().getBgmVolume().greaterThan(0));
|
||||
renderer.start();
|
||||
|
||||
if (progress.isBound()) {
|
||||
progress.unbind();
|
||||
}
|
||||
log.info("start downloading: ");
|
||||
task.messageProperty().addListener((obs, o, compressedFile) -> subLabel.setText(compressedFile));
|
||||
progress.bind(task.progressProperty());
|
||||
task.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHide() {
|
||||
renderer.stop();
|
||||
progress.unbind();
|
||||
label.setText("");
|
||||
subLabel.setText("");
|
||||
}
|
||||
|
||||
public void reset() {
|
||||
i = 0;
|
||||
{
|
||||
g.setFill(backgroundColor.get());
|
||||
g.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
g.fillRect(0, 0, canvas.getWidth(), canvas.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
public <T> void setTask(RunAsync<T> task) {
|
||||
this.task = task;
|
||||
}
|
||||
|
||||
public void setCanCancel(boolean canCancel) {
|
||||
cancel.setDisable(!canCancel);
|
||||
}
|
||||
|
||||
public BooleanBinding canCancelProperty() {
|
||||
return cancel.disableProperty().not();
|
||||
}
|
||||
|
||||
public void setColor(Paint color) {
|
||||
this.color.set(color);
|
||||
}
|
||||
|
||||
public ObjectProperty<Paint> colorProperty() {
|
||||
return color;
|
||||
}
|
||||
|
||||
public void setBackgroundColor(Paint backgroundColor) {
|
||||
this.backgroundColor.set(backgroundColor);
|
||||
}
|
||||
|
||||
public ObjectProperty<Paint> backgroundColorProperty() {
|
||||
return backgroundColor;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-07-01 23:24
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
private static class Pixel {
|
||||
|
||||
int x;
|
||||
|
||||
int y;
|
||||
}
|
||||
}
|
||||
139
src/main/java/cn/forevermc/launcher/ctrl/pane/Dialog.java
Normal file
@ -0,0 +1,139 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.pages.DialogPane;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.Node;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
|
||||
/**
|
||||
* 会话,按钮显示属性绑定事件,存在则显示
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 17:34
|
||||
*/
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class Dialog extends DialogPane {
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
private final ObjectProperty<Callback> confirmEvent, denyEvent, cancelEvent, closeEvent;
|
||||
|
||||
public Dialog() {
|
||||
confirmEvent = new SimpleObjectProperty<>();
|
||||
denyEvent = new SimpleObjectProperty<>();
|
||||
cancelEvent = new SimpleObjectProperty<>();
|
||||
closeEvent = new SimpleObjectProperty<>();
|
||||
|
||||
confirm.visibleProperty().bind(confirmEvent.isNotNull());
|
||||
deny.visibleProperty().bind(denyEvent.isNotNull());
|
||||
cancel.visibleProperty().bind(cancelEvent.isNotNull());
|
||||
close.visibleProperty().bind(closeEvent.isNotNull());
|
||||
|
||||
confirm.setOnAction(e -> confirmEvent.get().handler());
|
||||
deny.setOnAction(e -> denyEvent.get().handler());
|
||||
cancel.setOnAction(e -> cancelEvent.get().handler());
|
||||
close.setOnAction(e -> closeEvent.get().handler());
|
||||
}
|
||||
|
||||
/** 显示会话 */
|
||||
public void eject() {
|
||||
pageService.to(Page.DIALOG);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示会话
|
||||
*
|
||||
* @param content 内容
|
||||
*/
|
||||
public void show(String content) {
|
||||
show("", content);
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示会话
|
||||
*
|
||||
* @param title 标题
|
||||
* @param content 内容
|
||||
*/
|
||||
public void show(String title, String content) {
|
||||
setTitle(title);
|
||||
setContent(content);
|
||||
setCloseEvent(() -> pageService.back());
|
||||
eject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHide() {
|
||||
title.setText("");
|
||||
content.setText("");
|
||||
confirmEvent.set(null);
|
||||
denyEvent.set(null);
|
||||
cancelEvent.set(null);
|
||||
closeEvent.set(null);
|
||||
buttonPane.getChildren().setAll(confirm, deny, cancel, close);
|
||||
}
|
||||
|
||||
/** @param title 设置标题 */
|
||||
public void setTitle(String title) {
|
||||
super.title.setText(title);
|
||||
}
|
||||
|
||||
/** @param content 设置内容 */
|
||||
public void setContent(String content) {
|
||||
super.content.setText(content);
|
||||
}
|
||||
|
||||
/** @param buttons 设置按钮 */
|
||||
public void setButtons(MCButton... buttons) {
|
||||
buttonPane.getChildren().setAll(buttons);
|
||||
}
|
||||
|
||||
/** 清除按钮 */
|
||||
public void clearButtons() {
|
||||
buttonPane.getChildren().clear();
|
||||
}
|
||||
|
||||
/** @param confirmEvent 确认事件 */
|
||||
public void setConfirmEvent(Callback confirmEvent) {
|
||||
this.confirmEvent.set(confirmEvent);
|
||||
}
|
||||
|
||||
/** @param denyEvent 拒绝事件 */
|
||||
public void setDenyEvent(Callback denyEvent) {
|
||||
this.denyEvent.set(denyEvent);
|
||||
}
|
||||
|
||||
/** @param cancelEvent 取消事件 */
|
||||
public void setCancelEvent(Callback cancelEvent) {
|
||||
this.cancelEvent.set(cancelEvent);
|
||||
}
|
||||
|
||||
/** @param closeEvent 关闭事件 */
|
||||
public void setCloseEvent(Callback closeEvent) {
|
||||
this.closeEvent.set(closeEvent);
|
||||
}
|
||||
|
||||
/** @return 自定义组件 */
|
||||
public Node getGraphic() {
|
||||
return graphic.get();
|
||||
}
|
||||
|
||||
/** @return 自定义组件属性 */
|
||||
public ObjectProperty<Node> graphicProperty() {
|
||||
return graphic;
|
||||
}
|
||||
|
||||
/** @param graphic 设置自定义组件 */
|
||||
public void setGraphic(Node graphic) {
|
||||
this.graphic.set(graphic);
|
||||
}
|
||||
}
|
||||
176
src/main/java/cn/forevermc/launcher/ctrl/pane/GameOption.java
Normal file
@ -0,0 +1,176 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.FMCLoginService;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import cn.forevermc.launcher.service.MCPopupTipsService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.FabricBuilder;
|
||||
import cn.forevermc.launcher.view.pages.GameOptionPane;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.FileSelector;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 版本配置
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-10 00:50
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class GameOption extends GameOptionPane {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private FMCLoginService fmcLoginService;
|
||||
|
||||
@Inject
|
||||
private FabricBuilder fabricBuilder;
|
||||
|
||||
@Inject
|
||||
private MCPopupTipsService mcPopupTipsService;
|
||||
|
||||
private final ObjectProperty<Game> game;
|
||||
|
||||
public GameOption() {
|
||||
game = new SimpleObjectProperty<>();
|
||||
|
||||
// 选择 Java
|
||||
javaSelect.setOnAction(e -> {
|
||||
FileSelector selector = new FileSelector(java.getText());
|
||||
selector.setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.java.select_title"));
|
||||
selector.addFilter("Java", "javaw.exe;java.exe;java");
|
||||
File file = selector.single(getScene().getWindow());
|
||||
if (file != null) {
|
||||
java.setText(file.getAbsolutePath());
|
||||
}
|
||||
});
|
||||
|
||||
// 安装 Fabric
|
||||
installFabric.disableProperty().bind(Bindings.createBooleanBinding(() -> {
|
||||
Game game = this.game.get();
|
||||
if (game == null) {
|
||||
return true;
|
||||
}
|
||||
return game.isFabric() || gameService.getRealVersion(game).isSupportFabric();
|
||||
}, game));
|
||||
installFabric.setOnAction(e -> {
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.installing"));
|
||||
dialog.eject();
|
||||
|
||||
new FabricBuilder() {
|
||||
|
||||
{
|
||||
setBuildGame(game.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(Game builedGame) {
|
||||
pageService.to(Page.GAME_SELECT);
|
||||
|
||||
if (fmcLoginService.isSupportFMCLogin(game.get())) {
|
||||
game.get().getOption().getLoginType().set(Game.Option.LoginType.FOREVER_MC);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
// TODO 出错应该返回整合包具体设置页
|
||||
dialog.setContent(e.getMessage());
|
||||
dialog.setCloseEvent(() -> pageService.to(Page.GAME_SELECT));
|
||||
dialog.eject();
|
||||
log.error("fabric install error", e);
|
||||
}
|
||||
}.start();
|
||||
});
|
||||
|
||||
// 自动连接
|
||||
autoConnect.disableProperty().bind(Bindings.createBooleanBinding(() -> TimiJava.isEmpty(server.getText()), server.textProperty()));
|
||||
server.textProperty().addListener((obs, o, newServer) -> autoConnect.setValue(TimiJava.isEmpty(newServer)));
|
||||
|
||||
// 完成
|
||||
finish.setOnAction(e -> pageService.to(Page.GAME_SELECT));
|
||||
|
||||
// 编辑版本包变更
|
||||
game.addListener((obs, oldGame, newGame) -> {
|
||||
loginType.getItems().clear();
|
||||
if (oldGame != null) {
|
||||
java.textProperty().unbindBidirectional(newGame.getOption().getJava());
|
||||
autoConnect.valueProperty().unbindBidirectional(newGame.getOption().getAutoConnect());
|
||||
server.textProperty().unbindBidirectional(newGame.getOption().getServer());
|
||||
loginType.valueProperty().unbindBidirectional(newGame.getOption().getLoginType());
|
||||
|
||||
setTitle("");
|
||||
java.clear();
|
||||
autoConnect.setValue(false);
|
||||
server.clear();
|
||||
loginType.setDisable(true);
|
||||
loginType.setValue(Game.Option.LoginType.OFFLINE);
|
||||
installFabric.disableProperty().unbind();
|
||||
}
|
||||
if (newGame != null) {
|
||||
setTitle(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.pack_option.title", newGame.getName()));
|
||||
if (fmcLoginService.isSupportFMCLogin(newGame)) {
|
||||
loginType.getItems().setAll(Game.Option.LoginType.OFFLINE, Game.Option.LoginType.FOREVER_MC);
|
||||
} else {
|
||||
loginType.setDisable(true);
|
||||
}
|
||||
java.textProperty().bindBidirectional(newGame.getOption().getJava());
|
||||
autoConnect.valueProperty().bindBidirectional(newGame.getOption().getAutoConnect());
|
||||
server.textProperty().bindBidirectional(newGame.getOption().getServer());
|
||||
loginType.valueProperty().bindBidirectional(newGame.getOption().getLoginType());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
mcPopupTipsService.install(autoConnect, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.auto_connect.tips"));
|
||||
mcPopupTipsService.install(loginType, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.login_type.tips"));
|
||||
|
||||
// TODO auto download runtime
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHide() {
|
||||
gameService.storeOption(game.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置编辑配置的版本包
|
||||
*
|
||||
* @param game 版本包
|
||||
*/
|
||||
public void setGame(Game game) {
|
||||
this.game.set(game);
|
||||
}
|
||||
}
|
||||
229
src/main/java/cn/forevermc/launcher/ctrl/pane/GameSelect.java
Normal file
@ -0,0 +1,229 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.Path;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import cn.forevermc.launcher.view.pages.GameSelectPane;
|
||||
import com.google.gson.Gson;
|
||||
import javafx.util.Duration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.task.RunAsyncScheduled;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 启动选择
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 10:22
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class GameSelect extends GameSelectPane {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private GameOption gameOption;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
/** 记录从哪个页面进入的启动选择 */
|
||||
private Page from;
|
||||
|
||||
public GameSelect() {
|
||||
// 官方原版下载
|
||||
originalDL.setOnAction(e -> pageService.to(Page.ORIGINAL_SELECT));
|
||||
|
||||
// 整合版下载
|
||||
packDL.setOnAction(e -> pageService.to(Page.PACK_SELECT));
|
||||
|
||||
// 版本设置
|
||||
option.setOnAction(e -> {
|
||||
gameOption.setGame(list.getSelectionModel().getSelectedItem());
|
||||
pageService.to(Page.PACK_OPTION);
|
||||
});
|
||||
|
||||
// 删除
|
||||
destory.setOnAction(e -> {
|
||||
Game selectedGame = list.getSelectionModel().getSelectedItem();
|
||||
dialog.setTitle(TimiFXUI.MULTILINGUAL.text("warning"));
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.launch_select.delete.tips", selectedGame.getName()));
|
||||
dialog.getTitle().setTextFill(Colorful.RED);
|
||||
|
||||
MCButton confirm = new MCButton(ComponentSize.NORMAL, TimiFXUI.MULTILINGUAL.text("confirm"));
|
||||
confirm.setDisable(true);
|
||||
MCButton cancel = new MCButton(ComponentSize.NORMAL, TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||
|
||||
// 倒数
|
||||
new RunAsyncScheduled<Integer>(Duration.seconds(1)) {
|
||||
|
||||
int value = 5;
|
||||
|
||||
@Override
|
||||
protected Integer call() {
|
||||
return value--;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(Integer value) {
|
||||
confirm.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.launch_select.delete.confirm", value));
|
||||
if (value < 0) {
|
||||
confirm.getLabel().setText(TimiFXUI.MULTILINGUAL.text("confirm"));
|
||||
confirm.setDisable(false);
|
||||
cancel();
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
confirm.setOnAction(subE -> {
|
||||
dialog.setTitle("");
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.launch_select.delete.working", selectedGame.getName()));
|
||||
dialog.clearButtons();
|
||||
new RunAsync<Void>() {
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
Thread.sleep(500);
|
||||
IO.destroy(selectedGame.getPath());
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish() {
|
||||
pageService.to(Page.GAME_SELECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
dialog.setTitle(TimiFXUI.MULTILINGUAL.text("delete.error"));
|
||||
dialog.setContent(e.getMessage());
|
||||
dialog.setCloseEvent(() -> pageService.to(Page.GAME_SELECT));
|
||||
}
|
||||
}.start();
|
||||
});
|
||||
cancel.setOnAction(subE -> pageService.to(Page.GAME_SELECT));
|
||||
dialog.setOnHideEvent(() -> {
|
||||
dialog.getTitle().setTextFill(Colorful.LIGHT_GRAY);
|
||||
dialog.setOnHideEvent(null);
|
||||
});
|
||||
dialog.setButtons(confirm, cancel);
|
||||
dialog.eject();
|
||||
});
|
||||
|
||||
// 选择
|
||||
select.setOnAction(e -> {
|
||||
if (launchGame.get() != null && launchGame.get().equals(list.getSelectionModel().getSelectedItem())) {
|
||||
pageService.to(TimiJava.firstNotNull(from, Page.OPTION));
|
||||
} else {
|
||||
launchGame.set(list.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
});
|
||||
|
||||
// 返回
|
||||
back.setOnAction(e -> pageService.to(TimiJava.firstNotNull(from, Page.OPTION)));
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
parseList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
if (pageService.getPrev() == Page.MENU || pageService.getPrev() == Page.OPTION) {
|
||||
from = pageService.getPrev();
|
||||
} else if (pageService.getPrev() == Page.DIALOG) {
|
||||
// 启动失败的弹窗
|
||||
from = Page.MENU;
|
||||
}
|
||||
parseList();
|
||||
}
|
||||
|
||||
/** 解析版本列表 */
|
||||
private void parseList() {
|
||||
setLoading(true);
|
||||
|
||||
new RunAsync<List<Game>>() {
|
||||
|
||||
/** 启动版本 */
|
||||
private Game launchGame;
|
||||
|
||||
@Override
|
||||
protected List<Game> call() throws Exception {
|
||||
List<Game> result = new ArrayList<>();
|
||||
File[] games = IO.dir(Path.P_PACK).listFiles();
|
||||
if (games != null) {
|
||||
for (int i = 0; i < games.length; i++) {
|
||||
File json = new File(IO.fitPath(games[i].getAbsolutePath()) + games[i].getName() + ".json");
|
||||
if (json.exists() && json.isFile()) {
|
||||
// 只有版本 JSON 存在时才识别为正常版本包
|
||||
try {
|
||||
Game game = gameService.buildGame(games[i]);
|
||||
if (game.getName().equals(config.getLauncher().getGame().get())) {
|
||||
this.launchGame = game;
|
||||
}
|
||||
result.add(game);
|
||||
} catch (IOException e) {
|
||||
log.error("parse pack data error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(List<Game> gameData) {
|
||||
list.getItems().setAll(gameData);
|
||||
list.getSelectionModel().select(launchGame);
|
||||
GameSelect.this.launchGame.set(launchGame);
|
||||
if (list.getItems().size() == 1) {
|
||||
list.getSelectionModel().select(0);
|
||||
GameSelect.this.launchGame.set(list.getSelectionModel().getSelectedItem());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinally() {
|
||||
setLoading(false);
|
||||
if (list.getItems().isEmpty()) {
|
||||
list.placeholderProperty().unbind();
|
||||
list.setPlaceholder(new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.empty")));
|
||||
}
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.ctrl.Main;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.view.pages.LangSelectPane;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.TimiFX;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.TimiInject;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.bean.Language;
|
||||
import com.imyeyu.utils.OS;
|
||||
|
||||
/**
|
||||
* 语言选择
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-02 00:18
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class LangSelect extends LangSelectPane implements OS.FileSystem {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Main main;
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
public LangSelect() {
|
||||
// 确认
|
||||
confirm.setOnAction(e -> {
|
||||
Language selectedLang = list.getSelectionModel().getSelectedItem();
|
||||
if (selectedLang.equals(config.getMain().getLanguage().get())) {
|
||||
// 没有修改
|
||||
pageService.to(Page.MENU);
|
||||
return;
|
||||
}
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("tips.restart"));
|
||||
dialog.setConfirmEvent(() -> {
|
||||
config.getMain().getLanguage().set(selectedLang);
|
||||
// JRE
|
||||
String jre = System.getProperty("java.home") + SEP + "bin" + SEP + "java";
|
||||
// 启动参数
|
||||
String param = " -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -jar ";
|
||||
// 启动 Jar
|
||||
String jar = IO.getJarAbsolutePath(getClass());
|
||||
// 重启
|
||||
try {
|
||||
TimiFX.doRestart(main, jre + param + jar);
|
||||
} catch (Exception ex) {
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("tips.restart.error"));
|
||||
dialog.setConfirmEvent(null);
|
||||
dialog.setCancelEvent(null);
|
||||
dialog.setCloseEvent(() -> pageService.to(Page.MENU));
|
||||
log.error("restart error", ex);
|
||||
}
|
||||
});
|
||||
dialog.setCancelEvent(() -> pageService.to(Page.MENU));
|
||||
dialog.eject();
|
||||
});
|
||||
|
||||
// 取消
|
||||
cancel.setOnAction(e -> pageService.to(Page.MENU));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
list.getItems().setAll(Language.values());
|
||||
list.getSelectionModel().select(config.getMain().getLanguage().get());
|
||||
}
|
||||
}
|
||||
82
src/main/java/cn/forevermc/launcher/ctrl/pane/Launching.java
Normal file
@ -0,0 +1,82 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.pages.LaunchingPane;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
|
||||
/**
|
||||
* 启动过渡页
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-23 17:32
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class Launching extends LaunchingPane {
|
||||
|
||||
/** 正常背景 */
|
||||
private static final Background BG_NORMAL = new BgFill("#EF323D").build();
|
||||
|
||||
/** 远古版本背景 */
|
||||
private static final Background BG_ANCIENT = BG.WHITE;
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Stage stage;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
private final FadeTransition bgTransition, mojangTransition;
|
||||
|
||||
public Launching() {
|
||||
// 背景过渡
|
||||
bgTransition = new FadeTransition(Duration.millis(500), root);
|
||||
bgTransition.setDelay(Duration.millis(500));
|
||||
bgTransition.setFromValue(0);
|
||||
bgTransition.setToValue(1);
|
||||
|
||||
// LOGO 过渡
|
||||
mojangTransition = new FadeTransition(Duration.millis(800), mojang);
|
||||
mojangTransition.setDelay(Duration.millis(1000));
|
||||
mojangTransition.setFromValue(0);
|
||||
mojangTransition.setToValue(1);
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
// Logo 图片
|
||||
mojang.imageProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (launchGame.get() == null || launchGame.get().isLegacy()) {
|
||||
return Resources.MOJANG_ANCIENT;
|
||||
}
|
||||
return Resources.MOJANG;
|
||||
}, launchGame));
|
||||
// 背景
|
||||
root.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (mojang.getImage().equals(Resources.MOJANG)) {
|
||||
return BG_NORMAL;
|
||||
}
|
||||
return BG_ANCIENT;
|
||||
}, mojang.imageProperty()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
bgTransition.play();
|
||||
mojangTransition.play();
|
||||
}
|
||||
}
|
||||
287
src/main/java/cn/forevermc/launcher/ctrl/pane/Menu.java
Normal file
@ -0,0 +1,287 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.bean.MCPopupTips;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.core.Launcher;
|
||||
import cn.forevermc.launcher.service.BGService;
|
||||
import cn.forevermc.launcher.service.FMCLoginService;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import cn.forevermc.launcher.service.MCPopupTipsService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.service.SplashService;
|
||||
import cn.forevermc.launcher.util.FabricBuilder;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.pages.MenuPane;
|
||||
import com.sun.javafx.scene.control.skin.Utils;
|
||||
import javafx.animation.ScaleTransition;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.paint.Color;
|
||||
import javafx.stage.Stage;
|
||||
import javafx.util.Duration;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.ui.MinecraftFont;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.network.Network;
|
||||
|
||||
/**
|
||||
* 菜单
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-22 00:27
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class Menu extends MenuPane {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Stage stage;
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private Launcher launcher;
|
||||
|
||||
@Inject
|
||||
private BGService bgService;
|
||||
|
||||
@Inject
|
||||
private GameOption gameOption;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
@Inject
|
||||
private FMCLoginService fmcLoginService;
|
||||
|
||||
@Inject
|
||||
private SplashService splashService;
|
||||
|
||||
@Inject
|
||||
private MCPopupTipsService mcPopupTipsService;
|
||||
|
||||
private final AutoLaunchService autoLaunchService;
|
||||
|
||||
public Menu() {
|
||||
autoLaunchService = new AutoLaunchService();
|
||||
|
||||
// 闪烁标语
|
||||
GraphicsContext g = splashCanvas.getGraphicsContext2D();
|
||||
g.setFill(Colorful.YELLOW);
|
||||
g.setFont(MinecraftFont.X32());
|
||||
g.setEffect(new DropShadow(0, 1, 1, Color.BLACK));
|
||||
g.setTextBaseline(VPos.TOP);
|
||||
splashCanvas.widthProperty().bind(Bindings.createDoubleBinding(() -> {
|
||||
if (splash.isEmpty().get()) {
|
||||
return 1D;
|
||||
}
|
||||
return Utils.computeTextWidth(g.getFont(), splash.get(), -1);
|
||||
}, splash));
|
||||
ScaleTransition transition = new ScaleTransition(Duration.millis(300), splashCanvas);
|
||||
transition.setFromX(.9);
|
||||
transition.setFromY(.9);
|
||||
transition.setToX(1);
|
||||
transition.setToY(1);
|
||||
transition.setCycleCount(Timeline.INDEFINITE);
|
||||
transition.setAutoReverse(true);
|
||||
transition.play();
|
||||
|
||||
// 账号管理
|
||||
fmcAccount.visibleProperty().bind(loginTypeSelector.valueProperty().isEqualTo(Game.Option.LoginType.FOREVER_MC));
|
||||
fmcAccount.setOnAction(e -> Network.openURIInBrowser("https://space.imyeyu.net"));
|
||||
|
||||
// 服务器状态
|
||||
fmcServer.visibleProperty().bind(loginTypeSelector.valueProperty().isEqualTo(Game.Option.LoginType.FOREVER_MC));
|
||||
|
||||
// 启动版本管理
|
||||
launchGameSelect.setOnAction(e -> pageService.to(Page.GAME_SELECT));
|
||||
|
||||
// 启动
|
||||
launch.setOnAction(e -> {
|
||||
if (autoLaunchService.isRunning()) {
|
||||
autoLaunchService.isCancel = true;
|
||||
return;
|
||||
}
|
||||
// 安装 Fabric
|
||||
if (loginTypeSelector.isOnline() && launchGame.isSupportFMCLogin() && !launchGame.isFabric()) {
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.installing"));
|
||||
dialog.eject();
|
||||
|
||||
new FabricBuilder() {
|
||||
|
||||
{
|
||||
setBuildGame(launchGame.get());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(Game builedGame) {
|
||||
launchGame.set(builedGame);
|
||||
// TODO 应该自动设置?
|
||||
config.getLauncher().getGame().set(builedGame.getName());
|
||||
loginTypeSelector.setValue(Game.Option.LoginType.FOREVER_MC);
|
||||
launcher.launch();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
dialog.setContent(e.getMessage());
|
||||
dialog.setCloseEvent(() -> pageService.to(Page.MENU));
|
||||
dialog.eject();
|
||||
log.error("fabric install error", e);
|
||||
}
|
||||
}.start();
|
||||
return;
|
||||
}
|
||||
// 启动
|
||||
launcher.launch();
|
||||
});
|
||||
launch.getLabel().textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
if (!autoLaunchService.isCancel && autoLaunchService.isRunning()) {
|
||||
if (launch.isHover()) {
|
||||
return TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.launch.auto.cancel");
|
||||
} else {
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.auto", autoLaunchService.getValue());
|
||||
}
|
||||
} else {
|
||||
return TimiFXUI.MULTILINGUAL.text("launch");
|
||||
}
|
||||
}, autoLaunchService.valueProperty(), autoLaunchService.stateProperty(), launch.hoverProperty()));
|
||||
|
||||
// 语言
|
||||
langSelect.setOnAction(e -> pageService.to(Page.LANG_SELECT));
|
||||
|
||||
// 设置
|
||||
super.option.setOnAction(e -> pageService.to(Page.OPTION));
|
||||
|
||||
// 退出
|
||||
exit.setOnAction(e -> stage.close());
|
||||
|
||||
// 关于
|
||||
super.about.setOnAction(e -> pageService.to(Page.ABOUT));
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void config() {
|
||||
name.textProperty().bindBidirectional(config.getPlayer().getName());
|
||||
password.textProperty().bindBidirectional(config.getPlayer().getPassword());
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
// 闪烁标语
|
||||
splashService.valueProperty().addListener((obs, o, newText) -> {
|
||||
GraphicsContext g = splashCanvas.getGraphicsContext2D();
|
||||
g.clearRect(0, 0, splashCanvas.getWidth(), splashCanvas.getHeight());
|
||||
g.fillText(newText, 0, 0);
|
||||
});
|
||||
// 动态标题
|
||||
title.imageProperty().bind(Bindings.when(launchGame.newTitleProperty()).then(Resources.TITLE_NEW).otherwise(Resources.TITLE));
|
||||
// 账号管理
|
||||
mcPopupTipsService.install(fmcAccount, TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.account"));
|
||||
// 账号验证方式
|
||||
launchGame.addListener((obs, oldGame, newGame) -> {
|
||||
// 配置绑定
|
||||
if (oldGame != null && oldGame.getOption().getLoginType().isBound()) {
|
||||
oldGame.getOption().getLoginType().unbind();
|
||||
}
|
||||
loginTypeSelector.setValue(newGame.getOption().getLoginType().get());
|
||||
newGame.getOption().getLoginType().bind(loginTypeSelector.valueProperty());
|
||||
});
|
||||
// 必须离线启动
|
||||
loginTypeSelector.requiredOfflineProperty().bind(launchGame.supportFMCLoginProperty().not());
|
||||
// 账号验证方式提示
|
||||
MCPopupTips loginTypeTips = new MCPopupTips();
|
||||
loginTypeTips.enableProperty().bind(loginTypeTips.getNode().textProperty().isNotEmpty());
|
||||
loginTypeTips.getNode().setWrapText(true);
|
||||
loginTypeTips.getNode().setMaxWidth(280);
|
||||
loginTypeTips.getNode().textProperty().bind(Bindings.createStringBinding(() -> loginTypeSelector.getValue().getTips(), launchGame, loginTypeSelector.valueProperty()));
|
||||
mcPopupTipsService.install(loginTypeSelector, loginTypeTips);
|
||||
// 启动版本
|
||||
launchGameLabel.textProperty().bind(Bindings.createStringBinding(() -> {
|
||||
Game launchGame = this.launchGame.get();
|
||||
if (launchGame == null) {
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.pack", TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.launch.pack.null"));
|
||||
}
|
||||
// 版本名称
|
||||
String gameName = launchGame.getOption().getName().get();
|
||||
if (TimiJava.isEmpty(gameName)) {
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.pack", launchGame.getName());
|
||||
}
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.menu.launch.pack", gameName);
|
||||
}, launchGame));
|
||||
// 服务器状态
|
||||
mcPopupTipsService.install(fmcServer, TimiFXUI.MULTILINGUAL.text("server.status"));
|
||||
// 启动版本管理
|
||||
mcPopupTipsService.install(launchGameSelect, TimiFXUI.MULTILINGUAL.text("fmc.launcher.menu.launch.pack.list"));
|
||||
// 获取标语
|
||||
splash.bind(splashService.valueProperty());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
super.onShow();
|
||||
// 离线
|
||||
if (launchGame.isNotSupportFMCLogin()) {
|
||||
loginTypeSelector.setValue(Game.Option.LoginType.OFFLINE);
|
||||
}
|
||||
// 自动启动
|
||||
if (isFirstShow && config.getLauncher().getAutoStartup().get()) {
|
||||
autoLaunchService.start();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自启服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-03 20:18
|
||||
*/
|
||||
private class AutoLaunchService extends RunAsync<Integer> {
|
||||
|
||||
/** true 为取消 */
|
||||
boolean isCancel = false;
|
||||
|
||||
@Override
|
||||
protected Integer call() throws Exception {
|
||||
int i = 8;
|
||||
while (0 < i) {
|
||||
update(i--);
|
||||
if (isCancel) {
|
||||
break;
|
||||
}
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish() {
|
||||
if (!isCancel) {
|
||||
launcher.launch();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
82
src/main/java/cn/forevermc/launcher/ctrl/pane/Option.java
Normal file
@ -0,0 +1,82 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.MCPopupTipsService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.view.pages.OptionPane;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
|
||||
/**
|
||||
* 选项
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-24 22:27
|
||||
*/
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class Option extends OptionPane {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private MCPopupTipsService mcPopupTipsService;
|
||||
|
||||
public Option() {
|
||||
// 内存
|
||||
final double maxMem60p = (memory.getMax() / 1024 * .6) * 1024; // 总内存 60%
|
||||
final double maxMem80p = (memory.getMax() / 1024 * .8) * 1024; // 总内存 80%
|
||||
memory.valueProperty().addListener((obs, o, newMemory) -> {
|
||||
final int mb = newMemory.intValue();
|
||||
if (mb < 2048 || (maxMem60p < mb && mb <= maxMem80p)) {
|
||||
memory.getLabel().setTextFill(Colorful.ORANGE);
|
||||
} else {
|
||||
if (maxMem80p < mb) {
|
||||
memory.getLabel().setTextFill(Colorful.RED);
|
||||
} else {
|
||||
memory.getLabel().setTextFill(Colorful.GREEN);
|
||||
}
|
||||
}
|
||||
memory.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.memory", mb));
|
||||
});
|
||||
|
||||
// 多线程下载
|
||||
multiDownload.valueProperty().addListener((obs, o, size) -> {
|
||||
if (size.intValue() < 4) {
|
||||
multiDownload.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.multi_download", 1));
|
||||
} else {
|
||||
multiDownload.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.multi_download", size.intValue()));
|
||||
}
|
||||
});
|
||||
|
||||
// 启动版本
|
||||
launchSelect.setOnAction(e -> pageService.to(Page.GAME_SELECT));
|
||||
|
||||
// 完成
|
||||
finish.setOnAction(e -> pageService.to(Page.MENU));
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void config() {
|
||||
memory.valueProperty().bindBidirectional(config.getLauncher().getMemory());
|
||||
gameSource.valueProperty().bindBidirectional(config.getMain().getGameDownloadSource());
|
||||
multiDownload.valueProperty().bindBidirectional(config.getLauncher().getMultiDownload());
|
||||
runtimeSource.valueProperty().bindBidirectional(config.getMain().getRuntimeDownloadSource());
|
||||
autoStartup.valueProperty().bindBidirectional(config.getLauncher().getAutoStartup());
|
||||
bgmVolume.valueProperty().bindBidirectional(config.getMain().getBgmVolume());
|
||||
soundVolume.valueProperty().bindBidirectional(config.getMain().getSoundVolume());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
finish.requestFocus();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,137 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.Path;
|
||||
import cn.forevermc.launcher.view.pages.OriginalSelectPane;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
import com.imyeyu.network.FileRequest;
|
||||
import com.imyeyu.network.GsonRequest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 原版选择
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 15:43
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class OriginalSelect extends OriginalSelectPane implements OS.FileSystem {
|
||||
|
||||
/** 列表接口 */
|
||||
private static final String API_LIST = "https://launchermeta.mojang.com/mc/game/version_manifest.json";
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
public OriginalSelect() {
|
||||
// 下载
|
||||
download.setOnAction(e -> {
|
||||
Game game = list.getSelectionModel().getSelectedItem();
|
||||
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("downloading"));
|
||||
dialog.eject();
|
||||
|
||||
new RunAsync<String>() {
|
||||
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
File dir = IO.dir(Path.P_PACK + game.getId());
|
||||
Thread.sleep(500);
|
||||
FileRequest.get(game.getUrl()).toFile(IO.file(dir.getAbsolutePath() + SEP + game.getId() + ".json"));
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish() {
|
||||
pageService.to(Page.GAME_SELECT);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.mojang.download_error", e.getMessage()));
|
||||
dialog.setCloseEvent(() -> pageService.to(Page.ORIGINAL_SELECT));
|
||||
dialog.eject();
|
||||
log.error("download game error", e);
|
||||
}
|
||||
}.start();
|
||||
});
|
||||
|
||||
// 返回
|
||||
back.setOnAction(e -> pageService.to(Page.GAME_SELECT));
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
list.getSelectionModel().clearSelection();
|
||||
if (list.getItems().isEmpty()) {
|
||||
setLoading(true);
|
||||
new RunAsync<List<Game>>() {
|
||||
|
||||
@Override
|
||||
protected List<Game> call() throws Exception {
|
||||
List<Game> result = new ArrayList<>();
|
||||
|
||||
JsonObject root = GsonRequest.get(API_LIST).asJsonObject();
|
||||
|
||||
JsonArray array = root.get("versions").getAsJsonArray();
|
||||
JsonObject jo;
|
||||
Game.Type type;
|
||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss");
|
||||
for (int i = 0; i < array.size(); i++) {
|
||||
jo = array.get(i).getAsJsonObject();
|
||||
type = Ref.toType(Game.Type.class, jo.get("type").getAsString());
|
||||
if (type == Game.Type.RELEASE) {
|
||||
Game game = new Game();
|
||||
game.setId(jo.get("id").getAsString());
|
||||
game.setType(type);
|
||||
game.setUrl(jo.get("url").getAsString());
|
||||
game.setReleaseAt(dateFormat.parse(jo.get("releaseTime").getAsString()).getTime());
|
||||
result.add(game);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(List<Game> result) {
|
||||
list.getItems().setAll(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.mojang.api_error"));
|
||||
dialog.setCloseEvent(() -> pageService.to(Page.GAME_SELECT));
|
||||
dialog.eject();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinally() {
|
||||
setLoading(false);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
list.scrollTo(0);
|
||||
}
|
||||
}
|
||||
258
src/main/java/cn/forevermc/launcher/ctrl/pane/PackSelect.java
Normal file
@ -0,0 +1,258 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Pack;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.Path;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.pages.PackSelectPane;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.imyeyu.compress.CompressType;
|
||||
import com.imyeyu.compress.Decompressor;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.SuperIOC;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.io.IOSize;
|
||||
import com.imyeyu.io.IOSpeedService;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.network.Network;
|
||||
import com.imyeyu.network.ProgressiveRequest;
|
||||
import com.imyeyu.network.TimiRequest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.paint.Paint;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 整合版选择控制
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-13 23:32
|
||||
*/
|
||||
@Slf4j
|
||||
@SuperIOC
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class PackSelect extends PackSelectPane implements OS.FileSystem {
|
||||
|
||||
/** 整合版列表接口 */
|
||||
private static final String API = Resources.TIMI_SERVER_API + "/fmc/pack/list";
|
||||
|
||||
/** 附件信息接口 */
|
||||
private static final String ATTACH_API = Resources.TIMI_SERVER_API + "/attachment/%s";
|
||||
|
||||
/** 附件下载接口 */
|
||||
private static final String ATTACH_DL_API = Resources.TIMI_SERVER_API + "/attachment/download/%s";
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
@Inject
|
||||
private Dialog dialog;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
@Inject
|
||||
private CanvasProgress canvasProgress;
|
||||
|
||||
@Inject
|
||||
private IOSpeedService.Item ioSpeedItem;
|
||||
|
||||
public PackSelect() {
|
||||
// 安装
|
||||
install.setOnAction(e -> {
|
||||
String[] installedPacks = gameService.listGames();
|
||||
Pack pack = packListPane.getSelectionModel().getSelectedItem();
|
||||
Pack.Source source = sourceListPane.getSelectionModel().getSelectedItem();
|
||||
if (installedPacks != null) {
|
||||
for (int i = 0; i < installedPacks.length; i++) {
|
||||
if (installedPacks[i].trim().equals(pack.getName().trim())) {
|
||||
File json = new File(Path.ABS_PACK + installedPacks[i] + SEP + installedPacks[i] + ".json");
|
||||
if (json.exists() && json.isFile() && 0 < json.length()) {
|
||||
// 已存在安装
|
||||
dialog.setTitle(TimiFXUI.MULTILINGUAL.text("install.fail"));
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.exist_warning", pack.getName()));
|
||||
dialog.setConfirmEvent(() -> pageService.to(Page.GAME_SELECT));
|
||||
dialog.eject();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
canvasProgress.setTask(new RunAsync<String>() {
|
||||
|
||||
final CallbackArg<Double> DL_SPEED = d -> Platform.runLater(() -> canvasProgress.getSubLabel().setText(IOSize.format(d, IOSize.Unit.MB) + "/s"));
|
||||
|
||||
@Override
|
||||
protected String call() throws Exception {
|
||||
progress(0);
|
||||
|
||||
String url = switch (source.getType()) {
|
||||
case URL -> source.getData();
|
||||
case ATTACH -> ATTACH_DL_API.formatted(source.getData());
|
||||
};
|
||||
String fileName = switch (source.getType()) {
|
||||
case URL -> Network.uriFileName(source.getData());
|
||||
case ATTACH -> TimiRequest.<JsonObject>get(ATTACH_API.formatted(source.getData())).result().get("name").getAsString();
|
||||
};
|
||||
File compressedFile = new File("etc" + SEP + fileName);
|
||||
if (!compressedFile.exists() || compressedFile.length() != pack.getSize()) {
|
||||
// 下载文件
|
||||
log.info("download remote pack: {}", url);
|
||||
update("DOWNLOAD");
|
||||
ProgressiveRequest.get(url, (total, read, now) -> {
|
||||
progress(1D * read / pack.getSize());
|
||||
ioSpeedItem.push(now);
|
||||
return !isInterrupted;
|
||||
}).toFile(compressedFile);
|
||||
}
|
||||
if (!isInterrupted) {
|
||||
// 解压文件
|
||||
log.info("decompress pack file: {}", compressedFile.getAbsolutePath());
|
||||
progress(0);
|
||||
update("DECOMPRESS");
|
||||
Decompressor decompressor = CompressType.fromFile(compressedFile);
|
||||
decompressor.setFileCallback(file -> message(file.getName()));
|
||||
decompressor.setProgressCallback(this::progress);
|
||||
decompressor.run(compressedFile, Path.ABS_PACK + pack.getName());
|
||||
IO.destroy(compressedFile);
|
||||
|
||||
// 初始配置
|
||||
File optionFile = new File(Path.ABS_PACK + pack.getName() + SEP + pack.getName() + ".yaml");
|
||||
IO.toFile(optionFile, pack.getDefOption());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onUpdate(String status) {
|
||||
if (TimiJava.isNotEmpty(status)) {
|
||||
switch (status) {
|
||||
case "DOWNLOAD" -> {
|
||||
ioSpeedItem.addBufferListener(DL_SPEED);
|
||||
|
||||
canvasProgress.setCanCancel(true);
|
||||
canvasProgress.setBackgroundColor(Colorful.BLACK);
|
||||
canvasProgress.setColor(Paint.valueOf("#AAA"));
|
||||
canvasProgress.reset();
|
||||
}
|
||||
case "DECOMPRESS" -> {
|
||||
ioSpeedItem.removeBufferListener(DL_SPEED);
|
||||
|
||||
canvasProgress.setCanCancel(false);
|
||||
canvasProgress.setBackgroundColor(Paint.valueOf("#AAA"));
|
||||
canvasProgress.setColor(Paint.valueOf("#0A0"));
|
||||
canvasProgress.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish() {
|
||||
reset();
|
||||
if (isInterrupted) {
|
||||
log.info("install be cancel");
|
||||
pageService.to(Page.PACK_SELECT);
|
||||
} else {
|
||||
// 安装完成
|
||||
log.info("installed pack succeed");
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.succeed", pack.getName()));
|
||||
dialog.setConfirmEvent(() -> pageService.to(Page.GAME_SELECT));
|
||||
dialog.eject();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
log.error("install remote pack error", e);
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("error") + ": " + e.getMessage());
|
||||
dialog.setConfirmEvent(() -> pageService.to(Page.PACK_SELECT));
|
||||
dialog.eject();
|
||||
}
|
||||
});
|
||||
canvasProgress.setCanCancel(true);
|
||||
canvasProgress.setBackgroundColor(Colorful.BLACK);
|
||||
canvasProgress.setColor(Paint.valueOf("#AAA"));
|
||||
canvasProgress.getLabel().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.downloading", pack.getName()));
|
||||
pageService.to(Page.CANVAS_PROGRESS);
|
||||
});
|
||||
|
||||
// 返回
|
||||
back.setOnAction(e -> pageService.to(Page.GAME_SELECT));
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
// 下载源列表更新
|
||||
packListPane.getSelectionModel().selectedItemProperty().addListener((obs, o, newSelectedItem) -> {
|
||||
if (newSelectedItem == null) {
|
||||
sourceListPane.getItems().clear();
|
||||
} else {
|
||||
List<Pack.Source> sources = newSelectedItem.getSourceList();
|
||||
sourceListPane.getItems().setAll(sources);
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
if (sources.get(i).isDefault()) {
|
||||
|
||||
sourceListPane.getSelectionModel().select(sources.get(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
packListPane.setLoading(true);
|
||||
sourceListPane.setLoading(true);
|
||||
new RunAsync<List<Pack>>() {
|
||||
|
||||
@Override
|
||||
protected List<Pack> call() throws Exception {
|
||||
// 获取整合版列表
|
||||
return TimiRequest.<List<Pack>>post(API).result();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(List<Pack> packs) {
|
||||
packListPane.getItems().setAll(packs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
log.error("list remote pack error", e);
|
||||
dialog.setContent(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.fmc.error"));
|
||||
dialog.setConfirmEvent(() -> pageService.to(Page.GAME_SELECT));
|
||||
dialog.eject();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinally() {
|
||||
packListPane.setLoading(false);
|
||||
sourceListPane.setLoading(false);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHide() {
|
||||
packListPane.getItems().clear();
|
||||
sourceListPane.getItems().clear();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.service.FMCLoginService;
|
||||
import cn.forevermc.launcher.view.pages.PlayerSelectPane;
|
||||
import com.google.gson.Gson;
|
||||
import javafx.collections.ObservableList;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2024-05-21 19:40
|
||||
*/
|
||||
@Component
|
||||
public class PlayerSelect extends PlayerSelectPane {
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-05-21 23:14
|
||||
*/
|
||||
public enum Action {
|
||||
|
||||
CONFIRM,
|
||||
|
||||
CANCEL
|
||||
}
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
@Getter
|
||||
private Action action;
|
||||
|
||||
@Setter
|
||||
private Callback onCloseEvent;
|
||||
|
||||
public PlayerSelect() {
|
||||
// 确认
|
||||
confirm.setOnAction(e -> {
|
||||
action = Action.CONFIRM;
|
||||
onCloseEvent.handler();
|
||||
});
|
||||
// 取消
|
||||
cancel.setOnAction(e -> {
|
||||
action = Action.CANCEL;
|
||||
onCloseEvent.handler();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShow() {
|
||||
list.getItems().sort((o1, o2) -> {
|
||||
if (o1.getLastLoginAt() == null || o2.getLastLoginAt() == null) {
|
||||
return -1;
|
||||
}
|
||||
return o2.getLastLoginAt().compareTo(o1.getLastLoginAt());
|
||||
});
|
||||
list.getSelectionModel().select(0);
|
||||
}
|
||||
|
||||
/** @return 列表数据 */
|
||||
public ObservableList<FMCLoginService.MinecraftPlayer> getItems() {
|
||||
return list.getItems();
|
||||
}
|
||||
|
||||
/** @return 选中的玩家 */
|
||||
public FMCLoginService.MinecraftPlayer getSelected() {
|
||||
return list.getSelectionModel().getSelectedItem();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,217 @@
|
||||
package cn.forevermc.launcher.ctrl.pane;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.FileDownload;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.core.DownloadThread;
|
||||
import cn.forevermc.launcher.ctrl.Main;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.view.pages.ResourceDownloadPane;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.inject.annotation.Controller;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.SuperInject;
|
||||
import com.imyeyu.io.IOSize;
|
||||
import com.imyeyu.io.IOSpeedService;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 资源下载
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 19:30
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@SuperInject
|
||||
public class ResourceDownload extends ResourceDownloadPane {
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Main main;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private IOSpeedService.Item ioSpeedItem;
|
||||
|
||||
@Inject
|
||||
private ObjectProperty<MediaPlayer> bgmPlayer;
|
||||
|
||||
/** 完成事件(无论如何都要触发,否则锁死启动线程) */
|
||||
@Setter
|
||||
private Callback onFinallyEvent;
|
||||
|
||||
/** 队列 */
|
||||
private final Queue queue;
|
||||
|
||||
private long downloadSize;
|
||||
private DownloadThread[] threads;
|
||||
|
||||
public ResourceDownload() {
|
||||
queue = new Queue();
|
||||
|
||||
// 取消
|
||||
cancel.setOnAction(e -> {
|
||||
if (threads != null) {
|
||||
for (int i = 0; i < threads.length; i++) {
|
||||
threads[i].cancel();
|
||||
}
|
||||
}
|
||||
queue.clear();
|
||||
list.getItems().clear();
|
||||
pageService.to(Page.MENU);
|
||||
});
|
||||
|
||||
// 背景音乐
|
||||
bgmToggle.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
if (bgmPlayer.get() == null) {
|
||||
e.consume();
|
||||
}
|
||||
});
|
||||
bgmToggle.selectedProperty().addListener((obs, o, isSelected) -> {
|
||||
MediaPlayer player = bgmPlayer.get();
|
||||
if (player != null) {
|
||||
if (isSelected) {
|
||||
player.play();
|
||||
} else {
|
||||
player.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
ioSpeedItem.addBufferListener(d -> Platform.runLater(() -> speed.setText(IOSize.format(d, IOSize.Unit.MB) + "/s")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
bgmToggle.visibleProperty().bind(config.getMain().getBgmVolume().greaterThan(0));
|
||||
}
|
||||
|
||||
/** 开始下载 */
|
||||
public void start() {
|
||||
if (queue.isNotEmpty()) {
|
||||
pb.setProgress(-1);
|
||||
|
||||
// 下载线程
|
||||
int threadSize = config.getLauncher().getMultiDownload().get();
|
||||
threads = new DownloadThread[Math.min(threadSize, queue.size())];
|
||||
for (int i = 0; i < threads.length; i++) {
|
||||
threads[i] = new DownloadThread(queue::safePull);
|
||||
threads[i].setOnFinallyEvent(() -> {
|
||||
for (int j = 0; j < threads.length; j++) {
|
||||
if (threads[j].getState() != Worker.State.SUCCEEDED) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 所有任务结束
|
||||
onFinallyEvent.handler();
|
||||
});
|
||||
threads[i].setOnFinishEvent(file -> {
|
||||
// 完成
|
||||
file.progressProperty().unbind();
|
||||
list.getItems().remove(file);
|
||||
pb.setProgress(1 - 1D * list.getItems().size() / downloadSize);
|
||||
});
|
||||
threads[i].setOnErrorEvent(file -> {
|
||||
file.progressProperty().unbind();
|
||||
list.getItems().remove(file);
|
||||
});
|
||||
}
|
||||
for (int i = 0; i < threads.length; i++) {
|
||||
threads[i].start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置下载队列,此方法会重置总进度
|
||||
*
|
||||
* @param list 列表
|
||||
*/
|
||||
public void setDownloadDeque(List<FileDownload> list) {
|
||||
downloadSize = list.size();
|
||||
this.list.getItems().addAll(list);
|
||||
queue.putDownloadDeque(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加下载队列,此方法不会重置总进度
|
||||
*
|
||||
* @param list 列表
|
||||
*/
|
||||
public void putDownloadDeque(List<FileDownload> list) {
|
||||
this.list.getItems().addAll(this.list.getItems().size(), list);
|
||||
queue.putDownloadDeque(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载队列
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-29 00:57
|
||||
*/
|
||||
private static class Queue {
|
||||
|
||||
private final Deque<FileDownload> deque;
|
||||
|
||||
public Queue() {
|
||||
deque = new ArrayDeque<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加队列
|
||||
*
|
||||
* @param list 列表
|
||||
*/
|
||||
void putDownloadDeque(List<FileDownload> list) {
|
||||
deque.addAll(list);
|
||||
}
|
||||
|
||||
/** @return true 为队列已空 */
|
||||
boolean isEmpty() {
|
||||
return deque.isEmpty();
|
||||
}
|
||||
|
||||
/** @return true 为队列非空 */
|
||||
boolean isNotEmpty() {
|
||||
return !isEmpty();
|
||||
}
|
||||
|
||||
/** @return 队列大小 */
|
||||
int size() {
|
||||
return deque.size();
|
||||
}
|
||||
|
||||
/** 清除队列 */
|
||||
void clear() {
|
||||
deque.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步获取队列文件,可多线程操作
|
||||
*
|
||||
* @return 下载文件
|
||||
*/
|
||||
synchronized FileDownload safePull() {
|
||||
return deque.pollFirst();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.bean.APISetting;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
import com.imyeyu.network.TimiRequest;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.fluent.Request;
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2024-04-29 20:41
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class APISettingService {
|
||||
|
||||
private static final String API = Resources.TIMI_SERVER_API + "/setting/map";
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
private final ObjectProperty<APISetting> value;
|
||||
|
||||
public APISettingService() {
|
||||
value = new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
new RunAsync<APISetting>() {
|
||||
|
||||
@Override
|
||||
protected APISetting call() throws Exception {
|
||||
APISetting setting = new APISetting();
|
||||
Map<APISetting.Key, Map<String, Object>> reqArgs = new HashMap<>();
|
||||
for (APISetting.Key key : APISetting.Key.values()) {
|
||||
reqArgs.put(key, null);
|
||||
}
|
||||
Map<APISetting.Key, String> data = TimiRequest.<Map<APISetting.Key, String>>wrap(Request.post(API).bodyString(gson.toJson(reqArgs), ContentType.APPLICATION_JSON)).result();
|
||||
setting.setBg(gson.fromJson(data.get(APISetting.Key.FMC_BG), APISetting.DynamicList.class));
|
||||
setting.setBgm(gson.fromJson(data.get(APISetting.Key.FMC_BGM), APISetting.DynamicList.class));
|
||||
setting.setSplashes(gson.fromJson(data.get(APISetting.Key.FMC_SPLASHES), APISetting.DynamicList.class));
|
||||
return setting;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(APISetting setting) {
|
||||
APISettingService.this.value.set(setting);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
super.onException(e);
|
||||
}
|
||||
|
||||
}.start();
|
||||
}
|
||||
|
||||
public APISetting getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<APISetting> valueProperty() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
153
src/main/java/cn/forevermc/launcher/service/BGMService.java
Normal file
@ -0,0 +1,153 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.network.FileRequest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.MapChangeListener;
|
||||
import javafx.collections.ObservableMap;
|
||||
import javafx.scene.media.Media;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2024-04-29 20:46
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BGMService implements OS.FileSystem {
|
||||
|
||||
private static final String CACHE_PATH = "etc" + SEP + "bgm";
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private APISettingService apiSettingService;
|
||||
|
||||
private final ObservableMap<String, Media> map;
|
||||
private final Map<String, File> cacheList = new HashMap<>();
|
||||
|
||||
@Getter
|
||||
private MediaPlayer player;
|
||||
|
||||
private Media bgmOld;
|
||||
private Runnable bgmRun;
|
||||
|
||||
public BGMService() {
|
||||
map = FXCollections.observableHashMap();
|
||||
|
||||
bgmRun = () -> {
|
||||
if (player != null) {
|
||||
player.dispose();
|
||||
player = null;
|
||||
}
|
||||
List<Media> list = new ArrayList<>(map.values());
|
||||
Collections.shuffle(list);
|
||||
Media bgmNew = list.getFirst();
|
||||
if (bgmOld != null) {
|
||||
while (bgmOld.equals(bgmNew)) {
|
||||
Collections.shuffle(list);
|
||||
bgmNew = list.getFirst();
|
||||
}
|
||||
}
|
||||
bgmOld = bgmNew;
|
||||
|
||||
MediaPlayer player = new MediaPlayer(bgmNew);
|
||||
player.volumeProperty().bind(config.getMain().getBgmVolume().divide(100));
|
||||
player.setOnEndOfMedia(bgmRun);
|
||||
player.setOnReady(player::play);
|
||||
};
|
||||
|
||||
map.addListener((MapChangeListener<String, Media>) change -> {
|
||||
if (player == null && !map.isEmpty()) {
|
||||
// bgmRun.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
try {
|
||||
{
|
||||
File[] files = IO.dir(CACHE_PATH).listFiles();
|
||||
if (files != null) {
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
cacheList.put(files[i].getName(), files[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TimiJava.isNotEmpty(cacheList)) {
|
||||
List<File> files = new ArrayList<>(cacheList.values());
|
||||
Collections.shuffle(files);
|
||||
map.put(files.getFirst().getName(), new Media(files.getFirst().toURI().toURL().toExternalForm()));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
apiSettingService.valueProperty().addListener((obs, o, setting) -> {
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String active = setting.getBgm().getActive();
|
||||
if (TimiJava.isNotEmpty(active)) {
|
||||
if (cacheList.containsKey(active)) {
|
||||
map.put(active, new Media(cacheList.get(active).toURI().toURL().toExternalForm()));
|
||||
} else {
|
||||
fetch(active, active, value -> map.put(active, value));
|
||||
}
|
||||
} else {
|
||||
List<String> list = setting.getBgm().getList();
|
||||
if (TimiJava.isNotEmpty(list)){
|
||||
Collections.shuffle(list);
|
||||
String shuffleMongoId = list.getFirst();
|
||||
if (cacheList.containsKey(shuffleMongoId)) {
|
||||
map.put(shuffleMongoId, new Media(cacheList.get(shuffleMongoId).toURI().toURL().toExternalForm()));
|
||||
} else {
|
||||
fetch(shuffleMongoId, shuffleMongoId, value -> map.put(shuffleMongoId, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void fetch(String mongoId, String fileName, CallbackArg<Media> callback) {
|
||||
new RunAsync<Media>() {
|
||||
|
||||
@Override
|
||||
protected Media call() throws Exception {
|
||||
File file = IO.file(CACHE_PATH + SEP + fileName + ".mp3");
|
||||
FileRequest.get(Resources.TIMI_SERVER_API + "/attachment/read/" + mongoId).toFile(file);
|
||||
cacheList.put(mongoId, file);
|
||||
return new Media(file.toURI().toURL().toExternalForm());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(Media media) {
|
||||
callback.handler(media);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
}
|
||||
137
src/main/java/cn/forevermc/launcher/service/BGService.java
Normal file
@ -0,0 +1,137 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import com.google.gson.Gson;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.utils.BgImage;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.CallbackArg;
|
||||
import com.imyeyu.network.FileRequest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.layout.Background;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.fluent.Request;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 动态背景地址获取服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 00:12
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class BGService implements OS.FileSystem {
|
||||
|
||||
private static final String CACHE_PATH = "etc" + SEP + "bg";
|
||||
|
||||
@Inject
|
||||
private APISettingService apiSettingService;
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
private final ObjectProperty<Background> value;
|
||||
private final Map<String, File> cacheList = new HashMap<>();
|
||||
|
||||
public BGService() {
|
||||
value = new SimpleObjectProperty<>();
|
||||
setValue(IO.resourceToInputStream(getClass(), "assets/img/bg.png"));
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
try {
|
||||
{
|
||||
File[] files = IO.dir(CACHE_PATH).listFiles();
|
||||
if (files != null) {
|
||||
for (int i = 0; i < files.length; i++) {
|
||||
cacheList.put(files[i].getName(), files[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (TimiJava.isNotEmpty(cacheList)) {
|
||||
List<File> files = new ArrayList<>(cacheList.values());
|
||||
Collections.shuffle(files);
|
||||
setValue(IO.getInputStream(files.get(0)));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// 从缓存随机抽取
|
||||
apiSettingService.valueProperty().addListener((obs, o, setting) -> {
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String active = setting.getBg().getActive();
|
||||
if (TimiJava.isNotEmpty(active)) {
|
||||
if (cacheList.containsKey(active)) {
|
||||
setValue(IO.getInputStream(cacheList.get(active)));
|
||||
} else {
|
||||
fetch(active, active, this::setValue);
|
||||
}
|
||||
} else {
|
||||
List<String> list = setting.getBg().getList();
|
||||
if (TimiJava.isNotEmpty(list)){
|
||||
Collections.shuffle(list);
|
||||
String shuffleMongoId = list.get(0);
|
||||
if (cacheList.containsKey(shuffleMongoId)) {
|
||||
setValue(IO.getInputStream(cacheList.get(shuffleMongoId)));
|
||||
} else {
|
||||
fetch(shuffleMongoId, shuffleMongoId, this::setValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setValue(InputStream stream) {
|
||||
value.set(new BgImage(new Image(stream, -1, -1, false, false)).cover().build());
|
||||
}
|
||||
|
||||
private void fetch(String mongoId, String fileName, CallbackArg<InputStream> callback) {
|
||||
new RunAsync<InputStream>() {
|
||||
|
||||
@Override
|
||||
protected InputStream call() throws Exception {
|
||||
File file = IO.file(CACHE_PATH + SEP + fileName);
|
||||
FileRequest.get(Resources.TIMI_SERVER_API + "/attachment/read/" + mongoId).toFile(file);
|
||||
cacheList.put(mongoId, file);
|
||||
return IO.getInputStream(file);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(InputStream stream) {
|
||||
callback.handler(stream);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Background> valueProperty() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public Background getValue() {
|
||||
return value.get();
|
||||
}
|
||||
}
|
||||
265
src/main/java/cn/forevermc/launcher/service/FMCLoginService.java
Normal file
@ -0,0 +1,265 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.bean.FabricAPI;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.core.LauncherException;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.io.JarReader;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.network.FileRequest;
|
||||
import com.imyeyu.network.TimiRequest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import javafx.beans.property.MapProperty;
|
||||
import javafx.beans.property.ReadOnlyMapProperty;
|
||||
import javafx.beans.property.SimpleMapProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import lombok.Cleanup;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.fluent.Request;
|
||||
import org.apache.hc.core5.http.ContentType;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* ForeverMC 账号验证服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-30 00:31
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FMCLoginService implements OS.FileSystem {
|
||||
|
||||
/**
|
||||
* 登录玩家
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-05-21 18:11
|
||||
*/
|
||||
@Data
|
||||
public static class MinecraftPlayer {
|
||||
|
||||
/** 账号 ID */
|
||||
Long id;
|
||||
|
||||
/** 玩家名 */
|
||||
String name;
|
||||
|
||||
/** 上次登录时间 */
|
||||
Long lastLoginAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌请求
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-05-21 18:20
|
||||
*/
|
||||
@Data
|
||||
public static class TokenRequest {
|
||||
|
||||
/** 账号(ID、邮箱或玩家名) */
|
||||
String user;
|
||||
|
||||
/** 密码 */
|
||||
String password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌返回
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-05-21 18:19
|
||||
*/
|
||||
@Data
|
||||
public static class TokenResponse {
|
||||
|
||||
/** 令牌 */
|
||||
String token;
|
||||
|
||||
/** 过期于 */
|
||||
long expiredAt;
|
||||
}
|
||||
|
||||
private static final String SETTING_API = Resources.TIMI_SERVER_API + "/setting";
|
||||
|
||||
private static final String MIRROR_API = Resources.TIMI_SERVER_API + "/mirror";
|
||||
|
||||
private static final String ATTACH_DL_API = Resources.TIMI_SERVER_API + "/attachment/download";
|
||||
|
||||
/** Timi 数据中心验证接口 */
|
||||
private static final String LOGIN_API = Resources.TIMI_SERVER_API + "/fmc/player/login";
|
||||
|
||||
/** Timi 数据中心验证接口 */
|
||||
private static final String LIST_PLAYER_API = Resources.TIMI_SERVER_API + "/fmc/player/list";
|
||||
|
||||
/** Timi 数据中心验证接口 */
|
||||
private static final String LOGIN_TOKEN_API = Resources.TIMI_SERVER_API + "/fmc/player/login/token";
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
@Inject
|
||||
private GameService gameService;
|
||||
|
||||
/** 辅助登录模组映射 Map<版本, mongoId> */
|
||||
private static final MapProperty<String, String> loginModMap = new SimpleMapProperty<>(FXCollections.observableHashMap());
|
||||
|
||||
/** FabricAPI 镜像映射 Map<版本, FabricAPI> */
|
||||
private static final MapProperty<String, FabricAPI> fabricApiMap = new SimpleMapProperty<>(FXCollections.observableHashMap());
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
new RunAsync<Void>() {
|
||||
|
||||
Map<String, String> loginModMapTemp = null;
|
||||
|
||||
Map<String, FabricAPI> fabricAPIMapTemp = null;
|
||||
|
||||
@Override
|
||||
protected Void call() throws Exception {
|
||||
{
|
||||
String data = TimiRequest.<String>get(SETTING_API + "/FMC_LOGIN_FABRIC").result();
|
||||
loginModMapTemp = gson.fromJson(data, new TypeToken<Map<String, String>>() {}.getType());
|
||||
}
|
||||
{
|
||||
List<FabricAPI> data = TimiRequest.<List<FabricAPI>>get(MIRROR_API + "/FabricAPI").result();
|
||||
fabricAPIMapTemp = data.stream().collect(Collectors.toMap(FabricAPI::getMinecraftVer, item -> item));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinish(Void map) {
|
||||
fabricApiMap.clear();
|
||||
fabricApiMap.putAll(fabricAPIMapTemp);
|
||||
loginModMap.clear();
|
||||
loginModMap.putAll(loginModMapTemp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onException(Throwable e) {
|
||||
log.error("list support login mod map error", e);
|
||||
}
|
||||
}.start();
|
||||
}
|
||||
|
||||
public boolean isSupportFMCLogin(Game game) {
|
||||
if (game == null || game.isLegacy()) {
|
||||
return false;
|
||||
}
|
||||
if (loginModMap.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(game);
|
||||
|
||||
boolean supportLoginMod = loginModMap.containsKey(realVersion.getValue());
|
||||
try {
|
||||
if (!supportLoginMod) {
|
||||
return false;
|
||||
}
|
||||
if (hasMod(game, "fabric-api")) {
|
||||
return true;
|
||||
}
|
||||
for (String key : fabricApiMap.keySet()) {
|
||||
if (key.contains(realVersion.getValue())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("check token login support error", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void checkLoginMod(Game game) {
|
||||
try {
|
||||
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(game);
|
||||
if (!loginModMap.containsKey(realVersion.getValue())) {
|
||||
// 不支持的版本
|
||||
throw new LauncherException(Page.MENU, TimiFXUI.MULTILINGUAL.text("fmc.launcher.account.not_support"));
|
||||
}
|
||||
File modPath = IO.dir(IO.fitPath(game.getPath().getAbsolutePath()) + "mods" + SEP);
|
||||
if (!hasMod(game, "fmc-login-fabric")) {
|
||||
log.info("downloading fmc-login-fabric mod for {}", loginModMap.get(realVersion.getValue()));
|
||||
String dlURL = ATTACH_DL_API + "/%s".formatted(loginModMap.get(realVersion.getValue()));
|
||||
FileRequest.get(dlURL).toFile(modPath.getAbsolutePath(), "fmc-login-fabric.jar");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("check mod error", e);
|
||||
throw new TimiException(TimiCode.RESULT_BAD, "check mod fail");
|
||||
}
|
||||
}
|
||||
|
||||
public TokenResponse genLoginToken(String user, String password) throws Exception {
|
||||
TokenRequest request = new TokenRequest();
|
||||
request.setUser(user);
|
||||
request.setPassword(password);
|
||||
return TimiRequest.<TokenResponse>wrap(Request.post(LOGIN_TOKEN_API).bodyString(gson.toJson(request), ContentType.APPLICATION_JSON)).result();
|
||||
}
|
||||
|
||||
public List<MinecraftPlayer> listBoundPlayer(String token) throws Exception {
|
||||
return TimiRequest.<List<MinecraftPlayer>>wrap(Request.get(LIST_PLAYER_API).addHeader("Token", token)).result();
|
||||
}
|
||||
|
||||
public TokenResponse doLogin(MinecraftPlayer player, String token) throws Exception {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("playerId", player.id);
|
||||
return TimiRequest.<TokenResponse>wrap(Request.get(LIST_PLAYER_API)
|
||||
.addHeader("Token", token)
|
||||
.bodyString(gson.toJson(map), ContentType.APPLICATION_JSON)).result();
|
||||
}
|
||||
|
||||
private boolean hasMod(Game game, String name) {
|
||||
try {
|
||||
File modPath = IO.dir(IO.fitPath(game.getPath().getAbsolutePath()) + "mods" + SEP);
|
||||
File[] mods = modPath.listFiles();
|
||||
if (TimiJava.isNotEmpty(mods)) {
|
||||
for (int i = 0; i < mods.length; i++) {
|
||||
if (mods[i].getName().endsWith(".jar")) {
|
||||
@Cleanup
|
||||
JarReader jar = new JarReader(mods[i]);
|
||||
if (jar.has("fabric.mod.json")) {
|
||||
String fabricJson = new String(jar.getBytes("fabric.mod.json"));
|
||||
JsonObject root = JsonParser.parseString(fabricJson).getAsJsonObject();
|
||||
if (root.get("id").getAsString().equals(name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.error("check mod error", e);
|
||||
throw new TimiException(TimiCode.ERROR, "check mod fail");
|
||||
}
|
||||
}
|
||||
|
||||
public static ReadOnlyMapProperty<String, String> loginModMapProperty() {
|
||||
return loginModMap;
|
||||
}
|
||||
|
||||
public static ReadOnlyMapProperty<String, FabricAPI> fabricAPIMapProperty() {
|
||||
return fabricApiMap;
|
||||
}
|
||||
}
|
||||
625
src/main/java/cn/forevermc/launcher/service/GameService.java
Normal file
@ -0,0 +1,625 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import cn.forevermc.launcher.bean.FileDownload;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.GameDownloadSource;
|
||||
import cn.forevermc.launcher.util.Path;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.imyeyu.config.ConfigLoader;
|
||||
import com.imyeyu.fx.config.BindingsConfig;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.network.CommonRequest;
|
||||
import com.imyeyu.network.FileRequest;
|
||||
import com.imyeyu.network.GsonRequest;
|
||||
import com.imyeyu.utils.Digest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import com.imyeyu.utils.Text;
|
||||
import com.imyeyu.utils.Time;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 游戏服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-11 19:14
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class GameService implements OS.FileSystem {
|
||||
|
||||
/**
|
||||
* JNA 类型
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-29 13:19
|
||||
*/
|
||||
@AllArgsConstructor
|
||||
enum NativeType {
|
||||
|
||||
/** Windows */
|
||||
WINDOWS("natives-windows"),
|
||||
|
||||
/** X86 架构 Windows */
|
||||
WINDOWS_X86("natives-windows-x86"),
|
||||
|
||||
/** Linux */
|
||||
LINUX("natives-linux"),
|
||||
|
||||
/** MacOS */
|
||||
MACOS("natives-macos"),
|
||||
|
||||
/** ARM 架构 MacOS */
|
||||
MACOS_ARM("natives-macos-arm64");
|
||||
|
||||
/** 位于游戏配置的属性名称 */
|
||||
final String name;
|
||||
}
|
||||
|
||||
/** 原版启动类 */
|
||||
private static final String MAIN_CLASS_MOJANG = "net.minecraft.client.main.Main";
|
||||
|
||||
/** Fabric Loader 启动类 */
|
||||
private static final String MAIN_CLASS_FABRIC = "net.fabricmc.loader.impl.launch.knot.KnotClient";
|
||||
|
||||
/** 远古版本资源数据 */
|
||||
private static final String ASSETS_ANCIENT = "pre-1.6";
|
||||
|
||||
/** Key: 必须要指定运行时版本的 Minecraft 版本的发布开始时间,此时间点之后的版本需要 Value: Java 版本 */
|
||||
private static final Map<Long, String> REQ_RT_MAP = Collections.unmodifiableMap(new LinkedHashMap<>() {{
|
||||
|
||||
try {
|
||||
put(Time.dateTimeT.parse("2024-04-03T11:49:39").getTime(), "21"); // 24w14a
|
||||
put(Time.dateTimeT.parse("2021-11-16T17:04:48").getTime(), "17"); // 1.18-pre2
|
||||
put(0L, "8");
|
||||
} catch (ParseException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
}});
|
||||
|
||||
/** 配置加载器缓存 */
|
||||
private final Map<Game, ConfigLoader<?>> configLoaderMap;
|
||||
|
||||
@Inject
|
||||
private Config config;
|
||||
|
||||
@Inject
|
||||
private Gson gson;
|
||||
|
||||
public GameService() {
|
||||
configLoaderMap = new HashMap<>();
|
||||
}
|
||||
|
||||
/** @return 当前版本包列表 */
|
||||
public String[] listGames() {
|
||||
return new File(Path.P_PACK).list();
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取游戏配置
|
||||
*
|
||||
* @param game 版本
|
||||
* @return 游戏配置
|
||||
*/
|
||||
public Game.Option getOption(Game game) {
|
||||
ConfigLoader<Game.Option> loader = BindingsConfig.build("GameOption.yaml", IO.fitPath(game.getPath().getAbsolutePath()) + game.getName() + ".yaml", Game.Option.class);
|
||||
configLoaderMap.put(game, loader);
|
||||
return loader.load();
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存游戏配置
|
||||
*
|
||||
* @param game 游戏
|
||||
*/
|
||||
public void storeOption(Game game) {
|
||||
configLoaderMap.get(game).dump();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取游戏需要运行时最低版本。映射表静态储存 {@link #REQ_RT_MAP}
|
||||
*
|
||||
* @param game 游戏
|
||||
* @return 8、17、21 等 Java 运行时版本
|
||||
*/
|
||||
public String getRequiredRTVer(Game game) {
|
||||
for (Map.Entry<Long, String> item : REQ_RT_MAP.entrySet()) {
|
||||
if (item.getKey() < game.getReleaseAt()) {
|
||||
return item.getValue();
|
||||
}
|
||||
}
|
||||
throw new TimiException(TimiCode.ERROR).msgKey("TODO get req java version error");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建游戏,解析整合包基本数据,此方法不会深度解析
|
||||
*
|
||||
* @param gamePath 整合包路径文件,通常是 .minecraft/versions 路径的文件夹
|
||||
* @return 版本包数据
|
||||
* @throws IOException 读取异常
|
||||
*/
|
||||
public Game buildGame(File gamePath) throws Exception {
|
||||
File jar = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".jar");
|
||||
File json = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".json");
|
||||
File yaml = new File(IO.fitPath(gamePath.getAbsolutePath()) + gamePath.getName() + ".yaml");
|
||||
Game game = new Game();
|
||||
game.setName(gamePath.getName());
|
||||
game.setPath(gamePath);
|
||||
game.setJar(jar);
|
||||
game.setJson(json);
|
||||
game.setYaml(yaml);
|
||||
JsonObject root = JsonParser.parseString(IO.toString(json)).getAsJsonObject();
|
||||
String mainClass = root.get("mainClass").getAsString();
|
||||
|
||||
game.setJsonRoot(root);
|
||||
game.setOriginal(MAIN_CLASS_MOJANG.equals(mainClass));
|
||||
game.setFabric(MAIN_CLASS_FABRIC.equals(mainClass));
|
||||
game.setLegacy(ASSETS_ANCIENT.equals(root.get("assets").getAsString()));
|
||||
game.setOption(getOption(game));
|
||||
game.setReleaseAt(Time.dateTimeT.parse(root.get("releaseTime").getAsString()).getTime());
|
||||
return game;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取版本包元数据,深度解析所有属性,并确定缺少的依赖和资源,比较耗时
|
||||
*
|
||||
* @param game 版本包
|
||||
* @throws Exception 读取异常
|
||||
*/
|
||||
public void readMetaData(Game game) throws Exception {
|
||||
Game.MetaData metaData = new Game.MetaData();
|
||||
JsonObject root = game.getJsonRoot();
|
||||
|
||||
// 主版本
|
||||
metaData.setVersion(root.get("id").getAsString());
|
||||
// 真实版本
|
||||
metaData.setRealVersion(getRealVersion(game));
|
||||
// 启动核心
|
||||
JsonObject core = root.get("downloads").getAsJsonObject().get("client").getAsJsonObject();
|
||||
metaData.setCoreSHA1(core.get("sha1").getAsString());
|
||||
metaData.setCoreURL(core.get("url").getAsString());
|
||||
metaData.setCoreSize(core.get("size").getAsLong());
|
||||
// 资源文件
|
||||
log.info("parsing assets list..");
|
||||
metaData.setAssetsVersion(root.get("assets").getAsString());
|
||||
metaData.setAssetList(parseAssetsList(root.get("assetIndex").getAsJsonObject().get("url").getAsString(), metaData));
|
||||
// 依赖文件
|
||||
// TODO Create Astral 长时间卡这里
|
||||
log.info("parsing libraries list..");
|
||||
metaData.setLibrarieList(parseLibrariesList(root.get("libraries").getAsJsonArray()));
|
||||
// 启动类
|
||||
metaData.setMainClass(root.get("mainClass").getAsString());
|
||||
// Java 版本
|
||||
metaData.setJavaVersion(root.get("javaVersion").getAsJsonObject().get("majorVersion").getAsString());
|
||||
{
|
||||
// 启动参数模板
|
||||
log.info("generate arguments template for launch");
|
||||
if (root.has("arguments")) {
|
||||
JsonObject arguments = root.get("arguments").getAsJsonObject();
|
||||
if (arguments.has("game")) {
|
||||
metaData.setArgsGame(parseArgsArray(game, arguments.get("game").getAsJsonArray()));
|
||||
}
|
||||
if (arguments.has("jvm")) {
|
||||
metaData.setArgsJVM(parseArgsArray(game, arguments.get("jvm").getAsJsonArray()));
|
||||
}
|
||||
} else if (root.has("minecraftArguments")) {
|
||||
// 远古版本
|
||||
metaData.setArgsGame(root.get("minecraftArguments").getAsString());
|
||||
metaData.setArgsJVM(" -Dminecraft.applet.TargetDirectory=${game_directory} -Djava.library.path=${natives_directory} -cp ${classpath}");
|
||||
}
|
||||
}
|
||||
// 标题
|
||||
if (root.has("title")) {
|
||||
metaData.setTitle(root.get("title").getAsString());
|
||||
} else if (root.has("id")) {
|
||||
metaData.setTitle("Minecraft " + root.get("id").getAsString());
|
||||
}
|
||||
if (game.isLegacy()) {
|
||||
metaData.setTitle("Minecraft");
|
||||
}
|
||||
log.info("parsing required download file list");
|
||||
// 解析需要下载的资源
|
||||
metaData.setDownloadList(toFileDL(metaData));
|
||||
if (metaData.getDownloadList().isEmpty()) {
|
||||
log.info("nothing required download");
|
||||
} else {
|
||||
log.info("required download size {}", metaData.getDownloadList().size());
|
||||
}
|
||||
// 设置元数据
|
||||
game.setMetaData(metaData);
|
||||
// JSON 资源可能被补充 size 或 sha1,写入磁盘
|
||||
IO.toFile(game.getJson(), gson.toJson(root));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取版本包的真实游戏版本
|
||||
*
|
||||
* @param game 版本包
|
||||
* @return 真实游戏版本
|
||||
*/
|
||||
public Game.MetaData.RealVersion getRealVersion(Game game) {
|
||||
JsonObject root = game.getJsonRoot();
|
||||
// 真实版本
|
||||
Game.MetaData.RealVersion realVersion = new Game.MetaData.RealVersion();
|
||||
String value = root.get("id").getAsString();
|
||||
if (root.has("source-id")) {
|
||||
value = root.get("source-id").getAsString();
|
||||
}
|
||||
// HMCL 的原始版本
|
||||
if (root.has("patches")) {
|
||||
JsonArray patches = root.get("patches").getAsJsonArray();
|
||||
for (int j = 0; j < patches.size(); j++) {
|
||||
JsonObject object = patches.get(j).getAsJsonObject();
|
||||
if (object.has("id") && object.get("id").getAsString().equals("game")) {
|
||||
value = object.get("version").getAsString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
realVersion.setValue(value);
|
||||
return realVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析版本需要下载的依赖和资源
|
||||
*
|
||||
* @param metadata 版本数据
|
||||
* @return 依赖和资源文件
|
||||
*/
|
||||
private List<FileDownload> toFileDL(Game.MetaData metadata) throws IOException, NoSuchAlgorithmException {
|
||||
List<FileDownload> dlList = new ArrayList<>();
|
||||
|
||||
File file;
|
||||
FileDownload fileDL;
|
||||
String version = metadata.getVersion();
|
||||
|
||||
// TODO 告知 UI 正在检查项
|
||||
|
||||
// 核心
|
||||
{
|
||||
file = new File(Path.P_PACK + version + SEP + version + ".jar");
|
||||
if (!file.exists() || file.length() == 0 || file.length() != metadata.getCoreSize() || !Digest.sha1(IO.toBytes(file)).equals(metadata.getCoreSHA1())) {
|
||||
fileDL = new FileDownload(file);
|
||||
fileDL.setHash(metadata.getCoreSHA1());
|
||||
fileDL.setUrl(metadata.getCoreURL());
|
||||
fileDL.setSize(metadata.getCoreSize());
|
||||
dlList.add(fileDL);
|
||||
}
|
||||
}
|
||||
// 资源
|
||||
String assetsPath = Path.P_ASSETS + "objects" + SEP;
|
||||
List<Game.MetaData.Assets> assetList = metadata.getAssetList();
|
||||
for (int i = 0; i < assetList.size(); i++) {
|
||||
Game.MetaData.Assets asset = assetList.get(i);
|
||||
file = new File(assetsPath + asset.getHash().substring(0, 2) + SEP + asset.getHash());
|
||||
if (!file.exists() || file.length() == 0 || file.length() != asset.getSize() || !Digest.sha1(IO.toBytes(file)).equals(asset.getHash())) {
|
||||
fileDL = new FileDownload(file);
|
||||
fileDL.setHash(asset.getHash());
|
||||
fileDL.setUrl(asset.getUrl());
|
||||
fileDL.setSize(asset.getSize());
|
||||
dlList.add(fileDL);
|
||||
}
|
||||
}
|
||||
|
||||
// 依赖
|
||||
List<Game.MetaData.Libraries> librarieList = metadata.getLibrarieList();
|
||||
for (int i = 0; i < librarieList.size(); i++) {
|
||||
Game.MetaData.Libraries library = librarieList.get(i);
|
||||
String path = library.getPath();
|
||||
|
||||
file = new File(Path.P_LIB + path.substring(0, path.lastIndexOf("/")) + path.substring(path.lastIndexOf("/")));
|
||||
if (!file.exists() || file.length() == 0 || file.length() != library.getSize() || !Digest.sha1(IO.toBytes(file)).equals(library.getSha1())) {
|
||||
fileDL = new FileDownload(file);
|
||||
fileDL.setHash(library.getSha1());
|
||||
fileDL.setUrl(library.getUrl());
|
||||
fileDL.setSize(library.getSize());
|
||||
dlList.add(fileDL);
|
||||
}
|
||||
}
|
||||
|
||||
return dlList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JSON 配置启动参数
|
||||
*
|
||||
* @param array 启动参数 JSON
|
||||
* @return 启动参数命令
|
||||
*/
|
||||
private String parseArgsArray(Game game, JsonArray array) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
String osVersion = System.getProperty("os.version");
|
||||
|
||||
CallbackArgReturn<String, String> qmArgs = p -> {
|
||||
int i = p.indexOf("=");
|
||||
if (i == -1) {
|
||||
return p;
|
||||
} else {
|
||||
if (p.charAt(i + 1) == '$') {
|
||||
return p;
|
||||
} else {
|
||||
return p.substring(0, i + 1) + Text.quote(p.substring(i + 1));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
JsonObject arg, rule, os;
|
||||
JsonArray rules, values;
|
||||
args: for (int i = 0; i < array.size(); i++) {
|
||||
if (array.get(i).isJsonObject()) {
|
||||
arg = array.get(i).getAsJsonObject();
|
||||
rules = arg.get("rules").getAsJsonArray();
|
||||
if (arg.get("value").isJsonArray()) {
|
||||
// 数组参数
|
||||
for (int j = 0; j < rules.size(); j++) {
|
||||
// 约束规则
|
||||
rule = rules.get(j).getAsJsonObject();
|
||||
if (rule.get("action").getAsString().equals("allow")) {
|
||||
// 允许的规则
|
||||
if (rule.has("os")) {
|
||||
// 系统约束
|
||||
os = rule.get("os").getAsJsonObject();
|
||||
if (os.has("name")) {
|
||||
if (OS.IS_WINDOWS && os.get("name").getAsString().equals("windows")) {
|
||||
// windows 系统
|
||||
if (os.has("version")) {
|
||||
// 版本约束
|
||||
if (!Pattern.matches(os.get("version").getAsString(), osVersion.substring(0, osVersion.length() - 1))) {
|
||||
// 版本不通过
|
||||
continue args;
|
||||
}
|
||||
}
|
||||
if (arg.get("value").isJsonArray()) {
|
||||
// 多值
|
||||
values = arg.get("value").getAsJsonArray();
|
||||
for (int k = 0; k < values.size(); k++) {
|
||||
sb.append(qmArgs.handler(values.get(k).getAsString())).append(' ');
|
||||
}
|
||||
} else {
|
||||
// 单值
|
||||
sb.append(qmArgs.handler(arg.get("value").getAsString())).append(' ');
|
||||
}
|
||||
continue args;
|
||||
} // else Linux 系统
|
||||
} // else 非系统名称约束
|
||||
} else if (rule.has("features")) {
|
||||
// 可选规则(暂不支持自定义规则)
|
||||
// for (int k = 0; k < optionRules.size(); k++) {
|
||||
// if (rule.get("features").getAsJsonObject().has(optionRules.get(k))) {
|
||||
// if (arg.get("value").isJsonArray()) {
|
||||
// // 多值
|
||||
// values = arg.get("value").getAsJsonArray();
|
||||
// for (int m = 0; m < values.size(); m++) {
|
||||
// sb.append(qmArgs.handler(values.get(m).getAsString())).append(' ');
|
||||
// }
|
||||
// } else {
|
||||
// // 单值
|
||||
// sb.append(qmArgs.handler(arg.get("value").getAsString())).append(' ');
|
||||
// }
|
||||
// break;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
} // else 禁用的规则
|
||||
}
|
||||
} // 非数组参数
|
||||
} else {
|
||||
// 通用字符串参数
|
||||
sb.append(array.get(i).getAsString()).append(' ');
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析资源列表
|
||||
*
|
||||
* @param assetsURL 资源数据 JSON URL
|
||||
* @return 资源数据
|
||||
* @throws Exception 异常
|
||||
*/
|
||||
private List<Game.MetaData.Assets> parseAssetsList(String assetsURL, Game.MetaData metaData) throws Exception {
|
||||
List<Game.MetaData.Assets> list = new ArrayList<>();
|
||||
|
||||
final String urlDomain = config.getMain().getGameDownloadSource().get().getResources();
|
||||
|
||||
String indexJson = null;
|
||||
int retry = 0;
|
||||
while (retry < 10) {
|
||||
try {
|
||||
indexJson = CommonRequest.get(assetsURL).asString();
|
||||
break;
|
||||
} catch (SocketTimeoutException e) {
|
||||
retry++;
|
||||
log.error("official resources api error", e);
|
||||
}
|
||||
}
|
||||
if (indexJson == null) {
|
||||
throw new TimiException(TimiCode.ERROR, TimiFXUI.MULTILINGUAL.text("fmc.launcher.resource_error"));
|
||||
}
|
||||
IO.toFile(new File(Path.P_ASSETS + "indexes" + SEP + metaData.getAssetsVersion() + ".json"), indexJson);
|
||||
|
||||
JsonObject objects = GsonRequest.get(assetsURL).asJsonObject().get("objects").getAsJsonObject();
|
||||
|
||||
Game.MetaData.Assets assets;
|
||||
for (Map.Entry<String, JsonElement> object : objects.entrySet()) {
|
||||
if (object.getValue() instanceof JsonObject jo) {
|
||||
assets = new Game.MetaData.Assets();
|
||||
assets.setHash(jo.get("hash").getAsString());
|
||||
assets.setUrl(urlDomain + assets.getHash().substring(0, 2) + '/' + assets.getHash());
|
||||
assets.setSize(jo.get("size").getAsLong());
|
||||
|
||||
list.add(assets);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析依赖
|
||||
*
|
||||
* @param libs 依赖列表
|
||||
* @return 依赖
|
||||
*/
|
||||
private List<Game.MetaData.Libraries> parseLibrariesList(JsonArray libs) throws Exception {
|
||||
List<Game.MetaData.Libraries> list = new ArrayList<>();
|
||||
|
||||
Game.MetaData.Libraries libraries;
|
||||
JsonObject lib, rule, os, downloads, artifact, natives, classifiers, classifier;
|
||||
JsonArray rules;
|
||||
|
||||
libs:
|
||||
for (int i = 0; i < libs.size(); i++) {
|
||||
lib = libs.get(i).getAsJsonObject();
|
||||
|
||||
if (lib.has("name") && lib.has("url")) {
|
||||
// 兼容 HMCL 的 JSON 结构仅有 name 和 url,临时转为本启动器结构
|
||||
String name = lib.get("name").getAsString();
|
||||
String[] names = name.split(":");
|
||||
|
||||
// 合成路径
|
||||
StringBuilder pathSB = new StringBuilder();
|
||||
String[] paths = names[0].split("\\.");
|
||||
for (int j = 0; j < paths.length; j++) {
|
||||
pathSB.append(paths[j]).append('/');
|
||||
}
|
||||
pathSB.append(names[1]).append('/').append(names[2]).append('/').append(names[1]).append('-').append(names[2]).append(".jar");
|
||||
|
||||
downloads = new JsonObject();
|
||||
{
|
||||
String path = pathSB.toString();
|
||||
String url = lib.get("url").getAsString() + path;
|
||||
artifact = new JsonObject();
|
||||
artifact.addProperty("path", path);
|
||||
artifact.addProperty("url", url);
|
||||
artifact.addProperty("size", FileRequest.get(url).length());
|
||||
artifact.addProperty("sha1", Digest.sha1(FileRequest.get(url).asBytes()));
|
||||
|
||||
downloads.add("artifact", artifact);
|
||||
|
||||
lib.add("downloads", downloads);
|
||||
}
|
||||
}
|
||||
if (lib.has("downloads")) {
|
||||
downloads = lib.get("downloads").getAsJsonObject();
|
||||
if (lib.has("rules")) {
|
||||
// 约束规则
|
||||
rules = lib.get("rules").getAsJsonArray();
|
||||
for (int j = 0; j < rules.size(); j++) {
|
||||
rule = rules.get(j).getAsJsonObject();
|
||||
switch (rule.get("action").getAsString()) {
|
||||
case "allow" -> {
|
||||
if (rule.has("os")) {
|
||||
os = rule.get("os").getAsJsonObject();
|
||||
if (os.has("name") && os.get("name").getAsString().equals("osx") && !OS.IS_OSX) {
|
||||
// OSX 专用
|
||||
continue libs;
|
||||
}
|
||||
} // else 通用
|
||||
}
|
||||
case "disallow" -> {
|
||||
if (rule.has("os")) {
|
||||
os = rule.get("os").getAsJsonObject();
|
||||
if (os.has("name") && os.get("name").getAsString().equals("osx") && OS.IS_OSX) {
|
||||
// OSX 禁用
|
||||
continue libs;
|
||||
}
|
||||
} // else 通用
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// JNA
|
||||
if (lib.has("natives")) {
|
||||
// 1.18 及以下
|
||||
natives = lib.get("natives").getAsJsonObject();
|
||||
// Windows JNA
|
||||
if (natives.has("windows") && OS.IS_WINDOWS) {
|
||||
classifiers = downloads.get("classifiers").getAsJsonObject();
|
||||
classifier = classifiers.get(natives.get("windows").getAsString()).getAsJsonObject();
|
||||
|
||||
libraries = new Game.MetaData.Libraries();
|
||||
libraries.setNatives(true);
|
||||
libraries.setPath(classifier.get("path").getAsString());
|
||||
libraries.setSha1(classifier.get("sha1").getAsString());
|
||||
libraries.setSize(classifier.get("size").getAsLong());
|
||||
libraries.setUrl(classifier.get("url").getAsString());
|
||||
|
||||
list.add(libraries);
|
||||
}
|
||||
// 其他系统 JNA
|
||||
}
|
||||
|
||||
if (downloads.has("artifact")) {
|
||||
artifact = downloads.get("artifact").getAsJsonObject();
|
||||
|
||||
libraries = new Game.MetaData.Libraries();
|
||||
String name = lib.get("name").getAsString();
|
||||
if (OS.IS_WINDOWS) {
|
||||
libraries.setNatives(name.endsWith("natives-windows"));
|
||||
}
|
||||
if (OS.IS_UNIX) {
|
||||
libraries.setNatives(name.endsWith("natives-linux"));
|
||||
}
|
||||
if (OS.IS_OSX) {
|
||||
libraries.setNatives(name.endsWith("natives-macos"));
|
||||
}
|
||||
libraries.setPath(artifact.get("path").getAsString());
|
||||
|
||||
// TODO 告知 UI 耗时操作
|
||||
String url = artifact.get("url").getAsString();
|
||||
if (artifact.has("sha1")) {
|
||||
libraries.setSha1(artifact.get("sha1").getAsString());
|
||||
} else {
|
||||
String sha1 = Digest.sha1(FileRequest.get(url).asBytes());
|
||||
artifact.addProperty("sha1", sha1);
|
||||
libraries.setSha1(sha1);
|
||||
}
|
||||
if (artifact.has("size")) {
|
||||
libraries.setSize(artifact.get("size").getAsLong());
|
||||
} else {
|
||||
long size = FileRequest.get(url).length();
|
||||
artifact.addProperty("size", size);
|
||||
libraries.setSize(size);
|
||||
}
|
||||
libraries.setUrl(url);
|
||||
list.add(libraries);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 替换下载源
|
||||
GameDownloadSource source = config.getMain().getGameDownloadSource().get();
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
String fromURL = list.get(i).getUrl();
|
||||
String toURL = fromURL.replaceAll(GameDownloadSource.MOJANG.getLibraries(), source.getLibraries());
|
||||
list.get(i).setUrl(toURL);
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.bean.MCPopupTips;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.Pane;
|
||||
import com.imyeyu.fx.ui.components.popup.AbstractPopupTipsService;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
|
||||
/**
|
||||
* 弹出提示服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2021-12-04 11:03
|
||||
*/
|
||||
@Service
|
||||
public class MCPopupTipsService extends AbstractPopupTipsService<BorderPane> {
|
||||
|
||||
private static final Background BG = new BgFill("#101D").build();
|
||||
|
||||
private BorderPane main;
|
||||
|
||||
public MCPopupTipsService() {
|
||||
showOnRoot = node -> main.setCenter(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected BorderPane createRoot() {
|
||||
main = new BorderPane();
|
||||
main.setBackground(BG);
|
||||
|
||||
BorderPane root = new BorderPane(main);
|
||||
|
||||
Pane t = new Pane();
|
||||
Pane r = new Pane();
|
||||
Pane l = new Pane();
|
||||
Pane b = new Pane();
|
||||
|
||||
t.setPrefHeight(3);
|
||||
r.setPrefWidth(3);
|
||||
b.setPrefHeight(3);
|
||||
l.setPrefWidth(3);
|
||||
|
||||
t.setBackground(BG);
|
||||
r.setBackground(BG);
|
||||
b.setBackground(BG);
|
||||
l.setBackground(BG);
|
||||
|
||||
final Insets MARGIN = new Insets(0, 3, 0, 3);
|
||||
BorderPane.setMargin(t, MARGIN);
|
||||
BorderPane.setMargin(b, MARGIN);
|
||||
|
||||
root.setTop(t);
|
||||
root.setRight(r);
|
||||
root.setBottom(b);
|
||||
root.setLeft(l);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
/**
|
||||
* 安装提示
|
||||
*
|
||||
* @param node 被安装节点
|
||||
* @param text 文本
|
||||
* @return 弹出提示
|
||||
*/
|
||||
public MCPopupTips install(Node node, String text) {
|
||||
MCPopupTips tips = new MCPopupTips(text);
|
||||
install(node, tips);
|
||||
return tips;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
/**
|
||||
* 正版登录验证
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-08-03 12:06
|
||||
*/
|
||||
public class MojangLoginService {
|
||||
// TODO 待实现
|
||||
}
|
||||
75
src/main/java/cn/forevermc/launcher/service/PageService.java
Normal file
@ -0,0 +1,75 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.view.components.RootLayout;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.ReadOnlyObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import lombok.Getter;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
|
||||
/**
|
||||
* 页面服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 16:21
|
||||
*/
|
||||
@Service
|
||||
public class PageService {
|
||||
|
||||
@Inject
|
||||
private RootLayout root;
|
||||
|
||||
/** 上一页 */
|
||||
@Getter
|
||||
private Page prev;
|
||||
|
||||
private final ObjectProperty<Page> activatedPage;
|
||||
|
||||
private boolean isSneakyBack;
|
||||
|
||||
public PageService() {
|
||||
activatedPage = new SimpleObjectProperty<>();
|
||||
activatedPage.addListener((obs, prev, now) -> {
|
||||
this.prev = prev;
|
||||
if (!isSneakyBack && prev != null) {
|
||||
prev.getIOCPage().hide();
|
||||
}
|
||||
if (now != null) {
|
||||
now.getIOCPage().show();
|
||||
root.setPage(now.getIOCPage());
|
||||
}
|
||||
isSneakyBack = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转页面
|
||||
*
|
||||
* @param page 页面
|
||||
*/
|
||||
public void to(Page page) {
|
||||
activatedPage.set(page);
|
||||
}
|
||||
|
||||
/** 返回上一个页面 */
|
||||
public void back() {
|
||||
to(prev);
|
||||
}
|
||||
|
||||
/** 悄悄地返回,此动作不会调度页面的 onHide 事件 */
|
||||
public void back4Sneaky() {
|
||||
isSneakyBack = true;
|
||||
back();
|
||||
}
|
||||
|
||||
public Page getActivatedPage() {
|
||||
return activatedPage.get();
|
||||
}
|
||||
|
||||
/** @return 当前页面监听 */
|
||||
public ReadOnlyObjectProperty<Page> activatedPageProperty() {
|
||||
return activatedPage;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package cn.forevermc.launcher.service;
|
||||
|
||||
import cn.forevermc.launcher.bean.APISetting;
|
||||
import javafx.beans.property.ReadOnlyStringProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.inject.annotation.Service;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 动态闪烁标语服务
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-22 10:45
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SplashService {
|
||||
|
||||
@Inject
|
||||
private APISettingService apiSettingService;
|
||||
|
||||
private final StringProperty value;
|
||||
|
||||
public SplashService() {
|
||||
value = new SimpleStringProperty();
|
||||
value.set("Forever MC!");
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
apiSettingService.valueProperty().addListener((obs, o, setting) -> {
|
||||
if (setting == null) {
|
||||
return;
|
||||
}
|
||||
APISetting.DynamicList splashes = setting.getSplashes();
|
||||
if (TimiJava.isNotEmpty(splashes.getActive())) {
|
||||
value.set(splashes.getActive());
|
||||
return;
|
||||
}
|
||||
Collections.shuffle(splashes.getList());
|
||||
value.set(splashes.getList().get(0));
|
||||
});
|
||||
}
|
||||
|
||||
public ReadOnlyStringProperty valueProperty() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
230
src/main/java/cn/forevermc/launcher/util/FabricBuilder.java
Normal file
@ -0,0 +1,230 @@
|
||||
package cn.forevermc.launcher.util;
|
||||
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.service.GameService;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.StaticInject;
|
||||
import com.imyeyu.io.IO;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.network.FileRequest;
|
||||
import com.imyeyu.network.GsonRequest;
|
||||
import com.imyeyu.network.TimiRequest;
|
||||
import com.imyeyu.utils.Digest;
|
||||
import com.imyeyu.utils.OS;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.hc.client5.http.fluent.Request;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Fabric API 解析器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 10:33
|
||||
*/
|
||||
@Slf4j
|
||||
@StaticInject
|
||||
public class FabricBuilder extends RunAsync<Game> implements OS.FileSystem {
|
||||
|
||||
public static final String API = "https://maven.fabricmc.net/";
|
||||
public static final String LOADER_URL = "https://meta.fabricmc.net/v2/versions/loader";
|
||||
private static final String API_MIRROR_URL = Resources.TIMI_SERVER_API + "/mirror/%s";
|
||||
private static final String ATTACH_DL_URL = Resources.TIMI_SERVER_API + "/attachment/download/%s";
|
||||
|
||||
@Inject
|
||||
private static Gson gson;
|
||||
|
||||
@Inject
|
||||
private static GameService gameService;
|
||||
|
||||
@Setter
|
||||
private Game buildGame;
|
||||
|
||||
@Override
|
||||
protected final Game call() throws Exception {
|
||||
if (buildGame == null) {
|
||||
throw new TimiException(TimiCode.ARG_MISS);
|
||||
}
|
||||
String oldName = buildGame.getName();
|
||||
String newName = oldName + "-Fabric";
|
||||
log.info("installing fabric: {}", newName);
|
||||
Game.MetaData.RealVersion realVersion = gameService.getRealVersion(buildGame);
|
||||
// 原版数据
|
||||
File jsonFile = buildGame.getJson();
|
||||
JsonObject jsonRoot = buildGame.getJsonRoot();
|
||||
// 获取加载器
|
||||
log.info("fetching fabric loader..");
|
||||
JsonArray loaderList = GsonRequest.get(LOADER_URL).asJsonArray();
|
||||
JsonObject loader = null;
|
||||
for (int i = 0; i < loaderList.size(); i++) {
|
||||
loader = loaderList.get(i).getAsJsonObject();
|
||||
if (loader.get("stable").getAsBoolean()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (loader == null) {
|
||||
log.info("not found support fabric loader, game: {}", oldName);
|
||||
throw new TimiException(TimiCode.RESULT_NULL, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.not_support_api"));
|
||||
}
|
||||
// 获取 Fabric Loader 加载器的游戏版本版本依赖
|
||||
JsonObject fabricData, fabricMeta;
|
||||
{
|
||||
String loaderVersion = loader.get("version").getAsString();
|
||||
// Fabric Loader 数据
|
||||
String fabricMetaResp;
|
||||
try {
|
||||
fabricMetaResp = Request.get(LOADER_URL + "/" + oldName + "/" + loaderVersion).execute().returnContent().asString();
|
||||
} catch (IOException e) {
|
||||
log.error("not found support fabric api, game: {}", oldName);
|
||||
throw new TimiException(TimiCode.RESULT_BAD, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.not_support"));
|
||||
}
|
||||
log.info("injecting fabric lib to original json");
|
||||
fabricData = JsonParser.parseString(fabricMetaResp).getAsJsonObject();
|
||||
fabricMeta = fabricData.get("launcherMeta").getAsJsonObject();
|
||||
}
|
||||
// 改名
|
||||
jsonRoot.addProperty("id", newName);
|
||||
// 启动类
|
||||
jsonRoot.addProperty("mainClass", fabricMeta.get("mainClass").getAsJsonObject().get("client").getAsString());
|
||||
{
|
||||
// 向原版注入 Fabric Loader 依赖
|
||||
JsonArray minecraftLibs = jsonRoot.get("libraries").getAsJsonArray();
|
||||
{
|
||||
// Fabric Loader 加载器
|
||||
JsonObject fabricLoader = fabricData.get("loader").getAsJsonObject();
|
||||
fabricLoader.addProperty("url", API);
|
||||
fabricLoader.addProperty("name", fabricLoader.get("maven").getAsString());
|
||||
minecraftLibs.add(injectToMinecraftLib(fabricLoader));
|
||||
}
|
||||
{
|
||||
// Fabric Loader 中间件
|
||||
JsonObject fabricIntermediary = fabricData.get("intermediary").getAsJsonObject();
|
||||
fabricIntermediary.addProperty("url", API);
|
||||
fabricIntermediary.addProperty("name", fabricIntermediary.get("maven").getAsString());
|
||||
minecraftLibs.add(injectToMinecraftLib(fabricIntermediary));
|
||||
}
|
||||
{
|
||||
// Fabric API mod
|
||||
List<FabricAPIItem> apiItemList = TimiRequest.<List<FabricAPIItem>>get(API_MIRROR_URL.formatted("FabricAPI")).result();
|
||||
FabricAPIItem item = null;
|
||||
for (FabricAPIItem fabricAPI : apiItemList) {
|
||||
if (fabricAPI.minecraftVer.equals(realVersion.getValue())) {
|
||||
item = fabricAPI;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (item == null) {
|
||||
throw new TimiException(TimiCode.RESULT_NULL, "not found fabric-api");
|
||||
}
|
||||
File modFile = IO.file(IO.fitPath(buildGame.getPath().getAbsolutePath()) + "mods" + SEP + item.name);
|
||||
FileRequest.get(ATTACH_DL_URL.formatted(item.mongoId)).toFile(modFile);
|
||||
}
|
||||
{
|
||||
// 其他依赖
|
||||
JsonArray fabricLibsClient = fabricMeta.get("libraries").getAsJsonObject().get("client").getAsJsonArray();
|
||||
JsonArray fabricLibsCommon = fabricMeta.get("libraries").getAsJsonObject().get("common").getAsJsonArray();
|
||||
for (int i = 0; i < fabricLibsClient.size(); i++) {
|
||||
minecraftLibs.add(injectToMinecraftLib(fabricLibsClient.get(i).getAsJsonObject()));
|
||||
}
|
||||
for (int i = 0; i < fabricLibsCommon.size(); i++) {
|
||||
minecraftLibs.add(injectToMinecraftLib(fabricLibsCommon.get(i).getAsJsonObject()));
|
||||
}
|
||||
}
|
||||
}
|
||||
// 原版版本
|
||||
jsonRoot.addProperty("source-id", oldName);
|
||||
// 加载器会修改标题,此标题用于判定是否启动成功
|
||||
jsonRoot.addProperty("title", "Minecraft* " + oldName);
|
||||
log.info("rename game file");
|
||||
// 写入文件
|
||||
IO.toFile(jsonFile, gson.toJson(jsonRoot));
|
||||
// 重命名版本
|
||||
File minecraftJarFile = buildGame.getJar();
|
||||
if (minecraftJarFile != null && minecraftJarFile.exists()) {
|
||||
IO.rename(minecraftJarFile, newName + ".jar");
|
||||
}
|
||||
// 重命名配置
|
||||
buildGame.setYaml(IO.rename(buildGame.getYaml(), newName + ".yaml"));
|
||||
buildGame.setJson(IO.rename(jsonFile, newName + ".json"));
|
||||
IO.rename(jsonFile.getParentFile(), newName);
|
||||
// 重载
|
||||
return gameService.buildGame(new File(Path.ABS_PACK + newName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Fabric 依赖转为原版结构 JSON
|
||||
*
|
||||
* @param fabricLib Fabric 依赖
|
||||
* @return 原版结构 JSON
|
||||
*/
|
||||
private JsonObject injectToMinecraftLib(JsonObject fabricLib) throws Exception {
|
||||
JsonObject minecraftLib = new JsonObject();
|
||||
|
||||
// net.fabricmc:tiny-mappings-parser:0.3.0+build.17
|
||||
String name = fabricLib.get("name").getAsString();
|
||||
String[] names = name.split(":");
|
||||
|
||||
StringBuilder pathSB = new StringBuilder();
|
||||
String[] paths = names[0].split("\\.");
|
||||
for (int i = 0; i < paths.length; i++) {
|
||||
pathSB.append(paths[i]).append('/');
|
||||
}
|
||||
pathSB.append(names[1]).append('/').append(names[2]).append('/').append(names[1]).append('-').append(names[2]).append(".jar");
|
||||
|
||||
minecraftLib.addProperty("name", name);
|
||||
JsonObject downloads = new JsonObject();
|
||||
{
|
||||
String url = fabricLib.get("url").getAsString() + pathSB;
|
||||
|
||||
JsonObject artifact = new JsonObject();
|
||||
artifact.addProperty("path", pathSB.toString());
|
||||
artifact.addProperty("url", url);
|
||||
// TODO 耗时操作,告知 UI
|
||||
if (fabricLib.has("size")) {
|
||||
artifact.addProperty("size", fabricLib.get("size").getAsLong());
|
||||
} else {
|
||||
artifact.addProperty("size", FileRequest.get(url).length());
|
||||
}
|
||||
if (fabricLib.has("sha1")) {
|
||||
|
||||
artifact.addProperty("sha1", fabricLib.get("sha1").getAsString());
|
||||
} else {
|
||||
artifact.addProperty("sha1", Digest.sha1(FileRequest.get(url).asBytes()));
|
||||
}
|
||||
downloads.add("artifact", artifact);
|
||||
minecraftLib.add("downloads", downloads);
|
||||
}
|
||||
return minecraftLib;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fabric API mod 对象
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-07-04 19:48
|
||||
*/
|
||||
private static class FabricAPIItem {
|
||||
|
||||
/** 文件名 */
|
||||
String name;
|
||||
|
||||
/** Fabric 版本 */
|
||||
String fabricVer;
|
||||
|
||||
/** Minecraft 版本 */
|
||||
String minecraftVer;
|
||||
|
||||
/** 下载 MongoId */
|
||||
String mongoId;
|
||||
}
|
||||
}
|
||||
45
src/main/java/cn/forevermc/launcher/util/Path.java
Normal file
@ -0,0 +1,45 @@
|
||||
package cn.forevermc.launcher.util;
|
||||
|
||||
import com.imyeyu.utils.OS;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
/**
|
||||
* 路径
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-09 15:17
|
||||
*/
|
||||
public class Path implements OS.FileSystem {
|
||||
|
||||
// ---------- 相对路径 ----------
|
||||
|
||||
/** 根目录 */
|
||||
public static final String P_ROOT = ".minecraft" + SEP;
|
||||
|
||||
/** 资源 */
|
||||
public static final String P_ASSETS = P_ROOT + "assets" + SEP;
|
||||
|
||||
/** 依赖 */
|
||||
public static final String P_LIB = P_ROOT + "libraries" + SEP;
|
||||
|
||||
/** 版本 */
|
||||
public static final String P_PACK = P_ROOT + "versions" + SEP;
|
||||
|
||||
// ---------- 绝对路径 ----------
|
||||
|
||||
/** 绝对客户端所在目录 */
|
||||
public static final String ABS = new File("./").getAbsoluteFile().getParent() + SEP;
|
||||
|
||||
/** 绝对根目录 */
|
||||
public static final String ABS_ROOT = ABS + P_ROOT;
|
||||
|
||||
/** 绝对资源 */
|
||||
public static final String ABS_ASSETS = ABS + P_ASSETS;
|
||||
|
||||
/** 绝对依赖 */
|
||||
public static final String ABS_LIB = ABS + P_LIB;
|
||||
|
||||
/** 绝对版本 */
|
||||
public static final String ABS_PACK = ABS + P_PACK;
|
||||
}
|
||||
122
src/main/java/cn/forevermc/launcher/util/Resources.java
Normal file
@ -0,0 +1,122 @@
|
||||
package cn.forevermc.launcher.util;
|
||||
|
||||
import cn.forevermc.launcher.ForeverMC;
|
||||
import cn.forevermc.launcher.bean.APISetting;
|
||||
import cn.forevermc.launcher.bean.Config;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.imyeyu.inject.annotation.IOCReturn;
|
||||
import com.imyeyu.io.IOSize;
|
||||
import com.imyeyu.io.IOSpeedService;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.effect.DropShadow;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.media.MediaPlayer;
|
||||
import javafx.scene.paint.Color;
|
||||
|
||||
/**
|
||||
* 静态资源
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-18 15:41
|
||||
*/
|
||||
@com.imyeyu.inject.annotation.Resources
|
||||
public class Resources {
|
||||
|
||||
/** TimiServer 接口 */
|
||||
public static final String TIMI_SERVER_API = "http://localhost:8091";
|
||||
|
||||
/** TimiServer 接口 */
|
||||
// public static final String TIMI_SERVER_API = "https://api.imyeyu.com";
|
||||
|
||||
// ---------- 图标 ----------
|
||||
|
||||
/** 启动器图标 */
|
||||
public static final Image ICON = new Image("icon.png");
|
||||
|
||||
/** 窗体标题图标 */
|
||||
public static final Image ICON_MC = new Image("assets/img/icon-mc.png");
|
||||
|
||||
/** 远古版本窗体标题图标 */
|
||||
public static final Image ICON_MC_ANCIENT = new Image("assets/img/icon-mc-ancient.png");
|
||||
|
||||
/** 按钮 */
|
||||
public static final Image BTN = new Image("assets/img/btn.png", 396, 36, true, false, true);
|
||||
|
||||
/** 选择钩子 X36 */
|
||||
public static final Image CHECK = new Image("assets/img/check.png", 36, 36, false, false);
|
||||
|
||||
/** 选择钩子 X18 */
|
||||
public static final Image CHECK_SMALL = new Image("assets/img/check.png", 18, 18, false, false);
|
||||
|
||||
/** 标题 */
|
||||
public static final Image TITLE = new Image("assets/img/title.png");
|
||||
|
||||
/** 新标题(1.20 版本开始) */
|
||||
public static final Image TITLE_NEW = new Image("assets/img/title-modern.png", 680, 100, true, false);
|
||||
|
||||
/** 语言 */
|
||||
public static final Image LANG = new Image("assets/img/lang.png", 26, 26, true, true);
|
||||
|
||||
/** 启动过渡页 */
|
||||
public static final Image SPLASH_SCREEN = new Image("splash-screen.png");
|
||||
|
||||
/** Mojang Logo */
|
||||
public static final Image MOJANG = new Image("assets/img/mojang.png", 480, -1, true, true);
|
||||
|
||||
/** Mojang Logo 远古版本 */
|
||||
public static final Image MOJANG_ANCIENT = new Image("assets/img/mojang_ancient.png", 480, -1, true, true);
|
||||
|
||||
/** 泥土(平铺背景) */
|
||||
public static final Image DIRT = new Image("assets/img/dirt.png");
|
||||
|
||||
/** 信息 */
|
||||
public static final Image INFO = new Image("assets/img/info.png");
|
||||
|
||||
/** F(ForeverMC 登录方式图标) */
|
||||
public static final Image F = new Image("assets/img/f.png");
|
||||
|
||||
/** M(Mojang 登录方式图标) */
|
||||
public static final Image M = new Image("assets/img/m.png");
|
||||
|
||||
/** 不死图腾 */
|
||||
public static final Image TOTEM = new Image("assets/img/totem.png", 32, -1, true, false);
|
||||
|
||||
/** 命令方块 */
|
||||
public static final Image COMMAND_BLOCK = new Image("assets/img/command_block.png", 32, -1, true, false);
|
||||
|
||||
/** 文本投影 */
|
||||
public static final DropShadow TEXT_SHADOW = new DropShadow(0, 1, 1, Color.valueOf("#0009"));
|
||||
|
||||
/** @return Gson */
|
||||
@IOCReturn
|
||||
public Gson gson() {
|
||||
return new GsonBuilder().serializeNulls().create();
|
||||
}
|
||||
|
||||
/** @return 主配置 */
|
||||
@IOCReturn
|
||||
public Config config() {
|
||||
return ForeverMC.getConfig();
|
||||
}
|
||||
|
||||
@IOCReturn
|
||||
public APISetting apiSetting() {
|
||||
return new APISetting();
|
||||
}
|
||||
|
||||
@IOCReturn
|
||||
public ObjectProperty<MediaPlayer> bgmPlayer() {
|
||||
return new SimpleObjectProperty<>();
|
||||
}
|
||||
|
||||
/** @return 字节速度计算主项 */
|
||||
@IOCReturn
|
||||
public IOSpeedService.Item ioSpeedItem() {
|
||||
IOSpeedService service = IOSpeedService.getInstance();
|
||||
service.setSalt(IOSize.KB * 4);
|
||||
service.start();
|
||||
return service.createItem();
|
||||
}
|
||||
}
|
||||
15
src/main/java/cn/forevermc/launcher/util/Sound.java
Normal file
@ -0,0 +1,15 @@
|
||||
package cn.forevermc.launcher.util;
|
||||
|
||||
import javafx.scene.media.AudioClip;
|
||||
|
||||
/**
|
||||
* 音频
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-09 15:15
|
||||
*/
|
||||
public class Sound {
|
||||
|
||||
/** 点击音效 */
|
||||
public static final AudioClip CLICK = new AudioClip(ClassLoader.getSystemResource("assets/sound/click.wav").toExternalForm());
|
||||
}
|
||||
34
src/main/java/cn/forevermc/launcher/view/ViewMain.java
Normal file
@ -0,0 +1,34 @@
|
||||
package cn.forevermc.launcher.view;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.components.RootLayout;
|
||||
import javafx.application.Application;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.stage.Stage;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
|
||||
/**
|
||||
* 主视图
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-18 15:36
|
||||
*/
|
||||
public abstract class ViewMain extends Application implements TimiFXUI {
|
||||
|
||||
@Inject
|
||||
private RootLayout root;
|
||||
|
||||
@Override
|
||||
public void start(Stage stage) {
|
||||
Scene scene = new Scene(root);
|
||||
scene.getStylesheets().addAll(CSS_FONT, CSS_STYLE, "style.css");
|
||||
stage.setScene(scene);
|
||||
stage.getIcons().add(Resources.ICON);
|
||||
stage.setTitle("Forever MC");
|
||||
stage.setMinWidth(870);
|
||||
stage.setMinHeight(519);
|
||||
stage.setWidth(870);
|
||||
stage.setHeight(519);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
package cn.forevermc.launcher.view.components;
|
||||
|
||||
import cn.forevermc.launcher.ForeverMC;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.Region;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.inject.TimiInject;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.StaticInject;
|
||||
|
||||
/**
|
||||
* 内容面板,具有标题和底部操作面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-07-14 17:38
|
||||
*/
|
||||
@StaticInject
|
||||
public abstract class AbstractContentPane extends AbstractPane {
|
||||
|
||||
@Inject
|
||||
private static LaunchGame launchGame;
|
||||
|
||||
protected final GridPane bottom;
|
||||
protected final StackPane content;
|
||||
|
||||
private final Label title;
|
||||
private final Region shadowTop, shadowBottom;
|
||||
private final StackPane strokeWhitePane;
|
||||
|
||||
public AbstractContentPane() {
|
||||
title = new MCLabel();
|
||||
title.setPadding(new Insets(24, 0, 16, 0));
|
||||
|
||||
// 黑边
|
||||
StackPane strokeBlackPane = new StackPane();
|
||||
strokeBlackPane.setBorder(new BorderStroke(Color.valueOf("#000A")).width(2, 0, 2, 0).build());
|
||||
{
|
||||
// 白边
|
||||
strokeWhitePane = new StackPane();
|
||||
strokeWhitePane.setBackground(new BgFill("#0007").build());
|
||||
{
|
||||
// 旧 UI 阴影
|
||||
shadowTop = new Region();
|
||||
shadowBottom = new Region();
|
||||
{
|
||||
shadowTop.setMaxHeight(8);
|
||||
shadowTop.setBackground(new BgFill("#0006", "#0000").toBottom().build());
|
||||
shadowTop.setPickOnBounds(false);
|
||||
shadowTop.setFocusTraversable(true);
|
||||
shadowTop.setMouseTransparent(true);
|
||||
shadowBottom.setMaxHeight(8);
|
||||
shadowBottom.setBackground(new BgFill("#0000", "#0006").toBottom().build());
|
||||
shadowBottom.setPickOnBounds(false);
|
||||
shadowBottom.setFocusTraversable(true);
|
||||
shadowBottom.setMouseTransparent(true);
|
||||
}
|
||||
|
||||
// 内容面板
|
||||
content = new StackPane();
|
||||
StackPane.setAlignment(shadowTop, Pos.TOP_CENTER);
|
||||
StackPane.setAlignment(shadowBottom, Pos.BOTTOM_CENTER);
|
||||
strokeWhitePane.getChildren().addAll(content, shadowTop, shadowBottom);
|
||||
}
|
||||
strokeBlackPane.getChildren().addAll(strokeWhitePane);
|
||||
}
|
||||
|
||||
bottom = new GridPane();
|
||||
bottom.setHgap(8);
|
||||
bottom.setVgap(8);
|
||||
bottom.setAlignment(Pos.TOP_CENTER);
|
||||
bottom.setPadding(new Insets(12));
|
||||
|
||||
setAlignment(title, Pos.CENTER);
|
||||
setTop(title);
|
||||
setCenter(strokeBlackPane);
|
||||
setBottom(bottom);
|
||||
|
||||
ForeverMC.getInjectApp().addAfterInjectListener(timiInject -> {
|
||||
strokeWhitePane.borderProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (launchGame.isNewUI()) {
|
||||
return new BorderStroke(Color.valueOf("#FFF4")).width(2, 0, 2, 0).build();
|
||||
}
|
||||
return new BorderStroke(Color.valueOf("#000C")).width(2, 0, 2, 0).build();
|
||||
}, launchGame));
|
||||
shadowTop.visibleProperty().bind(launchGame.newUIProperty().not());
|
||||
shadowBottom.visibleProperty().bind(launchGame.newUIProperty().not());
|
||||
});
|
||||
}
|
||||
|
||||
protected void setTitle(String title) {
|
||||
this.title.setText(title);
|
||||
}
|
||||
|
||||
protected void setContentPane(Node node) {
|
||||
content.getChildren().setAll(node);
|
||||
}
|
||||
|
||||
protected void addBottomPaneValue(Node child, int columnIndex, int rowIndex, int colSpan, int rowSpan) {
|
||||
bottom.add(child, columnIndex, rowIndex, colSpan, rowSpan);
|
||||
}
|
||||
|
||||
protected void addBottomPaneValue(int rowIndex, Node... children) {
|
||||
bottom.addRow(rowIndex, children);
|
||||
}
|
||||
|
||||
protected void setBottomPaneValue(Node... children) {
|
||||
bottom.getChildren().clear();
|
||||
bottom.addRow(0, children);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package cn.forevermc.launcher.view.components;
|
||||
|
||||
import cn.forevermc.launcher.view.components.control.MCList;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.geometry.Insets;
|
||||
|
||||
/**
|
||||
* 列表面板,基于内容面板的列表视图
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-23 17:41
|
||||
*/
|
||||
public abstract class AbstractListPane<T> extends AbstractContentPane {
|
||||
|
||||
private static final Insets PADDING_SCROLL = new Insets(0, 0, 0, 32);
|
||||
private static final Insets PADDING_SCROLL_R = new Insets(0, 0, 0, 0);
|
||||
|
||||
protected final MCList<T> list;
|
||||
|
||||
public AbstractListPane() {
|
||||
list = new MCList<>();
|
||||
|
||||
content.paddingProperty().bind(Bindings.when(list.showingVScrollBarProperty()).then(PADDING_SCROLL).otherwise(PADDING_SCROLL_R));
|
||||
content.maxWidthProperty().bind(Bindings.when(list.showingVScrollBarProperty()).then(690D).otherwise(640));
|
||||
setContentPane(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHide() {
|
||||
list.getItems().clear();
|
||||
}
|
||||
|
||||
/** @return true 为正在加载 */
|
||||
public boolean isLoading() {
|
||||
return list.isLoading();
|
||||
}
|
||||
|
||||
/** @return 正在加载属性 */
|
||||
public BooleanProperty loadingProperty() {
|
||||
return list.loadingProperty();
|
||||
}
|
||||
|
||||
/** @param isLoading true 为正在加载 */
|
||||
public void setLoading(boolean isLoading) {
|
||||
list.setLoading(isLoading);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,52 @@
|
||||
package cn.forevermc.launcher.view.components;
|
||||
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.java.bean.Callback;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 抽象面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 14:23
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public abstract class AbstractPane extends BorderPane implements TimiFXUI {
|
||||
|
||||
/** 显示事件,触发慢于 {@link #onShow()},一次性对象,触发即删除 */
|
||||
private Callback onShowEvent;
|
||||
|
||||
/** 隐藏事件,触发慢于 {@link #onHide()},一次性对象,触发即删除 */
|
||||
private Callback onHideEvent;
|
||||
|
||||
/** 触发显示(页面服务调度) */
|
||||
public final void show() {
|
||||
onShow();
|
||||
if (onShowEvent != null) {
|
||||
onShowEvent.handler();
|
||||
onShowEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 触发隐藏(页面服务调度) */
|
||||
public final void hide() {
|
||||
onHide();
|
||||
if (onHideEvent != null) {
|
||||
onHideEvent.handler();
|
||||
onHideEvent = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 显示时触发,UI 线程 */
|
||||
protected void onShow() {
|
||||
// 子类实现
|
||||
}
|
||||
|
||||
/** 隐藏时触发,UI 线程 */
|
||||
protected void onHide() {
|
||||
// 子类实现
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package cn.forevermc.launcher.view.components;
|
||||
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ScrollBar;
|
||||
import javafx.scene.control.ScrollPane;
|
||||
import javafx.scene.control.skin.ScrollPaneSkin;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.fx.utils.SmoothScroll;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
|
||||
/**
|
||||
* 滚动面板,支持滚动的内容面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-07-14 17:43
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class AbstractScrollPane extends AbstractContentPane {
|
||||
|
||||
private static final Insets PADDING_SCROLL = new Insets(0, 0, 0, 32);
|
||||
private static final Insets PADDING_SCROLL_R = new Insets(0, 0, 0, 0);
|
||||
|
||||
private final StackPane content;
|
||||
|
||||
protected final BooleanBinding showingVScrollBar;
|
||||
|
||||
private ScrollBar vsb;
|
||||
|
||||
public AbstractScrollPane() {
|
||||
content = new StackPane();
|
||||
content.setAlignment(Pos.CENTER);
|
||||
content.setPadding(new Insets(16, 0, 12, 0));
|
||||
|
||||
ScrollPane scrollPane = new ScrollPane() {{
|
||||
setAlignment(content, Pos.TOP_CENTER);
|
||||
setFitToWidth(true);
|
||||
getStyleClass().add(CSS.BG_TP);
|
||||
|
||||
setContent(content);
|
||||
SmoothScroll.scrollPane(this);
|
||||
}};
|
||||
setContentPane(scrollPane);
|
||||
|
||||
showingVScrollBar = Bindings.createBooleanBinding(() -> {
|
||||
// TODO 最大化或最小化时无效
|
||||
try {
|
||||
if (vsb == null && scrollPane.getSkin() != null && scrollPane.getSkin() instanceof ScrollPaneSkin skin) {
|
||||
vsb = Ref.getFieldValue(skin, "vsb", ScrollBar.class);
|
||||
}
|
||||
if (vsb != null) {
|
||||
return vsb.isVisible();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("ref MCList virtual scroll bar error", e);
|
||||
}
|
||||
return false;
|
||||
}, scrollPane.skinProperty(), scrollPane.layoutBoundsProperty());
|
||||
|
||||
scrollPane.paddingProperty().bind(Bindings.when(showingVScrollBar).then(PADDING_SCROLL).otherwise(PADDING_SCROLL_R));
|
||||
scrollPane.maxWidthProperty().bind(Bindings.when(showingVScrollBar).then(690D).otherwise(640));
|
||||
}
|
||||
|
||||
protected void setScrollContent(Node node) {
|
||||
content.getChildren().setAll(node);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,112 @@
|
||||
package cn.forevermc.launcher.view.components;
|
||||
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Text;
|
||||
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
|
||||
import javax.swing.border.StrokeBorder;
|
||||
|
||||
/**
|
||||
* 登录方式选择器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2024-08-03 11:41
|
||||
*/
|
||||
public class LoginTypeSelector extends StackPane {
|
||||
|
||||
private final ObservableList<Game.Option.LoginType> items;
|
||||
|
||||
private final ObjectProperty<Game.Option.LoginType> value;
|
||||
|
||||
private final BooleanProperty requiredOffline;
|
||||
|
||||
public LoginTypeSelector() {
|
||||
items = FXCollections.observableArrayList();
|
||||
items.setAll(Game.Option.LoginType.OFFLINE, Game.Option.LoginType.FOREVER_MC); // 暂不支持 MOJANG
|
||||
value = new SimpleObjectProperty<>();
|
||||
requiredOffline = new SimpleBooleanProperty(false);
|
||||
|
||||
Text offline = TimiFXIcon.fromName("UNLINK", TimiFXUI.Colorful.WHITE);
|
||||
ImageView foreverMC = new ImageView(Resources.F);
|
||||
ImageView mojang = new ImageView(Resources.M);
|
||||
|
||||
setMaxSize(16, 16);
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
addEventFilter(MouseEvent.MOUSE_CLICKED, e -> {
|
||||
if (!isRequiredOffline()) {
|
||||
int nextIndex = items.indexOf(value.get()) + 1;
|
||||
if (items.size() <= nextIndex) {
|
||||
nextIndex = 0;
|
||||
}
|
||||
value.set(items.get(nextIndex));
|
||||
}
|
||||
});
|
||||
|
||||
value.addListener((obs, o, n) -> getChildren().setAll(switch (value.get()) {
|
||||
case OFFLINE -> offline;
|
||||
case FOREVER_MC -> foreverMC;
|
||||
case MOJANG -> mojang;
|
||||
}));
|
||||
|
||||
requiredOffline.addListener((obs, o, n) -> {
|
||||
if (isRequiredOffline()) {
|
||||
value.set(Game.Option.LoginType.OFFLINE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isOffline() {
|
||||
return value.get() == Game.Option.LoginType.OFFLINE;
|
||||
}
|
||||
|
||||
public boolean isOnline() {
|
||||
return !isOffline();
|
||||
}
|
||||
|
||||
public boolean isForeverMC() {
|
||||
return value.get() == Game.Option.LoginType.FOREVER_MC;
|
||||
}
|
||||
|
||||
public boolean isMojang() {
|
||||
return value.get() == Game.Option.LoginType.MOJANG;
|
||||
}
|
||||
|
||||
public void setValue(Game.Option.LoginType value) {
|
||||
this.value.set(value);
|
||||
}
|
||||
|
||||
public Game.Option.LoginType getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
public ObjectProperty<Game.Option.LoginType> valueProperty() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setRequiredOffline(boolean requiredOffline) {
|
||||
this.requiredOffline.set(requiredOffline);
|
||||
}
|
||||
|
||||
public boolean isRequiredOffline() {
|
||||
return requiredOffline.get();
|
||||
}
|
||||
|
||||
public BooleanProperty requiredOfflineProperty() {
|
||||
return requiredOffline;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
package cn.forevermc.launcher.view.components;
|
||||
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.bean.Page;
|
||||
import cn.forevermc.launcher.service.BGService;
|
||||
import cn.forevermc.launcher.service.PageService;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.effect.GaussianBlur;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import com.imyeyu.fx.utils.BgImage;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
|
||||
/**
|
||||
* 根布局
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 16:39
|
||||
*/
|
||||
@Component
|
||||
public class RootLayout extends StackPane {
|
||||
|
||||
@Inject
|
||||
private BGService bgService;
|
||||
|
||||
@Inject
|
||||
private PageService pageService;
|
||||
|
||||
@Inject
|
||||
private LaunchGame launchGame;
|
||||
|
||||
private final StackPane bg;
|
||||
private final BorderPane main;
|
||||
|
||||
public RootLayout() {
|
||||
// 背景面板
|
||||
bg = new StackPane();
|
||||
bg.setScaleX(1.1);
|
||||
bg.setScaleY(1.1);
|
||||
|
||||
main = new BorderPane();
|
||||
getChildren().addAll(bg, main);
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
// 动态背景
|
||||
final float bgStep = .1F;
|
||||
addEventFilter(MouseEvent.MOUSE_MOVED, new EventHandler<>() {
|
||||
|
||||
// 面板 xy 坐标,旧的鼠标 xy 坐标
|
||||
private double pX = 0, pY = 0, moX, moY;
|
||||
|
||||
@Override
|
||||
public void handle(MouseEvent event) {
|
||||
if (event.getX() < moX && pX < 0) {
|
||||
pX += bgStep;
|
||||
} else {
|
||||
if (moX < event.getX() && -20 < pX) {
|
||||
pX -= bgStep;
|
||||
}
|
||||
}
|
||||
if (event.getY() < moY && pY < 0) {
|
||||
pY += bgStep;
|
||||
} else {
|
||||
if (moY < event.getY() && -20 < pY) {
|
||||
pY -= bgStep;
|
||||
}
|
||||
}
|
||||
bg.setTranslateX(pX);
|
||||
bg.setTranslateY(pY);
|
||||
moX = event.getX();
|
||||
moY = event.getY();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
Background legacyBG = new BgImage(Resources.DIRT).build();
|
||||
bg.backgroundProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (launchGame.isNewUI()) {
|
||||
return bgService.getValue();
|
||||
}
|
||||
if (pageService.getActivatedPage() == Page.MENU) {
|
||||
return bgService.getValue();
|
||||
}
|
||||
return legacyBG;
|
||||
}, launchGame.newUIProperty(), pageService.activatedPageProperty()));
|
||||
BooleanBinding withOutBlur = pageService.activatedPageProperty().isEqualTo(Page.MENU).or(launchGame.newUIProperty().not());
|
||||
bg.effectProperty().bind(Bindings.when(withOutBlur).then((GaussianBlur) null).otherwise(new GaussianBlur(16)));
|
||||
}
|
||||
|
||||
public void setPage(AbstractPane page) {
|
||||
main.setCenter(page);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,75 @@
|
||||
package cn.forevermc.launcher.view.components;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.imyeyu.fx.task.RunAsync;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.ui.components.VersionLabel;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.timi.TimiCode;
|
||||
import com.imyeyu.java.bean.timi.TimiException;
|
||||
import com.imyeyu.network.GsonRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Timi 通用版本标签
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-05-31 23:24
|
||||
*/
|
||||
@Slf4j
|
||||
public class TimiVersionLabel extends VersionLabel<JsonObject> {
|
||||
|
||||
private final String nowVersion, appName;
|
||||
|
||||
public TimiVersionLabel(String nowVersion, String appName) {
|
||||
super(TimiFXUI.MULTILINGUAL.textArgs("version.checking", nowVersion));
|
||||
|
||||
this.appName = appName;
|
||||
this.nowVersion = nowVersion;
|
||||
|
||||
Status.NORMAL.setTextColor(WHITE);
|
||||
version.setEffect(Resources.TEXT_SHADOW);
|
||||
RunAsync.later(() -> checkVersion(nowVersion), 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected JsonObject run() {
|
||||
try {
|
||||
Thread.sleep(2000);
|
||||
log.info("checking version..");
|
||||
String api = Resources.TIMI_SERVER_API + "/versions/" + appName;
|
||||
return GsonRequest.get(api).asJsonObject();
|
||||
} catch (Exception e) {
|
||||
throw new TimiException(TimiCode.ERROR, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String onReturn(JsonObject resp) {
|
||||
int code = resp.get("code").getAsInt();
|
||||
if (code == 20000) {
|
||||
JsonObject data = resp.get("data").getAsJsonObject();
|
||||
setUpdateURL(data.get("url").getAsString());
|
||||
if (data.has("content")) {
|
||||
String content = data.get("content").getAsString();
|
||||
if (TimiJava.isNotEmpty(content)) {
|
||||
this.content.setText(content);
|
||||
}
|
||||
}
|
||||
return data.get("version").getAsString();
|
||||
} else {
|
||||
throw new TimiException(TimiCode.fromCode(code), resp.get("msg").getAsString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String updateText(String newVersion) {
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("version.new", nowVersion, newVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String failText(Throwable e) {
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("version.fail", nowVersion);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.util.Sound;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import lombok.Getter;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgImage;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
|
||||
/**
|
||||
* MC 按钮
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-22 23:05
|
||||
*/
|
||||
public class MCButton extends Button implements TimiFXUI {
|
||||
|
||||
private static final Border BORDER_WHITE = new BorderStroke(Colorful.WHITE).width(2).build();
|
||||
private static final Border BORDER_BLACK = new BorderStroke(Colorful.BLACK).width(2).build();
|
||||
|
||||
@Getter
|
||||
protected final Label label;
|
||||
protected final StackPane root;
|
||||
|
||||
public MCButton() {
|
||||
this("");
|
||||
}
|
||||
|
||||
public MCButton(String text) {
|
||||
this(null, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认构造
|
||||
*
|
||||
* @param size 尺寸,为 null 时根据文本自适应
|
||||
* @param text 按钮文本
|
||||
*/
|
||||
public MCButton(Double size, String text) {
|
||||
// 高光
|
||||
StackPane light = new StackPane();
|
||||
light.setBorder(new BorderStroke(Colorful.WHITE).width(2, 0, 0, 2).build());
|
||||
light.setOpacity(.4);
|
||||
light.visibleProperty().bind(disabledProperty().not());
|
||||
|
||||
// 阴影
|
||||
StackPane dark = new StackPane();
|
||||
dark.setBorder(new BorderStroke(Colorful.BLACK).width(0, 2, 3, 0).build());
|
||||
dark.setOpacity(.3);
|
||||
dark.visibleProperty().bind(disabledProperty().not());
|
||||
|
||||
// 禁用蒙版
|
||||
StackPane mark = new StackPane();
|
||||
mark.setOpacity(.7);
|
||||
mark.visibleProperty().bind(disabledProperty());
|
||||
mark.setBackground(BG.BLACK);
|
||||
|
||||
label = new Label(text);
|
||||
label.setEffect(Resources.TEXT_SHADOW);
|
||||
label.setTextFill(Colorful.WHITE);
|
||||
label.setPadding(new Insets(8, 16, 8, 16));
|
||||
|
||||
getStyleClass().clear();
|
||||
if (size != null) {
|
||||
setPrefWidth(size);
|
||||
}
|
||||
borderProperty().bind(Bindings.createObjectBinding(() -> {
|
||||
if (!isDisabled() && isHover()) {
|
||||
return BORDER_WHITE;
|
||||
}
|
||||
return BORDER_BLACK;
|
||||
}, hoverProperty(), disabledProperty()));
|
||||
setGraphic(root = new StackPane() {{
|
||||
setBackground(new BgImage(Resources.BTN).build());
|
||||
getChildren().addAll(light, dark, mark, label);
|
||||
}});
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
textProperty().addListener((obs, o, newText) -> {
|
||||
label.setText(newText);
|
||||
setText("");
|
||||
});
|
||||
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
if (!isDisabled()) {
|
||||
Sound.CLICK.play();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,98 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.util.Sound;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgImage;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
|
||||
/**
|
||||
* MC 复选框
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-24 01:14
|
||||
*/
|
||||
public class MCCheckBox extends StackPane implements TimiFXUI {
|
||||
|
||||
private static final Border BORDER_WHITE = new BorderStroke(Colorful.WHITE).width(2).build();
|
||||
private static final Border BORDER_BLACK = new BorderStroke(Colorful.BLACK).width(2).build();
|
||||
|
||||
private final BooleanProperty selected;
|
||||
private final ObjectProperty<Label> label;
|
||||
|
||||
public MCCheckBox() {
|
||||
selected = new SimpleBooleanProperty(false);
|
||||
label = new SimpleObjectProperty<>();
|
||||
|
||||
ImageView check = new ImageView(Resources.CHECK);
|
||||
check.visibleProperty().bind(selected);
|
||||
|
||||
setMaxSize(34, 34);
|
||||
setPrefSize(34, 34);
|
||||
borderProperty().bind(Bindings.when(hoverProperty()).then(BORDER_WHITE).otherwise(BORDER_BLACK));
|
||||
getChildren().addAll(new StackPane() {{
|
||||
setBackground(new BgImage(Resources.BTN).build());
|
||||
getChildren().add(new StackPane() {{
|
||||
setOpacity(.6);
|
||||
setBackground(BG.BLACK);
|
||||
}});
|
||||
}}, check);
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
EventHandler<MouseEvent> clickEvent = e -> {
|
||||
selected.set(!selected.get());
|
||||
Sound.CLICK.play();
|
||||
};
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, clickEvent);
|
||||
label.addListener((obs, oldLabel, newLabel) -> {
|
||||
if (newLabel != null) {
|
||||
newLabel.addEventFilter(MouseEvent.MOUSE_PRESSED, clickEvent);
|
||||
}
|
||||
if (oldLabel != null) {
|
||||
oldLabel.removeEventFilter(MouseEvent.MOUSE_PRESSED, clickEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @return true 为已选中 */
|
||||
public boolean isSelected() {
|
||||
return selected.get();
|
||||
}
|
||||
|
||||
/** @param selected true 为已选中 */
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected.set(selected);
|
||||
}
|
||||
|
||||
/** @return 选中监听 */
|
||||
public BooleanProperty selectedProperty() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
/** @return 标签,此标签点击触发复选 */
|
||||
public Label getLabel() {
|
||||
return label.get();
|
||||
}
|
||||
|
||||
/** @param label 设置标签,此标签点击触发复选 */
|
||||
public void setLabel(Label label) {
|
||||
this.label.set(label);
|
||||
}
|
||||
|
||||
/** @return 标签监听,此标签点击触发复选 */
|
||||
public ObjectProperty<Label> labelProperty() {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import javafx.scene.control.Label;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
|
||||
/**
|
||||
* MC 标签
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-24 21:44
|
||||
*/
|
||||
public class MCLabel extends Label implements TimiFXUI {
|
||||
|
||||
public MCLabel() {
|
||||
this("");
|
||||
}
|
||||
|
||||
public MCLabel(String text) {
|
||||
super(text);
|
||||
|
||||
setEffect(Resources.TEXT_SHADOW);
|
||||
setTextFill(Colorful.WHITE);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.util.Sound;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Border;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.network.Network;
|
||||
|
||||
/**
|
||||
* MC 链接
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-24 17:46
|
||||
*/
|
||||
public class MCLink extends Label implements TimiFXUI {
|
||||
|
||||
private static final Border BORDER_DEFAULT = new BorderStroke(Colorful.TRANSPARENT).width(0, 0, 2, 0).build();
|
||||
private static final Border BORDER_HOVER = new BorderStroke(Colorful.LIGHT_GRAY).width(0, 0, 2, 0).build();
|
||||
|
||||
private final StringProperty url;
|
||||
|
||||
public MCLink(String text) {
|
||||
this(text, text);
|
||||
}
|
||||
|
||||
public MCLink(String text, String url) {
|
||||
super(text);
|
||||
|
||||
this.url = new SimpleStringProperty(url);
|
||||
|
||||
setEffect(Resources.TEXT_SHADOW);
|
||||
setTextFill(Colorful.WHITE);
|
||||
borderProperty().bind(Bindings.when(hoverProperty()).then(BORDER_HOVER).otherwise(BORDER_DEFAULT));
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
Network.openURIInBrowser(this.url.get());
|
||||
Sound.CLICK.play();
|
||||
});
|
||||
}
|
||||
|
||||
/** @return 获取 URL */
|
||||
public String getUrl() {
|
||||
return url.get();
|
||||
}
|
||||
|
||||
/** @param url 设置 URL */
|
||||
public void setUrl(String url) {
|
||||
this.url.set(url);
|
||||
}
|
||||
|
||||
/** @return URL 监听 */
|
||||
public StringProperty urlProperty() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,77 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import com.sun.javafx.scene.control.VirtualScrollBar;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.binding.BooleanBinding;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.scene.control.ListView;
|
||||
import javafx.scene.control.skin.ListViewSkin;
|
||||
import javafx.scene.control.skin.VirtualFlow;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.SmoothScroll;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2024-07-14 20:38
|
||||
*/
|
||||
@Slf4j
|
||||
public class MCList<T> extends ListView<T> {
|
||||
|
||||
protected static final String STYLE_CLASS_LIST = "mc-list";
|
||||
|
||||
protected final BooleanProperty isLoading;
|
||||
|
||||
protected final BooleanBinding showingVScrollBar;
|
||||
|
||||
private VirtualScrollBar vBar;
|
||||
|
||||
public MCList() {
|
||||
isLoading = new SimpleBooleanProperty(true);
|
||||
|
||||
MCLabel emptyList = new MCLabel(TimiFXUI.MULTILINGUAL.text("select.empty"));
|
||||
MCLabel loading = new MCLabel(TimiFXUI.MULTILINGUAL.text("loading"));
|
||||
placeholderProperty().bind(Bindings.when(isLoading).then(loading).otherwise(emptyList));
|
||||
|
||||
getStyleClass().addAll(STYLE_CLASS_LIST, TimiFXUI.CSS.BORDER_N);
|
||||
|
||||
SmoothScroll.virtual(this);
|
||||
|
||||
showingVScrollBar = Bindings.createBooleanBinding(() -> {
|
||||
// TODO 最大化最小化无效
|
||||
try {
|
||||
if (vBar == null && getSkin() != null && getSkin() instanceof ListViewSkin<?> skin) {
|
||||
VirtualFlow<?> flow = Ref.getFieldValue(skin, "flow", VirtualFlow.class);
|
||||
vBar = Ref.getFieldValue(flow, "vbar", VirtualScrollBar.class);
|
||||
}
|
||||
if (vBar != null) {
|
||||
return vBar.isVisible();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("ref MCList virtual scroll bar error", e);
|
||||
}
|
||||
return false;
|
||||
}, skinProperty(), layoutBoundsProperty());
|
||||
}
|
||||
|
||||
public BooleanBinding showingVScrollBarProperty() {
|
||||
return showingVScrollBar;
|
||||
}
|
||||
|
||||
/** @return true 为正在加载 */
|
||||
public boolean isLoading() {
|
||||
return isLoading.get();
|
||||
}
|
||||
|
||||
/** @return 正在加载属性 */
|
||||
public BooleanProperty loadingProperty() {
|
||||
return isLoading;
|
||||
}
|
||||
|
||||
/** @param isLoading true 为正在加载 */
|
||||
public void setLoading(boolean isLoading) {
|
||||
this.isLoading.set(isLoading);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,84 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import cn.forevermc.launcher.util.Sound;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.skin.TextFieldSkin;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import com.imyeyu.fx.ui.MinecraftFont;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
|
||||
/**
|
||||
* MC 密码输入框
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-23 00:27
|
||||
*/
|
||||
public class MCPasswordField extends PasswordField implements TimiFXUI {
|
||||
|
||||
private static final Border BORDER_FOCUSED = new BorderStroke(Colorful.WHITE).width(2).build();
|
||||
private static final Border BORDER_DEFAULT = new BorderStroke("#A0A0A0").width(2).build();
|
||||
private static final Background BG = new BgFill("#000D").build();
|
||||
private static final Background BG_DISABLED = new BgFill("#444C").build();
|
||||
|
||||
private final Label label;
|
||||
|
||||
public MCPasswordField() {
|
||||
label = new Label();
|
||||
label.setTextFill(Colorful.LIGHT_GRAY);
|
||||
label.translateXProperty().bind(label.widthProperty().negate().subtract(4));
|
||||
|
||||
getStyleClass().setAll("mc-text-field");
|
||||
setFont(MinecraftFont.X16());
|
||||
borderProperty().bind(Bindings.when(focusedProperty()).then(BORDER_FOCUSED).otherwise(BORDER_DEFAULT));
|
||||
paddingProperty().bind(Bindings.createObjectBinding(() -> new Insets(8, 0, 8, label.getWidth() + 8), label.widthProperty()));
|
||||
backgroundProperty().bind(Bindings.when(disabledProperty()).then(BG_DISABLED).otherwise(BG));
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> Sound.CLICK.play());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
Skin<?> skin = super.createDefaultSkin();
|
||||
if (skin instanceof TextFieldSkin textFieldSkin) {
|
||||
try {
|
||||
Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
|
||||
|
||||
StackPane root = new StackPane();
|
||||
StackPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
root.getChildren().addAll(label, textGroup);
|
||||
|
||||
textFieldSkin.getChildren().setAll(root);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return skin;
|
||||
}
|
||||
|
||||
/** @return 标签 */
|
||||
public String getLabel() {
|
||||
return label.getText();
|
||||
}
|
||||
|
||||
/** @param label 设置标签 */
|
||||
public void setLabel(String label) {
|
||||
this.label.setText(label);
|
||||
}
|
||||
|
||||
/** @return 标签监听 */
|
||||
public StringProperty labelProperty() {
|
||||
return label.textProperty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
import com.imyeyu.java.bean.CallbackArgReturn;
|
||||
|
||||
/**
|
||||
* MC 选择器
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-23 17:04
|
||||
*/
|
||||
public class MCSelector<T> extends MCButton {
|
||||
|
||||
/** 数据列表 */
|
||||
@Getter
|
||||
private final ObservableList<T> items;
|
||||
|
||||
/** 字符串转换器 */
|
||||
@Setter
|
||||
private CallbackArgReturn<T, String> stringConverter;
|
||||
|
||||
private final ObjectProperty<T> value;
|
||||
|
||||
public MCSelector() {
|
||||
items = FXCollections.observableArrayList();
|
||||
value = new SimpleObjectProperty<>();
|
||||
|
||||
// 监听值
|
||||
value.addListener((obs, o, newValue) -> {
|
||||
if (stringConverter == null) {
|
||||
if (newValue == null) {
|
||||
label.setText("");
|
||||
} else {
|
||||
label.setText(newValue.toString());
|
||||
}
|
||||
} else {
|
||||
label.setText(stringConverter.handler(newValue));
|
||||
}
|
||||
});
|
||||
|
||||
// 点击切换
|
||||
setOnAction(e -> {
|
||||
if (TimiJava.isNotEmpty(items)) {
|
||||
int i = items.indexOf(value.get()) + 1;
|
||||
value.set(items.get(i == items.size() ? 0 : i));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择数据
|
||||
*
|
||||
* @param value 目标数据
|
||||
*/
|
||||
public void setValue(T value) {
|
||||
this.value.set(value);
|
||||
}
|
||||
|
||||
/** @return 当前选择 */
|
||||
public T getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
/** @return 值监听 */
|
||||
public ObjectProperty<T> valueProperty() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,228 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.util.Sound;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.DoubleProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import lombok.Getter;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.fx.utils.BgImage;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.utils.Calc;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* MC 滑动选择
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-23 15:31
|
||||
*/
|
||||
public class MCSlider extends StackPane implements TimiFXUI {
|
||||
|
||||
private static final Border BORDER_WHITE = new BorderStroke(Colorful.WHITE).width(2).build();
|
||||
private static final Border BORDER_BLACK = new BorderStroke(Colorful.BLACK).width(2).build();
|
||||
|
||||
/** 标签 */
|
||||
@Getter
|
||||
private final Label label;
|
||||
|
||||
private final MCButton thumb;
|
||||
|
||||
private final DoubleProperty min, max, step, value;
|
||||
private final ObjectProperty<Double[]> steps;
|
||||
|
||||
public MCSlider() {
|
||||
min = new SimpleDoubleProperty();
|
||||
max = new SimpleDoubleProperty();
|
||||
step = new SimpleDoubleProperty();
|
||||
value = new SimpleDoubleProperty();
|
||||
steps = new SimpleObjectProperty<>();
|
||||
|
||||
thumb = new MCButton() {{
|
||||
label.setVisible(false);
|
||||
label.setManaged(false);
|
||||
}};
|
||||
thumb.setPrefWidth(16);
|
||||
thumb.prefHeightProperty().bind(heightProperty());
|
||||
thumb.borderProperty().unbind();
|
||||
thumb.borderProperty().bind(Bindings.when(hoverProperty()).then(BORDER_WHITE).otherwise(BORDER_BLACK));
|
||||
thumb.setMouseTransparent(true);
|
||||
|
||||
label = new Label();
|
||||
label.setPadding(new Insets(8));
|
||||
label.setTextFill(Colorful.WHITE);
|
||||
label.setEffect(Resources.TEXT_SHADOW);
|
||||
label.setMouseTransparent(true);
|
||||
|
||||
setAlignment(thumb, Pos.CENTER_LEFT);
|
||||
setBackground(new BgFill(Colorful.BLACK).build());
|
||||
getChildren().addAll(new StackPane() {{
|
||||
setMargin(this, new Insets(2));
|
||||
setBackground(new BgImage(Resources.BTN).build());
|
||||
getChildren().add(new StackPane() {{
|
||||
setOpacity(.6);
|
||||
setBackground(BG.BLACK);
|
||||
}});
|
||||
}}, thumb, label);
|
||||
|
||||
// ---------- 事件 ----------
|
||||
|
||||
// 计算步进
|
||||
steps.bind(Bindings.createObjectBinding(() -> {
|
||||
if (step.get() <= 0) {
|
||||
return null;
|
||||
}
|
||||
List<Double> result = new ArrayList<>();
|
||||
// 去头尾所有步进值
|
||||
for (double i = min.get() + step.get(); i <= max.get() - step.get(); i += step.get()) {
|
||||
result.add(i);
|
||||
}
|
||||
return result.toArray(new Double[0]);
|
||||
}, min, max, step));
|
||||
min.set(0);
|
||||
max.set(100);
|
||||
step.set(-1);
|
||||
|
||||
// 点下
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
|
||||
Sound.CLICK.play();
|
||||
calcValue(e);
|
||||
e.consume();
|
||||
});
|
||||
|
||||
// 拖拽
|
||||
addEventFilter(MouseEvent.MOUSE_DRAGGED, e -> {
|
||||
calcValue(e);
|
||||
e.consume();
|
||||
});
|
||||
|
||||
widthProperty().addListener((obs, o, n) -> calcNearValue(value.get()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算滑块位置
|
||||
*
|
||||
* @param event 鼠标事件
|
||||
*/
|
||||
private void calcValue(MouseEvent event) {
|
||||
if (event.getX() < thumb.getWidth() * .5) {
|
||||
thumb.setTranslateX(0);
|
||||
value.set(min.get());
|
||||
return;
|
||||
}
|
||||
if (getWidth() - thumb.getWidth() * .5 < event.getX()) {
|
||||
thumb.setTranslateX(getWidth() - thumb.getWidth());
|
||||
value.set(max.get());
|
||||
return;
|
||||
}
|
||||
double percent = event.getX() / getWidth();
|
||||
if (step.get() == -1) {
|
||||
thumb.setTranslateX((int) getPrefWidth() * percent - thumb.getPrefWidth() * .5);
|
||||
value.set(max.get() * percent);
|
||||
} else {
|
||||
// 坐标对应值
|
||||
calcNearValue(event.getX() / getWidth() * max.get());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算最近位置
|
||||
*
|
||||
* @param value 当前值
|
||||
*/
|
||||
private void calcNearValue(double value) {
|
||||
if (steps.get() == null) {
|
||||
thumb.setTranslateX((getPrefWidth() - thumb.getPrefWidth()) * (value / (max.get() - min.get())));
|
||||
} else {
|
||||
// 最近步进位置
|
||||
double min = steps.get()[0];
|
||||
double max = steps.get()[steps.get().length - 1];
|
||||
double nearIndex = (steps.get().length - 1) * ((value - this.min.get()) / (max - min)) - 1;
|
||||
// 最近值
|
||||
int index = Calc.round(nearIndex);
|
||||
if (0 <= index) {
|
||||
if (index < this.steps.get().length) {
|
||||
// 反向计算滑块位置
|
||||
this.value.set(steps.get()[index]);
|
||||
thumb.setTranslateX((int) (this.value.get() / this.max.get() * getWidth() - thumb.getWidth() * .5));
|
||||
} else {
|
||||
// 达到最大值
|
||||
this.value.set(this.max.get());
|
||||
thumb.setTranslateX((int) (getWidth() - thumb.getPrefWidth()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return 最小值 */
|
||||
public double getMin() {
|
||||
return min.get();
|
||||
}
|
||||
|
||||
/** @param min 最小值 */
|
||||
public void setMin(double min) {
|
||||
this.min.set(min);
|
||||
}
|
||||
|
||||
/** @return 最小值监听 */
|
||||
public DoubleProperty minProperty() {
|
||||
return min;
|
||||
}
|
||||
|
||||
/** @return 最大值 */
|
||||
public double getMax() {
|
||||
return max.get();
|
||||
}
|
||||
|
||||
/** @param max 最大值 */
|
||||
public void setMax(double max) {
|
||||
this.max.set(max);
|
||||
}
|
||||
|
||||
/** @return 最大值监听 */
|
||||
public DoubleProperty maxProperty() {
|
||||
return max;
|
||||
}
|
||||
|
||||
/** @return 步进 */
|
||||
public double getStep() {
|
||||
return step.get();
|
||||
}
|
||||
|
||||
/** @param step 步进 */
|
||||
public void setStep(double step) {
|
||||
this.step.set(step);
|
||||
}
|
||||
|
||||
/** @return 步进监听 */
|
||||
public DoubleProperty stepProperty() {
|
||||
return step;
|
||||
}
|
||||
|
||||
/** @return 当前值 */
|
||||
public double getValue() {
|
||||
return value.get();
|
||||
}
|
||||
|
||||
/** @param value 当前值 */
|
||||
public void setValue(double value) {
|
||||
this.value.set(value);
|
||||
}
|
||||
|
||||
/** @return 当前值监听 */
|
||||
public DoubleProperty valueProperty() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
package cn.forevermc.launcher.view.components.control;
|
||||
|
||||
import cn.forevermc.launcher.util.Sound;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.event.Event;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Skin;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.control.skin.TextFieldSkin;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.input.MouseEvent;
|
||||
import javafx.scene.layout.Background;
|
||||
import javafx.scene.layout.Border;
|
||||
import javafx.scene.layout.Pane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import com.imyeyu.fx.ui.MinecraftFont;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.java.ref.Ref;
|
||||
|
||||
/**
|
||||
* MC 输入框
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-23 00:27
|
||||
*/
|
||||
public class MCTextField extends TextField implements TimiFXUI {
|
||||
|
||||
private static final Border BORDER_FOCUSED = new BorderStroke(Colorful.WHITE).width(2).build();
|
||||
private static final Border BORDER_DEFAULT = new BorderStroke("#A0A0A0").width(2).build();
|
||||
private static final Background BG = new BgFill("#000A").build();
|
||||
private static final Background BG_DISABLED = new BgFill("#444C").build();
|
||||
|
||||
protected final Label label;
|
||||
|
||||
public MCTextField() {
|
||||
this("");
|
||||
}
|
||||
|
||||
public MCTextField(String label) {
|
||||
this.label = new Label(label);
|
||||
this.label.setTextFill(Colorful.LIGHT_GRAY);
|
||||
this.label.setMouseTransparent(true);
|
||||
this.label.translateXProperty().bind(this.label.widthProperty().negate());
|
||||
|
||||
getStyleClass().setAll("mc-text-field");
|
||||
setFont(MinecraftFont.X16());
|
||||
|
||||
borderProperty().bind(Bindings.when(focusedProperty()).then(BORDER_FOCUSED).otherwise(BORDER_DEFAULT));
|
||||
paddingProperty().bind(Bindings.createObjectBinding(() -> new Insets(8, 0, 8, this.label.getWidth() + 8), this.label.widthProperty()));
|
||||
backgroundProperty().bind(Bindings.when(disabledProperty()).then(BG_DISABLED).otherwise(BG));
|
||||
addEventFilter(MouseEvent.MOUSE_PRESSED, e -> Sound.CLICK.play());
|
||||
addEventFilter(ContextMenuEvent.CONTEXT_MENU_REQUESTED, Event::consume);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Skin<?> createDefaultSkin() {
|
||||
Skin<?> skin = super.createDefaultSkin();
|
||||
if (skin instanceof TextFieldSkin textFieldSkin) {
|
||||
try {
|
||||
Pane textGroup = Ref.getFieldValue(textFieldSkin, "textGroup", Pane.class);
|
||||
|
||||
StackPane root = new StackPane();
|
||||
StackPane.setAlignment(label, Pos.CENTER_LEFT);
|
||||
StackPane.setMargin(textGroup, new Insets(0, 0, 0, 4));
|
||||
root.getChildren().addAll(label, textGroup);
|
||||
|
||||
textFieldSkin.getChildren().setAll(root);
|
||||
} catch (IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return skin;
|
||||
}
|
||||
|
||||
/** @return 标签 */
|
||||
public String getLabel() {
|
||||
return label.getText();
|
||||
}
|
||||
|
||||
/** @param label 标签 */
|
||||
public void setLabel(String label) {
|
||||
this.label.setText(label);
|
||||
}
|
||||
|
||||
/** @return 标签监听 */
|
||||
public StringProperty labelProperty() {
|
||||
return label.textProperty();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.ForeverMC;
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.components.AbstractPane;
|
||||
import cn.forevermc.launcher.view.components.TimiVersionLabel;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import cn.forevermc.launcher.view.components.control.MCLink;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgImage;
|
||||
import com.imyeyu.fx.utils.Column;
|
||||
|
||||
/**
|
||||
* 关于面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-24 17:44
|
||||
*/
|
||||
public abstract class AboutPane extends AbstractPane {
|
||||
|
||||
protected final MCButton close;
|
||||
protected final ImageView title;
|
||||
|
||||
public AboutPane() {
|
||||
// 标题
|
||||
title = new ImageView(Resources.TITLE);
|
||||
|
||||
// 说明
|
||||
MCLabel description = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.about.description"));
|
||||
description.setPadding(new Insets(0, 140, 16, 140));
|
||||
description.setWrapText(true);
|
||||
|
||||
// 关闭
|
||||
close = new MCButton(TimiFXUI.MULTILINGUAL.text("close"));
|
||||
close.setPrefWidth(ComponentSize.SMALL);
|
||||
|
||||
setMargin(close, new Insets(0, 0, 30, 0));
|
||||
setAlignment(close, Pos.CENTER);
|
||||
setAlignment(title, Pos.CENTER);
|
||||
|
||||
setPadding(new Insets(64, 0, 0, 0));
|
||||
setTop(title);
|
||||
setCenter(new VBox() {{
|
||||
setSpacing(12);
|
||||
setPadding(new Insets(16));
|
||||
getChildren().addAll(new TimiVersionLabel(ForeverMC.VERSION, ForeverMC.class.getSimpleName()), description, new GridPane() {{
|
||||
getColumnConstraints().add(Column.build(HPos.RIGHT));
|
||||
setHgap(6);
|
||||
setVgap(6);
|
||||
setAlignment(Pos.CENTER);
|
||||
|
||||
int row = 0;
|
||||
addRow(row++, new MCLabel() {{
|
||||
setText(TimiFXUI.MULTILINGUAL.text("source"));
|
||||
setTextFill(Colorful.LIGHT_GRAY);
|
||||
}}, new MCLink("https://git.imyeyu.net/Timi/ForeverMC"));
|
||||
addRow(row++, new MCLabel() {{
|
||||
setText(TimiFXUI.MULTILINGUAL.text("developer"));
|
||||
setTextFill(Colorful.LIGHT_GRAY);
|
||||
}}, new MCLabel("夜雨"));
|
||||
addRow(row, new MCLabel() {{
|
||||
setText(TimiFXUI.MULTILINGUAL.text("blog"));
|
||||
setTextFill(Colorful.LIGHT_GRAY);
|
||||
}}, new MCLink("https://www.imyeyu.net"));
|
||||
|
||||
}});
|
||||
}});
|
||||
setBottom(close);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,87 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.components.AbstractPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCCheckBox;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.canvas.GraphicsContext;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import lombok.Getter;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgImage;
|
||||
|
||||
/**
|
||||
* 画板进度页面
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-15 17:49
|
||||
*/
|
||||
public abstract class CanvasProgressPane extends AbstractPane {
|
||||
|
||||
protected static final int SIZE = 128;
|
||||
|
||||
@Getter
|
||||
protected final MCLabel label, subLabel;
|
||||
|
||||
protected final Canvas canvas;
|
||||
protected final MCButton cancel;
|
||||
protected final MCCheckBox bgmToggle;
|
||||
protected final GraphicsContext g;
|
||||
|
||||
public CanvasProgressPane() {
|
||||
// 主提示
|
||||
label = new MCLabel();
|
||||
|
||||
// 副提示
|
||||
subLabel = new MCLabel();
|
||||
|
||||
// 图形进度
|
||||
canvas = new Canvas();
|
||||
canvas.setWidth(SIZE);
|
||||
canvas.setHeight(SIZE);
|
||||
g = canvas.getGraphicsContext2D();
|
||||
|
||||
// 取消
|
||||
cancel = new MCButton(ComponentSize.LARGE, TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||
|
||||
// 背景音乐
|
||||
bgmToggle = new MCCheckBox();
|
||||
bgmToggle.setSelected(true);
|
||||
|
||||
setCenter(new BorderPane() {{
|
||||
setAlignment(canvas, Pos.CENTER);
|
||||
setMargin(canvas, new Insets(8, 0, 12, 0));
|
||||
setCenter(new VBox() {{
|
||||
setSpacing(8);
|
||||
setAlignment(Pos.CENTER);
|
||||
getChildren().addAll(label, subLabel);
|
||||
}});
|
||||
setBottom(new BorderPane() {{
|
||||
setCenter(canvas);
|
||||
setBottom(new HBox() {{
|
||||
setSpacing(4);
|
||||
setAlignment(Pos.CENTER);
|
||||
visibleProperty().bind(bgmToggle.visibleProperty());
|
||||
managedProperty().bind(visibleProperty());
|
||||
getChildren().addAll(bgmToggle, new MCLabel() {{
|
||||
bgmToggle.setLabel(this);
|
||||
setText(TimiFXUI.MULTILINGUAL.text("bgm"));
|
||||
}});
|
||||
}});
|
||||
}});
|
||||
}});
|
||||
setBottom(new HBox() {{
|
||||
setSpacing(16);
|
||||
setPadding(new Insets(16, 16, 64, 16));
|
||||
setAlignment(Pos.CENTER);
|
||||
getChildren().setAll(cancel);
|
||||
}});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,93 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.view.components.AbstractPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import lombok.Getter;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
|
||||
/**
|
||||
* 会话面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 17:32
|
||||
*/
|
||||
public abstract class DialogPane extends AbstractPane {
|
||||
|
||||
/** 标题 */
|
||||
@Getter
|
||||
protected final MCLabel title;
|
||||
|
||||
/** 内容 */
|
||||
@Getter
|
||||
protected final MCLabel content;
|
||||
|
||||
protected HBox buttonPane;
|
||||
protected MCButton confirm, deny, cancel, close;
|
||||
protected ObjectProperty<Node> graphic;
|
||||
|
||||
private final StackPane graphicPane;
|
||||
|
||||
public DialogPane() {
|
||||
graphic = new SimpleObjectProperty<>();
|
||||
|
||||
// 标题
|
||||
title = new MCLabel();
|
||||
title.setTextFill(Colorful.LIGHT_GRAY);
|
||||
|
||||
// 内容
|
||||
content = new MCLabel("content");
|
||||
content.setPadding(new Insets(0, 8, 0, 8));
|
||||
|
||||
// 自定义组件
|
||||
graphicPane = new StackPane();
|
||||
graphic.addListener((obs, o, newGraphic) -> {
|
||||
if (newGraphic == null) {
|
||||
graphicPane.getChildren().clear();
|
||||
} else {
|
||||
graphicPane.getChildren().setAll(newGraphic);
|
||||
}
|
||||
});
|
||||
|
||||
// 确认
|
||||
confirm = new MCButton(TimiFXUI.MULTILINGUAL.text("confirm"));
|
||||
confirm.setPrefWidth(ComponentSize.SMALL);
|
||||
confirm.managedProperty().bind(confirm.visibleProperty());
|
||||
|
||||
// 拒绝
|
||||
deny = new MCButton(TimiFXUI.MULTILINGUAL.text("no"));
|
||||
deny.setPrefWidth(ComponentSize.SMALL);
|
||||
deny.managedProperty().bind(deny.visibleProperty());
|
||||
|
||||
// 取消
|
||||
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||
cancel.setPrefWidth(ComponentSize.SMALL);
|
||||
cancel.managedProperty().bind(cancel.visibleProperty());
|
||||
|
||||
// 关闭
|
||||
close = new MCButton(TimiFXUI.MULTILINGUAL.text("close"));
|
||||
close.setPrefWidth(ComponentSize.SMALL);
|
||||
close.managedProperty().bind(close.visibleProperty());
|
||||
|
||||
setCenter(new VBox() {{
|
||||
setSpacing(8);
|
||||
setAlignment(Pos.CENTER);
|
||||
getChildren().addAll(title, content, graphicPane);
|
||||
}});
|
||||
setBottom(buttonPane = new HBox() {{
|
||||
setSpacing(16);
|
||||
setPadding(new Insets(16, 16, 64, 16));
|
||||
setAlignment(Pos.CENTER);
|
||||
getChildren().setAll(confirm, deny, cancel, close);
|
||||
}});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,86 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.view.components.AbstractContentPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCSelector;
|
||||
import cn.forevermc.launcher.view.components.control.MCTextField;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
|
||||
/**
|
||||
* 版本配置面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-10 00:49
|
||||
*/
|
||||
public abstract class GameOptionPane extends AbstractContentPane {
|
||||
|
||||
protected final MCButton installFabric, javaSelect, finish;
|
||||
protected final MCTextField java, server;
|
||||
protected final MCSelector<Boolean> autoConnect;
|
||||
protected final MCSelector<Game.Option.LoginType> loginType;
|
||||
|
||||
public GameOptionPane() {
|
||||
// Java
|
||||
java = new MCTextField();
|
||||
java.setLabel("Java: ");
|
||||
java.setFocusTraversable(false);
|
||||
javaSelect = new MCButton(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.java.select"));
|
||||
|
||||
// 安装 Fabric
|
||||
installFabric = new MCButton(ComponentSize.MEDIUM, TimiFXUI.MULTILINGUAL.text("fmc.launcher.pack_option.fabric.install"));
|
||||
|
||||
// 自动连接
|
||||
autoConnect = new MCSelector<>();
|
||||
autoConnect.setPrefWidth(ComponentSize.MEDIUM);
|
||||
autoConnect.getItems().addAll(true, false);
|
||||
autoConnect.setStringConverter(isSelected -> {
|
||||
if (isSelected) {
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_connect", TimiFXUI.MULTILINGUAL.text("turn.on"));
|
||||
}
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_connect", TimiFXUI.MULTILINGUAL.text("turn.off"));
|
||||
});
|
||||
|
||||
// 服务器地址
|
||||
server = new MCTextField();
|
||||
server.setPrefWidth(ComponentSize.MEDIUM);
|
||||
server.setLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.server"));
|
||||
server.disableProperty().bind(autoConnect.valueProperty().isEqualTo(false));
|
||||
|
||||
// 登录方式
|
||||
loginType = new MCSelector<>();
|
||||
loginType.setPrefWidth(ComponentSize.MEDIUM);
|
||||
loginType.setStringConverter(type -> {
|
||||
if (type == null) {
|
||||
return "";
|
||||
}
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.pack_option.login_type", type.getName());
|
||||
});
|
||||
loginType.getItems().setAll(Game.Option.LoginType.values());
|
||||
|
||||
// 完成
|
||||
finish = new MCButton(TimiFXUI.MULTILINGUAL.text("finish"));
|
||||
finish.setPrefWidth(ComponentSize.LARGE);
|
||||
|
||||
setContentPane(new GridPane() {{
|
||||
setHgap(24);
|
||||
setVgap(12);
|
||||
setAlignment(Pos.TOP_CENTER);
|
||||
|
||||
int row = 0;
|
||||
add(new BorderPane() {{
|
||||
setMargin(java, new Insets(0, 10, 0, 0));
|
||||
setCenter(java);
|
||||
setRight(javaSelect);
|
||||
}}, 0, row++, 2, 1);
|
||||
addRow(row++, installFabric, loginType);
|
||||
addRow(row, autoConnect, server);
|
||||
}});
|
||||
setBottomPaneValue(finish);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,104 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.bean.LaunchGame;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.components.AbstractListPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.java.TimiJava;
|
||||
|
||||
/**
|
||||
* 启动版本选择面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 10:09
|
||||
*/
|
||||
public abstract class GameSelectPane extends AbstractListPane<Game> {
|
||||
|
||||
@Inject
|
||||
protected LaunchGame launchGame;
|
||||
|
||||
protected final MCButton originalDL, packDL, select, destory, option, back;
|
||||
|
||||
public GameSelectPane() {
|
||||
// 列表
|
||||
list.setCellFactory(cell -> new ListCell<>() {
|
||||
|
||||
final MCLabel name;
|
||||
final MCLabel title;
|
||||
final ImageView activatedIcon;
|
||||
final BorderPane root;
|
||||
|
||||
{
|
||||
activatedIcon = new ImageView(Resources.CHECK_SMALL);
|
||||
name = new MCLabel();
|
||||
title = new MCLabel();
|
||||
title.setTextFill(Colorful.LIGHT_GRAY);
|
||||
title.getStyleClass().clear();
|
||||
title.visibleProperty().bind(title.textProperty().isNotEmpty());
|
||||
title.managedProperty().bind(title.visibleProperty());
|
||||
root = new BorderPane();
|
||||
root.setLeft(activatedIcon);
|
||||
root.setCenter(new HBox() {{
|
||||
setPadding(new Insets(0, 0, 0, 4));
|
||||
getChildren().addAll(name, title);
|
||||
}});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Game item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
if (activatedIcon.visibleProperty().isBound()) {
|
||||
activatedIcon.visibleProperty().unbind();
|
||||
}
|
||||
activatedIcon.visibleProperty().bind(itemProperty().isEqualTo(launchGame));
|
||||
name.setText(item.getName());
|
||||
if (TimiJava.isEmpty(item.getOption().getName().get())) {
|
||||
title.setText("");
|
||||
} else {
|
||||
title.setText(" - " + item.getOption().getName().get());
|
||||
}
|
||||
setGraphic(root);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 原版下载
|
||||
originalDL = new MCButton(ComponentSize.MEDIUM + 10, TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.download.mojang"));
|
||||
|
||||
// 整合下载
|
||||
packDL = new MCButton(ComponentSize.MEDIUM + 10, TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.download.fmc"));
|
||||
|
||||
// 选择
|
||||
select = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("select"));
|
||||
select.disableProperty().bind(loadingProperty().or(list.getSelectionModel().selectedItemProperty().isNull()));
|
||||
|
||||
// 销毁
|
||||
destory = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("delete"));
|
||||
destory.disableProperty().bind(loadingProperty().or(list.getSelectionModel().selectedItemProperty().isNull()));
|
||||
|
||||
// 设置
|
||||
option = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select.option"));
|
||||
option.disableProperty().bind(loadingProperty().or(list.getSelectionModel().selectedItemProperty().isNull()));
|
||||
|
||||
// 返回
|
||||
back = new MCButton(ComponentSize.SMALL, TimiFXUI.MULTILINGUAL.text("back"));
|
||||
|
||||
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.launch_select"));
|
||||
addBottomPaneValue(originalDL, 0, 0, 2, 1);
|
||||
addBottomPaneValue(packDL, 2, 0, 2, 1);
|
||||
addBottomPaneValue(1, destory, option, select, back);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,47 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.view.components.AbstractListPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.ui.components.EnumListCell;
|
||||
import com.imyeyu.java.bean.Language;
|
||||
|
||||
/**
|
||||
* 语言选择面板
|
||||
*
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-12-02 00:09
|
||||
*/
|
||||
public abstract class LangSelectPane extends AbstractListPane<Language> {
|
||||
|
||||
protected final MCButton confirm, cancel;
|
||||
|
||||
public LangSelectPane() {
|
||||
setLoading(false);
|
||||
|
||||
// 列表
|
||||
list.setCellFactory(cell -> new EnumListCell<>());
|
||||
|
||||
// 提示
|
||||
MCLabel tips = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.lang_Select.tips"));
|
||||
tips.setTextFill(Colorful.LIGHT_GRAY);
|
||||
|
||||
// 确认
|
||||
confirm = new MCButton(TimiFXUI.MULTILINGUAL.text("confirm"));
|
||||
confirm.setPrefWidth(ComponentSize.MEDIUM);
|
||||
|
||||
// 取消
|
||||
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||
cancel.setPrefWidth(ComponentSize.MEDIUM);
|
||||
|
||||
GridPane.setHalignment(tips, HPos.CENTER);
|
||||
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.lang_select"));
|
||||
addBottomPaneValue(tips, 0, 0, 2, 1);
|
||||
addBottomPaneValue(1, confirm, cancel);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.view.components.AbstractPane;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
|
||||
/**
|
||||
* 启动中过渡面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 21:19
|
||||
*/
|
||||
public abstract class LaunchingPane extends AbstractPane {
|
||||
|
||||
protected StackPane root;
|
||||
protected ImageView mojang;
|
||||
|
||||
public LaunchingPane() {
|
||||
mojang = new ImageView();
|
||||
mojang.setOpacity(0);
|
||||
|
||||
root = new StackPane(mojang);
|
||||
root.setOpacity(0);
|
||||
|
||||
setCenter(root);
|
||||
}
|
||||
}
|
||||
192
src/main/java/cn/forevermc/launcher/view/pages/MenuPane.java
Normal file
@ -0,0 +1,192 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.ForeverMC;
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.components.AbstractPane;
|
||||
import cn.forevermc.launcher.view.components.LoginTypeSelector;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import cn.forevermc.launcher.view.components.control.MCPasswordField;
|
||||
import cn.forevermc.launcher.view.components.control.MCTextField;
|
||||
import javafx.animation.FadeTransition;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.SimpleDoubleProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.geometry.VPos;
|
||||
import javafx.scene.canvas.Canvas;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.util.Duration;
|
||||
import com.imyeyu.fx.icon.TimiFXIcon;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BgFill;
|
||||
import com.imyeyu.fx.utils.Row;
|
||||
|
||||
/**
|
||||
* 菜单面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-21 19:33
|
||||
*/
|
||||
public abstract class MenuPane extends AbstractPane {
|
||||
|
||||
protected final Canvas splashCanvas;
|
||||
protected final MCLabel launchGameLabel;
|
||||
protected final MCButton fmcAccount, fmcServer, launchGameSelect, launch, option, exit, langSelect, about;
|
||||
protected final ImageView title;
|
||||
protected final MCTextField name;
|
||||
protected final StringProperty splash;
|
||||
protected final MCPasswordField password;
|
||||
protected final LoginTypeSelector loginTypeSelector;
|
||||
|
||||
private final StackPane root, transitionPane;
|
||||
|
||||
protected boolean isFirstShow = true;
|
||||
|
||||
public MenuPane() {
|
||||
splash = new SimpleStringProperty();
|
||||
|
||||
// 过渡面板
|
||||
transitionPane = new StackPane(new ImageView(Resources.SPLASH_SCREEN));
|
||||
transitionPane.setBackground(new BgFill("#EF323D").build());
|
||||
|
||||
// 标题
|
||||
title = new ImageView(Resources.TITLE);
|
||||
|
||||
// 闪烁标语,使用 Canvas 减少动画导致抖动
|
||||
splashCanvas = new Canvas(1, 32);
|
||||
splashCanvas.setTranslateX(180);
|
||||
splashCanvas.setTranslateY(20);
|
||||
splashCanvas.setRotate(-20);
|
||||
|
||||
// 账号管理
|
||||
fmcAccount = new MCButton();
|
||||
fmcAccount.getLabel().setEffect(null);
|
||||
fmcAccount.getLabel().setGraphic(new ImageView(Resources.TOTEM));
|
||||
fmcAccount.getLabel().setPadding(new Insets(1));
|
||||
|
||||
// 游戏名
|
||||
name = new MCTextField() {{
|
||||
paddingProperty().unbind();
|
||||
paddingProperty().bind(Bindings.createObjectBinding(() -> new Insets(8, 28, 8, label.getWidth() + 8), label.widthProperty()));
|
||||
}};
|
||||
name.setLabel(TimiFXUI.MULTILINGUAL.text("name"));
|
||||
|
||||
// 登录方式
|
||||
loginTypeSelector = new LoginTypeSelector();
|
||||
|
||||
// 启动版本
|
||||
launchGameLabel = new MCLabel();
|
||||
launchGameLabel.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
|
||||
launchGameLabel.setPadding(new Insets(4));
|
||||
|
||||
// 密码
|
||||
password = new MCPasswordField();
|
||||
password.setLabel(TimiFXUI.MULTILINGUAL.text("password"));
|
||||
password.disableProperty().bind(loginTypeSelector.valueProperty().isEqualTo(Game.Option.LoginType.OFFLINE));
|
||||
|
||||
// 启动版本
|
||||
launchGameSelect = new MCButton();
|
||||
launchGameSelect.getLabel().setEffect(null);
|
||||
launchGameSelect.getLabel().setGraphic(TimiFXIcon.fromName("LIST", 32, Colorful.WHITE));
|
||||
launchGameSelect.getLabel().setPadding(Insets.EMPTY);
|
||||
launchGameSelect.getLabel().setPadding(new Insets(1, 2, 1, 2));
|
||||
|
||||
// 服务器
|
||||
fmcServer = new MCButton();
|
||||
fmcServer.getLabel().setEffect(null);
|
||||
fmcServer.getLabel().setGraphic(new ImageView(Resources.COMMAND_BLOCK));
|
||||
fmcServer.getLabel().setPadding(new Insets(1));
|
||||
|
||||
// 启动
|
||||
launch = new MCButton(TimiFXUI.MULTILINGUAL.text("launch"));
|
||||
launch.prefWidthProperty().bind(new SimpleDoubleProperty(ComponentSize.NORMAL).subtract(launchGameSelect.widthProperty()));
|
||||
|
||||
// 语言
|
||||
langSelect = new MCButton();
|
||||
langSelect.getLabel().setGraphic(new ImageView(Resources.LANG));
|
||||
langSelect.getLabel().setPadding(new Insets(4));
|
||||
|
||||
// 关于
|
||||
about = new MCButton();
|
||||
about.getLabel().setGraphic(new ImageView(Resources.INFO));
|
||||
about.getLabel().setPadding(new Insets(9, 15, 9, 15));
|
||||
|
||||
// 设置
|
||||
option = new MCButton(TimiFXUI.MULTILINGUAL.text("option"));
|
||||
option.setPrefWidth(ComponentSize.NORMAL);
|
||||
|
||||
// 退出
|
||||
exit = new MCButton(TimiFXUI.MULTILINGUAL.text("exit"));
|
||||
exit.setPrefWidth(ComponentSize.NORMAL);
|
||||
|
||||
setCenter(root = new StackPane() {{
|
||||
getChildren().addAll(new BorderPane() {{
|
||||
setTop(new StackPane() {{
|
||||
setPadding(new Insets(64, 0, 0, 0));
|
||||
getChildren().addAll(title, splashCanvas);
|
||||
}});
|
||||
setCenter(new GridPane() {{
|
||||
// 纵向排版
|
||||
getRowConstraints().addAll(Row.build(), Row.build().top().height(48), Row.build());
|
||||
setHgap(7);
|
||||
setVgap(7);
|
||||
setAlignment(Pos.BOTTOM_CENTER);
|
||||
|
||||
int col = 0;
|
||||
add(fmcAccount, col, 0);
|
||||
add(langSelect, col, 2);
|
||||
|
||||
col++;
|
||||
addColumn(col, new StackPane() {{
|
||||
setMargin(loginTypeSelector, new Insets(0, 8, 0, 0));
|
||||
setAlignment(loginTypeSelector, Pos.CENTER_RIGHT);
|
||||
getChildren().addAll(name, loginTypeSelector);
|
||||
}}, password, option);
|
||||
|
||||
col++;
|
||||
addColumn(col, new StackPane() {{
|
||||
setBackground(new BgFill("#000A").build());
|
||||
getChildren().setAll(launchGameLabel);
|
||||
}}, new BorderPane() {{
|
||||
setAlignment(launch, Pos.TOP_CENTER);
|
||||
setLeft(launchGameSelect);
|
||||
setCenter(launch);
|
||||
}}, exit);
|
||||
|
||||
col++;
|
||||
add(fmcServer, col, 0);
|
||||
add(about, col, 2);
|
||||
}});
|
||||
setBottom(new MCLabel() {{
|
||||
setMargin(this, new Insets(16, 4, 2, 4));
|
||||
setText("ForeverMC " + ForeverMC.VERSION);
|
||||
}});
|
||||
}}, transitionPane);
|
||||
}});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShow() {
|
||||
if (isFirstShow) {
|
||||
FadeTransition welcomeFT = new FadeTransition(Duration.millis(650));
|
||||
welcomeFT.setNode(transitionPane);
|
||||
welcomeFT.setFromValue(1);
|
||||
welcomeFT.setToValue(0);
|
||||
welcomeFT.setDelay(Duration.millis(2000));
|
||||
welcomeFT.setOnFinished(e -> {
|
||||
root.getChildren().remove(transitionPane);
|
||||
isFirstShow = false;
|
||||
});
|
||||
welcomeFT.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/main/java/cn/forevermc/launcher/view/pages/OptionPane.java
Normal file
@ -0,0 +1,128 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.bean.GameDownloadSource;
|
||||
import cn.forevermc.launcher.bean.RuntimeDownloadSource;
|
||||
import cn.forevermc.launcher.view.components.AbstractScrollPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import cn.forevermc.launcher.view.components.control.MCSelector;
|
||||
import cn.forevermc.launcher.view.components.control.MCSlider;
|
||||
import cn.forevermc.launcher.view.components.control.MCTextField;
|
||||
import javafx.geometry.HPos;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.Column;
|
||||
import com.imyeyu.utils.OS;
|
||||
|
||||
/**
|
||||
* 选项面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-24 22:27
|
||||
*/
|
||||
public abstract class OptionPane extends AbstractScrollPane {
|
||||
|
||||
protected final MCButton launchSelect, finish;
|
||||
protected final MCSlider memory, bgmVolume, soundVolume, multiDownload;
|
||||
protected final MCTextField proxyHost, proxyPort;
|
||||
protected final MCSelector<Boolean> autoStartup;
|
||||
protected final MCSelector<GameDownloadSource> gameSource;
|
||||
protected final MCSelector<RuntimeDownloadSource> runtimeSource;
|
||||
|
||||
public OptionPane() {
|
||||
// 内存
|
||||
memory = new MCSlider();
|
||||
memory.setMin(1024);
|
||||
memory.setMax(OS.getSystemMemorySize() / Math.pow(1024, 2));
|
||||
memory.setStep(512);
|
||||
memory.setPrefWidth(ComponentSize.MEDIUM);
|
||||
|
||||
// 版本
|
||||
launchSelect = new MCButton(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.launch_pack"));
|
||||
launchSelect.setPrefWidth(ComponentSize.MEDIUM);
|
||||
|
||||
// 自动启动
|
||||
autoStartup = new MCSelector<>();
|
||||
autoStartup.setPrefWidth(ComponentSize.MEDIUM);
|
||||
autoStartup.getItems().addAll(true, false);
|
||||
autoStartup.setStringConverter(isSelected -> {
|
||||
if (isSelected) {
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_launch", TimiFXUI.MULTILINGUAL.text("turn.on"));
|
||||
}
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.auto_launch", TimiFXUI.MULTILINGUAL.text("turn.off"));
|
||||
});
|
||||
|
||||
// 背景音量
|
||||
bgmVolume = new MCSlider();
|
||||
bgmVolume.setPrefWidth(ComponentSize.MEDIUM);
|
||||
bgmVolume.getLabel().textProperty().bind(bgmVolume.valueProperty().asString(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.bgm")));
|
||||
|
||||
// 音效音量
|
||||
soundVolume = new MCSlider();
|
||||
soundVolume.setPrefWidth(ComponentSize.MEDIUM);
|
||||
soundVolume.getLabel().textProperty().bind(soundVolume.valueProperty().asString(TimiFXUI.MULTILINGUAL.text("fmc.launcher.option.sound")));
|
||||
|
||||
MCLabel network = new MCLabel(TimiFXUI.MULTILINGUAL.text("network"));
|
||||
|
||||
// 游戏下载源
|
||||
gameSource = new MCSelector<>();
|
||||
gameSource.setPrefWidth(ComponentSize.MEDIUM);
|
||||
gameSource.setStringConverter(type -> {
|
||||
if (type == null) {
|
||||
return "";
|
||||
}
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.game_source", type.toString());
|
||||
});
|
||||
gameSource.getItems().setAll(GameDownloadSource.values());
|
||||
|
||||
// 运行时下载源
|
||||
runtimeSource = new MCSelector<>();
|
||||
runtimeSource.setPrefWidth(ComponentSize.MEDIUM);
|
||||
runtimeSource.setStringConverter(type -> {
|
||||
if (type == null) {
|
||||
return "";
|
||||
}
|
||||
return TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.option.runtime_source", type.getName());
|
||||
});
|
||||
runtimeSource.getItems().setAll(RuntimeDownloadSource.values());
|
||||
|
||||
// 多线程下载
|
||||
multiDownload = new MCSlider();
|
||||
multiDownload.setMin(0);
|
||||
multiDownload.setMax(64);
|
||||
multiDownload.setStep(4);
|
||||
multiDownload.setPrefWidth(ComponentSize.MEDIUM);
|
||||
|
||||
proxyHost = new MCTextField();
|
||||
proxyHost.setLabel(TimiFXUI.MULTILINGUAL.text("proxy.host"));
|
||||
|
||||
proxyPort = new MCTextField();
|
||||
proxyPort.setLabel(TimiFXUI.MULTILINGUAL.text("proxy.port"));
|
||||
|
||||
// 完成
|
||||
finish = new MCButton(TimiFXUI.MULTILINGUAL.text("finish"));
|
||||
finish.setPrefWidth(ComponentSize.LARGE);
|
||||
|
||||
setTitle(TimiFXUI.MULTILINGUAL.text("option"));
|
||||
setScrollContent(new GridPane() {{
|
||||
setMargin(network, new Insets(16, 0, 0, 0));
|
||||
getColumnConstraints().addAll(Column.build(HPos.CENTER), Column.build(HPos.CENTER));
|
||||
setHgap(20);
|
||||
setVgap(12);
|
||||
setAlignment(Pos.TOP_CENTER);
|
||||
|
||||
int row = 0;
|
||||
add(memory, 0, row++, 2, 1);
|
||||
addRow(row++, autoStartup, launchSelect);
|
||||
addRow(row++, soundVolume, bgmVolume);
|
||||
add(network, 0, row++, 2, 1);
|
||||
addRow(row++, runtimeSource, gameSource);
|
||||
addRow(row++, proxyHost, proxyPort);
|
||||
addRow(row, multiDownload);
|
||||
}});
|
||||
setBottomPaneValue(finish);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.bean.Game;
|
||||
import cn.forevermc.launcher.view.components.AbstractListPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.utils.Time;
|
||||
|
||||
/**
|
||||
* 原版选择面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 10:09
|
||||
*/
|
||||
public abstract class OriginalSelectPane extends AbstractListPane<Game> {
|
||||
|
||||
protected final MCButton download, back;
|
||||
|
||||
public OriginalSelectPane() {
|
||||
// 列表
|
||||
list.setCellFactory(cell -> new ListCell<>() {
|
||||
|
||||
private final MCLabel version, date;
|
||||
private final BorderPane root;
|
||||
|
||||
{
|
||||
version = new MCLabel();
|
||||
date = new MCLabel();
|
||||
root = new BorderPane();
|
||||
BorderPane.setAlignment(version, Pos.CENTER_LEFT);
|
||||
root.setCenter(version);
|
||||
root.setRight(date);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Game item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
version.setText(item.getId());
|
||||
date.setText(Time.toDateTime(item.getReleaseAt()));
|
||||
setGraphic(root);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 下载
|
||||
download = new MCButton(TimiFXUI.MULTILINGUAL.text("download"));
|
||||
download.setPrefWidth(ComponentSize.MEDIUM);
|
||||
download.disableProperty().bind(list.getSelectionModel().selectedItemProperty().isNull());
|
||||
|
||||
// 返回
|
||||
back = new MCButton(TimiFXUI.MULTILINGUAL.text("back"));
|
||||
back.setPrefWidth(ComponentSize.MEDIUM);
|
||||
|
||||
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.mojang.title"));
|
||||
setBottomPaneValue(download, back);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,224 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.bean.MCPopupTips;
|
||||
import cn.forevermc.launcher.bean.Pack;
|
||||
import cn.forevermc.launcher.service.MCPopupTipsService;
|
||||
import cn.forevermc.launcher.util.Resources;
|
||||
import cn.forevermc.launcher.view.components.AbstractContentPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import cn.forevermc.launcher.view.components.control.MCList;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.paint.Color;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.inject.annotation.Component;
|
||||
import com.imyeyu.inject.annotation.Inject;
|
||||
import com.imyeyu.inject.annotation.InvokeForInjected;
|
||||
import com.imyeyu.io.IOSize;
|
||||
import com.imyeyu.utils.Time;
|
||||
|
||||
/**
|
||||
* 整合版下载页面
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-13 23:30
|
||||
*/
|
||||
public abstract class PackSelectPane extends AbstractContentPane {
|
||||
|
||||
@Inject
|
||||
protected PackListPane packListPane;
|
||||
|
||||
@Inject
|
||||
protected SourceListPane sourceListPane;
|
||||
|
||||
protected MCButton install, back;
|
||||
|
||||
public PackSelectPane() {
|
||||
MCLabel titleList = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.fmc.list"));
|
||||
titleList.setPadding(new Insets(24, 0, 16, 8));
|
||||
titleList.prefWidthProperty().bind(widthProperty().multiply(.618));
|
||||
|
||||
MCLabel titleUrl = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.download.fmc.url"));
|
||||
titleUrl.setPadding(new Insets(24, 0, 16, 0));
|
||||
titleUrl.prefWidthProperty().bind(widthProperty().multiply(1 - .618).subtract(8));
|
||||
|
||||
// 安装
|
||||
install = new MCButton(ComponentSize.MEDIUM, TimiFXUI.MULTILINGUAL.text("install"));
|
||||
|
||||
// 返回
|
||||
back = new MCButton(ComponentSize.MEDIUM, TimiFXUI.MULTILINGUAL.text("back"));
|
||||
|
||||
content.setPadding(Insets.EMPTY);
|
||||
setTop(new HBox(titleList, titleUrl));
|
||||
setBottomPaneValue(install, back);
|
||||
}
|
||||
|
||||
@InvokeForInjected
|
||||
private void injected() {
|
||||
install.disableProperty().bind(sourceListPane.getSelectionModel().selectedItemProperty().isNull());
|
||||
|
||||
packListPane.prefWidthProperty().bind(widthProperty().multiply(.618));
|
||||
sourceListPane.prefWidthProperty().bind(widthProperty().multiply(1 - .618));
|
||||
setContentPane(new BorderPane() {{
|
||||
setCenter(new StackPane() {{
|
||||
setBorder(new BorderStroke(Color.valueOf("#FFF4")).right(2).build());
|
||||
getChildren().setAll(packListPane);
|
||||
}});
|
||||
setRight(sourceListPane);
|
||||
}});
|
||||
}
|
||||
|
||||
/**
|
||||
* 整合版列表面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-15 10:17
|
||||
*/
|
||||
@Component
|
||||
protected static class PackListPane extends MCList<Pack> {
|
||||
|
||||
@Inject
|
||||
private MCPopupTipsService mcPopupTipsService;
|
||||
|
||||
public PackListPane() {
|
||||
setCellFactory(cell -> new ListCell<>() {
|
||||
|
||||
final MCLabel gameVer, name, title, deprecated, time;
|
||||
final BorderPane root;
|
||||
final MCPopupTips tips;
|
||||
|
||||
{
|
||||
// 提示
|
||||
tips = new MCPopupTips();
|
||||
tips.getNode().setMaxWidth(440);
|
||||
tips.getNode().setWrapText(true);
|
||||
tips.enableProperty().bind(tips.getNode().textProperty().isNotEmpty());
|
||||
|
||||
// 名称
|
||||
name = new MCLabel();
|
||||
|
||||
// 标题
|
||||
title = new MCLabel();
|
||||
title.setTextFill(Colorful.LIGHT_GRAY);
|
||||
title.getStyleClass().clear();
|
||||
|
||||
// 游戏版本
|
||||
gameVer = new MCLabel();
|
||||
|
||||
// 过时提示
|
||||
deprecated = new MCLabel(TimiFXUI.MULTILINGUAL.text("deprecated"));
|
||||
deprecated.setTextFill(Colorful.LIGHT_GRAY);
|
||||
|
||||
// 更新时间
|
||||
time = new MCLabel();
|
||||
|
||||
root = new BorderPane();
|
||||
BorderPane.setAlignment(name, Pos.CENTER_LEFT);
|
||||
root.setLeft(new BorderPane() {{
|
||||
setPadding(new Insets(0, 4, 0, 0));
|
||||
setLeft(new MCLabel("["));
|
||||
setCenter(new StackPane() {{
|
||||
setAlignment(Pos.CENTER_LEFT);
|
||||
setPrefWidth(50);
|
||||
getChildren().add(gameVer);
|
||||
}});
|
||||
setRight(new MCLabel("]"));
|
||||
}});
|
||||
root.setCenter(new HBox(4, name, title));
|
||||
root.setRight(new HBox(8, deprecated, time));
|
||||
|
||||
mcPopupTipsService.install(this, tips);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Pack item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
tips.getNode().setText("");
|
||||
deprecated.setVisible(false);
|
||||
} else {
|
||||
name.setText(item.getName());
|
||||
title.setText(" - " + item.getTitle());
|
||||
gameVer.setText(item.getGameVer());
|
||||
time.setText(Time.toDate(item.getCreatedAt()));
|
||||
|
||||
// 提示内容
|
||||
String tipsContent = item.getDescription();
|
||||
if (item.isDeprecated()) {
|
||||
setOpacity(.6);
|
||||
deprecated.setVisible(true);
|
||||
}
|
||||
Object[] tipsParams = {
|
||||
item.getName(),
|
||||
item.getTitle(),
|
||||
item.getGameVer(),
|
||||
item.getVer(),
|
||||
IOSize.format(item.getSize()),
|
||||
Time.toDateTime(item.getCreatedAt()),
|
||||
tipsContent
|
||||
};
|
||||
tips.getNode().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.tips", tipsParams));
|
||||
setGraphic(root);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载源列表面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2023-06-15 10:17
|
||||
*/
|
||||
@Component
|
||||
protected static class SourceListPane extends MCList<Pack.Source> {
|
||||
|
||||
private static final String ATTACH_DL_API = Resources.TIMI_SERVER_API + "/attachment/download";
|
||||
|
||||
@Inject
|
||||
private MCPopupTipsService mcPopupTipsService;
|
||||
|
||||
public SourceListPane() {
|
||||
setCellFactory(cell -> new ListCell<>() {
|
||||
|
||||
final MCPopupTips tips;
|
||||
|
||||
{
|
||||
// 提示
|
||||
tips = new MCPopupTips();
|
||||
tips.enableProperty().bind(tips.getNode().textProperty().isNotEmpty());
|
||||
|
||||
mcPopupTipsService.install(this, tips);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(Pack.Source item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
tips.getNode().setText("");
|
||||
} else {
|
||||
setText(item.getName());
|
||||
|
||||
String url = switch (item.getType()) {
|
||||
case URL -> item.getData();
|
||||
case ATTACH -> ATTACH_DL_API + "/%s".formatted(item.getData());
|
||||
};
|
||||
String domain = url.substring(0, url.indexOf("/", url.indexOf("//") + 2));
|
||||
tips.getNode().setText(TimiFXUI.MULTILINGUAL.textArgs("fmc.launcher.download.fmc.url.tips", domain));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.ComponentSize;
|
||||
import cn.forevermc.launcher.service.FMCLoginService;
|
||||
import cn.forevermc.launcher.view.components.AbstractListPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.layout.VBox;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.utils.Time;
|
||||
|
||||
/**
|
||||
* @author 夜雨
|
||||
* @since 2024-05-21 19:40
|
||||
*/
|
||||
public abstract class PlayerSelectPane extends AbstractListPane<FMCLoginService.MinecraftPlayer> {
|
||||
|
||||
protected final MCButton confirm, cancel;
|
||||
|
||||
public PlayerSelectPane() {
|
||||
// 列表
|
||||
list.setCellFactory(cell -> new ListCell<>() {
|
||||
|
||||
private final MCLabel name, lastLoginAt;
|
||||
private final VBox root;
|
||||
|
||||
{
|
||||
name = new MCLabel();
|
||||
lastLoginAt = new MCLabel();
|
||||
lastLoginAt.getStyleClass().add("last-login-at");
|
||||
root = new VBox(4);
|
||||
root.getChildren().addAll(name, lastLoginAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(FMCLoginService.MinecraftPlayer item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
name.setText(item.getName());
|
||||
if (item.getLastLoginAt() != null) {
|
||||
lastLoginAt.setText("最近登录: " + Time.toDateTime(item.getLastLoginAt()));
|
||||
}
|
||||
setGraphic(root);
|
||||
}
|
||||
}
|
||||
});
|
||||
list.getStyleClass().add("player");
|
||||
|
||||
// 下载
|
||||
confirm = new MCButton(TimiFXUI.MULTILINGUAL.text("confirm"));
|
||||
confirm.setPrefWidth(ComponentSize.MEDIUM);
|
||||
confirm.disableProperty().bind(list.getSelectionModel().selectedItemProperty().isNull());
|
||||
|
||||
// 返回
|
||||
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||
cancel.setPrefWidth(ComponentSize.MEDIUM);
|
||||
|
||||
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.player.select.title"));
|
||||
setBottomPaneValue(confirm, cancel);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
package cn.forevermc.launcher.view.pages;
|
||||
|
||||
import cn.forevermc.launcher.bean.FileDownload;
|
||||
import cn.forevermc.launcher.view.components.AbstractListPane;
|
||||
import cn.forevermc.launcher.view.components.control.MCButton;
|
||||
import cn.forevermc.launcher.view.components.control.MCCheckBox;
|
||||
import cn.forevermc.launcher.view.components.control.MCLabel;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.ListCell;
|
||||
import javafx.scene.control.OverrunStyle;
|
||||
import javafx.scene.control.ProgressBar;
|
||||
import javafx.scene.layout.BorderPane;
|
||||
import javafx.scene.layout.GridPane;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.text.TextAlignment;
|
||||
import com.imyeyu.fx.ui.TimiFXUI;
|
||||
import com.imyeyu.fx.utils.BorderStroke;
|
||||
import com.imyeyu.fx.utils.Column;
|
||||
import com.imyeyu.fx.utils.NoSelectionModel;
|
||||
|
||||
/**
|
||||
* 资源下载面板
|
||||
*
|
||||
* @author 夜雨
|
||||
* @since 2022-11-28 19:18
|
||||
*/
|
||||
public abstract class ResourceDownloadPane extends AbstractListPane<FileDownload> {
|
||||
|
||||
protected final MCLabel speed;
|
||||
protected final MCButton cancel;
|
||||
protected final MCCheckBox bgmToggle;
|
||||
protected final ProgressBar pb;
|
||||
|
||||
public ResourceDownloadPane() {
|
||||
// 列表
|
||||
list.setSelectionModel(new NoSelectionModel<>());
|
||||
list.setCellFactory(cell -> new ListCell<>() {
|
||||
|
||||
private final MCLabel text;
|
||||
private final BorderPane root;
|
||||
private final ProgressBar pb;
|
||||
|
||||
{
|
||||
text = new MCLabel();
|
||||
text.setAlignment(Pos.CENTER_RIGHT);
|
||||
text.setTextOverrun(OverrunStyle.LEADING_ELLIPSIS);
|
||||
text.setTextAlignment(TextAlignment.RIGHT);
|
||||
pb = new ProgressBar();
|
||||
pb.setPrefHeight(20);
|
||||
root = new BorderPane();
|
||||
root.setCenter(text);
|
||||
root.setRight(pb);
|
||||
|
||||
text.maxWidthProperty().bind(root.widthProperty().subtract(pb.widthProperty()).subtract(40));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(FileDownload item, boolean empty) {
|
||||
super.updateItem(item, empty);
|
||||
if (empty || item == null) {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
text.setText(item.getDisplayName());
|
||||
if (text.textFillProperty().isBound()) {
|
||||
text.textFillProperty().unbind();
|
||||
}
|
||||
text.textFillProperty().bind(Bindings.when(item.errorProperty()).then(Colorful.RED).otherwise(Colorful.WHITE));
|
||||
|
||||
if (pb.progressProperty().isBound()) {
|
||||
pb.progressProperty().unbind();
|
||||
}
|
||||
pb.progressProperty().bind(item.progressProperty());
|
||||
setGraphic(root);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 总进度
|
||||
MCLabel labelPB = new MCLabel(TimiFXUI.MULTILINGUAL.text("fmc.launcher.resource_download.total"));
|
||||
labelPB.setTextFill(Colorful.WHITE);
|
||||
|
||||
speed = new MCLabel();
|
||||
|
||||
pb = new ProgressBar();
|
||||
pb.setMaxWidth(Double.MAX_VALUE);
|
||||
pb.setPrefHeight(28);
|
||||
|
||||
// 取消
|
||||
cancel = new MCButton(TimiFXUI.MULTILINGUAL.text("cancel"));
|
||||
cancel.getLabel().setPadding(new Insets(8, 24, 8, 24));
|
||||
|
||||
// 背景音乐
|
||||
bgmToggle = new MCCheckBox();
|
||||
bgmToggle.setSelected(true);
|
||||
bgmToggle.managedProperty().bind(bgmToggle.visibleProperty());
|
||||
|
||||
setTitle(TimiFXUI.MULTILINGUAL.text("fmc.launcher.resource_download"));
|
||||
setBottomPaneValue(new BorderPane() {{
|
||||
setMargin(cancel, new Insets(0, 0, 0, 16));
|
||||
setAlignment(labelPB, Pos.CENTER_RIGHT);
|
||||
prefWidthProperty().bind(bottom.widthProperty());
|
||||
setLeft(labelPB);
|
||||
setCenter(new StackPane(pb, speed));
|
||||
setRight(new HBox() {{
|
||||
setMargin(bgmToggle, new Insets(0, 0, 0, 16));
|
||||
setSpacing(4);
|
||||
setPadding(new Insets(0, 0, 0, 12));
|
||||
setAlignment(Pos.CENTER);
|
||||
getChildren().addAll(cancel, new HBox() {{
|
||||
setSpacing(4);
|
||||
setAlignment(Pos.CENTER);
|
||||
visibleProperty().bind(bgmToggle.visibleProperty());
|
||||
managedProperty().bind(visibleProperty());
|
||||
getChildren().addAll(bgmToggle, new MCLabel() {{
|
||||
bgmToggle.setLabel(this);
|
||||
setText(TimiFXUI.MULTILINGUAL.text("bgm"));
|
||||
}});
|
||||
}});
|
||||
}});
|
||||
}});
|
||||
}
|
||||
}
|
||||
17
src/main/resources/ForeverMC.yaml
Normal file
@ -0,0 +1,17 @@
|
||||
main:
|
||||
language: zh_CN
|
||||
bgmVolume: 32
|
||||
soundVolume: 100
|
||||
gameDownloadSource: MOJANG
|
||||
runtimeDownloadSource: TUNA
|
||||
|
||||
player:
|
||||
name:
|
||||
password:
|
||||
|
||||
launcher:
|
||||
memory: 2048
|
||||
game:
|
||||
clientId:
|
||||
autoStartup: false
|
||||
multiDownload: 32
|
||||
5
src/main/resources/GameOption.yaml
Normal file
@ -0,0 +1,5 @@
|
||||
name:
|
||||
loginType: OFFLINE
|
||||
java:
|
||||
server:
|
||||
autoConnect:
|
||||
BIN
src/main/resources/assets/img/bg.png
Normal file
|
After Width: | Height: | Size: 3.2 MiB |
BIN
src/main/resources/assets/img/btn.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
src/main/resources/assets/img/check.png
Normal file
|
After Width: | Height: | Size: 323 B |
BIN
src/main/resources/assets/img/command_block.png
Normal file
|
After Width: | Height: | Size: 543 B |
BIN
src/main/resources/assets/img/dirt.png
Normal file
|
After Width: | Height: | Size: 600 B |
BIN
src/main/resources/assets/img/f.png
Normal file
|
After Width: | Height: | Size: 110 B |
BIN
src/main/resources/assets/img/icon-mc-ancient.png
Normal file
|
After Width: | Height: | Size: 648 B |
BIN
src/main/resources/assets/img/icon-mc.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
src/main/resources/assets/img/info.png
Normal file
|
After Width: | Height: | Size: 91 B |
BIN
src/main/resources/assets/img/lang.png
Normal file
|
After Width: | Height: | Size: 428 B |
BIN
src/main/resources/assets/img/m.png
Normal file
|
After Width: | Height: | Size: 116 B |
BIN
src/main/resources/assets/img/mojang.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
src/main/resources/assets/img/mojang_ancient.png
Normal file
|
After Width: | Height: | Size: 7.5 KiB |
BIN
src/main/resources/assets/img/title-modern.png
Normal file
|
After Width: | Height: | Size: 44 KiB |
BIN
src/main/resources/assets/img/title.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
src/main/resources/assets/img/totem.png
Normal file
|
After Width: | Height: | Size: 214 B |