Compare commits
102 Commits
Author | SHA1 | Date |
---|---|---|
Johannes Frohnmeyer | aede030a7f | |
Johannes Frohnmeyer | 3e70e9e4e1 | |
Johannes Frohnmeyer | 4629d5f68e | |
Johannes Frohnmeyer | 7dd1d4dee2 | |
Johannes Frohnmeyer | f4ed8a4bcb | |
Johannes Frohnmeyer | af84674c26 | |
Johannes Frohnmeyer | 4aa1f24461 | |
Johannes Frohnmeyer | 9fd5d870cf | |
Johannes Frohnmeyer | e61df73dd3 | |
Johannes Frohnmeyer | c94c8b59af | |
Johannes Frohnmeyer | 475717b6b4 | |
Johannes Frohnmeyer | 5cc650921b | |
Johannes Frohnmeyer | 5b79987dcf | |
Johannes Frohnmeyer | 502026bb22 | |
Johannes Frohnmeyer | 3746e30ec7 | |
Johannes Frohnmeyer | ad033711f9 | |
Johannes Frohnmeyer | f1f2e95dd2 | |
Johannes Frohnmeyer | 9e1c20737d | |
Johannes Frohnmeyer | 04d8121ca2 | |
Johannes Frohnmeyer | f4157bae09 | |
Johannes Frohnmeyer | c7eabc37d3 | |
Johannes Frohnmeyer | 0bd675fc7c | |
Johannes Frohnmeyer | bcd4e34f7a | |
Johannes Frohnmeyer | 7a7d009e29 | |
Johannes Frohnmeyer | e14294fdd6 | |
Johannes Frohnmeyer | 98cc37405a | |
Johannes Frohnmeyer | b1b82c423a | |
Johannes Frohnmeyer | 32a01547c0 | |
Johannes Frohnmeyer | e19d7cfb3b | |
Johannes Frohnmeyer | d7ddde6c4d | |
Johannes Frohnmeyer | f091cb7a2b | |
Johannes Frohnmeyer | 5faa505235 | |
Johannes Frohnmeyer | 8aa0555a2a | |
Johannes Frohnmeyer | a7a135c598 | |
Johannes Frohnmeyer | 7f168ded43 | |
Johannes Frohnmeyer | cbba1ab70a | |
Johannes Frohnmeyer | ec67a80389 | |
Johannes Frohnmeyer | 1759e5c3de | |
Johannes Frohnmeyer | b8f30247ea | |
Johannes Frohnmeyer | 7805400e43 | |
Johannes Frohnmeyer | 55b9e5986f | |
Johannes Frohnmeyer | 7cefa88dcb | |
Johannes Frohnmeyer | 8f46e4887f | |
Johannes Frohnmeyer | 1be0d68a56 | |
Johannes Frohnmeyer | e9f8af5617 | |
Johannes Frohnmeyer | 9d287c6521 | |
Johannes Frohnmeyer | 7c4461a275 | |
Johannes Frohnmeyer | d8bd25b438 | |
Johannes Frohnmeyer | 218e12714d | |
Johannes Frohnmeyer | 3d73879bed | |
Johannes Frohnmeyer | d1c6b746f0 | |
Johannes Frohnmeyer | 71e784fdff | |
Johannes Frohnmeyer | 1db7bcde18 | |
Johannes Frohnmeyer | 8f056468a7 | |
Johannes Frohnmeyer | 25874c40f7 | |
Johannes Frohnmeyer | 3aaf7e3652 | |
Johannes Frohnmeyer | 17e89ba829 | |
Johannes Frohnmeyer | a89f51aa5d | |
Johannes Frohnmeyer | 441a9b26b2 | |
Johannes Frohnmeyer | 50e9db1fcc | |
Johannes Frohnmeyer | 6151f0e71e | |
Johannes Frohnmeyer | 8d45fdff84 | |
Johannes Frohnmeyer | 2cb852c5cb | |
Johannes Frohnmeyer | fc256a1376 | |
Johannes Frohnmeyer | 1353de777e | |
Johannes Frohnmeyer | 3f273bf6f8 | |
Johannes Frohnmeyer | 7a80132ab0 | |
Johannes Frohnmeyer | c027885364 | |
Johannes Frohnmeyer | 4967410f51 | |
Johannes Frohnmeyer | 14a23fdfed | |
Johannes Frohnmeyer | 7284193981 | |
Johannes Frohnmeyer | eb9601d6cf | |
Johannes Frohnmeyer | 5c9ce78ebf | |
Johannes Frohnmeyer | fe0c23b97b | |
Johannes Frohnmeyer | 94dbaadaf1 | |
Johannes Frohnmeyer | cd3c8a1852 | |
Johannes Frohnmeyer | 18c4953b36 | |
Johannes Frohnmeyer | 0cb3df331a | |
Johannes Frohnmeyer | 08bb13f994 | |
Johannes Frohnmeyer | a93f2c8411 | |
Johannes Frohnmeyer | e15ef8c485 | |
Johannes Frohnmeyer | 2ca25a7bee | |
Johannes Frohnmeyer | bdd86c7683 | |
Johannes Frohnmeyer | 442d462843 | |
Johannes Frohnmeyer | 37872e6c79 | |
Johannes Frohnmeyer | dff05af62f | |
Johannes Frohnmeyer | 05a18765c9 | |
Johannes Frohnmeyer | 36f462597a | |
Johannes Frohnmeyer | c52f1f3350 | |
Johannes Frohnmeyer | d4a016771f | |
Johannes Frohnmeyer | 8af7c214d2 | |
Johannes Frohnmeyer | 71faae3b9a | |
Johannes Frohnmeyer | 379f02c41c | |
Johannes Frohnmeyer | be8252ce58 | |
Johannes Frohnmeyer | fb56c9e922 | |
Johannes Frohnmeyer | 18810b255b | |
Johannes Frohnmeyer | 3370495207 | |
Johannes Frohnmeyer | b0326536c7 | |
Johannes Frohnmeyer | eb6c2538b5 | |
Johannes Frohnmeyer | 3563d7449b | |
Johannes Frohnmeyer | 7dee85292c | |
Johannes Frohnmeyer | d2b041ef59 |
|
@ -3,36 +3,32 @@
|
|||
|
||||
pipeline:
|
||||
export_metadata:
|
||||
image: gradle:jammy
|
||||
image: gradle:jdk21-jammy
|
||||
pull: true
|
||||
commands:
|
||||
- mkdir public
|
||||
- gradle --build-cache :exportMetadata -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
|
||||
- mv version.json public/
|
||||
build_platform_jars:
|
||||
image: gradle:jammy
|
||||
image: git.frohnmeyer-wds.de/johannes/ci-wine
|
||||
pull: true
|
||||
commands:
|
||||
- gradle --build-cache :launcher-dist:build -Pflavor=fat -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
|
||||
- gradle --build-cache :launcher-dist:build -Pflavor=windows -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
|
||||
- gradle --build-cache :launcher-dist:build -Pflavor=linux -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
|
||||
- gradle --build-cache :launcher-dist:build -Pflavor=macos -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
|
||||
- for f in launcher-dist/build/libs/Inceptum-*-*-*.jar; do mv "$f" "public/Inceptum-$${f##*-}"; done
|
||||
- mv public/Inceptum-fat.jar public/Inceptum.jar
|
||||
- ./platform_jars.sh
|
||||
build_wrapper:
|
||||
image: gradle:jammy
|
||||
image: gradle:jdk21-jammy
|
||||
commands:
|
||||
- gradle --build-cache :wrapper:build -Pflavor=windows -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
|
||||
- cp wrapper/build/libs/*.exe public/wrapper.exe
|
||||
- cp wrapper/build/libs/*-all.jar public/wrapper.jar
|
||||
publish_debug:
|
||||
image: gradle:jammy
|
||||
image: gradle:jdk21-jammy
|
||||
commands:
|
||||
- gradle --build-cache build publish -Pflavor=maven -Ppublic -Ptimestamp=${CI_PIPELINE_STARTED}
|
||||
secrets: [ maven_token, maven_name ]
|
||||
when:
|
||||
- branch: master
|
||||
publish_release:
|
||||
image: gradle:jammy
|
||||
image: gradle:jdk21-jammy
|
||||
commands:
|
||||
- gradle --build-cache build publish -Pflavor=maven -Ppublic -Prelease
|
||||
secrets: [ maven_token, maven_name ]
|
||||
|
@ -40,15 +36,13 @@ pipeline:
|
|||
- event: tag
|
||||
branch: master
|
||||
portable:
|
||||
image: gradle:jammy
|
||||
image: git.frohnmeyer-wds.de/johannes/ci-wine
|
||||
commands:
|
||||
- apt update
|
||||
- apt install -y p7zip-full curl jq
|
||||
- mkdir -p portable/jvm
|
||||
- cp public/wrapper.jar portable/
|
||||
- curl -L "https://github.com/pal1000/mesa-dist-win/releases/download/21.2.5/mesa3d-21.2.5-release-msvc.7z" --output mesa.7z
|
||||
- 7z e mesa.7z -oportable/run/natives/forceload x64/dxil.dll x64/libglapi.dll x64/opengl32.dll
|
||||
- curl -L "https://api.adoptium.net/v3/binary/latest/18/ga/windows/x64/jre/hotspot/normal/eclipse?project=jdk" --output jvm.zip
|
||||
- curl -L "https://api.adoptium.net/v3/binary/latest/19/ga/windows/x64/jre/hotspot/normal/eclipse?project=jdk" --output jvm.zip
|
||||
- 7z x jvm.zip -oportable/
|
||||
- mv portable/jdk*/* portable/jvm/
|
||||
- rm -r portable/jdk*
|
||||
|
|
2
LICENSE
2
LICENSE
|
@ -1,5 +1,5 @@
|
|||
Inceptum - A FOSS Launcher for Minecraft written in Java
|
||||
Copyright (C) 2021 JFronny
|
||||
Copyright (C) 2021-2023 JFronny
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
|
|
@ -14,6 +14,8 @@ Inceptum utilizes code/libraries/assets from:
|
|||
- [imgui-java](https://github.com/SpaiR/imgui-java): The library used for UI
|
||||
- [Dear ImGui](https://github.com/ocornut/imgui): Included and wrapped in imgui-java, UI library
|
||||
- [LWJGL](https://github.com/LWJGL/lwjgl3): Used as a backend for imgui-java
|
||||
- [java-gi](https://github.com/jwharm/java-gi): The library used for the new UI
|
||||
- [GTK4](https://www.gtk.org/) (and dependencies): Wrapped in java-gi-generated code, the core UI library
|
||||
- [gson](https://github.com/google/gson): Used for interacting with various APIs and configs
|
||||
- [Ubuntu](https://design.ubuntu.com/font/): Used with nerd font symbols as the font
|
||||
- [meteor-client](https://github.com/MeteorDevelopment/meteor-client): A simple HTTP client
|
||||
|
|
|
@ -9,11 +9,16 @@ allprojects {
|
|||
group = "io.gitlab.jfronny.inceptum"
|
||||
}
|
||||
|
||||
val lwjglVersion by extra("3.3.1")
|
||||
val imguiVersion by extra("1.86.4")
|
||||
val jfCommonsVersion by extra("1.0-SNAPSHOT")
|
||||
val gsonCompileVersion by extra("1.0-SNAPSHOT")
|
||||
val jlhttpVersion by extra("2.6")
|
||||
// common
|
||||
val jfCommonsVersion by extra(libs.versions.jf.commons.get())
|
||||
val gsonCompileVersion by extra(libs.versions.gson.compile.get())
|
||||
val jbAnnotationsVersion by extra(libs.versions.annotations.get())
|
||||
// launcher-imgui
|
||||
val lwjglVersion by extra(libs.versions.lwjgl.get())
|
||||
val imguiVersion by extra(libs.versions.imgui.get())
|
||||
// launcher-gtk
|
||||
val javagiVersion by extra(libs.versions.javagi.get())
|
||||
|
||||
val flavorProp: String by extra(prop("flavor", "custom"))
|
||||
if (!setOf("custom", "maven", "fat", "windows", "linux", "macos").contains(flavorProp)) throw IllegalStateException("Unsupported flavor: $flavorProp")
|
||||
val flavor: String by extra(if (flavorProp != "custom") flavorProp else OS.TYPE.codename)
|
||||
|
|
|
@ -4,9 +4,12 @@ plugins {
|
|||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
maven("https://maven.frohnmeyer-wds.de/artifacts")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("gradle.plugin.com.github.johnrengelman:shadow:7.1.2")
|
||||
implementation("de.undercouch:gradle-download-task:5.1.2")
|
||||
implementation(libs.plugin.shadow)
|
||||
implementation(libs.plugin.download)
|
||||
implementation(libs.plugin.jf.convention)
|
||||
implementation(libs.plugin.jlink)
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
#!/bin/bash
|
||||
firejail --net=none "$G_ORIGINAL_EXECUTABLE" "$@"
|
|
@ -1 +1,8 @@
|
|||
rootProject.name="inceptum-conventions"
|
||||
rootProject.name="inceptum-conventions"
|
||||
dependencyResolutionManagement {
|
||||
versionCatalogs {
|
||||
create("libs") {
|
||||
from(files("../gradle/libs.versions.toml"))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import java.io.FileOutputStream
|
||||
import de.undercouch.gradle.tasks.download.Download
|
||||
import java.io.FileOutputStream
|
||||
|
||||
plugins {
|
||||
application
|
||||
id("inceptum.java-conventions")
|
||||
id("com.github.johnrengelman.shadow")
|
||||
id("de.undercouch.download")
|
||||
id("inceptum.java")
|
||||
com.github.johnrengelman.shadow
|
||||
de.undercouch.download
|
||||
}
|
||||
|
||||
abstract class FileOutput : DefaultTask() {
|
||||
|
@ -18,14 +18,14 @@ val bootstrapArch = "i686"
|
|||
|
||||
val downloadBootstrap by tasks.registering(Download::class) {
|
||||
src("https://maven.fabricmc.net/net/fabricmc/fabric-installer-native-bootstrap/windows-${bootstrapArch}/${bootstrapVersion}/windows-${bootstrapArch}-${bootstrapVersion}.exe")
|
||||
dest(project.buildDir)
|
||||
dest(project.layout.buildDirectory)
|
||||
}
|
||||
|
||||
val nativeExe by tasks.registering(FileOutput::class) {
|
||||
dependsOn(downloadBootstrap)
|
||||
dependsOn(tasks.shadowJar)
|
||||
|
||||
output = file("$buildDir/libs/${project.name}-${project.version}.exe")
|
||||
output = project.layout.buildDirectory.file("libs/${project.name}-${project.version}.exe").get().asFile
|
||||
outputs.upToDateWhen { false }
|
||||
|
||||
doFirst {
|
||||
|
@ -43,4 +43,8 @@ val nativeExe by tasks.registering(FileOutput::class) {
|
|||
|
||||
if (rootProject.extra["flavor"] == "windows") {
|
||||
tasks.build.get().dependsOn(nativeExe)
|
||||
}
|
||||
|
||||
tasks.runShadow {
|
||||
workingDir = rootProject.projectDir
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
plugins {
|
||||
application
|
||||
id("inceptum.java-conventions")
|
||||
id("inceptum.java")
|
||||
}
|
||||
|
||||
publishing {
|
||||
|
@ -9,4 +9,6 @@ publishing {
|
|||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.run.get().workingDir = rootProject.projectDir
|
|
@ -1,13 +1,12 @@
|
|||
import org.gradle.kotlin.dsl.extra
|
||||
|
||||
plugins {
|
||||
id("inceptum.library-conventions")
|
||||
id("inceptum.library")
|
||||
}
|
||||
|
||||
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
dependencies {
|
||||
api("io.gitlab.jfronny.gson:gson-compile-core:${rootProject.extra["gsonCompileVersion"]}")
|
||||
compileOnly("io.gitlab.jfronny.gson:gson-compile-annotations:${rootProject.extra["gsonCompileVersion"]}")
|
||||
annotationProcessor("io.gitlab.jfronny.gson:gson-compile-processor:${rootProject.extra["gsonCompileVersion"]}")
|
||||
api(libs.findLibrary("gson-compile-core").orElseThrow())
|
||||
compileOnly(libs.findLibrary("gson-compile-annotations").orElseThrow())
|
||||
annotationProcessor(libs.findLibrary("gson-compile-processor").orElseThrow())
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
plugins {
|
||||
`java-library`
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://maven.frohnmeyer-wds.de/artifacts")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly("org.jetbrains:annotations:23.0.0")
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
|
||||
if (rootProject.extra["isPublic"] == true) {
|
||||
maven("https://maven.frohnmeyer-wds.de/artifacts") {
|
||||
name = "public"
|
||||
|
||||
credentials(PasswordCredentials::class) {
|
||||
username = System.getenv()["MAVEN_NAME"]
|
||||
password = System.getenv()["MAVEN_TOKEN"]
|
||||
}
|
||||
authentication {
|
||||
create<BasicAuthentication>("basic")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
plugins {
|
||||
jf.java
|
||||
`maven-publish`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven("https://maven.frohnmeyer-wds.de/artifacts")
|
||||
}
|
||||
|
||||
val libs = extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
dependencies {
|
||||
compileOnly(libs.findLibrary("annotations").orElseThrow())
|
||||
}
|
||||
|
||||
publishing {
|
||||
repositories {
|
||||
mavenLocal()
|
||||
|
||||
if (rootProject.extra["isPublic"] == true) {
|
||||
maven("https://maven.frohnmeyer-wds.de/artifacts") {
|
||||
name = "public"
|
||||
|
||||
credentials(PasswordCredentials::class) {
|
||||
username = System.getenv()["MAVEN_NAME"]
|
||||
password = System.getenv()["MAVEN_TOKEN"]
|
||||
}
|
||||
authentication {
|
||||
create<BasicAuthentication>("basic")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
if (hasProperty("offline")) {
|
||||
tasks.withType(JavaExec::class) {
|
||||
environment("G_ORIGINAL_EXECUTABLE", executable ?: "java")
|
||||
val originalMetadata = javaLauncher.get().metadata
|
||||
val field = org.gradle.api.internal.provider.AbstractProperty::class.java.getDeclaredField("value")
|
||||
field.isAccessible = true
|
||||
val customLauncher = object: JavaLauncher {
|
||||
override fun getMetadata(): JavaInstallationMetadata = originalMetadata
|
||||
override fun getExecutablePath(): RegularFile = rootProject.layout.projectDirectory
|
||||
.dir("buildSrc")
|
||||
.file("java-offline")
|
||||
}
|
||||
field.set(javaLauncher, org.gradle.api.internal.provider.Providers.of(customLauncher))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
plugins {
|
||||
id("inceptum.java-conventions")
|
||||
id("inceptum.java")
|
||||
}
|
||||
|
||||
publishing {
|
|
@ -1,14 +1,14 @@
|
|||
import io.gitlab.jfronny.scripts.*
|
||||
import javax.lang.model.element.Modifier
|
||||
|
||||
plugins {
|
||||
id("inceptum.library-conventions")
|
||||
id("jf.codegen")
|
||||
id("inceptum.gson-compile")
|
||||
inceptum.library
|
||||
jf.codegen
|
||||
inceptum.`gson-compile`
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api("io.gitlab.jfronny:commons:${rootProject.extra["jfCommonsVersion"]}")
|
||||
api("io.gitlab.jfronny:commons-gson:${rootProject.extra["jfCommonsVersion"]}")
|
||||
api(libs.bundles.commons)
|
||||
}
|
||||
|
||||
val javaVersion by extra(project.java.targetCompatibility)
|
||||
|
@ -17,14 +17,15 @@ sourceSets {
|
|||
main {
|
||||
generate(project) {
|
||||
`class`("io.gitlab.jfronny.inceptum.common", "BuildMetadata") {
|
||||
val modifiers = "public static final"
|
||||
modifiers(Modifier.PUBLIC)
|
||||
val modifiers = arrayOf(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
|
||||
|
||||
field("VERSION", versionS, modifiers)
|
||||
field("BUILD_TIME", rootProject.extra["buildTime"] as Long, modifiers)
|
||||
field("IS_PUBLIC", rootProject.extra["isPublic"] as Boolean, modifiers)
|
||||
field("IS_RELEASE", rootProject.extra["isRelease"] as Boolean, modifiers)
|
||||
field("VM_VERSION", javaVersion.majorVersion.toInt(), modifiers)
|
||||
field("WRAPPER_VERSION", rootProject.extra["wrapperVersion"] as Int, modifiers)
|
||||
field("VERSION", versionS, *modifiers)
|
||||
field("BUILD_TIME", rootProject.extra["buildTime"] as Long, *modifiers)
|
||||
field("IS_PUBLIC", rootProject.extra["isPublic"] as Boolean, *modifiers)
|
||||
field("IS_RELEASE", rootProject.extra["isRelease"] as Boolean, *modifiers)
|
||||
field("VM_VERSION", javaVersion.majorVersion.toInt(), *modifiers)
|
||||
field("WRAPPER_VERSION", rootProject.extra["wrapperVersion"] as Int, *modifiers)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package io.gitlab.jfronny.inceptum.common;
|
||||
|
||||
import io.gitlab.jfronny.gson.stream.JsonReader;
|
||||
import io.gitlab.jfronny.gson.stream.JsonWriter;
|
||||
|
||||
public class GsonPreset {
|
||||
public static class Config {
|
||||
public static void configure(JsonReader reader) {
|
||||
reader.setSerializeSpecialFloatingPointValues(true);
|
||||
reader.setLenient(true);
|
||||
}
|
||||
|
||||
public static void configure(JsonWriter writer) {
|
||||
writer.setSerializeNulls(true);
|
||||
writer.setSerializeSpecialFloatingPointValues(true);
|
||||
writer.setLenient(true);
|
||||
writer.setIndent(" ");
|
||||
writer.setOmitQuotes(true);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Api {
|
||||
public static void configure(JsonReader reader) {
|
||||
reader.setSerializeSpecialFloatingPointValues(true);
|
||||
reader.setLenient(true);
|
||||
}
|
||||
|
||||
public static void configure(JsonWriter writer) {
|
||||
writer.setSerializeNulls(false);
|
||||
writer.setSerializeSpecialFloatingPointValues(true);
|
||||
writer.setLenient(false);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,20 +1,30 @@
|
|||
package io.gitlab.jfronny.inceptum.common;
|
||||
|
||||
import io.gitlab.jfronny.gson.stream.*;
|
||||
import io.gitlab.jfronny.gson.compile.annotations.GComment;
|
||||
import io.gitlab.jfronny.gson.compile.annotations.GSerializable;
|
||||
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateChannel;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
@GSerializable(configure = GsonPreset.Config.class, isStatic = true)
|
||||
public class InceptumConfig {
|
||||
@GComment("Whether to show snapshots in the version selector for new instances")
|
||||
public static boolean snapshots = false;
|
||||
@GComment("Whether to launch the ImGUI in dark mode\nConfigurable in Settings->Dark Theme")
|
||||
public static boolean darkTheme = false;
|
||||
@GComment("Whether the GTK UI should default to a list view instead of a grid")
|
||||
public static boolean listView = false;
|
||||
@GComment("Whether to require an account to launch the game\nIntended to allow running the game from USB sticks on constrained networks")
|
||||
public static boolean enforceAccount = true;
|
||||
@GComment("The currently selected account\nUsed to launch the game")
|
||||
public static String lastAccount = null;
|
||||
@GComment("The last name used for an offline session")
|
||||
public static String offlineAccountLastName = null;
|
||||
@GComment("The update channel. Either \"CI\" or \"Stable\"\nI personally recommend the CI channel as it gets the latest fixes and features quicker")
|
||||
public static UpdateChannel channel = UpdateChannel.Stable;
|
||||
@GComment("The author name to add to packs where the metadata format requires specifying one")
|
||||
public static String authorName = "Inceptum";
|
||||
|
||||
public static void load() throws IOException {
|
||||
|
@ -31,80 +41,14 @@ public class InceptumConfig {
|
|||
saveConfig();
|
||||
}
|
||||
}
|
||||
try (JsonReader jr = new JsonReader(Files.newBufferedReader(MetaHolder.CONFIG_PATH))) {
|
||||
jr.setLenient(true);
|
||||
jr.beginObject();
|
||||
while (jr.peek() != JsonToken.END_OBJECT) {
|
||||
String name = null;
|
||||
try {
|
||||
name = jr.nextName();
|
||||
switch (name) {
|
||||
case "snapshots" -> snapshots = jr.nextBoolean();
|
||||
case "darkTheme" -> darkTheme = jr.nextBoolean();
|
||||
case "listView" -> listView = jr.nextBoolean();
|
||||
case "enforceAccount" -> enforceAccount = jr.nextBoolean();
|
||||
case "lastAccount" -> lastAccount = nullableString(jr);
|
||||
case "offlineAccountLastName" -> offlineAccountLastName = nullableString(jr);
|
||||
case "channel" -> {
|
||||
try {
|
||||
channel = UpdateChannel.valueOf(jr.nextString());
|
||||
} catch (IllegalArgumentException e) {
|
||||
Utils.LOGGER.error("Could not read channel", e);
|
||||
}
|
||||
}
|
||||
case "authorName" -> authorName = jr.nextString();
|
||||
default -> {
|
||||
Utils.LOGGER.error("Unexpected entry name: " + name);
|
||||
jr.skipValue();
|
||||
}
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
if (name == null) Utils.LOGGER.error("Could not read config entry", t);
|
||||
else Utils.LOGGER.error("Could not read config entry: " + name, t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
jr.endObject();
|
||||
}
|
||||
GC_InceptumConfig.read(MetaHolder.CONFIG_PATH);
|
||||
}
|
||||
|
||||
public static void saveConfig() {
|
||||
try (JsonWriter jw = new JsonWriter(Files.newBufferedWriter(MetaHolder.CONFIG_PATH))) {
|
||||
jw.setLenient(true);
|
||||
jw.setOmitQuotes(true);
|
||||
jw.setIndent(" ");
|
||||
jw.beginObject()
|
||||
.comment("Whether to show snapshots in the version selector for new instances")
|
||||
.name("snapshots").value(snapshots)
|
||||
.comment("Whether to launch the GUI in dark mode")
|
||||
.comment("Configurable in Settings->Dark Theme")
|
||||
.name("darkTheme").value(darkTheme)
|
||||
.comment("Whether the GTK UI should default to a list view instead of a grid")
|
||||
.name("listView").value(listView)
|
||||
.comment("Whether to require an account to launch the game")
|
||||
.comment("Intended to allow running the game from USB sticks on constrained networks")
|
||||
.name("enforceAccount").value(enforceAccount)
|
||||
.comment("The currently selected account")
|
||||
.comment("Used to launch the game")
|
||||
.name("lastAccount").value(lastAccount)
|
||||
.comment("The last name used for an offline session")
|
||||
.name("offlineAccountLastName").value(offlineAccountLastName)
|
||||
.comment("The update channel. Either \"CI\" or \"Stable\"")
|
||||
.comment("I personally recommend the CI channel as it gets the latest fixes and features quicker")
|
||||
.name("channel").value(channel.toString())
|
||||
.comment("The author name to add to packs where the metadata format requires specifying one")
|
||||
.name("authorName").value(authorName)
|
||||
.endObject();
|
||||
try {
|
||||
GC_InceptumConfig.write(MetaHolder.CONFIG_PATH);
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not save config", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String nullableString(JsonReader jr) throws IOException {
|
||||
if (jr.peek() == JsonToken.NULL) {
|
||||
jr.nextNull();
|
||||
return null;
|
||||
}
|
||||
return jr.nextString();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,19 @@
|
|||
package io.gitlab.jfronny.inceptum.common;
|
||||
|
||||
import io.gitlab.jfronny.commons.HttpUtils;
|
||||
import io.gitlab.jfronny.commons.log.Logger;
|
||||
import io.gitlab.jfronny.commons.log.StdoutLogger;
|
||||
import io.gitlab.jfronny.commons.http.client.HttpClient;
|
||||
import io.gitlab.jfronny.commons.logging.Logger;
|
||||
import io.gitlab.jfronny.commons.logging.StdoutLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class InceptumEnvironmentInitializer {
|
||||
public static void initialize() throws IOException {
|
||||
Logger.registerFactory(InceptumEnvironmentInitializer::defaultFactory);
|
||||
HttpUtils.setUserAgent("jfmods/inceptum/" + BuildMetadata.VERSION);
|
||||
HttpClient.setUserAgent("jfmods/inceptum/" + BuildMetadata.VERSION);
|
||||
InceptumConfig.load();
|
||||
}
|
||||
|
||||
public static Logger defaultFactory(String name) {
|
||||
return new StdoutLogger(name, true, true, true);
|
||||
return StdoutLogger.fancy(name);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ public class MetaHolder {
|
|||
case WINDOWS -> getPath(System.getenv("APPDATA"));
|
||||
case MAC_OS -> getPath(System.getProperty("user.home")).resolve("Library").resolve("Application Support");
|
||||
case LINUX -> {
|
||||
String s = System.getenv().get("XDG_CONFIG_HOME");
|
||||
String s = System.getenv("XDG_CONFIG_HOME");
|
||||
if (s == null)
|
||||
yield getPath(System.getProperty("user.home")).resolve(".config");
|
||||
else
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package io.gitlab.jfronny.inceptum.common;
|
||||
|
||||
import io.gitlab.jfronny.commons.HashUtils;
|
||||
import io.gitlab.jfronny.commons.HttpUtils;
|
||||
import io.gitlab.jfronny.commons.http.client.HttpClient;
|
||||
import io.gitlab.jfronny.commons.io.HashUtils;
|
||||
import io.gitlab.jfronny.commons.throwable.ThrowingFunction;
|
||||
import io.gitlab.jfronny.commons.throwable.ThrowingSupplier;
|
||||
|
||||
|
@ -18,7 +18,7 @@ public class Net {
|
|||
private static final ObjectCache OBJECT_CACHE = new ObjectCache(MetaHolder.CACHE_DIR);
|
||||
|
||||
public static byte[] downloadData(String url) throws IOException, URISyntaxException {
|
||||
try (InputStream is = HttpUtils.get(url).sendInputStream()) {
|
||||
try (InputStream is = HttpClient.get(url).sendInputStream()) {
|
||||
return is.readAllBytes();
|
||||
}
|
||||
}
|
||||
|
@ -35,11 +35,11 @@ public class Net {
|
|||
}
|
||||
|
||||
public static <T> T downloadObject(String url, ThrowingFunction<String, T, IOException> func, boolean cache) throws IOException {
|
||||
return downloadObject(url, () -> HttpUtils.get(url).sendString(), func, cache);
|
||||
return downloadObject(url, () -> HttpClient.get(url).sendString(), func, cache);
|
||||
}
|
||||
|
||||
public static <T> T downloadObject(String url, ThrowingFunction<String, T, IOException> func, String apiKey) throws IOException {
|
||||
return downloadObject(url, () -> HttpUtils.get(url).header("x-api-key", apiKey).sendString(), func, true);
|
||||
return downloadObject(url, () -> downloadStringAuthenticated(url, apiKey), func, true);
|
||||
}
|
||||
|
||||
public static <T> T downloadObject(String url, String sha1, ThrowingFunction<String, T, IOException> func) throws IOException {
|
||||
|
@ -75,13 +75,17 @@ public class Net {
|
|||
}
|
||||
|
||||
public static String downloadString(String url) throws IOException, URISyntaxException {
|
||||
return HttpUtils.get(url).sendString();
|
||||
return HttpClient.get(url).sendString();
|
||||
}
|
||||
|
||||
public static String downloadString(String url, String sha1) throws IOException, URISyntaxException {
|
||||
return new String(downloadData(url, sha1), StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static String downloadStringAuthenticated(String url, String apiKey) throws IOException, URISyntaxException {
|
||||
return HttpClient.get(url).header("x-api-key", apiKey).sendString();
|
||||
}
|
||||
|
||||
public static void downloadFile(String url, Path path) throws IOException, URISyntaxException {
|
||||
if (!Files.exists(path.getParent())) Files.createDirectories(path.getParent());
|
||||
Files.write(path, downloadData(url));
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.common;
|
||||
|
||||
public class OutdatedException extends RuntimeException {
|
||||
public OutdatedException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.common;
|
||||
|
||||
/**
|
||||
* Class containing methods for nop lambdas. Use these instead of () -> {}
|
||||
*/
|
||||
public class R {
|
||||
public static void nop() {
|
||||
}
|
||||
|
||||
public static void nop(Object a1) {
|
||||
}
|
||||
|
||||
public static void nop(Object a1, Object a2) {
|
||||
}
|
||||
}
|
|
@ -31,28 +31,29 @@ public class Updater {
|
|||
}
|
||||
|
||||
public static void update(UpdateMetadata source, boolean relaunch) throws IOException, URISyntaxException {
|
||||
Utils.LOGGER.info("Downloading version " + source.version);
|
||||
Utils.LOGGER.info("Downloading version " + source.version());
|
||||
|
||||
WrapperConfig config = new WrapperConfig();
|
||||
config.natives = new HashMap<>();
|
||||
config.libraries = new LinkedHashSet<>();
|
||||
config.repositories = new LinkedHashSet<>(source.repositories);
|
||||
source.natives.forEach((k, v) -> config.natives.put(k, new LinkedHashSet<>(v)));
|
||||
WrapperConfig config = new WrapperConfig(
|
||||
new LinkedHashSet<>(),
|
||||
new LinkedHashSet<>(source.repositories()),
|
||||
new HashMap<>()
|
||||
);
|
||||
source.natives().forEach((k, v) -> config.natives().put(k, new LinkedHashSet<>(v)));
|
||||
|
||||
DependencyNode node = downloadLibrary(source.repositories, "io.gitlab.jfronny.inceptum:launcher-dist:" + source.version, config.libraries);
|
||||
DependencyNode node = downloadLibrary(source.repositories(), "io.gitlab.jfronny.inceptum:launcher-dist:" + source.version(), config.libraries());
|
||||
Utils.LOGGER.info("Downloaded Dependencies:\n" + node);
|
||||
|
||||
List<String> currentLibraries = new LinkedList<>(config.libraries);
|
||||
if (source.natives.containsKey(Utils.getCurrentFlavor())) {
|
||||
List<String> currentLibraries = new LinkedList<>(config.libraries());
|
||||
if (source.natives().containsKey(Utils.getCurrentFlavor())) {
|
||||
Set<String> natives = new LinkedHashSet<>();
|
||||
for (String lib : source.natives.get(Utils.getCurrentFlavor())) {
|
||||
downloadLibrary(source.repositories, lib, natives);
|
||||
for (String lib : source.natives().get(Utils.getCurrentFlavor())) {
|
||||
downloadLibrary(source.repositories(), lib, natives);
|
||||
}
|
||||
currentLibraries.addAll(natives);
|
||||
config.natives.put(Utils.getCurrentFlavor(), natives);
|
||||
config.natives().put(Utils.getCurrentFlavor(), natives);
|
||||
}
|
||||
|
||||
GC_WrapperConfig.write(MetaHolder.WRAPPER_CONFIG_PATH, config);
|
||||
GC_WrapperConfig.write(config, MetaHolder.WRAPPER_CONFIG_PATH);
|
||||
|
||||
if (relaunch) {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
|
||||
|
@ -61,7 +62,7 @@ public class Updater {
|
|||
"-cp",
|
||||
buildClasspath(currentLibraries.stream())
|
||||
.map(Path::toString)
|
||||
.collect(Collectors.joining("" + File.pathSeparatorChar))
|
||||
.collect(Collectors.joining(String.valueOf(File.pathSeparatorChar)))
|
||||
).inheritIO().start();
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not relaunch", e);
|
||||
|
@ -71,9 +72,9 @@ public class Updater {
|
|||
}
|
||||
|
||||
public static List<Path> getLaunchClasspath(WrapperConfig wrapperConfig) throws IOException, URISyntaxException {
|
||||
Set<String> natives = wrapperConfig.natives.get(Utils.getCurrentFlavor());
|
||||
Set<String> natives = wrapperConfig.natives().get(Utils.getCurrentFlavor());
|
||||
if (natives == null) natives = new LinkedHashSet<>();
|
||||
Set<String> libs = wrapperConfig.libraries;
|
||||
Set<String> libs = wrapperConfig.libraries();
|
||||
if (libs == null) libs = new LinkedHashSet<>();
|
||||
|
||||
boolean configChanged = false;
|
||||
|
@ -82,18 +83,18 @@ public class Updater {
|
|||
Path p = ArtifactMeta.parse(lib).getLocalPath();
|
||||
if (!Files.exists(p)) {
|
||||
configChanged = true;
|
||||
downloadLibrary(wrapperConfig.repositories, lib, libs);
|
||||
downloadLibrary(wrapperConfig.repositories(), lib, libs);
|
||||
}
|
||||
}
|
||||
for (String lib : natives) {
|
||||
Path p = ArtifactMeta.parse(lib).getLocalPath();
|
||||
if (!Files.exists(p)) {
|
||||
configChanged = true;
|
||||
downloadLibrary(wrapperConfig.repositories, lib, natives);
|
||||
downloadLibrary(wrapperConfig.repositories(), lib, natives);
|
||||
}
|
||||
}
|
||||
|
||||
if (configChanged) GC_WrapperConfig.write(MetaHolder.WRAPPER_CONFIG_PATH, wrapperConfig);
|
||||
if (configChanged) GC_WrapperConfig.write(wrapperConfig, MetaHolder.WRAPPER_CONFIG_PATH);
|
||||
|
||||
return buildClasspath(Stream.concat(libs.stream(), natives.stream())).toList();
|
||||
}
|
||||
|
@ -123,9 +124,9 @@ public class Updater {
|
|||
throw new IOException("Could not download artifact " + meta.getMavenNotation() + " from " + repository, e);
|
||||
}
|
||||
Set<DependencyNode> dependencies = new LinkedHashSet<>();
|
||||
if (pom.dependencies != null) {
|
||||
for (MavenDependency dependency : pom.dependencies) {
|
||||
String mvnName = dependency.groupId + ":" + dependency.artifactId + ":" + dependency.version;
|
||||
if (pom.dependencies() != null) {
|
||||
for (MavenDependency dependency : pom.dependencies()) {
|
||||
String mvnName = dependency.groupId() + ":" + dependency.artifactId() + ":" + dependency.version();
|
||||
dependencies.add(downloadLibrary(repositories, mvnName, libraries));
|
||||
}
|
||||
}
|
||||
|
@ -140,10 +141,10 @@ public class Updater {
|
|||
|
||||
public static @Nullable UpdateMetadata check(UpdateChannel channel, boolean versionCompare, boolean checkEnv, Consumer<UpdateChannel> channelInvalid) throws UpdateCheckException {
|
||||
try {
|
||||
UpdateMetadata experimental = Net.downloadObject(ARTIFACTS_URL + "version.json", GC_UpdateMetadata::read);
|
||||
UpdateMetadata experimental = Net.downloadObject(ARTIFACTS_URL + "version.json", json -> GC_UpdateMetadata.read(json));
|
||||
UpdateMetadata stable = null;
|
||||
try {
|
||||
stable = Net.downloadObject(STABLE_URL + "version.json", GC_UpdateMetadata::read);
|
||||
stable = Net.downloadObject(STABLE_URL + "version.json", json -> GC_UpdateMetadata.read(json));
|
||||
} catch (Throwable ignored) {}
|
||||
if (stable == null && channel == UpdateChannel.Stable) {
|
||||
channel = UpdateChannel.CI;
|
||||
|
@ -154,12 +155,12 @@ public class Updater {
|
|||
case Stable -> stable;
|
||||
};
|
||||
if (checkEnv) {
|
||||
if (info.jvm > Runtime.version().feature()) throw new UpdateCheckException("A newer JVM is required to use the latest inceptum version. Please update!", "Outdated Java");
|
||||
if (info.wrapperVersion != BuildMetadata.WRAPPER_VERSION) throw new UpdateCheckException("A different version of the Inceptum Wrapper is required for this update!", "Mismatched Wrapper");
|
||||
if (info.jvm() > Runtime.version().feature()) throw new UpdateCheckException("A newer JVM is required to use the latest inceptum version. Please update!", "Outdated Java");
|
||||
if (info.wrapperVersion() != BuildMetadata.WRAPPER_VERSION) throw new UpdateCheckException("A different version of the Inceptum Wrapper is required for this update!", "Mismatched Wrapper");
|
||||
}
|
||||
if (versionCompare) {
|
||||
Utils.LOGGER.info("Latest version is " + info.version + ", current is " + BuildMetadata.VERSION);
|
||||
if (BuildMetadata.BUILD_TIME >= info.buildTime) {
|
||||
Utils.LOGGER.info("Latest version is " + info.version() + ", current is " + BuildMetadata.VERSION);
|
||||
if (BuildMetadata.BUILD_TIME >= info.buildTime()) {
|
||||
Utils.LOGGER.info("Up-to-date");
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ package io.gitlab.jfronny.inceptum.common;
|
|||
|
||||
import io.gitlab.jfronny.commons.OSUtils;
|
||||
import io.gitlab.jfronny.commons.io.JFiles;
|
||||
import io.gitlab.jfronny.commons.log.Logger;
|
||||
import io.gitlab.jfronny.commons.logging.Logger;
|
||||
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
|
@ -17,6 +17,7 @@ import java.util.stream.Collectors;
|
|||
|
||||
public class Utils {
|
||||
public static final int CACHE_SIZE = 128;
|
||||
public static final Pattern NEW_LINE = Pattern.compile("[\r\n]+");
|
||||
public static final Pattern VALID_FILENAME = Pattern.compile("[a-zA-Z0-9_\\-.][a-zA-Z0-9 _\\-.]*[a-zA-Z0-9_\\-.]");
|
||||
public static final Logger LOGGER = Logger.forName("Inceptum");
|
||||
private static ClassLoader SYSTEM_LOADER = ClassLoader.getSystemClassLoader();
|
||||
|
@ -41,7 +42,7 @@ public class Utils {
|
|||
Desktop.getDesktop().open(file);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Utils.LOGGER.error("Error opening web browser!", e);
|
||||
Utils.LOGGER.error("Error opening file!", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.gitlab.jfronny.inceptum.common.api;
|
||||
|
||||
import io.gitlab.jfronny.commons.HttpUtils;
|
||||
import io.gitlab.jfronny.commons.http.client.HttpClient;
|
||||
import io.gitlab.jfronny.inceptum.common.Net;
|
||||
import io.gitlab.jfronny.inceptum.common.Utils;
|
||||
import io.gitlab.jfronny.inceptum.common.model.maven.*;
|
||||
|
@ -35,150 +35,122 @@ public class MavenApi {
|
|||
}
|
||||
|
||||
public static Pom getPom(String repo, ArtifactMeta meta) throws IOException, SAXException, URISyntaxException, XMLStreamException {
|
||||
try (InputStream is = HttpUtils.get(Utils.join("/", repo, meta.getPomPath())).sendInputStream()) {
|
||||
try (InputStream is = HttpClient.get(Utils.join("/", repo, meta.getPomPath())).sendInputStream()) {
|
||||
Document doc = FACTORY.parse(is);
|
||||
doc.getDocumentElement().normalize();
|
||||
Pom result = new Pom();
|
||||
if (!"project".equals(doc.getDocumentElement().getNodeName())) throw new IOException("Illegal document name");
|
||||
boolean hasModelVersion = false;
|
||||
boolean hasGroupId = false;
|
||||
boolean hasArtifactId = false;
|
||||
boolean hasVersion = false;
|
||||
for (Node node : iterable(doc.getDocumentElement().getChildNodes())) {
|
||||
String modelVersion = null;
|
||||
String groupId = null;
|
||||
String artifactId = null;
|
||||
String version = null;
|
||||
String packaging = null;
|
||||
List<MavenDependency> dependencies = null;
|
||||
String classifier = null;
|
||||
for (Node node : children(doc.getDocumentElement())) {
|
||||
switch (node.getNodeName()) {
|
||||
case "modelVersion" -> {
|
||||
hasModelVersion = true;
|
||||
result.modelVersion = node.getTextContent();
|
||||
}
|
||||
case "modelVersion" -> modelVersion = node.getTextContent();
|
||||
case "parent" -> {
|
||||
// Dirty hack to get slf4j working: simply assume the groupId and version of the parent is also the groupId of this
|
||||
if (!hasGroupId) {
|
||||
for (Node child : iterable(node.getChildNodes())) {
|
||||
if (groupId == null) {
|
||||
for (Node child : children(node)) {
|
||||
switch (child.getNodeName()) {
|
||||
case "groupId" -> {
|
||||
if (!hasGroupId) {
|
||||
hasGroupId = true;
|
||||
result.groupId = node.getTextContent();
|
||||
if (groupId == null) {
|
||||
groupId = child.getTextContent();
|
||||
}
|
||||
}
|
||||
case "version" -> {
|
||||
if (!hasVersion) {
|
||||
hasVersion = true;
|
||||
result.version = node.getTextContent();
|
||||
if (version == null) {
|
||||
version = child.getTextContent();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case "groupId" -> {
|
||||
hasGroupId = true;
|
||||
result.groupId = node.getTextContent();
|
||||
}
|
||||
case "groupId" -> groupId = node.getTextContent();
|
||||
case "artifactId" -> {
|
||||
hasArtifactId = true;
|
||||
result.artifactId = node.getTextContent();
|
||||
artifactId = node.getTextContent();
|
||||
}
|
||||
case "version" -> {
|
||||
hasVersion = true;
|
||||
result.version = node.getTextContent();
|
||||
version = node.getTextContent();
|
||||
}
|
||||
case "packaging" -> result.packaging = node.getTextContent();
|
||||
case "packaging" -> packaging = node.getTextContent();
|
||||
case "dependencies" -> {
|
||||
result.dependencies = new LinkedList<>();
|
||||
for (Node dep : iterable(node.getChildNodes())) {
|
||||
dependencies = new LinkedList<>();
|
||||
for (Node dep : children(node)) {
|
||||
MavenDependency resolved = parseDependency(dep);
|
||||
if (resolved != null) {
|
||||
result.dependencies.add(resolved);
|
||||
dependencies.add(resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
case "classifier" -> result.classifier = node.getTextContent();
|
||||
case "classifier" -> classifier = node.getTextContent();
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
if (!hasModelVersion) throw new IOException("Pom lacks modelVersion");
|
||||
if (!hasGroupId) throw new IOException("Pom lacks groupId");
|
||||
if (!hasArtifactId) throw new IOException("Pom lacks artifactId");
|
||||
if (!hasVersion) throw new IOException("Pom lacks version");
|
||||
return result;
|
||||
if (modelVersion == null) throw new IOException("Pom lacks modelVersion");
|
||||
if (groupId == null) throw new IOException("Pom lacks groupId");
|
||||
if (artifactId == null) throw new IOException("Pom lacks artifactId");
|
||||
if (version == null) throw new IOException("Pom lacks version");
|
||||
return new Pom(modelVersion, groupId, artifactId, version, classifier, null, packaging, dependencies);
|
||||
}
|
||||
}
|
||||
|
||||
private static @Nullable MavenDependency parseDependency(Node doc) throws IOException {
|
||||
MavenDependency result = new MavenDependency();
|
||||
boolean hasGroupId = false;
|
||||
boolean hasArtifactId = false;
|
||||
boolean hasVersion = false;
|
||||
boolean hasScope = false;
|
||||
for (Node node : iterable(doc.getChildNodes())) {
|
||||
String groupId = null;
|
||||
String artifactId = null;
|
||||
String version = null;
|
||||
String scope = null;
|
||||
for (Node node : children(doc)) {
|
||||
switch (node.getNodeName()) {
|
||||
case "groupId" -> {
|
||||
hasGroupId = true;
|
||||
result.groupId = node.getTextContent();
|
||||
}
|
||||
case "artifactId" -> {
|
||||
hasArtifactId = true;
|
||||
result.artifactId = node.getTextContent();
|
||||
}
|
||||
case "version" -> {
|
||||
hasVersion = true;
|
||||
result.version = node.getTextContent();
|
||||
}
|
||||
case "groupId" -> groupId = node.getTextContent();
|
||||
case "artifactId" -> artifactId = node.getTextContent();
|
||||
case "version" -> version = node.getTextContent();
|
||||
case "scope" -> {
|
||||
hasScope = true;
|
||||
result.scope = node.getTextContent();
|
||||
if (!RUNTIME_SCOPES.contains(result.scope)) return null;
|
||||
scope = node.getTextContent();
|
||||
if (!RUNTIME_SCOPES.contains(scope)) return null;
|
||||
}
|
||||
case "optional" -> {
|
||||
if (node.getTextContent().equals("true")) return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasGroupId) throw new IOException("Pom lacks groupId");
|
||||
if (!hasArtifactId) throw new IOException("Pom lacks artifactId");
|
||||
if (!hasVersion) {
|
||||
if (result.groupId.equals("org.lwjgl")) {
|
||||
if (groupId == null) throw new IOException("Pom lacks groupId");
|
||||
if (artifactId == null) throw new IOException("Pom lacks artifactId");
|
||||
if (version == null) {
|
||||
if (groupId.equals("org.lwjgl")) {
|
||||
// Lwjgl uses a shared bom for versions which I don't want to support
|
||||
// The required modules are explicit dependencies of launcher-imgui anyway
|
||||
return null;
|
||||
}
|
||||
throw new IOException("Dependency " + result.groupId + ":" + result.artifactId + " lacks version");
|
||||
throw new IOException("Dependency " + groupId + ":" + artifactId + " lacks version");
|
||||
}
|
||||
if (!hasScope) throw new IOException("Pom lacks scope");
|
||||
return result;
|
||||
if (scope == null) throw new IOException("Pom lacks scope");
|
||||
return new MavenDependency(groupId, artifactId, version, scope);
|
||||
}
|
||||
|
||||
public static ArtifactMeta getMetadata(String repo, String artifact) throws IOException, SAXException, URISyntaxException {
|
||||
ArtifactMeta sourceMeta = ArtifactMeta.parse(artifact);
|
||||
try (InputStream is = HttpUtils.get(Utils.join("/", repo, sourceMeta.getMetadataPath())).sendInputStream()) {
|
||||
try (InputStream is = HttpClient.get(Utils.join("/", repo, sourceMeta.getMetadataPath())).sendInputStream()) {
|
||||
Document doc = FACTORY.parse(is);
|
||||
doc.getDocumentElement().normalize();
|
||||
ArtifactMeta result = new ArtifactMeta();
|
||||
if (!"metadata".equals(doc.getDocumentElement().getNodeName())) throw new IOException("Illegal document name");
|
||||
boolean hasGroupId = false;
|
||||
boolean hasArtifactId = false;
|
||||
boolean hasVersion = false;
|
||||
for (Node node : iterable(doc.getDocumentElement().getChildNodes())) {
|
||||
String groupId = null;
|
||||
String artifactId = null;
|
||||
String version = null;
|
||||
String snapshotVersion = null;
|
||||
for (Node node : children(doc.getDocumentElement())) {
|
||||
switch (node.getNodeName()) {
|
||||
case "groupId" -> {
|
||||
hasGroupId = true;
|
||||
result.groupId = node.getTextContent();
|
||||
}
|
||||
case "artifactId" -> {
|
||||
hasArtifactId = true;
|
||||
result.artifactId = node.getTextContent();
|
||||
}
|
||||
case "version" -> {
|
||||
hasVersion = true;
|
||||
result.version = node.getTextContent();
|
||||
}
|
||||
case "groupId" -> groupId = node.getTextContent();
|
||||
case "artifactId" -> artifactId = node.getTextContent();
|
||||
case "version" -> version = node.getTextContent();
|
||||
case "versioning" -> {
|
||||
for (Node node1 : iterable(node.getChildNodes())) {
|
||||
for (Node node1 : children(node)) {
|
||||
if (node1.getNodeName().equals("snapshot")) {
|
||||
String timestamp = null;
|
||||
String buildNumber = null;
|
||||
for (Node node2 : iterable(node1.getChildNodes())) {
|
||||
for (Node node2 : children(node1)) {
|
||||
switch (node2.getNodeName()) {
|
||||
case "timestamp" -> timestamp = node2.getTextContent();
|
||||
case "buildNumber" -> buildNumber = node2.getTextContent();
|
||||
|
@ -187,37 +159,37 @@ public class MavenApi {
|
|||
}
|
||||
if (timestamp == null) throw new IOException("Pom snapshots lack timestamp");
|
||||
if (buildNumber == null) throw new IOException("Pom snapshots lack buildNumber");
|
||||
result.snapshotVersion = timestamp + '-' + buildNumber;
|
||||
snapshotVersion = timestamp + '-' + buildNumber;
|
||||
}
|
||||
}
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
if (!hasGroupId) throw new IOException("Pom lacks groupId");
|
||||
if (!hasArtifactId) throw new IOException("Pom lacks artifactId");
|
||||
if (!hasVersion) throw new IOException("Pom lacks version");
|
||||
result.classifier = sourceMeta.classifier;
|
||||
return result;
|
||||
if (groupId == null) throw new IOException("Pom lacks groupId");
|
||||
if (artifactId == null) throw new IOException("Pom lacks artifactId");
|
||||
if (version == null) throw new IOException("Pom lacks version");
|
||||
return new ArtifactMeta(groupId, artifactId, version, sourceMeta.classifier(), snapshotVersion);
|
||||
}
|
||||
}
|
||||
|
||||
private static Iterable<Node> iterable(NodeList list) {
|
||||
return () -> new Iterator<>() {
|
||||
private static Iterable<Node> children(Node node) {
|
||||
return () -> new Iterator<Node>() {
|
||||
NodeList children = node.getChildNodes();
|
||||
int index = 0;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
while (index < list.getLength() && isWhitespace(list.item(index))) {
|
||||
while (index < children.getLength() && isWhitespace(children.item(index))) {
|
||||
index++;
|
||||
}
|
||||
return index < list.getLength();
|
||||
return index < children.getLength();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Node next() {
|
||||
if (!hasNext()) throw new NoSuchElementException();
|
||||
return list.item(index++);
|
||||
return children.item(index++);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
package io.gitlab.jfronny.inceptum.common.model.inceptum;
|
||||
|
||||
import io.gitlab.jfronny.gson.compile.annotations.GSerializable;
|
||||
import io.gitlab.jfronny.inceptum.common.GsonPreset;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@GSerializable
|
||||
public class UpdateMetadata {
|
||||
public Integer wrapperVersion;
|
||||
public String version;
|
||||
public Long buildTime;
|
||||
public Boolean isPublic;
|
||||
public Boolean isRelease;
|
||||
public Integer jvm;
|
||||
public Set<String> repositories;
|
||||
public Map<String, Set<String>> natives;
|
||||
@GSerializable(configure = GsonPreset.Api.class)
|
||||
public record UpdateMetadata(int wrapperVersion,
|
||||
String version,
|
||||
long buildTime,
|
||||
boolean isPublic,
|
||||
boolean isRelease,
|
||||
int jvm,
|
||||
Set<String> repositories,
|
||||
Map<String, Set<String>> natives) {
|
||||
}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
package io.gitlab.jfronny.inceptum.common.model.inceptum;
|
||||
|
||||
import io.gitlab.jfronny.gson.compile.annotations.GSerializable;
|
||||
import io.gitlab.jfronny.inceptum.common.GsonPreset;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@GSerializable
|
||||
public class WrapperConfig {
|
||||
public Set<String> libraries;
|
||||
public Set<String> repositories;
|
||||
public Map<String, Set<String>> natives;
|
||||
@GSerializable(configure = GsonPreset.Config.class)
|
||||
public record WrapperConfig(Set<String> libraries, Set<String> repositories, Map<String, Set<String>> natives) {
|
||||
}
|
||||
|
|
|
@ -1,32 +1,25 @@
|
|||
package io.gitlab.jfronny.inceptum.common.model.maven;
|
||||
|
||||
import io.gitlab.jfronny.inceptum.common.MetaHolder;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Objects;
|
||||
|
||||
public class ArtifactMeta {
|
||||
public record ArtifactMeta(String groupId, String artifactId, String version, @Nullable String classifier, @Nullable String snapshotVersion) {
|
||||
public ArtifactMeta(String groupId, String artifactId, String version) {
|
||||
this(groupId, artifactId, version, null, null);
|
||||
}
|
||||
|
||||
public static ArtifactMeta parse(String mavenNotation) {
|
||||
if (Objects.requireNonNull(mavenNotation).isEmpty()) throw new IllegalArgumentException("The notation is empty");
|
||||
String[] lib = mavenNotation.split(":");
|
||||
if (lib.length <= 1) throw new IllegalArgumentException("Not in maven notation");
|
||||
if (lib.length == 2) throw new IllegalArgumentException("Skipping versions is not supported");
|
||||
if (lib.length >= 5) throw new IllegalArgumentException("Unkown elements in maven notation");
|
||||
ArtifactMeta meta = new ArtifactMeta();
|
||||
meta.groupId = lib[0];
|
||||
meta.artifactId = lib[1];
|
||||
meta.version = lib[2];
|
||||
if (lib.length > 3) meta.classifier = lib[3];
|
||||
return meta;
|
||||
return new ArtifactMeta(lib[0], lib[1], lib[2], lib.length > 3 ? lib[3] : null, null);
|
||||
}
|
||||
|
||||
public String groupId;
|
||||
public String artifactId;
|
||||
public String version;
|
||||
|
||||
public String classifier;
|
||||
public String snapshotVersion;
|
||||
|
||||
public String getPomPath() {
|
||||
String path = groupId.replace('.', '/') + '/';
|
||||
path += artifactId + '/';
|
||||
|
|
|
@ -1,29 +1,9 @@
|
|||
package io.gitlab.jfronny.inceptum.common.model.maven;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class DependencyNode {
|
||||
private final String name;
|
||||
private final Set<DependencyNode> dependencies;
|
||||
|
||||
public DependencyNode(String name, Set<DependencyNode> dependencies) {
|
||||
this.name = Objects.requireNonNull(name);
|
||||
this.dependencies = Objects.requireNonNull(dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
DependencyNode that = (DependencyNode) o;
|
||||
return name.equals(that.name) && dependencies.equals(that.dependencies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, dependencies);
|
||||
}
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
public record DependencyNode(String name, Set<DependencyNode> dependencies) {
|
||||
@Override
|
||||
public String toString() {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
package io.gitlab.jfronny.inceptum.common.model.maven;
|
||||
|
||||
public class MavenDependency {
|
||||
public String groupId;
|
||||
public String artifactId;
|
||||
public String version;
|
||||
public String scope;
|
||||
public record MavenDependency(String groupId, String artifactId, String version, String scope) {
|
||||
}
|
||||
|
|
|
@ -4,13 +4,12 @@ import org.jetbrains.annotations.Nullable;
|
|||
|
||||
import java.util.List;
|
||||
|
||||
public class Pom {
|
||||
public String modelVersion;
|
||||
public String groupId;
|
||||
public String artifactId;
|
||||
public String version;
|
||||
public String classifier;
|
||||
public String snapshotVersion;
|
||||
@Nullable public String packaging;
|
||||
@Nullable public List<MavenDependency> dependencies;
|
||||
public record Pom(String modelVersion,
|
||||
String groupId,
|
||||
String artifactId,
|
||||
String version,
|
||||
String classifier,
|
||||
String snapshotVersion,
|
||||
@Nullable String packaging,
|
||||
@Nullable List<MavenDependency> dependencies) {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
module io.gitlab.jfronny.inceptum.common {
|
||||
exports io.gitlab.jfronny.inceptum.common;
|
||||
exports io.gitlab.jfronny.inceptum.common.api;
|
||||
exports io.gitlab.jfronny.inceptum.common.model.inceptum;
|
||||
exports io.gitlab.jfronny.inceptum.common.model.maven;
|
||||
|
||||
requires transitive java.desktop;
|
||||
requires java.xml;
|
||||
requires transitive io.gitlab.jfronny.commons;
|
||||
requires transitive io.gitlab.jfronny.commons.gson;
|
||||
requires transitive io.gitlab.jfronny.commons.http.client;
|
||||
requires transitive io.gitlab.jfronny.commons.io;
|
||||
requires transitive io.gitlab.jfronny.commons.logging;
|
||||
requires transitive io.gitlab.jfronny.gson;
|
||||
requires transitive io.gitlab.jfronny.gson.compile.core;
|
||||
requires static org.jetbrains.annotations;
|
||||
requires static io.gitlab.jfronny.gson.compile.annotations;
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
[versions]
|
||||
jf-commons = "1.5-SNAPSHOT"
|
||||
gson-compile = "1.4-SNAPSHOT"
|
||||
annotations = "24.0.1"
|
||||
lwjgl = "3.3.2"
|
||||
imgui = "1.86.10"
|
||||
javagi = "0.9.0"
|
||||
kotlin = "1.9.20"
|
||||
|
||||
[libraries]
|
||||
plugin-shadow = "gradle.plugin.com.github.johnrengelman:shadow:7.1.2"
|
||||
plugin-download = "de.undercouch:gradle-download-task:5.1.2"
|
||||
plugin-jf-convention = "io.gitlab.jfronny:convention:1.5-SNAPSHOT"
|
||||
plugin-jlink = "org.beryx:badass-jlink-plugin:3.0.1"
|
||||
|
||||
lwjgl-core = { module = "org.lwjgl:lwjgl", version.ref = "lwjgl" }
|
||||
lwjgl-glfw = { module = "org.lwjgl:lwjgl-glfw", version.ref = "lwjgl" }
|
||||
lwjgl-opengl = { module = "org.lwjgl:lwjgl-opengl", version.ref = "lwjgl" }
|
||||
lwjgl-tinyfd = { module = "org.lwjgl:lwjgl-tinyfd", version.ref = "lwjgl" }
|
||||
lwjgl-core-natives = { module = "org.lwjgl:lwjgl", version.ref = "lwjgl" }
|
||||
lwjgl-glfw-natives = { module = "org.lwjgl:lwjgl-glfw", version.ref = "lwjgl" }
|
||||
lwjgl-opengl-natives = { module = "org.lwjgl:lwjgl-opengl", version.ref = "lwjgl" }
|
||||
lwjgl-tinyfd-natives = { module = "org.lwjgl:lwjgl-tinyfd", version.ref = "lwjgl" }
|
||||
|
||||
imgui = { module = "io.github.spair:imgui-java-binding", version.ref = "imgui" } # https://github.com/SpaiR/imgui-java
|
||||
imgui-lwjgl = { module = "io.github.spair:imgui-java-lwjgl3", version.ref = "imgui" }
|
||||
imgui-natives-linux = { module = "io.github.spair:imgui-java-natives-linux", version.ref = "imgui" }
|
||||
imgui-natives-windows = { module = "io.github.spair:imgui-java-natives-windows", version.ref = "imgui" }
|
||||
imgui-natives-macos = { module = "io.github.spair:imgui-java-natives-macos", version.ref = "imgui" }
|
||||
|
||||
javagi-glib = { module = "io.github.jwharm.javagi:glib", version.ref = "javagi" }
|
||||
javagi-gtk = { module = "io.github.jwharm.javagi:gtk", version.ref = "javagi" }
|
||||
javagi-adw = { module = "io.github.jwharm.javagi:adw", version.ref = "javagi" }
|
||||
|
||||
commons = { module = "io.gitlab.jfronny:commons", version.ref = "jf-commons" }
|
||||
commons-http-client = { module = "io.gitlab.jfronny:commons-http-client", version.ref = "jf-commons" }
|
||||
commons-http-server = { module = "io.gitlab.jfronny:commons-http-server", version.ref = "jf-commons" }
|
||||
commons-io = { module = "io.gitlab.jfronny:commons-io", version.ref = "jf-commons" }
|
||||
commons-logging = { module = "io.gitlab.jfronny:commons-logging", version.ref = "jf-commons" }
|
||||
commons-serialize-gson = { module = "io.gitlab.jfronny:commons-serialize-gson", version.ref = "jf-commons" }
|
||||
|
||||
gson-compile-core = { module = "io.gitlab.jfronny.gson:gson-compile-core", version.ref = "gson-compile" }
|
||||
gson-compile-annotations = { module = "io.gitlab.jfronny.gson:gson-compile-annotations", version.ref = "gson-compile" }
|
||||
gson-compile-processor = { module = "io.gitlab.jfronny.gson:gson-compile-processor", version.ref = "gson-compile" }
|
||||
|
||||
annotations = { module = "org.jetbrains:annotations", version.ref = "annotations" }
|
||||
|
||||
[bundles]
|
||||
lwjgl = ["lwjgl-core", "lwjgl-glfw", "lwjgl-opengl", "lwjgl-tinyfd"]
|
||||
lwjgl-natives = ["lwjgl-core-natives", "lwjgl-glfw-natives", "lwjgl-opengl-natives", "lwjgl-tinyfd-natives"]
|
||||
javagi = ["javagi-glib", "javagi-gtk", "javagi-adw"]
|
||||
commons = ["commons", "commons-http-client", "commons-io", "commons-logging", "commons-serialize-gson"]
|
|
@ -1,5 +1,5 @@
|
|||
plugins {
|
||||
id("inceptum.application-conventions")
|
||||
inceptum.application
|
||||
}
|
||||
|
||||
application {
|
||||
|
@ -7,5 +7,5 @@ application {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":launcher"))
|
||||
implementation(projects.launcher)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package io.gitlab.jfronny.inceptum.cli;
|
|||
|
||||
import io.gitlab.jfronny.inceptum.common.MetaHolder;
|
||||
import io.gitlab.jfronny.inceptum.common.Utils;
|
||||
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.GC_InstanceMeta;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList;
|
||||
|
||||
|
@ -11,11 +12,7 @@ import java.nio.file.Path;
|
|||
import java.util.List;
|
||||
|
||||
public abstract class BaseInstanceCommand extends Command {
|
||||
public BaseInstanceCommand(String help, String usage, String... aliases) {
|
||||
super(help, mutateUsage(usage), aliases);
|
||||
}
|
||||
|
||||
public BaseInstanceCommand(String help, String usage, List<String> aliases, List<Command> subCommands) {
|
||||
protected BaseInstanceCommand(String help, String usage, List<String> aliases, List<Command> subCommands) {
|
||||
super(help, mutateUsage(usage), aliases, subCommands);
|
||||
}
|
||||
|
||||
|
@ -31,29 +28,33 @@ public abstract class BaseInstanceCommand extends Command {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void invoke(CommandArgs args) {
|
||||
protected void invoke(CommandArgs args) throws Exception {
|
||||
if (args.length == 0) {
|
||||
Utils.LOGGER.error("You must specify an instance to commit in");
|
||||
return;
|
||||
}
|
||||
Path instancePath = MetaHolder.INSTANCE_DIR.resolve(args.get(0));
|
||||
if (!Files.exists(instancePath)) {
|
||||
Utils.LOGGER.error("Invalid instance: \"" + args.get(0) + "\"");
|
||||
return;
|
||||
}
|
||||
Instance instance;
|
||||
try {
|
||||
instance = InstanceList.read(instancePath);
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not read instance metadata", e);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
invoke(args.subArgs(), instance);
|
||||
} catch (Exception e) {
|
||||
Utils.LOGGER.error("Could not execute command", e);
|
||||
return;
|
||||
Path normalPath = Path.of(args.get(0));
|
||||
if (Files.exists(normalPath.resolve(Instance.CONFIG_NAME))) {
|
||||
instance = new Instance(normalPath, GC_InstanceMeta.read(normalPath.resolve(Instance.CONFIG_NAME)));
|
||||
} else {
|
||||
Path instancePath = MetaHolder.INSTANCE_DIR.resolve(args.get(0)).normalize();
|
||||
if (!instancePath.startsWith(MetaHolder.INSTANCE_DIR)) {
|
||||
Utils.LOGGER.error("Specified instance path doesn't exist");
|
||||
return;
|
||||
}
|
||||
if (!Files.exists(instancePath)) {
|
||||
Utils.LOGGER.error("Invalid instance: \"" + args.get(0) + "\"");
|
||||
return;
|
||||
}
|
||||
try {
|
||||
instance = InstanceList.read(instancePath);
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not read instance metadata", e);
|
||||
return;
|
||||
}
|
||||
}
|
||||
invoke(args.subArgs(), instance);
|
||||
}
|
||||
|
||||
protected abstract void invoke(CommandArgs args, Instance instance) throws Exception;
|
||||
|
|
|
@ -47,6 +47,8 @@ public class CliMain {
|
|||
|
||||
try {
|
||||
command.invoke();
|
||||
} catch (Exception e) {
|
||||
Utils.LOGGER.error("Could not execute command", e);
|
||||
} finally {
|
||||
LauncherEnv.terminate();
|
||||
}
|
||||
|
|
|
@ -41,7 +41,7 @@ public class HelpBuilder {
|
|||
builder.append(", ");
|
||||
}
|
||||
builder.append(": ").append(help.replace("\n", "\n " + indent));
|
||||
if (level == 0 && !usage.isBlank() && !aliases.isEmpty()) {
|
||||
if (level == 0 && !usage.isBlank()) {
|
||||
StringBuilder usagePrefix = new StringBuilder("inceptum");
|
||||
for (String s : upper) {
|
||||
usagePrefix.append(" ").append(s);
|
||||
|
|
|
@ -24,9 +24,9 @@ public class ExportCommand extends BaseInstanceCommand {
|
|||
@Override
|
||||
protected void invoke(CommandArgs args, Instance instance) throws Exception {
|
||||
if (args.length == 0) throw new IllegalAccessException("You must specify a target path");
|
||||
if (args.length == 1) throw new IllegalAccessException("You must specify a version number");
|
||||
if (args.length != 2) throw new IllegalAccessException("Too many arguments");
|
||||
Exporters.CURSE_FORGE.generate(new ProcessState(), instance, Paths.get(args.get(0)), args.get(1));
|
||||
if (args.length > 2) throw new IllegalAccessException("Too many arguments");
|
||||
if (args.length > 1) instance.meta().instanceVersion = args.get(1);
|
||||
Exporters.CURSE_FORGE.generate(new ProcessState(), instance, Paths.get(args.get(0)));
|
||||
}
|
||||
|
||||
private static class MultiMCExportCommand extends BaseInstanceCommand {
|
||||
|
@ -38,7 +38,7 @@ public class ExportCommand extends BaseInstanceCommand {
|
|||
protected void invoke(CommandArgs args, Instance instance) throws Exception {
|
||||
if (args.length == 0) throw new IllegalAccessException("You must specify a target path");
|
||||
if (args.length != 1) throw new IllegalAccessException("Too many arguments");
|
||||
Exporters.MULTI_MC.generate(new ProcessState(), instance, Paths.get(args.get(0)), "1.0");
|
||||
Exporters.MULTI_MC.generate(new ProcessState(), instance, Paths.get(args.get(0)));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,9 +50,9 @@ public class ExportCommand extends BaseInstanceCommand {
|
|||
@Override
|
||||
protected void invoke(CommandArgs args, Instance instance) throws Exception {
|
||||
if (args.length == 0) throw new IllegalAccessException("You must specify a target path");
|
||||
if (args.length == 1) throw new IllegalAccessException("You must specify a version number");
|
||||
if (args.length != 2) throw new IllegalAccessException("Too many arguments");
|
||||
Exporters.MODRINTH.generate(new ProcessState(), instance, Paths.get(args.get(0)), args.get(1));
|
||||
if (args.length > 2) throw new IllegalAccessException("Too many arguments");
|
||||
if (args.length > 1) instance.meta().instanceVersion = args.get(1);
|
||||
Exporters.MODRINTH.generate(new ProcessState(), instance, Paths.get(args.get(0)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package io.gitlab.jfronny.inceptum.cli.commands;
|
||||
|
||||
import io.gitlab.jfronny.commons.log.OutputColors;
|
||||
import io.gitlab.jfronny.commons.logging.OutputColors;
|
||||
import io.gitlab.jfronny.inceptum.cli.Command;
|
||||
import io.gitlab.jfronny.inceptum.cli.CommandArgs;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.importer.Importers;
|
||||
|
@ -19,7 +19,7 @@ public class ImportCommand extends Command {
|
|||
if (args.length == 0) throw new IllegalAccessException("You must specify a pack file");
|
||||
if (args.length != 1) throw new IllegalAccessException("Too many arguments");
|
||||
ProcessState state = new ProcessState();
|
||||
String name = Importers.importPack(Paths.get(args.get(0)), state).getFileName().toString();
|
||||
String name = Importers.importPack(Paths.get(args.get(0)), state).path().getFileName().toString();
|
||||
System.out.println(OutputColors.GREEN_BOLD + "Imported as " + name + OutputColors.RESET);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,6 @@ public class JvmStateCommand extends Command {
|
|||
else
|
||||
System.out.println("\t(cannot display components as not a URLClassLoader)");
|
||||
|
||||
if (loader.getParent() != null)
|
||||
dumpClasspath(loader.getParent());
|
||||
if (loader.getParent() != null) dumpClasspath(loader.getParent());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,9 @@ import io.gitlab.jfronny.inceptum.launcher.system.launch.*;
|
|||
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class LaunchCommand extends BaseInstanceCommand {
|
||||
private final boolean server;
|
||||
|
@ -47,12 +49,10 @@ public class LaunchCommand extends BaseInstanceCommand {
|
|||
}
|
||||
if (args.length > 1) {
|
||||
InstanceMeta meta = instance.meta();
|
||||
if (meta.arguments == null) meta.arguments = new InstanceMeta.Arguments();
|
||||
meta.arguments.client = meta.arguments.client == null ? new ArrayList<>() : new ArrayList<>(meta.arguments.client);
|
||||
meta.arguments.server = meta.arguments.server == null ? new ArrayList<>() : new ArrayList<>(meta.arguments.server);
|
||||
meta.arguments.jvm = meta.arguments.jvm == null ? new ArrayList<>() : new ArrayList<>(meta.arguments.jvm);
|
||||
meta.arguments.client.addAll(args.after(0));
|
||||
meta.arguments.server.addAll(args.after(0));
|
||||
meta.checkArguments();
|
||||
meta.arguments = meta.arguments
|
||||
.withClient(Stream.concat(meta.arguments.client().stream(), args.after(0).stream()).toList())
|
||||
.withServer(Stream.concat(meta.arguments.server().stream(), args.after(0).stream()).toList());
|
||||
}
|
||||
Steps.reDownload(instance, Steps.createProcessState());
|
||||
if (server) {
|
||||
|
|
|
@ -23,7 +23,7 @@ public class ListCommand extends Command {
|
|||
List<Path> paths = JFiles.list(MetaHolder.INSTANCE_DIR);
|
||||
if (paths.isEmpty()) System.out.println("No instances are currently present");
|
||||
for (Path path : paths) {
|
||||
if (!Files.exists(path.resolve("instance.json"))) {
|
||||
if (!Files.exists(path.resolve(Instance.CONFIG_NAME))) {
|
||||
System.out.println("- Invalid instance: " + path + " (no instance metadata)");
|
||||
continue;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import io.gitlab.jfronny.inceptum.common.Utils;
|
|||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Mod;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.mds.ModsDirScanner;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.source.ModSource;
|
||||
import io.gitlab.jfronny.inceptum.launcher.util.Unchecked;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
@ -53,8 +52,8 @@ public class ModCommand extends Command {
|
|||
}
|
||||
System.out.println("Scanning installed mods, this might take a while");
|
||||
instance.mds().runOnce((path, mod) -> {
|
||||
boolean hasSources = !mod.getMetadata().sources.isEmpty();
|
||||
boolean updatable = hasSources && mod.getMetadata().sources.values().stream().anyMatch(Optional::isPresent);
|
||||
boolean hasSources = !mod.getMetadata().sources().isEmpty();
|
||||
boolean updatable = hasSources && mod.getMetadata().sources().values().stream().anyMatch(Optional::isPresent);
|
||||
if (filterUpdatable && !updatable) return;
|
||||
System.out.println("- " + path.getFileName().toString());
|
||||
System.out.println(" " + mod.getName());
|
||||
|
@ -63,11 +62,10 @@ public class ModCommand extends Command {
|
|||
}
|
||||
if (hasSources) {
|
||||
System.out.println(" Sources:");
|
||||
for (Map.Entry<ModSource, Optional<ModSource>> entry : mod.getMetadata().sources.entrySet()) {
|
||||
for (var entry : mod.getMetadata().sources().entrySet()) {
|
||||
System.out.println(" - " + entry.getKey().getName() + " (" + entry.getKey().getVersion() + ")");
|
||||
System.out.println(" Local: " + entry.getKey().getJarPath().toString());
|
||||
if (entry.getValue().isPresent())
|
||||
System.out.println(" Updatable to: " + entry.getValue().get().getVersion());
|
||||
if (entry.getValue().isPresent()) System.out.println(" Updatable to: " + entry.getValue().get().getVersion());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -100,10 +98,10 @@ public class ModCommand extends Command {
|
|||
}
|
||||
Set<Path> mods = new HashSet<>();
|
||||
for (String arg : args) {
|
||||
Path p = instance.modsDir().resolve(arg);
|
||||
if (!Files.exists(p)) p = instance.modsDir().resolve(arg + ".imod");
|
||||
Path p = instance.getModsDir().resolve(arg);
|
||||
if (!Files.exists(p)) p = instance.getModsDir().resolve(arg + ".imod");
|
||||
if (!Files.exists(p)) {
|
||||
Utils.LOGGER.error("Nonexistant mod file: " + instance.modsDir().resolve(arg));
|
||||
Utils.LOGGER.error("Nonexistant mod file: " + instance.getModsDir().resolve(arg));
|
||||
return;
|
||||
}
|
||||
mods.add(p);
|
||||
|
@ -165,9 +163,9 @@ public class ModCommand extends Command {
|
|||
}
|
||||
for (Path mod : mods) {
|
||||
try {
|
||||
mds.get(mod).delete();
|
||||
Mod md = mds.get(mod);
|
||||
md.getMetadata().sources.values().stream()
|
||||
md.delete();
|
||||
md.getMetadata().sources().values().stream()
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.findFirst()
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
module io.gitlab.jfronny.inceptum.launcher.cli {
|
||||
exports io.gitlab.jfronny.inceptum.cli;
|
||||
|
||||
requires transitive io.gitlab.jfronny.inceptum.launcher;
|
||||
requires static org.jetbrains.annotations;
|
||||
}
|
|
@ -1,15 +1,22 @@
|
|||
import io.gitlab.jfronny.scripts.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
plugins {
|
||||
id("inceptum.application-standalone-conventions")
|
||||
inceptum.`application-standalone`
|
||||
org.beryx.jlink
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("io.gitlab.jfronny.inceptum.Inceptum")
|
||||
mainModule.set("io.gitlab.jfronny.inceptum.launcher.dist")
|
||||
applicationName = "Inceptum"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":launcher"))
|
||||
implementation(project(":launcher-cli"))
|
||||
implementation(project(":launcher-imgui"))
|
||||
implementation(projects.launcher)
|
||||
implementation(projects.launcherCli)
|
||||
implementation(projects.launcherImgui)
|
||||
}
|
||||
|
||||
tasks.shadowJar {
|
||||
|
@ -30,4 +37,99 @@ publishing {
|
|||
from(components["java"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val flavor: String by rootProject.extra
|
||||
val os = org.gradle.internal.os.OperatingSystem.current()!!
|
||||
val crosscompile = flavor == "windows" && os.isLinux && project.hasProperty("crosscompile")
|
||||
|
||||
val verifyFlavorConfiguration by tasks.registering {
|
||||
when (flavor) {
|
||||
"macos" -> if (!os.isMacOsX) throw IllegalArgumentException("Cross-compilation to MacOS is unsupported!")
|
||||
"windows" -> if (!os.isWindows && !crosscompile) {
|
||||
if (os.isLinux) throw IllegalArgumentException("Cross-compilation to Windows is not enabled, please do so to build this")
|
||||
else throw IllegalArgumentException("Cross-compilation to Windows from this platform is unsupported!")
|
||||
}
|
||||
"linux" -> if (!os.isLinux) throw IllegalArgumentException("Cross-compilation to Linux is unsupported!")
|
||||
else -> throw IllegalArgumentException("Unexpected flavor: $flavor")
|
||||
}
|
||||
}
|
||||
|
||||
tasks.prepareMergedJarsDir { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.createMergedModule { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.createDelegatingModules { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.prepareModulesDir { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.jlink { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.jlinkZip { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.suggestMergedModuleInfo { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.jpackageImage { dependsOn(verifyFlavorConfiguration) }
|
||||
tasks.jpackage { dependsOn(verifyFlavorConfiguration) }
|
||||
|
||||
if (crosscompile) System.setProperty("badass.jlink.jpackage.home", "/root/jpackage-win")
|
||||
jlink {
|
||||
if (crosscompile) javaHome.set("/root/jpackage-win")
|
||||
addOptions(
|
||||
"--strip-debug",
|
||||
"--compress", "2",
|
||||
"--no-header-files",
|
||||
"--no-man-pages",
|
||||
"--verbose"
|
||||
)
|
||||
launcher {
|
||||
name = application.applicationName
|
||||
}
|
||||
if (crosscompile) targetPlatform("win", "/root/java")
|
||||
jpackage {
|
||||
imageName = application.applicationName
|
||||
if (crosscompile) {
|
||||
outputDir = "/root/jpackage-out"
|
||||
imageOutputDir = File(outputDir)
|
||||
installerOutputDir = File(outputDir)
|
||||
}
|
||||
appVersion = versionStripped
|
||||
// vendor
|
||||
when (flavor) {
|
||||
"macos" -> {
|
||||
installerType = "app-image"
|
||||
installerOptions.addAll(listOf(
|
||||
"--mac-package-name", "inceptum"
|
||||
))
|
||||
}
|
||||
"windows" -> {
|
||||
installerType = "msi"
|
||||
installerOptions.addAll(listOf(
|
||||
"--win-per-user-install",
|
||||
"--win-dir-chooser",
|
||||
"--win-menu",
|
||||
"--win-upgrade-uuid", "180becd8-a867-40d4-86ef-20949cae68b5" // Update this UUID if you fork the project!!!
|
||||
))
|
||||
//imageOptions.add("--win-console") // Enable this for debugging
|
||||
}
|
||||
else -> {
|
||||
// might also be able to push rpm with appropriate image
|
||||
installerType = "deb"
|
||||
installerOptions.addAll(listOf(
|
||||
"--linux-package-name", "inceptum",
|
||||
"--linux-shortcut"
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (crosscompile) {
|
||||
tasks.jpackage {
|
||||
doLast {
|
||||
val src = Path.of("/root/jpackage-out")
|
||||
val trg = layout.buildDirectory.dir("jpackage").get().asFile.toPath()
|
||||
Files.createDirectories(trg)
|
||||
Files.list(src).use {
|
||||
it.filter { Files.isRegularFile(it) }.forEach {
|
||||
val t = trg.resolve(it.fileName.toString())
|
||||
println("Moving $it to $t")
|
||||
Files.move(it, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
module io.gitlab.jfronny.inceptum.launcher.dist {
|
||||
requires io.gitlab.jfronny.inceptum.launcher.imgui;
|
||||
requires io.gitlab.jfronny.inceptum.launcher.cli;
|
||||
}
|
|
@ -1,19 +1,49 @@
|
|||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
id("inceptum.application-conventions")
|
||||
id("com.github.johnrengelman.shadow")
|
||||
inceptum.application
|
||||
com.github.johnrengelman.shadow
|
||||
kotlin("jvm") version libs.versions.kotlin
|
||||
kotlin("plugin.sam.with.receiver") version libs.versions.kotlin
|
||||
}
|
||||
|
||||
application {
|
||||
mainClass.set("io.gitlab.jfronny.inceptum.gtk.GtkMain")
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
samWithReceiver {
|
||||
annotation("io.gitlab.jfronny.commons.SamWithReceiver")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("com.github.bailuk:java-gtk:0.2")
|
||||
implementation(project(":launcher"))
|
||||
val javagiVersion: String by rootProject.extra
|
||||
|
||||
implementation(libs.bundles.javagi)
|
||||
implementation(projects.launcher)
|
||||
}
|
||||
|
||||
tasks.runShadow.get().workingDir = rootProject.projectDir
|
||||
tasks.compileJava {
|
||||
options.compilerArgs.add("--enable-preview")
|
||||
}
|
||||
|
||||
tasks.runShadow {
|
||||
if (project.hasProperty("showcase")) {
|
||||
environment("GTK_THEME", "Adwaita")
|
||||
environment("GDK_BACKEND", "broadway")
|
||||
environment("BROADWAY_DISPLAY", ":5")
|
||||
var proc: Process? = null
|
||||
doFirst {
|
||||
proc = Runtime.getRuntime().exec(arrayOf("gtk4-broadwayd", ":5"))
|
||||
Runtime.getRuntime().exec(arrayOf("xdg-open", "http://127.0.0.1:8085"))
|
||||
Thread.sleep(1000)
|
||||
}
|
||||
doLast {
|
||||
if (proc != null) Runtime.getRuntime().exec(arrayOf("kill", proc!!.pid().toString()))
|
||||
}
|
||||
}
|
||||
workingDir = rootProject.projectDir
|
||||
environment("GTK_DEBUG", "interactive") // interactive:actions
|
||||
jvmArgs("--enable-preview", "--enable-native-access=ALL-UNNAMED")
|
||||
}
|
||||
|
||||
tasks.withType(KotlinCompile::class) { compilerOptions.freeCompilerArgs.addAll("-Xlambdas=indy") }
|
|
@ -1,97 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.gtk.*;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import io.gitlab.jfronny.commons.StringFormatter;
|
||||
import io.gitlab.jfronny.inceptum.common.Utils;
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
|
||||
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public enum GtkEnvBackend implements LauncherEnv.EnvBackend { //TODO test
|
||||
INSTANCE;
|
||||
|
||||
public Window dialogParent = null;
|
||||
|
||||
@Override
|
||||
public void showError(String message, String title) {
|
||||
Utils.LOGGER.error(message);
|
||||
simpleDialog(message, title, MessageType.ERROR, ButtonsType.CLOSE, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(String message, Throwable t) {
|
||||
simpleDialog(StringFormatter.toString(t), message, MessageType.ERROR, ButtonsType.CLOSE, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showInfo(String message, String title) {
|
||||
Utils.LOGGER.info(message);
|
||||
simpleDialog(message, title, MessageType.INFO, ButtonsType.CLOSE, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showOkCancel(String message, String title, Runnable ok, Runnable cancel, boolean defaultCancel) {
|
||||
Utils.LOGGER.info(message);
|
||||
simpleDialog(message, title, MessageType.QUESTION, ButtonsType.OK_CANCEL, ok, cancel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getInput(String prompt, String details, String defaultValue, Consumer<String> ok, Runnable cancel) {
|
||||
//TODO spacing
|
||||
//TODO run on main thread
|
||||
GtkMain.schedule(() -> {
|
||||
Dialog dialog = new Dialog();
|
||||
if (dialogParent != null) dialog.setTransientFor(dialogParent);
|
||||
dialog.setModal(GTK.TRUE);
|
||||
if (dialogParent != null) dialog.setDestroyWithParent(GTK.TRUE);
|
||||
dialog.setTitle(new Str(prompt));
|
||||
Box box = dialog.getContentArea();
|
||||
box.append(new Label(new Str(details)));
|
||||
Entry entry = new Entry();
|
||||
Editable entryEditable = new Editable(entry.cast());
|
||||
entryEditable.setText(new Str(defaultValue));
|
||||
box.append(entry);
|
||||
dialog.addButton(I18n.str("ok"), ResponseType.OK);
|
||||
dialog.addButton(I18n.str("cancel"), ResponseType.CANCEL);
|
||||
dialog.onResponse(processResponses(dialog, () -> ok.accept(entryEditable.getText().toString()), cancel));
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showLoginRefreshPrompt(MicrosoftAccount account) {
|
||||
//TODO
|
||||
}
|
||||
|
||||
private void simpleDialog(String markup, String title, int type, int buttons, Runnable ok, Runnable cancel) {
|
||||
GtkMain.schedule(() -> {
|
||||
MessageDialog dialog = new MessageDialog(dialogParent, DialogFlags.MODAL | DialogFlags.DESTROY_WITH_PARENT, type, buttons, null);
|
||||
dialog.setTitle(new Str(title));
|
||||
dialog.setMarkup(new Str(markup));
|
||||
dialog.onResponse(processResponses(dialog, ok, cancel));
|
||||
dialog.show();
|
||||
});
|
||||
}
|
||||
|
||||
private Dialog.OnResponse processResponses(Dialog dialog, @Nullable Runnable ok, @Nullable Runnable cancel) {
|
||||
return response_id -> {
|
||||
switch (response_id) {
|
||||
case ResponseType.OK -> {
|
||||
dialog.close();
|
||||
if (ok != null) ok.run();
|
||||
}
|
||||
case ResponseType.CLOSE, ResponseType.CANCEL -> {
|
||||
dialog.close();
|
||||
if (cancel != null) cancel.run();
|
||||
}
|
||||
case ResponseType.DELETE_EVENT -> dialog.destroy();
|
||||
default -> Utils.LOGGER.error("Unexpected response type: " + response_id);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.gio.ApplicationFlags;
|
||||
import ch.bailu.gtk.glib.Glib;
|
||||
import ch.bailu.gtk.gtk.Application;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import ch.bailu.gtk.type.Strs;
|
||||
import io.gitlab.jfronny.inceptum.common.*;
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.MainWindow;
|
||||
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class GtkMain {
|
||||
public static final Str ID = new Str("io.gitlab.jfronny.inceptum");
|
||||
private static final Queue<Runnable> SCHEDULED = new ArrayDeque<>();
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
LauncherEnv.initialize(GtkEnvBackend.INSTANCE);
|
||||
Utils.LOGGER.info("Launching Inceptum v" + BuildMetadata.VERSION);
|
||||
Utils.LOGGER.info("Loading from " + MetaHolder.BASE_PATH);
|
||||
//TODO look into java-gtk samples for window architecture
|
||||
int statusCode = -1;
|
||||
try {
|
||||
statusCode = showGui(args);
|
||||
} finally {
|
||||
LauncherEnv.terminate();
|
||||
System.exit(statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
public static void schedule(Runnable task) {
|
||||
SCHEDULED.add(Objects.requireNonNull(task));
|
||||
}
|
||||
|
||||
public static int showGui(String[] args) throws IOException {
|
||||
var app = new Application(ID, ApplicationFlags.FLAGS_NONE);
|
||||
app.onActivate(() -> {
|
||||
GtkMenubar.create(app);
|
||||
var window = new MainWindow(app);
|
||||
window.show();
|
||||
Glib.idleAdd(user_data -> {
|
||||
Runnable r;
|
||||
while ((r = SCHEDULED.poll()) != null) {
|
||||
try {
|
||||
r.run();
|
||||
} catch (Throwable t) {
|
||||
Utils.LOGGER.error("Could not run scheduled task", t);
|
||||
}
|
||||
}
|
||||
return GTK.TRUE;
|
||||
}, null);
|
||||
GtkEnvBackend.INSTANCE.dialogParent = window;
|
||||
window.onCloseRequest(() -> {
|
||||
GtkEnvBackend.INSTANCE.dialogParent = null;
|
||||
return GTK.FALSE;
|
||||
});
|
||||
});
|
||||
return app.run(args.length, new Strs(args));
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk;
|
||||
|
||||
import ch.bailu.gtk.gtk.Application;
|
||||
import io.gitlab.jfronny.inceptum.common.R;
|
||||
import io.gitlab.jfronny.inceptum.common.Utils;
|
||||
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder;
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.AboutWindow;
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.NewInstanceWindow;
|
||||
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager;
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.launch.InstanceLauncher;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
|
||||
public class GtkMenubar {
|
||||
public static MenuBuilder accountsMenu;
|
||||
public static MenuBuilder launchMenu;
|
||||
|
||||
public static void create(Application app) {
|
||||
var menu = new MenuBuilder(app);
|
||||
var file = menu.submenu("file");
|
||||
file.button("new", NewInstanceWindow::createAndShow);
|
||||
file.button("redownload", () -> {
|
||||
//TODO
|
||||
});
|
||||
file.button("exit", app::quit);
|
||||
launchMenu = menu.submenu("launch");
|
||||
generateLaunchMenu();
|
||||
accountsMenu = menu.submenu("account");
|
||||
generateAccountsMenu();
|
||||
var help = menu.submenu("help");
|
||||
help.button("about", AboutWindow::createAndShow);
|
||||
help.button("log", () -> {
|
||||
//TODO
|
||||
});
|
||||
}
|
||||
|
||||
public static void generateLaunchMenu() {
|
||||
Objects.requireNonNull(launchMenu);
|
||||
launchMenu.clear();
|
||||
try {
|
||||
InstanceList.forEach(entry -> {
|
||||
launchMenu.literalButton(entry.id(), entry.toString(), () -> launch(entry));
|
||||
});
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not generate launch menu", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void launch(Instance instance) {
|
||||
//TODO show popup during launch w/ cancel option (lock main UI)
|
||||
Runnable launch = () -> {
|
||||
try {
|
||||
Steps.reDownload(instance, Steps.createProcessState());
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not redownload instance, trying to start anyways", e);
|
||||
}
|
||||
InstanceLauncher.launchClient(instance);
|
||||
};
|
||||
if (instance.isSetupLocked()) {
|
||||
LauncherEnv.showError(I18n.get("instance.launch.locked.setup"), I18n.get("instance.launch.locked"));
|
||||
return;
|
||||
}
|
||||
if (instance.isRunningLocked()) {
|
||||
LauncherEnv.showOkCancel(I18n.get("instance.launch.locked.running"), I18n.get("instance.launch.locked"), () -> {
|
||||
new Thread(launch).start(); //TODO loom
|
||||
}, R::nop);
|
||||
}
|
||||
new Thread(launch).start();
|
||||
}
|
||||
|
||||
public static void generateAccountsMenu() {
|
||||
Objects.requireNonNull(accountsMenu);
|
||||
accountsMenu.clear();
|
||||
accountsMenu.button("new", () -> {
|
||||
//TODO
|
||||
});
|
||||
accountsMenu.button("manage", () -> {
|
||||
//TODO UI to add/remove accounts
|
||||
});
|
||||
List<MicrosoftAccount> accounts = new ArrayList<>(AccountManager.getAccounts());
|
||||
accounts.add(null);
|
||||
accountsMenu.literalRadio("account", accounts.get(AccountManager.getSelectedIndex()), accounts, (i, acc) -> {
|
||||
if (acc == null) return I18n.get("account.none");
|
||||
return acc.minecraftUsername;
|
||||
}, AccountManager::switchAccount);
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.gio.ApplicationFlags;
|
||||
import ch.bailu.gtk.gtk.*;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import ch.bailu.gtk.type.Strs;
|
||||
import io.gitlab.jfronny.inceptum.common.R;
|
||||
|
||||
public class TestStart {
|
||||
public static void main(String[] args) {
|
||||
var app = new Application(GtkMain.ID, ApplicationFlags.FLAGS_NONE);
|
||||
app.onActivate(() -> {
|
||||
var button = Button.newWithLabelButton(new Str("Test"));
|
||||
button.onClicked(TestStart::test);
|
||||
|
||||
var window = new ApplicationWindow(app);
|
||||
window.setDefaultSize(200, 50);
|
||||
window.setTitle(new Str("Inceptum"));
|
||||
window.setChild(button);
|
||||
window.show();
|
||||
GtkEnvBackend.INSTANCE.dialogParent = window;
|
||||
window.onCloseRequest(() -> {
|
||||
GtkEnvBackend.INSTANCE.dialogParent = null;
|
||||
return GTK.FALSE;
|
||||
});
|
||||
});
|
||||
System.exit(app.run(args.length, new Strs(args)));
|
||||
}
|
||||
|
||||
private static void test() {
|
||||
GtkEnvBackend backend = GtkEnvBackend.INSTANCE;
|
||||
|
||||
backend.getInput("Ae", "IoU\naee", "Def", R::nop, R::nop);
|
||||
// backend.showInfo("Some message", "Title");
|
||||
// backend.showError("Yes!", "AAee");
|
||||
// backend.showError("Nes!", new ArrayIndexOutOfBoundsException("Top 500 cheese"));
|
||||
// backend.showOkCancel("Some Message\nYes!", "TitL", () -> backend.showInfo("Pressed OK", "OK"), () -> backend.showError("Pressed CANCEL", "CANCEL"));
|
||||
// backend.getInput("This is a prompt", """
|
||||
// These are some extremely interesting and detailed (hence the name) details!
|
||||
// This is a second line, since these details
|
||||
// are just THAT important!""", "Default", value -> backend.showInfo("Received " + value, "Input"), () -> backend.showInfo("Got no input :/", "Input"));
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.bridge.ListIndex;
|
||||
import ch.bailu.gtk.gtk.*;
|
||||
import ch.bailu.gtk.pango.EllipsizeMode;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class InstanceGridEntryFactory extends SignalListItemFactory {
|
||||
public InstanceGridEntryFactory(List<Instance> instanceList) {
|
||||
super();
|
||||
//TODO better design
|
||||
onSetup(item -> {
|
||||
var box = new Box(Orientation.VERTICAL, 5);
|
||||
|
||||
var thumbnail = new InstanceThumbnail();
|
||||
box.append(thumbnail);
|
||||
|
||||
var label = new Label(Str.NULL);
|
||||
label.setSizeRequest(192, -1);
|
||||
label.setMaxWidthChars(20);
|
||||
label.setJustify(Justification.CENTER);
|
||||
label.setHalign(Align.START);
|
||||
label.setHexpand(GTK.TRUE);
|
||||
label.setValign(Align.CENTER);
|
||||
label.setEllipsize(EllipsizeMode.MIDDLE);
|
||||
label.setLines(3);
|
||||
label.setWrap(GTK.TRUE);
|
||||
label.setWrapMode(WrapMode.WORD_CHAR);
|
||||
label.setMarginTop(10);
|
||||
box.append(label);
|
||||
// Label label = new Label(Str.NULL);
|
||||
// label.setXalign(0);
|
||||
// label.setWidthChars(20);
|
||||
// label.setMarginEnd(10);
|
||||
// box.append(label);
|
||||
//
|
||||
// Button launch = new Button();
|
||||
// launch.setIconName(new Str("computer-symbolic"));
|
||||
// launch.setTooltipText(I18n.str("instance.launch"));
|
||||
// launch.setHasTooltip(GTK.TRUE);
|
||||
// box.append(launch);
|
||||
//
|
||||
// Button openDir = new Button();
|
||||
// openDir.setIconName(new Str("folder-symbolic"));
|
||||
// openDir.setTooltipText(I18n.str("instance.directory"));
|
||||
// openDir.setHasTooltip(GTK.TRUE);
|
||||
// box.append(openDir);
|
||||
|
||||
item.setChild(box);
|
||||
//TODO server launch with network-server-symbolic
|
||||
//TODO kill current instance
|
||||
});
|
||||
onBind(item -> {
|
||||
// Label label = new Label(item.getChild().getFirstChild().cast());
|
||||
// Button launch = new Button(label.getNextSibling().cast());
|
||||
// Button openDir = new Button(launch.getNextSibling().cast());
|
||||
// InstanceList.Entry instance = instanceList.get(ListIndex.toIndex(item));
|
||||
// label.setText(new Str(instance.toString()));
|
||||
// launch.onClicked(() -> GtkMenubar.launch(instance));
|
||||
// openDir.onClicked(() -> Utils.openFile(instance.path().toFile()));
|
||||
Box box = new Box(item.getChild().cast());
|
||||
InstanceThumbnail thumbnail = new InstanceThumbnail(box.getFirstChild().cast());
|
||||
Label label = new Label(thumbnail.getNextSibling().cast());
|
||||
|
||||
Instance instance = instanceList.get(ListIndex.toIndex(item));
|
||||
thumbnail.bind(instance);
|
||||
label.setText(new Str(instance.toString()));
|
||||
//TODO right click menu + double click action
|
||||
//TODO edit button document-edit-symbolic -> edit-delete-symbolic, edit-copy-symbolic
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,80 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.adw.ActionRow;
|
||||
import ch.bailu.gtk.bridge.ListIndex;
|
||||
import ch.bailu.gtk.gtk.*;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import io.gitlab.jfronny.inceptum.common.Utils;
|
||||
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar;
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class InstanceListEntryFactory extends SignalListItemFactory {
|
||||
public InstanceListEntryFactory(List<Instance> instanceList) {
|
||||
super();
|
||||
onSetup(item -> {
|
||||
var thumbnail = new InstanceThumbnail();
|
||||
thumbnail.setName(new Str("inceptum-thumbnail"));
|
||||
|
||||
var launch = new Button();
|
||||
launch.setName(new Str("inceptum-launch"));
|
||||
launch.setIconName(new Str("computer-symbolic"));
|
||||
launch.setTooltipText(I18n.str("instance.launch"));
|
||||
launch.setHasTooltip(GTK.TRUE);
|
||||
|
||||
var openDir = new Button();
|
||||
openDir.setName(new Str("inceptum-open-dir"));
|
||||
openDir.setIconName(new Str("folder-symbolic"));
|
||||
openDir.setTooltipText(I18n.str("instance.directory"));
|
||||
openDir.setHasTooltip(GTK.TRUE);
|
||||
|
||||
var row = new ActionRow();
|
||||
row.setName(new Str("inceptum-row"));
|
||||
row.setActivatableWidget(launch);
|
||||
row.addPrefix(thumbnail);
|
||||
row.addSuffix(launch);
|
||||
row.addSuffix(openDir);
|
||||
|
||||
item.setChild(row);
|
||||
|
||||
//TODO server launch with network-server-symbolic
|
||||
//TODO kill current instance
|
||||
});
|
||||
onBind(item -> {
|
||||
Instance instance = instanceList.get(ListIndex.toIndex(item));
|
||||
ActionRow row = new ActionRow(item.getChild().cast());
|
||||
Box prefixes = new Box(row.getFirstChild().getFirstChild().cast());
|
||||
Box suffixes = new Box(row.getFirstChild().getLastChild().cast());
|
||||
|
||||
InstanceThumbnail thumbnail = new InstanceThumbnail(prefixes.getFirstChild().cast());
|
||||
Button launch = new Button(suffixes.getFirstChild().cast());
|
||||
Button openDir = new Button(launch.getNextSibling().cast());
|
||||
row.setTitle(new Str(instance.toString()));
|
||||
|
||||
// InstanceThumbnail thumbnail = new InstanceThumbnail(row.getFirstChild().cast());
|
||||
// Label label = new Label(thumbnail.getNextSibling().cast());
|
||||
// Button launch = new Button(label.getNextSibling().cast());
|
||||
// Button openDir = new Button(launch.getNextSibling().cast());
|
||||
|
||||
thumbnail.bind(instance);
|
||||
// label.setText(new Str(instance.toString()));
|
||||
launch.onClicked(() -> GtkMenubar.launch(instance));
|
||||
openDir.onClicked(() -> Utils.openFile(instance.path().toFile()));
|
||||
|
||||
//TODO why the hell does this crash the VM?
|
||||
//TODO GestureClick.setButton(GDK_BUTTON_SECONDARY)
|
||||
// var controller = new EventControllerLegacy();
|
||||
// controller.onEvent(event -> {
|
||||
// if (event.getEventType() == EventType.BUTTON_RELEASE) {
|
||||
// Utils.LOGGER.info("Button " + new ButtonEvent(event.cast()).getButton());
|
||||
// }
|
||||
// return GTK.FALSE;
|
||||
// });
|
||||
// row.addController(controller);
|
||||
//TODO edit button document-edit-symbolic -> edit-delete-symbolic, edit-copy-symbolic
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,43 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control;
|
||||
|
||||
import ch.bailu.gtk.gtk.*;
|
||||
import ch.bailu.gtk.type.*;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
|
||||
|
||||
public class InstanceThumbnail extends Stack {
|
||||
private static final Str SPINNER = new Str("spinner");
|
||||
private static final Str IMAGE = new Str("image");
|
||||
private static final Str GENERIC = new Str("generic");
|
||||
|
||||
public InstanceThumbnail(CPointer pointer) {
|
||||
super(pointer);
|
||||
}
|
||||
|
||||
public InstanceThumbnail() {
|
||||
super();
|
||||
var spinner = new Spinner();
|
||||
var image = new Image();
|
||||
var generic = new Image();
|
||||
spinner.setName(SPINNER);
|
||||
image.setName(IMAGE);
|
||||
generic.setName(GENERIC);
|
||||
generic.setFromIconName(new Str("media-playback-start-symbolic")); //TODO better default icon
|
||||
addNamed(spinner, SPINNER);
|
||||
addNamed(image, IMAGE);
|
||||
addNamed(generic, GENERIC);
|
||||
}
|
||||
|
||||
public void bind(Instance entry) {
|
||||
var spinner = new Spinner(getChildByName(SPINNER).cast());
|
||||
var image = new Image(getChildByName(IMAGE).cast()); //TODO
|
||||
var generic = new Image(getChildByName(GENERIC).cast());
|
||||
//TODO mark instance being played
|
||||
if (entry.isSetupLocked()) {
|
||||
setVisibleChild(spinner);
|
||||
} else if (false) { // if the instance has an image, load the image data and set it as the visible child
|
||||
setVisibleChild(image);
|
||||
} else {
|
||||
setVisibleChild(generic);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu;
|
||||
|
||||
import ch.bailu.gtk.gio.SimpleAction;
|
||||
|
||||
public class ButtonItem extends MenuItem {
|
||||
public ButtonItem(SimpleAction action) {
|
||||
super(action);
|
||||
}
|
||||
}
|
|
@ -1,138 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.gio.MenuItem;
|
||||
import ch.bailu.gtk.gio.*;
|
||||
import ch.bailu.gtk.glib.Variant;
|
||||
import ch.bailu.gtk.glib.VariantType;
|
||||
import ch.bailu.gtk.gtk.Application;
|
||||
import ch.bailu.gtk.gtk.PopoverMenu;
|
||||
import ch.bailu.gtk.type.Pointer;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable;
|
||||
import io.gitlab.jfronny.inceptum.common.Utils;
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class MenuBuilder {
|
||||
private static final Object LOCK = new Object();
|
||||
|
||||
private static Menu getRootMenu(Application app) {
|
||||
synchronized (LOCK) {
|
||||
var currentMenu = app.getMenubar();
|
||||
if (currentMenu.equals(Pointer.NULL)) {
|
||||
var menu = new Menu();
|
||||
app.setMenubar(menu);
|
||||
return menu;
|
||||
} else {
|
||||
return new Menu(currentMenu.cast());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final ActionMap map;
|
||||
private final Map<String, Action> refs = new LinkedHashMap<>();
|
||||
private final Menu menu;
|
||||
private final String prefix;
|
||||
|
||||
public MenuBuilder(Application app) {
|
||||
this(app, getRootMenu(app), "");
|
||||
}
|
||||
|
||||
public MenuBuilder(Application app, Menu menu, String prefix) {
|
||||
this(new ActionMap(app.cast()), menu, prefix);
|
||||
}
|
||||
|
||||
public MenuBuilder(ActionMap map, Menu menu, String prefix) {
|
||||
if (!prefix.isEmpty() && !prefix.endsWith(".")) prefix += ".";
|
||||
this.map = Objects.requireNonNull(map);
|
||||
this.menu = Objects.requireNonNull(menu);
|
||||
this.prefix = Objects.requireNonNull(prefix);
|
||||
}
|
||||
|
||||
public ButtonItem button(String name, ThrowingRunnable<?> onClick) {
|
||||
return literalButton(name, I18n.get("menu." + prefix + name), onClick);
|
||||
}
|
||||
|
||||
public ButtonItem literalButton(String internalName, String label, ThrowingRunnable<?> onClick) {
|
||||
internalName = prefix + internalName;
|
||||
SimpleAction sAct = new SimpleAction(new Str(internalName), null);
|
||||
addAction(internalName, sAct);
|
||||
sAct.onActivate(v -> {
|
||||
try {
|
||||
onClick.run();
|
||||
} catch (Throwable e) {
|
||||
Utils.LOGGER.error("Could not execute action", e);
|
||||
}
|
||||
});
|
||||
menu.appendItem(new MenuItem(new Str(label), new Str("app." + internalName)));
|
||||
return new ButtonItem(sAct);
|
||||
}
|
||||
|
||||
public ToggleItem toggle(String name, boolean initial, Consumer<Boolean> action) {
|
||||
name = prefix + name;
|
||||
SimpleAction sAct = SimpleAction.newStatefulSimpleAction(new Str(name), null, Variant.newBooleanVariant(GTK.is(initial)));
|
||||
Action rAct = addAction(name, sAct);
|
||||
sAct.onActivate(parameter -> {
|
||||
boolean state = !GTK.is(rAct.getState().getBoolean());
|
||||
sAct.setState(Variant.newBooleanVariant(GTK.is(state)));
|
||||
action.accept(state);
|
||||
});
|
||||
menu.appendItem(new MenuItem(I18n.str("menu." + name), new Str("app." + name)));
|
||||
return new ToggleItem(sAct);
|
||||
}
|
||||
|
||||
public <T> RadioItem<T> radio(String name, T initial, List<T> options, Consumer<T> action) {
|
||||
return literalRadio(name, initial, options, (i, t) -> I18n.get("menu." + name, i), action);
|
||||
}
|
||||
|
||||
public <T> RadioItem<T> literalRadio(String name, T initial, List<T> options, BiFunction<Integer, T, String> stringifier, Consumer<T> action) {
|
||||
Objects.requireNonNull(options);
|
||||
name = prefix + name;
|
||||
SimpleAction sAct = SimpleAction.newStatefulSimpleAction(new Str(name), new VariantType(new Str("i")), Variant.newInt32Variant(options.indexOf(initial)));
|
||||
addAction(name, sAct);
|
||||
sAct.onActivate(parameter -> {
|
||||
sAct.setState(parameter);
|
||||
action.accept(options.get(parameter.getInt32()));
|
||||
});
|
||||
int i = 0;
|
||||
for (T option : options) {
|
||||
menu.appendItem(new MenuItem(new Str(stringifier.apply(i, option)), new Str("app." + name + "(" + i + ")")));
|
||||
i++;
|
||||
}
|
||||
return new RadioItem<>(sAct, options);
|
||||
}
|
||||
|
||||
public MenuBuilder submenu(String name) {
|
||||
name = prefix + name;
|
||||
Menu submenu = new Menu();
|
||||
menu.appendSubmenu(I18n.str("menu." + name), submenu);
|
||||
return new MenuBuilder(map, submenu, name);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
menu.removeAll();
|
||||
refs.forEach((name, action) -> {
|
||||
map.removeAction(new Str(name));
|
||||
});
|
||||
refs.clear();
|
||||
}
|
||||
|
||||
private Action addAction(String name, SimpleAction action) {
|
||||
Action rAct = new Action(action.cast());
|
||||
map.addAction(rAct);
|
||||
refs.put(name, rAct);
|
||||
return rAct;
|
||||
}
|
||||
|
||||
public Menu getMenu() {
|
||||
return menu;
|
||||
}
|
||||
|
||||
public PopoverMenu asPopover() {
|
||||
return PopoverMenu.newFromModelPopoverMenu(menu);
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.gio.Action;
|
||||
import ch.bailu.gtk.gio.SimpleAction;
|
||||
|
||||
public abstract class MenuItem {
|
||||
protected final SimpleAction sAction;
|
||||
protected final Action action;
|
||||
|
||||
public MenuItem(SimpleAction action) {
|
||||
this.sAction = action;
|
||||
this.action = new Action(action.cast());
|
||||
}
|
||||
|
||||
public boolean getEnabled() {
|
||||
return GTK.is(action.getEnabled());
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
sAction.setEnabled(GTK.is(enabled));
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu;
|
||||
|
||||
import ch.bailu.gtk.gio.SimpleAction;
|
||||
import ch.bailu.gtk.glib.Variant;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class RadioItem<T> extends MenuItem {
|
||||
private final List<T> options;
|
||||
|
||||
public RadioItem(SimpleAction action, List<T> options) {
|
||||
super(action);
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public void setSelected(T selected) {
|
||||
sAction.setState(Variant.newInt32Variant(options.indexOf(selected)));
|
||||
}
|
||||
|
||||
public T getSelected() {
|
||||
return options.get(action.getState().getInt32());
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.gio.SimpleAction;
|
||||
import ch.bailu.gtk.glib.Variant;
|
||||
|
||||
public class ToggleItem extends MenuItem {
|
||||
public ToggleItem(SimpleAction action) {
|
||||
super(action);
|
||||
}
|
||||
|
||||
public boolean getState() {
|
||||
return GTK.is(action.getState().getBoolean());
|
||||
}
|
||||
|
||||
public void setState(boolean state) {
|
||||
sAction.setState(Variant.newBooleanVariant(GTK.is(state)));
|
||||
}
|
||||
|
||||
public boolean toggle() {
|
||||
boolean toggled = !getState();
|
||||
setState(toggled);
|
||||
return toggled;
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util;
|
||||
|
||||
import ch.bailu.gtk.gtk.Widget;
|
||||
import ch.bailu.gtk.type.Pointer;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Dbg {
|
||||
public static String inspect(Widget ptr) {
|
||||
if (ptr.isNull()) return "<null>";
|
||||
StringBuilder sb = new StringBuilder();
|
||||
inspect(ptr, sb, "");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static void inspect(Widget ptr, StringBuilder bld, String indent) {
|
||||
bld.append(indent).append("<").append(ptr.getName().toString()).append("#").append(ptr.getCPointer()).append("> ")
|
||||
.append(Arrays.stream(Pointer.toJnaPointer(ptr.getCssClasses().getCPointer()).getPointerArray(0))
|
||||
.map(p -> p.getString(0))
|
||||
.collect(Collectors.joining(", ")));
|
||||
ptr = ptr.getFirstChild();
|
||||
if (ptr.isNotNull()) {
|
||||
while (ptr.isNotNull()) {
|
||||
bld.append("\n");
|
||||
inspect(ptr, bld, indent + " ");
|
||||
ptr = ptr.getNextSibling();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util;
|
||||
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import org.jetbrains.annotations.PropertyKey;
|
||||
|
||||
import java.util.ResourceBundle;
|
||||
|
||||
public class I18n {
|
||||
private static final String BUNDLE = "inceptum";
|
||||
private static final ResourceBundle bundle = ResourceBundle.getBundle(BUNDLE);
|
||||
|
||||
public static String get(@PropertyKey(resourceBundle = BUNDLE) String key) {
|
||||
return bundle.getString(key);
|
||||
}
|
||||
|
||||
public static String get(@PropertyKey(resourceBundle = BUNDLE) String key, Object... args) {
|
||||
return String.format(bundle.getString(key), args);
|
||||
}
|
||||
|
||||
public static Str str(@PropertyKey(resourceBundle = BUNDLE) String key) {
|
||||
return new Str(get(key));
|
||||
}
|
||||
|
||||
public static Str str(@PropertyKey(resourceBundle = BUNDLE) String key, Object... args) {
|
||||
return new Str(get(key, args));
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window;
|
||||
|
||||
import ch.bailu.gtk.gtk.AboutDialog;
|
||||
import ch.bailu.gtk.gtk.License;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import io.gitlab.jfronny.inceptum.common.BuildMetadata;
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
|
||||
|
||||
public class AboutWindow extends AboutDialog {
|
||||
public AboutWindow() {
|
||||
setProgramName(new Str("Inceptum"));
|
||||
setCopyright(new Str("Copyright (C) 2021 JFronny"));
|
||||
setVersion(new Str(BuildMetadata.VERSION.toString()));
|
||||
setLicenseType(License.MIT_X11);
|
||||
setLicense(I18n.str("about.license"));
|
||||
setWebsiteLabel(I18n.str("about.contact"));
|
||||
setWebsite(new Str("https://jfronny.gitlab.io/contact.html"));
|
||||
if (!BuildMetadata.IS_PUBLIC) {
|
||||
setComments(I18n.str("about.unsupported-build"));
|
||||
}
|
||||
int vm = Runtime.version().feature();
|
||||
setSystemInformation(I18n.str(BuildMetadata.VM_VERSION == vm ? "about.jvm" : "about.jvm.unsupported", vm));
|
||||
//TODO setLogo
|
||||
}
|
||||
|
||||
public static void createAndShow() {
|
||||
new AboutWindow().show();
|
||||
}
|
||||
}
|
|
@ -1,161 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window;
|
||||
|
||||
import ch.bailu.gtk.GTK;
|
||||
import ch.bailu.gtk.adw.Clamp;
|
||||
import ch.bailu.gtk.adw.StatusPage;
|
||||
import ch.bailu.gtk.bridge.ListIndex;
|
||||
import ch.bailu.gtk.gio.Menu;
|
||||
import ch.bailu.gtk.glib.Glib;
|
||||
import ch.bailu.gtk.gtk.*;
|
||||
import ch.bailu.gtk.type.Str;
|
||||
import io.gitlab.jfronny.inceptum.common.*;
|
||||
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar;
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.InstanceGridEntryFactory;
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.InstanceListEntryFactory;
|
||||
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder;
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance;
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.file.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import static java.nio.file.StandardWatchEventKinds.*;
|
||||
|
||||
public class MainWindow extends ApplicationWindow {
|
||||
private final Button listButton;
|
||||
private final Button gridButton;
|
||||
private final Stack stack;
|
||||
private final StatusPage empty;
|
||||
private final Clamp listView;
|
||||
private final GridView gridView;
|
||||
private final List<Instance> instanceList;
|
||||
private final ListIndex instanceListIndex;
|
||||
|
||||
public MainWindow(Application app) {
|
||||
super(app);
|
||||
|
||||
HeaderBar header = new HeaderBar();
|
||||
Button newButton = new Button();
|
||||
newButton.setIconName(new Str("list-add-symbolic"));
|
||||
newButton.onClicked(NewInstanceWindow::createAndShow);
|
||||
|
||||
MenuButton accountsButton = new MenuButton();
|
||||
accountsButton.setIconName(new Str("avatar-default-symbolic"));
|
||||
accountsButton.setPopover(GtkMenubar.accountsMenu.asPopover());
|
||||
|
||||
listButton = new Button();
|
||||
listButton.setIconName(new Str("view-list-symbolic"));
|
||||
listButton.onClicked(() -> {
|
||||
InceptumConfig.listView = true;
|
||||
InceptumConfig.saveConfig();
|
||||
generateWindowBody();
|
||||
});
|
||||
|
||||
gridButton = new Button();
|
||||
gridButton.setIconName(new Str("view-grid-symbolic"));
|
||||
gridButton.onClicked(() -> {
|
||||
InceptumConfig.listView = false;
|
||||
InceptumConfig.saveConfig();
|
||||
generateWindowBody();
|
||||
});
|
||||
|
||||
//TODO search button like boxes
|
||||
|
||||
MenuBuilder uiMenu = new MenuBuilder(app, new Menu(), "hamburger");
|
||||
uiMenu.button("support", () -> Utils.openWebBrowser(new URI("https://gitlab.com/jfmods/inceptum/-/issues")));
|
||||
uiMenu.button("preferences", () -> {}); //TODO preferences UI inspired by boxes
|
||||
uiMenu.button("about", AboutWindow::createAndShow);
|
||||
MenuButton menuButton = new MenuButton();
|
||||
menuButton.setIconName(new Str("open-menu-symbolic"));
|
||||
menuButton.setPopover(uiMenu.asPopover());
|
||||
|
||||
header.packStart(newButton);
|
||||
|
||||
header.packEnd(menuButton);
|
||||
header.packEnd(gridButton);
|
||||
header.packEnd(listButton);
|
||||
header.packEnd(accountsButton);
|
||||
|
||||
instanceList = new ArrayList<>();
|
||||
instanceListIndex = new ListIndex();
|
||||
|
||||
listView = new Clamp();
|
||||
listView.setMaximumSize(900);
|
||||
listView.setChild(new ListView(instanceListIndex.inSelectionModel(), new InstanceListEntryFactory(instanceList)));
|
||||
gridView = new GridView(instanceListIndex.inSelectionModel(), new InstanceGridEntryFactory(instanceList));
|
||||
empty = new StatusPage();
|
||||
empty.setTitle(I18n.str("main.empty.title"));
|
||||
empty.setDescription(I18n.str("main.empty.description"));
|
||||
//TODO empty.setIconName(new Str());
|
||||
stack = new Stack();
|
||||
stack.addChild(listView);
|
||||
stack.addChild(gridView);
|
||||
|
||||
ScrolledWindow scroll = new ScrolledWindow();
|
||||
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC);
|
||||
scroll.setChild(stack);
|
||||
|
||||
setDefaultSize(360, 720);
|
||||
setTitle(new Str("Inceptum"));
|
||||
setTitlebar(header);
|
||||
setShowMenubar(GTK.FALSE);
|
||||
setChild(scroll);
|
||||
|
||||
generateWindowBody();
|
||||
//TODO DropTarget to add mods/instances
|
||||
|
||||
try {
|
||||
setupDirWatcher();
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not set up watch service, live updates of the instance dir will be unavailable", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void setupDirWatcher() throws IOException { //TODO test (including after lock state change)
|
||||
WatchService ws = FileSystems.getDefault().newWatchService();
|
||||
MetaHolder.INSTANCE_DIR.register(ws, ENTRY_MODIFY, ENTRY_CREATE, ENTRY_DELETE);
|
||||
int source = Glib.idleAdd(user_data -> {
|
||||
//TODO watch instance dirs for locks
|
||||
WatchKey key = ws.poll();
|
||||
boolean instancesChanged = false;
|
||||
if (key != null) {
|
||||
for (WatchEvent<?> event : key.pollEvents()) {
|
||||
if (event.context() instanceof Path p) {
|
||||
p = MetaHolder.INSTANCE_DIR.resolve(p);
|
||||
instancesChanged |= Files.exists(p.resolve("instance.json"));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (instancesChanged) generateWindowBody();
|
||||
return GTK.TRUE;
|
||||
}, null);
|
||||
onCloseRequest(() -> {
|
||||
try {
|
||||
ws.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
Glib.sourceRemove(source);
|
||||
return GTK.FALSE;
|
||||
});
|
||||
}
|
||||
|
||||
private void generateWindowBody() {
|
||||
if (listButton != null) listButton.setVisible(GTK.is(!InceptumConfig.listView));
|
||||
if (gridButton != null) gridButton.setVisible(GTK.is(InceptumConfig.listView));
|
||||
try {
|
||||
instanceList.clear();
|
||||
instanceList.addAll(InstanceList.ordered());
|
||||
instanceListIndex.setSize(instanceList.size());
|
||||
|
||||
if (InstanceList.isEmpty()) stack.setVisibleChild(empty);
|
||||
else if (InceptumConfig.listView) stack.setVisibleChild(listView);
|
||||
else stack.setVisibleChild(gridView);
|
||||
} catch (IOException e) {
|
||||
Utils.LOGGER.error("Could not generate window body", e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window;
|
||||
|
||||
public class NewInstanceWindow {
|
||||
public static void createAndShow() {
|
||||
//TODO
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk
|
||||
|
||||
import io.gitlab.jfronny.commons.StringFormatter
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.dialog.StringInputDialog
|
||||
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv.EnvBackend
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount
|
||||
import org.gnome.gio.Cancellable
|
||||
import org.gnome.gtk.*
|
||||
import java.util.function.Consumer
|
||||
|
||||
object GtkEnvBackend : EnvBackend {
|
||||
var dialogParent: Window? = null
|
||||
|
||||
override fun showError(message: String, title: String) {
|
||||
Log.error(message)
|
||||
simpleDialog(message, title, null, null)
|
||||
}
|
||||
|
||||
override fun showError(message: String, t: Throwable) {
|
||||
simpleDialog(StringFormatter.toString(t), message, null, null)
|
||||
}
|
||||
|
||||
override fun showInfo(message: String, title: String) {
|
||||
Log.info(message)
|
||||
simpleDialog(message, title, null, null)
|
||||
}
|
||||
|
||||
override fun showOkCancel(message: String, title: String, ok: Runnable, cancel: Runnable, defaultCancel: Boolean) {
|
||||
Log.info(message)
|
||||
simpleDialog(message, title, ok, cancel)
|
||||
}
|
||||
|
||||
override fun getInput(
|
||||
prompt: String,
|
||||
details: String,
|
||||
defaultValue: String,
|
||||
ok: Consumer<String>,
|
||||
cancel: Runnable
|
||||
) = schedule {
|
||||
var flags = DialogFlags.DESTROY_WITH_PARENT
|
||||
if (dialogParent != null) flags = flags.or(DialogFlags.MODAL)
|
||||
val dialog = StringInputDialog(
|
||||
dialogParent,
|
||||
flags,
|
||||
MessageType.QUESTION,
|
||||
ButtonsType.OK_CANCEL,
|
||||
details,
|
||||
defaultValue
|
||||
)
|
||||
dialog.title = prompt
|
||||
dialog.onResponse(processResponses(dialog, { ok.accept(dialog.input) }, cancel))
|
||||
dialog.visible = true
|
||||
}
|
||||
|
||||
override fun showLoginRefreshPrompt(account: MicrosoftAccount) =
|
||||
schedule { MicrosoftLoginDialog(dialogParent, account).visible = true }
|
||||
|
||||
private fun simpleDialog(
|
||||
markup: String,
|
||||
title: String,
|
||||
ok: Runnable?,
|
||||
cancel: Runnable?
|
||||
) = schedule { simpleDialog(dialogParent, markup, title, ok, cancel) }
|
||||
|
||||
fun simpleDialog(
|
||||
parent: Window?,
|
||||
markup: String,
|
||||
title: String,
|
||||
ok: Runnable?,
|
||||
cancel: Runnable?
|
||||
) {
|
||||
val dialog = AlertDialog.builder()
|
||||
.setMessage(title)
|
||||
.setDetail(markup)
|
||||
.setModal(true)
|
||||
when {
|
||||
cancel == null -> dialog.setButtons(arrayOf(I18n["ok"]))
|
||||
.setDefaultButton(0)
|
||||
.setCancelButton(-1)
|
||||
ok == null -> dialog.setButtons(arrayOf("Cancel"))
|
||||
.setDefaultButton(-1)
|
||||
.setCancelButton(0)
|
||||
else -> dialog.setButtons(arrayOf("OK", "Cancel"))
|
||||
.setDefaultButton(0)
|
||||
.setCancelButton(1)
|
||||
}
|
||||
dialog.build().apply {
|
||||
choose(parent, Cancellable()) { _, res, _ ->
|
||||
val result = chooseFinish(res)
|
||||
val cancelIdx = cancelButton
|
||||
val defaultIdx = defaultButton
|
||||
if (result == cancelIdx) cancel?.run()
|
||||
if (result == defaultIdx) ok?.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun processResponses(dialog: Dialog, ok: Runnable?, cancel: Runnable?): Dialog.ResponseCallback = Dialog.ResponseCallback { responseId: Int ->
|
||||
when (ResponseType.of(responseId)) {
|
||||
ResponseType.OK -> {
|
||||
dialog.close()
|
||||
ok?.run()
|
||||
}
|
||||
|
||||
ResponseType.CLOSE, ResponseType.CANCEL -> {
|
||||
dialog.close()
|
||||
cancel?.run()
|
||||
}
|
||||
|
||||
ResponseType.DELETE_EVENT -> dialog.destroy()
|
||||
else -> Log.error("Unexpected response type: $responseId")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk
|
||||
|
||||
import io.gitlab.jfronny.inceptum.common.BuildMetadata
|
||||
import io.gitlab.jfronny.inceptum.common.MetaHolder
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.MainWindow
|
||||
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager
|
||||
import org.gnome.gio.ApplicationFlags
|
||||
import org.gnome.glib.GLib
|
||||
import org.gnome.gtk.*
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
object GtkMain {
|
||||
const val ID = "io.gitlab.jfronny.inceptum"
|
||||
@Throws(IOException::class)
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
LauncherEnv.initialize(GtkEnvBackend)
|
||||
Log.info("Launching Inceptum v" + BuildMetadata.VERSION)
|
||||
Log.info("Loading from " + MetaHolder.BASE_PATH)
|
||||
exitProcess(try {
|
||||
showGui(args)
|
||||
} catch (_: Throwable) {
|
||||
-1
|
||||
} finally {
|
||||
LauncherEnv.terminate()
|
||||
})
|
||||
}
|
||||
|
||||
fun showGui(args: Array<String>): Int = setupApplication(args) {
|
||||
//TODO update check
|
||||
AccountManager.loadAccounts()
|
||||
GtkMenubar.create(this)
|
||||
val window = MainWindow(this)
|
||||
window.visible = true
|
||||
GtkEnvBackend.dialogParent = window
|
||||
window.onCloseRequest {
|
||||
GtkEnvBackend.dialogParent = null
|
||||
this.quit()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun setupApplication(args: Array<String>, onActivate: Application.() -> Unit): Int {
|
||||
val app = Application(ID, ApplicationFlags.FLAGS_NONE)
|
||||
app.onActivate {
|
||||
GLib.idleAdd(GLib.PRIORITY_DEFAULT_IDLE) {
|
||||
runScheduledTasks()
|
||||
true
|
||||
}
|
||||
onActivate(app)
|
||||
}
|
||||
return app.run(args)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,215 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk
|
||||
|
||||
import io.gitlab.jfronny.commons.OSUtils
|
||||
import io.gitlab.jfronny.commons.io.JFiles
|
||||
import io.gitlab.jfronny.inceptum.common.MetaHolder
|
||||
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.AboutWindow
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.create.NewInstanceWindow
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.dialog.MicrosoftLoginDialog
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.settings.launcher.LauncherSettingsWindow
|
||||
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.importer.Importers
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.launch.InstanceLauncher
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps
|
||||
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
|
||||
import org.gnome.gio.Cancellable
|
||||
import org.gnome.gio.Menu
|
||||
import org.gnome.gtk.*
|
||||
import java.awt.Toolkit
|
||||
import java.awt.datatransfer.DataFlavor
|
||||
import java.io.IOException
|
||||
import java.nio.file.Path
|
||||
|
||||
object GtkMenubar {
|
||||
var newMenu: MenuBuilder? = null
|
||||
var accountsMenu: MenuBuilder? = null
|
||||
var launchMenu: MenuBuilder? = null
|
||||
|
||||
fun create(app: Application) {
|
||||
val menu = MenuBuilder(app, Menu()) // this should be MenuBuilder(app), but that breaks this
|
||||
val file = menu.submenu("file")
|
||||
newMenu = file.submenu("new")
|
||||
generateNewMenu(app)
|
||||
file.button("redownload") {
|
||||
val state = ProcessState(3 + Steps.STEPS.size * InstanceList.size(), "Initializing")
|
||||
ProcessStateWatcherDialog.show(
|
||||
GtkEnvBackend.dialogParent,
|
||||
"Reloading data",
|
||||
"Could not execute refresh task",
|
||||
state
|
||||
) {
|
||||
state.incrementStep("Clearing cache directories")
|
||||
JFiles.clearDirectory(MetaHolder.ASSETS_DIR)
|
||||
JFiles.clearDirectory(MetaHolder.LIBRARIES_DIR) { path: Path -> !path.startsWith(MetaHolder.LIBRARIES_DIR.resolve("io/gitlab/jfronny")) }
|
||||
JFiles.clearDirectory(MetaHolder.NATIVES_DIR) { path: Path -> !path.startsWith(MetaHolder.NATIVES_DIR.resolve("forceload")) }
|
||||
JFiles.clearDirectory(MetaHolder.CACHE_DIR)
|
||||
if (state.isCancelled) return@show
|
||||
state.incrementStep("Reloading instance list")
|
||||
InstanceList.reset()
|
||||
InstanceList.forEach<IOException> { instance: Instance? ->
|
||||
if (state.isCancelled) return@forEach
|
||||
Steps.reDownload(instance, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
file.button("exit") { app.quit() }
|
||||
launchMenu = menu.submenu("launch")
|
||||
generateLaunchMenu(app)
|
||||
accountsMenu = menu.submenu("account")
|
||||
generateAccountsMenu(app)
|
||||
val help = menu.submenu("help")
|
||||
help.button("about") { AboutWindow.createAndShow() }
|
||||
help.button("log") {
|
||||
//TODO
|
||||
}
|
||||
}
|
||||
|
||||
fun generateNewMenu(app: Application) {
|
||||
newMenu!!.clear()
|
||||
newMenu!!.button("new") { NewInstanceWindow(app).visible = true }
|
||||
newMenu!!.button("file") {
|
||||
val dialog = FileChooserNative(
|
||||
I18n["menu.file.new.file"],
|
||||
GtkEnvBackend.dialogParent,
|
||||
FileChooserAction.OPEN,
|
||||
"_" + I18n["select"],
|
||||
"_" + I18n["cancel"]
|
||||
)
|
||||
val filter = FileFilter()
|
||||
filter.addPattern("*.zip")
|
||||
filter.addPattern("*.mrpack")
|
||||
dialog.addFilter(filter)
|
||||
dialog.onResponse { responseId: Int ->
|
||||
if (responseId == ResponseType.ACCEPT.value) {
|
||||
val file = dialog.file!!.path
|
||||
if (file == null) {
|
||||
LauncherEnv.showError("The path returned by the file dialog is null", "Could not import")
|
||||
return@onResponse
|
||||
}
|
||||
val state = ProcessState(Importers.MAX_STEPS, "Initializing")
|
||||
ProcessStateWatcherDialog.show(
|
||||
GtkEnvBackend.dialogParent,
|
||||
I18n["menu.file.new.file"],
|
||||
I18n["menu.file.new.file.error"],
|
||||
state
|
||||
) {
|
||||
Importers.importPack(Path.of(file), state)
|
||||
}
|
||||
}
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
newMenu!!.button("url") {
|
||||
readClipboard { clipboard ->
|
||||
LauncherEnv.getInput(
|
||||
I18n["menu.file.new.url"],
|
||||
I18n["menu.file.new.url.details"],
|
||||
clipboard ?: "",
|
||||
{ s: String? ->
|
||||
val state = ProcessState(Importers.MAX_STEPS, "Initializing")
|
||||
ProcessStateWatcherDialog.show(
|
||||
GtkEnvBackend.dialogParent,
|
||||
I18n["menu.file.new.url"],
|
||||
I18n["menu.file.new.url.error"],
|
||||
state
|
||||
) {
|
||||
Importers.importPack(s, state)
|
||||
}
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun readClipboard(continuation: (String?) -> Unit) {
|
||||
if (OSUtils.TYPE == OSUtils.Type.LINUX) {
|
||||
val clipboard = GtkEnvBackend.dialogParent!!.display.clipboard
|
||||
clipboard.readTextAsync(Cancellable()) { _, res, _ ->
|
||||
continuation(clipboard.readTextFinish(res))
|
||||
}
|
||||
} else {
|
||||
continuation(Toolkit.getDefaultToolkit().systemClipboard.getData(DataFlavor.stringFlavor) as String?)
|
||||
}
|
||||
}
|
||||
|
||||
fun generateLaunchMenu(app: Application) {
|
||||
launchMenu!!.clear()
|
||||
try {
|
||||
InstanceList.forEach<RuntimeException> { entry: Instance ->
|
||||
launchMenu!!.literalButton(entry.id + ".launch", entry.toString()) {
|
||||
launch(entry, LaunchType.Client)
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.error("Could not generate launch menu", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun launch(instance: Instance, launchType: LaunchType) {
|
||||
if (instance.isSetupLocked) {
|
||||
LauncherEnv.showError(I18n["instance.launch.locked.setup"], I18n["instance.launch.locked"])
|
||||
} else if (instance.isRunningLocked) {
|
||||
LauncherEnv.showOkCancel(
|
||||
I18n["instance.launch.locked.running"],
|
||||
I18n["instance.launch.locked"]
|
||||
) { forceLaunch(instance, launchType) }
|
||||
} else forceLaunch(instance, launchType)
|
||||
}
|
||||
|
||||
private fun forceLaunch(instance: Instance, launchType: LaunchType) {
|
||||
val state = Steps.createProcessState()
|
||||
ProcessStateWatcherDialog.show(
|
||||
GtkEnvBackend.dialogParent,
|
||||
I18n["instance.launch.title"],
|
||||
I18n["instance.launch.error"],
|
||||
state
|
||||
) {
|
||||
try {
|
||||
Steps.reDownload(instance, state)
|
||||
} catch (e: IOException) {
|
||||
Log.error("Could not fetch instance, trying to start anyways", e)
|
||||
}
|
||||
if (state.isCancelled) return@show
|
||||
state.updateStep("Starting Game")
|
||||
try {
|
||||
if (launchType == LaunchType.Client) InstanceLauncher.launchClient(instance)
|
||||
else InstanceLauncher.launch(
|
||||
instance,
|
||||
launchType,
|
||||
false,
|
||||
AccountManager.NULL_AUTH
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
LauncherEnv.showError("Could not start instance", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun generateAccountsMenu(app: Application) {
|
||||
accountsMenu!!.clear()
|
||||
accountsMenu!!.button("new") { MicrosoftLoginDialog(GtkEnvBackend.dialogParent).visible = true }
|
||||
accountsMenu!!.button("manage") {
|
||||
val window = LauncherSettingsWindow(app)
|
||||
window.activePage = "settings.accounts"
|
||||
window.visible = true
|
||||
}
|
||||
val accounts: MutableList<MicrosoftAccount?> = ArrayList(AccountManager.getAccounts())
|
||||
accounts.add(null)
|
||||
accountsMenu!!.literalRadio(
|
||||
"account",
|
||||
accounts[AccountManager.getSelectedIndex()],
|
||||
accounts,
|
||||
{ _, acc: MicrosoftAccount? ->
|
||||
if (acc == null) return@literalRadio I18n["account.none"]
|
||||
acc.minecraftUsername
|
||||
}) { account: MicrosoftAccount? -> AccountManager.switchAccount(account) }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import java.util.*
|
||||
|
||||
private val SCHEDULED: Queue<Runnable> = ArrayDeque()
|
||||
|
||||
fun schedule(task: Runnable) {
|
||||
SCHEDULED.add(task)
|
||||
}
|
||||
|
||||
fun runScheduledTasks() {
|
||||
var r: Runnable?
|
||||
while (SCHEDULED.poll().also { r = it } != null) {
|
||||
try {
|
||||
r!!.run()
|
||||
} catch (t: Throwable) {
|
||||
Log.error("Could not run scheduled task", t)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.GtkMain
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import org.gnome.gtk.CssProvider
|
||||
import org.gnome.gtk.Gtk
|
||||
import org.gnome.gtk.Label
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
|
||||
class ILabel(
|
||||
str: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
|
||||
mode: Mode,
|
||||
vararg args: Any?
|
||||
) : Label(I18n.get(str, *args)) {
|
||||
constructor(str: @PropertyKey(resourceBundle = I18n.BUNDLE) String, vararg args: Any?) : this(str, Mode.NORMAL, *args)
|
||||
|
||||
init {
|
||||
theme(this, mode)
|
||||
}
|
||||
|
||||
enum class Mode {
|
||||
NORMAL,
|
||||
HEADING,
|
||||
SUBTITLE
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val provider by lazy {
|
||||
val provider = CssProvider()
|
||||
try {
|
||||
GtkMain::class.java.classLoader.getResourceAsStream("inceptum.css")!!
|
||||
.use {
|
||||
val bytes = it.readAllBytes()
|
||||
provider.loadFromData(String(bytes), bytes.size.toLong())
|
||||
}
|
||||
} catch (t: Throwable) {
|
||||
throw RuntimeException(t)
|
||||
}
|
||||
provider
|
||||
}
|
||||
|
||||
fun theme(label: Label, mode: Mode) = when (mode) {
|
||||
Mode.HEADING -> label.addCssClass("heading")
|
||||
Mode.SUBTITLE -> {
|
||||
label.addCssClass("jf-subtitle")
|
||||
label.styleContext.addProvider(provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
|
||||
}
|
||||
|
||||
Mode.NORMAL -> {}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.get
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
|
||||
import org.gnome.gtk.*
|
||||
import org.gnome.pango.EllipsizeMode
|
||||
import org.gnome.pango.WrapMode
|
||||
|
||||
class InstanceGridEntryFactory(
|
||||
private val instanceList: List<Instance>
|
||||
) : KSignalListItemFactory<InstanceGridEntryFactory.Decomposed, Box>() {
|
||||
//TODO better design
|
||||
override fun setup(): Box {
|
||||
val box = Box(Orientation.VERTICAL, 5)
|
||||
|
||||
val thumbnail = InstanceThumbnail()
|
||||
box.append(thumbnail)
|
||||
|
||||
val label = Label(null as String?)
|
||||
label.setSizeRequest(192, -1)
|
||||
label.maxWidthChars = 20
|
||||
label.justify = Justification.CENTER
|
||||
label.halign = Align.START
|
||||
label.hexpand = true
|
||||
label.valign = Align.CENTER
|
||||
label.ellipsize = EllipsizeMode.MIDDLE
|
||||
label.lines = 3
|
||||
label.wrap = true
|
||||
label.wrapMode = WrapMode.WORD_CHAR
|
||||
label.marginTop = 10
|
||||
box.append(label)
|
||||
// Label label = new Label(Str.NULL);
|
||||
// label.setXalign(0);
|
||||
// label.setWidthChars(20);
|
||||
// label.setMarginEnd(10);
|
||||
// box.append(label);
|
||||
//
|
||||
// Button launch = new Button();
|
||||
// launch.setIconName(new Str("computer-symbolic"));
|
||||
// launch.setTooltipText(I18n.str("instance.launch"));
|
||||
// launch.setHasTooltip(GTK.TRUE);
|
||||
// box.append(launch);
|
||||
//
|
||||
// Button openDir = new Button();
|
||||
// openDir.setIconName(new Str("folder-symbolic"));
|
||||
// openDir.setTooltipText(I18n.str("instance.directory"));
|
||||
// openDir.setHasTooltip(GTK.TRUE);
|
||||
// box.append(openDir);
|
||||
//TODO server launch with network-server-symbolic
|
||||
//TODO kill current instance
|
||||
return box
|
||||
}
|
||||
|
||||
override fun BindContext.bind(widget: Box, data: Decomposed) {
|
||||
// Label label = new Label(item.getChild().getFirstChild().cast());
|
||||
// Button launch = new Button(label.getNextSibling().cast());
|
||||
// Button openDir = new Button(launch.getNextSibling().cast());
|
||||
// InstanceList.Entry instance = instanceList.get(ListIndex.toIndex(item));
|
||||
// label.setText(new Str(instance.toString()));
|
||||
// launch.onClicked(() -> GtkMenubar.launch(instance));
|
||||
// openDir.onClicked(() -> Utils.openFile(instance.path().toFile()));
|
||||
val thumbnail = InstanceThumbnail.castFrom(widget.firstChild as Stack)
|
||||
val label = thumbnail.nextSibling as Label
|
||||
|
||||
val instance = instanceList[data.id]
|
||||
thumbnail.bind(instance!!)
|
||||
label.text = instance.toString()
|
||||
}
|
||||
|
||||
override fun UnbindContext.unbind(widget: Box, data: Decomposed) {
|
||||
}
|
||||
|
||||
override fun ActionContext.castWidget(widget: Widget): Box = widget as Box
|
||||
override fun ActionContext.lookup(id: String, widget: Box): Decomposed = Decomposed(id)
|
||||
|
||||
class Decomposed(val id: String)
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import io.gitlab.jfronny.commons.io.JFiles
|
||||
import io.gitlab.jfronny.commons.ref.R
|
||||
import io.gitlab.jfronny.inceptum.common.MetaHolder
|
||||
import io.gitlab.jfronny.inceptum.common.Utils
|
||||
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar
|
||||
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.fixSubtitle
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.get
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.margin
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.settings.instance.InstanceSettingsWindow
|
||||
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceNameTool
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
|
||||
import org.gnome.adw.ActionRow
|
||||
import org.gnome.gdk.Gdk
|
||||
import org.gnome.gio.Menu
|
||||
import org.gnome.gtk.*
|
||||
import java.io.IOException
|
||||
|
||||
class InstanceListEntryFactory(
|
||||
private val app: Application?,
|
||||
private val instanceList: List<Instance>
|
||||
) : KSignalListItemFactory<InstanceListEntryFactory.Decomposed, ActionRow>() {
|
||||
override fun setup(): ActionRow {
|
||||
val thumbnail = InstanceThumbnail()
|
||||
thumbnail.name = "inceptum-thumbnail"
|
||||
|
||||
val launch = Button.fromIconName("media-playback-start-symbolic")
|
||||
launch.addCssClass("flat")
|
||||
launch.name = "inceptum-launch"
|
||||
launch.tooltipText = I18n["instance.launch"]
|
||||
|
||||
val menu = MenuButton()
|
||||
menu.addCssClass("flat")
|
||||
menu.iconName = "view-more-symbolic"
|
||||
menu.setPopover(PopoverMenu.fromModel(Menu()))
|
||||
|
||||
val row = ActionRow()
|
||||
row.margin = 8
|
||||
row.name = "inceptum-row"
|
||||
row.removeCssClass("activatable") //TODO remove this workaround if a better way to support opening the menu is found
|
||||
row.addPrefix(thumbnail)
|
||||
row.addSuffix(launch)
|
||||
row.addSuffix(menu)
|
||||
row.fixSubtitle()
|
||||
|
||||
val rightClicked = GestureClick()
|
||||
rightClicked.button = Gdk.BUTTON_SECONDARY
|
||||
rightClicked.onPressed { nPress, _, _ -> if (nPress == 1) menu.emitActivate() }
|
||||
row.addController(rightClicked)
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
override fun BindContext.bind(widget: ActionRow, data: Decomposed) {
|
||||
if (data.instance?.isLocked ?: true) {
|
||||
data.item.activatable = false
|
||||
widget.subtitle = if (data.instance?.isRunningLocked ?: false) I18n["instance.launch.locked.running"]
|
||||
else I18n["instance.launch.locked.setup"]
|
||||
}
|
||||
|
||||
widget.title = data.instance.toString()
|
||||
|
||||
data.thumbnail.bind(data.instance!!)
|
||||
|
||||
val menuBuilder = MenuBuilder(data.popoverMenu, data.instanceId)
|
||||
val launchSection = menuBuilder.literalSection("launch", null)
|
||||
val kill = launchSection.literalButton("kill", I18n["instance.kill"]) {
|
||||
//TODO test
|
||||
LauncherEnv.showOkCancel(I18n["instance.kill.prompt"], I18n["instance.kill.details"]) {
|
||||
if (!data.instance.kill()) LauncherEnv.showError(I18n["instance.kill.fail"], I18n["failed"])
|
||||
}
|
||||
}
|
||||
kill.enabled = data.instance.isRunningLocked
|
||||
|
||||
launchSection.literalButton(
|
||||
"launch.client", I18n["instance.launch.client"]
|
||||
) { GtkMenubar.launch(data.instance, LaunchType.Client) }.iconName = "media-playback-start-symbolic"
|
||||
launchSection.literalButton(
|
||||
"launch.server", I18n["instance.launch.server"]
|
||||
) { GtkMenubar.launch(data.instance, LaunchType.Server) }.iconName = "network-server-symbolic"
|
||||
val settingsSection = menuBuilder.literalSection("settings", null)
|
||||
settingsSection.literalButton("settings", I18n["instance.settings"]) {
|
||||
//TODO keep track of properties windows and don't allow opening two
|
||||
InstanceSettingsWindow(app, data.instance).visible = true
|
||||
}.iconName = "document-edit-symbolic"
|
||||
settingsSection.literalButton(
|
||||
"directory", I18n["instance.directory"]
|
||||
) { Utils.openFile(data.instance.path.toFile()) }.iconName = "folder-symbolic"
|
||||
settingsSection.literalButton("copy", I18n["instance.copy"]) {
|
||||
LauncherEnv.getInput(
|
||||
I18n["instance.copy.prompt"],
|
||||
I18n["instance.copy.details"],
|
||||
InstanceNameTool.getNextValid(data.instance.name),
|
||||
{ s: String? ->
|
||||
try {
|
||||
JFiles.copyRecursive(
|
||||
data.instance.path,
|
||||
MetaHolder.INSTANCE_DIR.resolve(InstanceNameTool.getNextValid(s))
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
LauncherEnv.showError(I18n["instance.copy.fail"], e)
|
||||
}
|
||||
}) { R.nop() }
|
||||
}.iconName = "edit-copy-symbolic"
|
||||
settingsSection.literalButton("delete", I18n["instance.delete"]) {
|
||||
LauncherEnv.showOkCancel(
|
||||
I18n["instance.delete.confirm"],
|
||||
I18n["instance.delete.confirm.title"]
|
||||
) {
|
||||
try {
|
||||
JFiles.deleteRecursive(data.instance.path)
|
||||
} catch (e: IOException) {
|
||||
LauncherEnv.showError(I18n["instance.delete.fail"], e)
|
||||
}
|
||||
}
|
||||
}.iconName = "edit-delete-symbolic"
|
||||
|
||||
registerForUnbind(data.launch.onClicked { GtkMenubar.launch(data.instance, LaunchType.Client) })
|
||||
}
|
||||
|
||||
override fun UnbindContext.unbind(widget: ActionRow, data: Decomposed) {
|
||||
data.popoverMenu.insertActionGroup(data.instanceId, null)
|
||||
}
|
||||
|
||||
override fun ActionContext.castWidget(widget: Widget): ActionRow = widget as ActionRow
|
||||
|
||||
override fun ActionContext.lookup(id: String, widget: ActionRow): Decomposed {
|
||||
val instance = instanceList[id]
|
||||
val prefixes = widget.firstChild!!.firstChild as Box
|
||||
val suffixes = widget.firstChild!!.lastChild as Box
|
||||
val thumbnail = InstanceThumbnail.castFrom(prefixes.firstChild as Stack)
|
||||
val launch = suffixes.firstChild as Button
|
||||
val menuButton = launch.nextSibling as MenuButton
|
||||
val popoverMenu = menuButton.popover as PopoverMenu
|
||||
return Decomposed(listItem, id, instance, thumbnail, launch, popoverMenu)
|
||||
}
|
||||
|
||||
class Decomposed(
|
||||
val item: ListItem,
|
||||
val instanceId: String,
|
||||
val instance: Instance?,
|
||||
val thumbnail: InstanceThumbnail,
|
||||
val launch: Button,
|
||||
val popoverMenu: PopoverMenu
|
||||
)
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
|
||||
import org.gnome.gtk.Image
|
||||
import org.gnome.gtk.Spinner
|
||||
import org.gnome.gtk.Stack
|
||||
import java.lang.foreign.MemorySegment
|
||||
|
||||
class InstanceThumbnail : Stack {
|
||||
private constructor(address: MemorySegment) : super(address)
|
||||
|
||||
constructor() : super() {
|
||||
val spinner = Spinner()
|
||||
val image = Image()
|
||||
val generic = Image()
|
||||
spinner.name = SPINNER
|
||||
image.name = IMAGE
|
||||
generic.name = GENERIC
|
||||
generic.setFromIconName("media-playback-start-symbolic") //TODO better default icon
|
||||
addNamed(spinner, SPINNER)
|
||||
addNamed(image, IMAGE)
|
||||
addNamed(generic, GENERIC)
|
||||
}
|
||||
|
||||
fun bind(entry: Instance) {
|
||||
val spinner = getChildByName(SPINNER) as Spinner
|
||||
val image = getChildByName(IMAGE) as Image //TODO
|
||||
val generic = getChildByName(GENERIC) as Image
|
||||
//TODO mark instance being played
|
||||
visibleChild = if (entry.isSetupLocked) {
|
||||
spinner
|
||||
} else if (false) { // if the instance has an image, load the image data and set it as the visible child
|
||||
image
|
||||
} else {
|
||||
generic
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val SPINNER = "spinner"
|
||||
private const val IMAGE = "image"
|
||||
private const val GENERIC = "generic"
|
||||
|
||||
fun castFrom(stack: Stack): InstanceThumbnail = InstanceThumbnail(stack.handle())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import org.gnome.gtk.DropDown
|
||||
import org.gnome.gtk.PropertyExpression
|
||||
import org.gnome.gtk.StringList
|
||||
import org.gnome.gtk.StringObject
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import java.util.function.IntConsumer
|
||||
|
||||
class KDropDown<T>(options: Array<T>, private val stringify: (T) -> String, selected: Int): DropDown(options.toModel(stringify), null) {
|
||||
private val onChange = ArrayList<IntConsumer>()
|
||||
|
||||
init {
|
||||
this.selected = selected
|
||||
onNotify("selected") { _ -> onChange.forEach { it.accept(this.selected) } }
|
||||
expression = PropertyExpression(StringObject.getType(), null, "string")
|
||||
}
|
||||
|
||||
fun onChange(changed: IntConsumer) {
|
||||
onChange.add(changed)
|
||||
}
|
||||
|
||||
fun updateOptions(newOptions: Array<T>, newSelected: Int) {
|
||||
selected = 0
|
||||
model = newOptions.toModel(stringify)
|
||||
selected = newSelected
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun <T> Array<T>.toModel(stringify: (T) -> String) = StringList(map(stringify).toTypedArray())
|
||||
}
|
||||
}
|
||||
|
||||
fun KDropDown(vararg options: @PropertyKey(resourceBundle = I18n.BUNDLE) String, selected: Int = 0): KDropDown<String> =
|
||||
KDropDown(options.map { it }.toTypedArray(), { I18n[it] }, selected)
|
|
@ -0,0 +1,17 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import org.gnome.gtk.Entry
|
||||
import java.util.function.Consumer
|
||||
|
||||
class KEntry(value: String? = ""): Entry() {
|
||||
private val onChange = ArrayList<Consumer<String>>()
|
||||
|
||||
init {
|
||||
text = value ?: ""
|
||||
onChanged { onChange.forEach { it.accept(text) } }
|
||||
}
|
||||
|
||||
fun onChange(changed: Consumer<String>) {
|
||||
onChange.add(changed)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import io.github.jwharm.javagi.gobject.SignalConnection
|
||||
import org.gnome.gtk.ListItem
|
||||
import org.gnome.gtk.SignalListItemFactory
|
||||
import org.gnome.gtk.StringObject
|
||||
import org.gnome.gtk.Widget
|
||||
|
||||
abstract class KSignalListItemFactory<TData, TWidget : Widget> : SignalListItemFactory() {
|
||||
private val toDisconnect: MutableMap<String, MutableSet<SignalConnection<*>>> = HashMap()
|
||||
init {
|
||||
onSetup {
|
||||
val li = it as ListItem
|
||||
li.child = setup()
|
||||
}
|
||||
onBind {
|
||||
val li = it as ListItem
|
||||
val id = (li.item as StringObject).string
|
||||
val context = BindContextImpl(id, li)
|
||||
val widget = context.castWidget(li.child!!)
|
||||
context.bind(widget, context.lookup(id, widget))
|
||||
}
|
||||
onUnbind {
|
||||
val li = it as ListItem
|
||||
val id = (li.item as StringObject).string
|
||||
val context = UnbindContextImpl(li)
|
||||
val widget = context.castWidget(li.child!!)
|
||||
toDisconnect.remove(id)?.forEach { it.disconnect() }
|
||||
context.unbind(widget, context.lookup(id, widget))
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun setup(): TWidget
|
||||
abstract fun BindContext.bind(widget: TWidget, data: TData)
|
||||
abstract fun UnbindContext.unbind(widget: TWidget, data: TData)
|
||||
abstract fun ActionContext.lookup(id: String, widget: TWidget): TData
|
||||
abstract fun ActionContext.castWidget(widget: Widget): TWidget
|
||||
|
||||
interface ActionContext {
|
||||
val listItem: ListItem
|
||||
}
|
||||
|
||||
interface BindContext: ActionContext {
|
||||
fun registerForUnbind(signal: SignalConnection<*>)
|
||||
}
|
||||
|
||||
interface UnbindContext: ActionContext {
|
||||
}
|
||||
|
||||
private inner class BindContextImpl(private val id: String, override val listItem: ListItem) : BindContext {
|
||||
override fun registerForUnbind(signal: SignalConnection<*>) {
|
||||
toDisconnect.computeIfAbsent(id) { _ -> HashSet() }
|
||||
.add(signal)
|
||||
}
|
||||
}
|
||||
|
||||
private inner class UnbindContextImpl(override val listItem: ListItem): UnbindContext
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control
|
||||
|
||||
import org.gnome.gtk.Align
|
||||
import org.gnome.gtk.ProgressBar
|
||||
import org.gnome.gtk.Revealer
|
||||
import org.gnome.gtk.RevealerTransitionType
|
||||
|
||||
class LoadingRevealer: Revealer() {
|
||||
private val progressBar: ProgressBar
|
||||
|
||||
init {
|
||||
transitionType = RevealerTransitionType.CROSSFADE
|
||||
revealChild = false
|
||||
vexpand = false
|
||||
hexpand = false
|
||||
valign = Align.CENTER
|
||||
halign = Align.FILL
|
||||
progressBar = ProgressBar().apply {
|
||||
hexpand = true
|
||||
vexpand = false
|
||||
addCssClass("osd")
|
||||
}
|
||||
child = progressBar
|
||||
}
|
||||
|
||||
fun setRunning(state: Boolean) {
|
||||
revealChild = state
|
||||
}
|
||||
|
||||
fun setProgress(progress: Double) {
|
||||
progressBar.fraction = progress.coerceIn(0.0, 1.0)
|
||||
}
|
||||
|
||||
fun pulse() {
|
||||
progressBar.pulse()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control.assistant
|
||||
|
||||
import org.gnome.gtk.AssistantPageType
|
||||
import org.gnome.gtk.Box
|
||||
import org.gnome.gtk.Orientation
|
||||
|
||||
class AssistantPage(val title: String, val type: AssistantPageType): Box(Orientation.VERTICAL, 8) {
|
||||
lateinit var setComplete: (Boolean) -> Unit
|
||||
private val onOpen = ArrayList<Runnable>()
|
||||
fun onOpen(action: Runnable) {
|
||||
onOpen.add(action)
|
||||
}
|
||||
|
||||
fun emitOpen() = onOpen.forEach { it.run() }
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control.assistant
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import org.gnome.gtk.Application
|
||||
import org.gnome.gtk.Assistant
|
||||
import org.gnome.gtk.AssistantPageType
|
||||
|
||||
open class KAssistant(app: Application) : Assistant() {
|
||||
private val pages = ArrayList<AssistantPage>()
|
||||
|
||||
init {
|
||||
application = app
|
||||
onPrepare { next ->
|
||||
val page = pages.firstOrNull { it.handle() == next.handle() }
|
||||
if (page == null) {
|
||||
Log.error("Unknown page opened in assistant")
|
||||
} else {
|
||||
page.emitOpen()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun page(title: String, type: AssistantPageType, setup: AssistantPage.() -> Unit): Int {
|
||||
val page = AssistantPage(title, type)
|
||||
val idx = appendPage(page)
|
||||
pages.add(page)
|
||||
page.setComplete = { setPageComplete(page, it) }
|
||||
page.setup()
|
||||
setPageType(page, page.type)
|
||||
setPageTitle(page, page.title)
|
||||
return idx
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control.settings
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.KDropDown
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.KEntry
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.margin
|
||||
import org.gnome.gtk.*
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import java.util.function.Consumer
|
||||
import java.util.function.DoubleConsumer
|
||||
import java.util.function.IntConsumer
|
||||
|
||||
class IRow(
|
||||
title: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
|
||||
subtitle: @PropertyKey(resourceBundle = I18n.BUNDLE) String?,
|
||||
vararg args: Any?
|
||||
) : Box(Orientation.HORIZONTAL, 40) {
|
||||
init {
|
||||
margin = 8
|
||||
val head: Widget
|
||||
val lab = ILabel(title, *args)
|
||||
lab.halign = Align.START
|
||||
if (subtitle != null) {
|
||||
val headB = Box(Orientation.VERTICAL, 0)
|
||||
headB.append(lab)
|
||||
val lab1 = ILabel(subtitle, ILabel.Mode.SUBTITLE, *args)
|
||||
lab1.halign = Align.START
|
||||
headB.append(lab1)
|
||||
head = headB
|
||||
} else {
|
||||
head = lab
|
||||
}
|
||||
head.halign = Align.START
|
||||
head.valign = Align.CENTER
|
||||
append(head)
|
||||
}
|
||||
|
||||
fun setButton(text: @PropertyKey(resourceBundle = I18n.BUNDLE) String, action: Button.ClickedCallback?): Button =
|
||||
Button.withLabel(I18n[text]).apply {
|
||||
packSmallEnd()
|
||||
onClicked(action)
|
||||
}
|
||||
|
||||
fun setDropdown(options: Array<String>, defaultIndex: Int, changed: IntConsumer): KDropDown<String> =
|
||||
KDropDown(options, { it } , defaultIndex).apply {
|
||||
onChange(changed)
|
||||
packSmallEnd()
|
||||
}
|
||||
|
||||
fun setSwitch(value: Boolean, changed: Consumer<Boolean>): Switch =
|
||||
Switch().apply {
|
||||
packSmallEnd()
|
||||
active = value
|
||||
onStateSet { state: Boolean ->
|
||||
changed.accept(state)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun setSpinButton(value: Double, min: Double, max: Double, step: Double, changed: DoubleConsumer): SpinButton =
|
||||
SpinButton.withRange(min, max, step).apply {
|
||||
packSmallEnd()
|
||||
this.value = value
|
||||
onValueChanged { changed.accept(this.value) }
|
||||
}
|
||||
|
||||
fun setEntry(value: String?, changed: Consumer<String>): KEntry =
|
||||
KEntry(value).apply {
|
||||
hexpand = true
|
||||
valign = Align.CENTER
|
||||
halign = Align.FILL
|
||||
onChange(changed)
|
||||
append(this)
|
||||
}
|
||||
|
||||
private fun Widget.packSmallEnd() {
|
||||
firstChild!!.hexpand = true
|
||||
valign = Align.CENTER
|
||||
halign = Align.END
|
||||
this@IRow.append(this)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control.settings
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.marginHorizontal
|
||||
import org.gnome.gtk.*
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
open class SectionedSettingsTab<W : Window>(window: W?): SettingsTab<Box, W>(window, Box(Orientation.VERTICAL, 8)) {
|
||||
init {
|
||||
content.marginHorizontal = 24
|
||||
content.marginTop = 12
|
||||
}
|
||||
|
||||
fun append(child: Widget) = content.append(child)
|
||||
|
||||
protected fun section(title: @PropertyKey(resourceBundle = I18n.BUNDLE) String?, builder: Section.() -> Unit) {
|
||||
if (title != null) append(ILabel(title, ILabel.Mode.HEADING))
|
||||
val frame = Frame(null as String?)
|
||||
val listBox = ListBox()
|
||||
listBox.selectionMode = SelectionMode.NONE
|
||||
listBox.showSeparators = true
|
||||
frame.child = listBox
|
||||
val count = AtomicInteger(0)
|
||||
builder(object : Section {
|
||||
override fun row(title: String, subtitle: String?, vararg args: Any?, build: IRow.() -> Unit): IRow {
|
||||
val row = IRow(title, subtitle, *args)
|
||||
row(row)
|
||||
build(row)
|
||||
return row
|
||||
}
|
||||
|
||||
override fun row(row: Widget): ListBoxRow {
|
||||
listBox.append(row)
|
||||
return listBox.getRowAtIndex(count.getAndIncrement())!!
|
||||
}
|
||||
|
||||
override fun remove(row: Widget) {
|
||||
listBox.remove(row)
|
||||
count.decrementAndGet()
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
var i = 0
|
||||
val len = count.getAndSet(0)
|
||||
while (i < len) {
|
||||
listBox.remove(listBox.getRowAtIndex(0))
|
||||
i++
|
||||
}
|
||||
}
|
||||
})
|
||||
append(frame)
|
||||
}
|
||||
|
||||
interface Section {
|
||||
fun row(
|
||||
title: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
|
||||
subtitle: @PropertyKey(resourceBundle = I18n.BUNDLE) String?,
|
||||
vararg args: Any?
|
||||
): IRow = row(title, subtitle, *args, build = {})
|
||||
|
||||
fun row(
|
||||
title: @PropertyKey(resourceBundle = I18n.BUNDLE) String,
|
||||
subtitle: @PropertyKey(resourceBundle = I18n.BUNDLE) String?,
|
||||
vararg args: Any?,
|
||||
build: IRow.() -> Unit
|
||||
): IRow
|
||||
|
||||
fun row(row: Widget): ListBoxRow?
|
||||
fun remove(row: Widget)
|
||||
fun clear()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control.settings
|
||||
|
||||
import io.gitlab.jfronny.commons.StringFormatter
|
||||
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend
|
||||
import org.gnome.gtk.Widget
|
||||
import org.gnome.gtk.Window
|
||||
|
||||
open class SettingsTab<T : Widget, W : Window>(
|
||||
protected val window: W?,
|
||||
val content: T
|
||||
) {
|
||||
protected fun showError(message: String, t: Throwable) =
|
||||
GtkEnvBackend.simpleDialog(
|
||||
window,
|
||||
StringFormatter.toString(t),
|
||||
message,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.control.settings
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import org.gnome.adw.HeaderBar
|
||||
import org.gnome.adw.ViewStack
|
||||
import org.gnome.adw.ViewSwitcherBar
|
||||
import org.gnome.adw.ViewSwitcherTitle
|
||||
import org.gnome.gobject.BindingFlags
|
||||
import org.gnome.gtk.*
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
|
||||
open class SettingsWindow(app: Application?) : Window() {
|
||||
protected val stack: ViewStack
|
||||
|
||||
init {
|
||||
application = app
|
||||
|
||||
stack = ViewStack()
|
||||
|
||||
val header = HeaderBar()
|
||||
val viewSwitcher = ViewSwitcherTitle()
|
||||
viewSwitcher.stack = stack
|
||||
header.titleWidget = viewSwitcher
|
||||
titlebar = header
|
||||
|
||||
val scroll = ScrolledWindow()
|
||||
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC)
|
||||
scroll.child = stack
|
||||
scroll.vexpand = true
|
||||
|
||||
val bottomBar = ViewSwitcherBar()
|
||||
bottomBar.stack = stack
|
||||
viewSwitcher.bindProperty("title-visible", bottomBar, "reveal", BindingFlags.DEFAULT)
|
||||
val view = Box(Orientation.VERTICAL, 0)
|
||||
view.append(scroll)
|
||||
view.append(bottomBar)
|
||||
|
||||
child = view
|
||||
|
||||
setDefaultSize(720, 360)
|
||||
}
|
||||
|
||||
fun addTab(tab: SettingsTab<*, *>, title: @PropertyKey(resourceBundle = I18n.BUNDLE) String, iconName: String) {
|
||||
stack.addTitledWithIcon(tab.content, title, I18n[title], iconName)
|
||||
}
|
||||
|
||||
var activePage: String
|
||||
get() = stack.visibleChildName!!
|
||||
set(@PropertyKey(resourceBundle = I18n.BUNDLE) title) { stack.visibleChildName = title }
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu
|
||||
|
||||
import org.gnome.gio.MenuItem
|
||||
import org.gnome.gio.SimpleAction
|
||||
|
||||
class BuiltButtonItem(action: SimpleAction, menuItem: MenuItem?) : BuiltMenuItem(action, menuItem)
|
|
@ -0,0 +1,23 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu
|
||||
|
||||
import org.gnome.gio.MenuItem
|
||||
import org.gnome.gio.SimpleAction
|
||||
import org.gnome.gio.ThemedIcon
|
||||
|
||||
abstract class BuiltMenuItem protected constructor(action: SimpleAction, protected val menuItem: MenuItem?) {
|
||||
protected val action: SimpleAction
|
||||
|
||||
init {
|
||||
this.action = action
|
||||
}
|
||||
|
||||
var enabled: Boolean
|
||||
get() = action.enabled
|
||||
set(enabled) {
|
||||
action.enabled = enabled
|
||||
}
|
||||
|
||||
var iconName: String?
|
||||
set(iconName) { menuItem!!.setIcon(ThemedIcon(iconName)) }
|
||||
get() = throw NotImplementedError()
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu
|
||||
|
||||
import org.gnome.gio.SimpleAction
|
||||
import org.gnome.glib.Variant
|
||||
|
||||
class BuiltRadioItem<T>(action: SimpleAction, private val options: List<T>) : BuiltMenuItem(action, null) {
|
||||
var selected: T
|
||||
get() = options[action.state!!.int32]
|
||||
set(selected) {
|
||||
action.state = Variant.int32(options.indexOf(selected))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu
|
||||
|
||||
import org.gnome.gio.MenuItem
|
||||
import org.gnome.gio.SimpleAction
|
||||
import org.gnome.glib.Variant
|
||||
|
||||
class BuiltToggleItem(action: SimpleAction, menuItem: MenuItem?) : BuiltMenuItem(action, menuItem) {
|
||||
var state: Boolean
|
||||
get() = action.state!!.boolean
|
||||
set(state) {
|
||||
action.state = Variant.boolean_(state)
|
||||
}
|
||||
|
||||
fun toggle(): Boolean {
|
||||
val toggled = !state
|
||||
state = toggled
|
||||
return toggled
|
||||
}
|
||||
}
|
|
@ -0,0 +1,169 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.menu
|
||||
|
||||
import io.github.jwharm.javagi.glib.types.VariantTypes
|
||||
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import org.gnome.gio.*
|
||||
import org.gnome.glib.Variant
|
||||
import org.gnome.gtk.Application
|
||||
import org.gnome.gtk.MenuButton
|
||||
import org.gnome.gtk.PopoverMenu
|
||||
import java.util.function.BiFunction
|
||||
import java.util.function.Consumer
|
||||
|
||||
class MenuBuilder private constructor(map: ActionMap, menu: Menu, prefix: String, groupName: String) {
|
||||
private val map: ActionMap
|
||||
private val refs: MutableMap<String, Action> = LinkedHashMap()
|
||||
val menu: Menu
|
||||
private val prefix: String
|
||||
private val groupName: String
|
||||
|
||||
constructor(menu: PopoverMenu, groupName: String) : this(
|
||||
insertMap(menu, groupName),
|
||||
menu.menuModel as Menu,
|
||||
"",
|
||||
groupName
|
||||
)
|
||||
|
||||
constructor(app: Application, menu: Menu = getRootMenu(app), prefix: String = "") : this(app, menu, prefix, "app.")
|
||||
|
||||
init {
|
||||
fun String.suffix() = if (isNotEmpty() && !endsWith(".")) "$this." else this
|
||||
|
||||
this.map = map
|
||||
this.menu = menu
|
||||
this.prefix = prefix.suffix()
|
||||
this.groupName = groupName.suffix()
|
||||
}
|
||||
|
||||
fun button(name: String, onClick: ThrowingRunnable<*>): BuiltButtonItem {
|
||||
return literalButton(name, I18n["menu.$prefix$name"], onClick)
|
||||
}
|
||||
|
||||
fun literalButton(internalName: String, label: String?, onClick: ThrowingRunnable<*>): BuiltButtonItem {
|
||||
var internalName = internalName
|
||||
internalName = prefix + internalName
|
||||
val action = SimpleAction(internalName, null)
|
||||
addAction(internalName, action)
|
||||
action.onActivate { _ ->
|
||||
try {
|
||||
onClick.run()
|
||||
} catch (e: Throwable) {
|
||||
Log.error("Could not execute action", e)
|
||||
}
|
||||
}
|
||||
val menuItem = MenuItem(label, groupName + internalName)
|
||||
menu.appendItem(menuItem)
|
||||
action.enabled = true
|
||||
return BuiltButtonItem(action, menuItem)
|
||||
}
|
||||
|
||||
fun toggle(name: String, initial: Boolean, onToggle: Consumer<Boolean?>): BuiltToggleItem {
|
||||
var name = name
|
||||
name = prefix + name
|
||||
val action = SimpleAction.stateful(name, null, Variant.boolean_(initial))
|
||||
addAction(name, action)
|
||||
action.onActivate { _ ->
|
||||
val state = !action.getState()!!.boolean
|
||||
action.state = Variant.boolean_(state)
|
||||
onToggle.accept(state)
|
||||
}
|
||||
val menuItem = MenuItem(I18n["menu.$name"], groupName + name)
|
||||
menu.appendItem(menuItem)
|
||||
return BuiltToggleItem(action, menuItem)
|
||||
}
|
||||
|
||||
fun <T> radio(name: String, initial: T, options: List<T>, onCheck: Consumer<T>): BuiltRadioItem<T> {
|
||||
return literalRadio(name, initial, options, { i, _ -> I18n["menu.$prefix$name", i] }, onCheck)
|
||||
}
|
||||
|
||||
fun <T> literalRadio(
|
||||
name: String,
|
||||
initial: T,
|
||||
options: List<T>,
|
||||
stringifier: BiFunction<Int, T, String?>,
|
||||
onCheck: Consumer<T>
|
||||
): BuiltRadioItem<T> {
|
||||
var name = name
|
||||
name = prefix + name
|
||||
val action = SimpleAction.stateful(name, VariantTypes.INT32, Variant.int32(options.indexOf(initial)))
|
||||
addAction(name, action)
|
||||
action.onActivate { variant: Variant? ->
|
||||
action.state = variant
|
||||
onCheck.accept(options[variant!!.int32])
|
||||
}
|
||||
for ((i, option) in options.withIndex()) {
|
||||
menu.appendItem(MenuItem(stringifier.apply(i, option), "$groupName$name($i)"))
|
||||
}
|
||||
return BuiltRadioItem(action, options)
|
||||
}
|
||||
|
||||
fun submenu(name: String): MenuBuilder {
|
||||
return literalSubmenu(name, I18n["menu.$prefix$name"])
|
||||
}
|
||||
|
||||
fun literalSubmenu(name: String, label: String?): MenuBuilder {
|
||||
var name = name
|
||||
name = prefix + name
|
||||
val submenu = Menu()
|
||||
menu.appendSubmenu(label, submenu)
|
||||
return MenuBuilder(map, submenu, name, groupName)
|
||||
}
|
||||
|
||||
fun section(name: String): MenuBuilder {
|
||||
return literalSection(name, I18n["section.$prefix$name"])
|
||||
}
|
||||
|
||||
fun literalSection(name: String, label: String?): MenuBuilder {
|
||||
var name = name
|
||||
name = prefix + name
|
||||
val section = Menu()
|
||||
menu.appendSection(label, section)
|
||||
return MenuBuilder(map, section, name, groupName)
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
menu.removeAll()
|
||||
refs.forEach { (name, _) -> map.removeAction(name) }
|
||||
refs.clear()
|
||||
}
|
||||
|
||||
private fun addAction(name: String, action: SimpleAction) {
|
||||
map.addAction(action)
|
||||
refs[name] = action
|
||||
}
|
||||
|
||||
fun asPopover(): PopoverMenu {
|
||||
return PopoverMenu.fromModel(menu)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOCK = Any()
|
||||
private fun getRootMenu(app: Application): Menu {
|
||||
synchronized(LOCK) {
|
||||
val currentMenu = app.menubar
|
||||
return if (currentMenu == null) {
|
||||
val menu = Menu()
|
||||
app.menubar = menu
|
||||
menu
|
||||
} else {
|
||||
currentMenu as Menu
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun create(target: MenuButton, groupName: String): MenuBuilder {
|
||||
val menu = Menu()
|
||||
val pm = PopoverMenu.fromModel(menu)
|
||||
target.setPopover(pm)
|
||||
return MenuBuilder(pm, groupName)
|
||||
}
|
||||
|
||||
private fun insertMap(menu: PopoverMenu, groupName: String): ActionMap {
|
||||
val ag = SimpleActionGroup()
|
||||
menu.insertActionGroup(groupName, ag)
|
||||
return ag
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util
|
||||
|
||||
import org.jetbrains.annotations.PropertyKey
|
||||
import java.util.*
|
||||
|
||||
object I18n {
|
||||
const val BUNDLE = "inceptum"
|
||||
private val bundle = ResourceBundle.getBundle(BUNDLE)
|
||||
|
||||
operator fun get(key: @PropertyKey(resourceBundle = BUNDLE) String): String =
|
||||
bundle.getString(key)
|
||||
|
||||
operator fun get(key: @PropertyKey(resourceBundle = BUNDLE) String, vararg args: Any?): String =
|
||||
String.format(bundle.getString(key), *args)
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util
|
||||
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
|
||||
|
||||
operator fun List<Instance>.get(id: String) = firstOrNull { it.id == id }
|
|
@ -0,0 +1,6 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util
|
||||
|
||||
import io.gitlab.jfronny.commons.logging.Logger
|
||||
import io.gitlab.jfronny.inceptum.common.Utils
|
||||
|
||||
object Log : Logger by Utils.LOGGER
|
|
@ -0,0 +1,92 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util
|
||||
|
||||
import io.gitlab.jfronny.commons.OSUtils
|
||||
import java.io.IOException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.util.regex.Pattern
|
||||
|
||||
object Memory {
|
||||
const val KB: Long = 1024
|
||||
const val MB = KB * 1024
|
||||
const val GB = MB * 1024
|
||||
private val impl = when (OSUtils.TYPE) {
|
||||
OSUtils.Type.LINUX -> LinuxMI
|
||||
OSUtils.Type.WINDOWS -> WindowsMI
|
||||
OSUtils.Type.MAC_OS -> MacOsMI
|
||||
}
|
||||
private val totalMemory by lazy { impl.getTotalMemory() }
|
||||
val maxMBForInstance: Long get() = (totalMemory / MB - 1024).coerceAtLeast(1024)
|
||||
|
||||
private interface MI {
|
||||
fun getTotalMemory(): Long
|
||||
}
|
||||
|
||||
private object LinuxMI : MI {
|
||||
override fun getTotalMemory(): Long {
|
||||
try {
|
||||
Files.lines(Path.of("/proc/meminfo")).use { stream ->
|
||||
val memTotal = stream
|
||||
.filter { s: String -> s.startsWith("MemTotal:") }
|
||||
.map { s: String -> s.substring("MemTotal:".length) }
|
||||
.map { obj: String -> obj.trim { it <= ' ' } }
|
||||
.findFirst()
|
||||
return if (memTotal.isPresent()) {
|
||||
parseDecimalMemorySizeToBinary(memTotal.get())
|
||||
} else {
|
||||
Log.error("Could not find total memory")
|
||||
32 * GB
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.error("Could not get total memory", e)
|
||||
return 32 * GB
|
||||
}
|
||||
}
|
||||
|
||||
// Taken from oshi
|
||||
private val BYTES_PATTERN = Pattern.compile("(\\d+) ?([kKMGT]?B?).*")
|
||||
private val WHITESPACES = Pattern.compile("\\s+")
|
||||
private fun parseDecimalMemorySizeToBinary(size: String): Long {
|
||||
var mem = WHITESPACES.split(size)
|
||||
if (mem.size < 2) {
|
||||
// If no spaces, use regexp
|
||||
val matcher = BYTES_PATTERN.matcher(size.trim { it <= ' ' })
|
||||
if (matcher.find() && matcher.groupCount() == 2) {
|
||||
mem = arrayOfNulls(2)
|
||||
mem[0] = matcher.group(1)
|
||||
mem[1] = matcher.group(2)
|
||||
}
|
||||
}
|
||||
var capacity = parseLongOrDefault(mem[0], 0L)
|
||||
if (mem.size == 2 && mem[1]!!.length > 1) {
|
||||
when (mem[1]!![0]) {
|
||||
'T' -> capacity = capacity shl 40
|
||||
'G' -> capacity = capacity shl 30
|
||||
'M' -> capacity = capacity shl 20
|
||||
'K', 'k' -> capacity = capacity shl 10
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return capacity
|
||||
}
|
||||
|
||||
private fun parseLongOrDefault(s: String, defaultLong: Long): Long = try {
|
||||
s.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
defaultLong
|
||||
}
|
||||
}
|
||||
|
||||
private object WindowsMI : MI {
|
||||
override fun getTotalMemory(): Long {
|
||||
return 32 * GB // This is currently unsupported, but any implementations by Windows user using panama are welcome
|
||||
}
|
||||
}
|
||||
|
||||
private object MacOsMI : MI {
|
||||
override fun getTotalMemory(): Long {
|
||||
return 32 * GB // This is currently unsupported, but any implementations by MacOS user using panama are welcome
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util
|
||||
|
||||
import java.util.stream.Stream
|
||||
|
||||
inline fun <reified T> Stream<T>.toTypedArray(): Array<T> = toArray(::arrayOfNulls)
|
|
@ -0,0 +1,10 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util
|
||||
|
||||
import org.gnome.gio.ListModel
|
||||
import org.gnome.gtk.StringList
|
||||
|
||||
fun StringList.clear() = splice(0, size, null)
|
||||
fun StringList.addAll(values: Array<String>) = splice(size, 0, values)
|
||||
fun StringList.replaceAll(values: Array<String>) = splice(0, size, values)
|
||||
|
||||
val ListModel.size get() = nItems
|
|
@ -0,0 +1,46 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.util
|
||||
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.ILabel
|
||||
import org.gnome.adw.ActionRow
|
||||
import org.gnome.gtk.EntryBuffer
|
||||
import org.gnome.gtk.Label
|
||||
import org.gnome.gtk.MessageDialog
|
||||
import org.gnome.gtk.Widget
|
||||
|
||||
var Widget.margin: Int
|
||||
set(value) {
|
||||
marginVertical = value
|
||||
marginHorizontal = value
|
||||
}
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
var Widget.marginVertical: Int
|
||||
set(value) {
|
||||
marginTop = value
|
||||
marginBottom = value
|
||||
}
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
var Widget.marginHorizontal: Int
|
||||
set(value) {
|
||||
marginStart = value
|
||||
marginEnd = value
|
||||
}
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
var MessageDialog.markup: String
|
||||
set(value) { setMarkup(value.escapedMarkup) }
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
var Label.markup: String
|
||||
set(value) { setMarkup(value.escapedMarkup) }
|
||||
get() = throw NotImplementedError()
|
||||
|
||||
val String.escapedMarkup: String
|
||||
get() = replace(unmatchedAnd, "&")
|
||||
|
||||
private val unmatchedAnd = Regex("&(?![a-z]{1,6};|#[0-9]+;|#x[0-9A-F]+;)")
|
||||
|
||||
fun ActionRow.fixSubtitle() = ILabel.theme(firstChild!!.lastChild!!.prevSibling!!.lastChild as Label, ILabel.Mode.SUBTITLE)
|
||||
|
||||
fun EntryBuffer.clear() = deleteText(0, length)
|
|
@ -0,0 +1,30 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window
|
||||
|
||||
import io.gitlab.jfronny.inceptum.common.BuildMetadata
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import org.gnome.gtk.AboutDialog
|
||||
import org.gnome.gtk.License
|
||||
|
||||
class AboutWindow : AboutDialog() {
|
||||
init {
|
||||
programName = "Inceptum"
|
||||
copyright = "Copyright (C) 2021-2023 JFronny"
|
||||
version = BuildMetadata.VERSION
|
||||
licenseType = License.MIT_X11
|
||||
license = I18n["about.license"]
|
||||
websiteLabel = I18n["about.contact"]
|
||||
website = "https://jfronny.gitlab.io/contact.html"
|
||||
if (!BuildMetadata.IS_PUBLIC) {
|
||||
comments = I18n["about.unsupported-build"]
|
||||
}
|
||||
val vm = Runtime.version().feature()
|
||||
systemInformation = I18n[if (BuildMetadata.VM_VERSION == vm) "about.jvm" else "about.jvm.unsupported", vm]
|
||||
//TODO setLogo
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun createAndShow() {
|
||||
AboutWindow().visible = true
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window
|
||||
|
||||
import io.gitlab.jfronny.inceptum.common.Utils
|
||||
import io.gitlab.jfronny.inceptum.gtk.GtkMenubar
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.InstanceListEntryFactory
|
||||
import io.gitlab.jfronny.inceptum.gtk.menu.MenuBuilder
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.*
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.settings.launcher.LauncherSettingsWindow
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceList
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceListWatcher
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.launch.LaunchType
|
||||
import org.gnome.adw.Clamp
|
||||
import org.gnome.adw.StatusPage
|
||||
import org.gnome.gio.*
|
||||
import org.gnome.glib.GLib
|
||||
import org.gnome.gtk.*
|
||||
import org.gnome.gtk.Application
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
|
||||
class MainWindow(app: Application) : ApplicationWindow(app) {
|
||||
// private val listButton: Button
|
||||
// private val gridButton: Button
|
||||
private val stack: Stack
|
||||
private val empty: StatusPage
|
||||
private val listContainer: Clamp
|
||||
// private val gridView: GridView
|
||||
private val instanceList: MutableList<Instance>
|
||||
private val instanceListModel: StringList
|
||||
|
||||
init {
|
||||
val header = HeaderBar()
|
||||
val newButton = MenuButton()
|
||||
newButton.iconName = "list-add-symbolic"
|
||||
newButton.menuModel = GtkMenubar.newMenu!!.menu
|
||||
val accountsButton = MenuButton()
|
||||
accountsButton.iconName = "avatar-default-symbolic"
|
||||
accountsButton.menuModel = GtkMenubar.accountsMenu!!.menu
|
||||
// listButton = Button.newFromIconName("view-list-symbolic")
|
||||
// listButton.onClicked {
|
||||
// InceptumConfig.listView = true
|
||||
// InceptumConfig.saveConfig()
|
||||
// generateWindowBody()
|
||||
// }
|
||||
// gridButton = Button.newFromIconName("view-grid-symbolic")
|
||||
// gridButton.onClicked {
|
||||
// InceptumConfig.listView = false
|
||||
// InceptumConfig.saveConfig()
|
||||
// generateWindowBody()
|
||||
// }
|
||||
|
||||
//TODO search button like boxes
|
||||
|
||||
val uiMenu = MenuBuilder(app, Menu(), "hamburger")
|
||||
uiMenu.button("support") { Utils.openWebBrowser(URI("https://git.frohnmeyer-wds.de/JfMods/Inceptum/issues")) }
|
||||
uiMenu.button("preferences") { LauncherSettingsWindow(app).visible = true }
|
||||
uiMenu.button("about") { AboutWindow.createAndShow() }
|
||||
val menuButton = MenuButton()
|
||||
menuButton.iconName = "open-menu-symbolic"
|
||||
menuButton.menuModel = uiMenu.menu
|
||||
|
||||
header.packStart(newButton)
|
||||
|
||||
header.packEnd(menuButton)
|
||||
// header.packEnd(gridButton)
|
||||
// header.packEnd(listButton)
|
||||
header.packEnd(accountsButton)
|
||||
|
||||
instanceList = ArrayList()
|
||||
instanceListModel = StringList(arrayOf())
|
||||
val selection = NoSelection(instanceListModel)
|
||||
|
||||
val listView = ListView(selection, InstanceListEntryFactory(app, instanceList))
|
||||
listView.addCssClass("rich-list")
|
||||
listView.showSeparators = true
|
||||
listView.onActivate { position: Int ->
|
||||
// Double click
|
||||
GtkMenubar.launch(instanceList[position], LaunchType.Client)
|
||||
}
|
||||
val frame = Frame(null as String?)
|
||||
frame.child = listView
|
||||
frame.marginHorizontal = 24
|
||||
frame.marginVertical = 12
|
||||
frame.valign = Align.START
|
||||
listContainer = Clamp()
|
||||
listContainer.maximumSize = 900
|
||||
listContainer.child = frame
|
||||
// gridView = GridView(selection, InstanceGridEntryFactory(instanceList))
|
||||
empty = StatusPage()
|
||||
empty.title = I18n["main.empty.title"]
|
||||
empty.description = I18n["main.empty.description"]
|
||||
//TODO empty.setIconName(new Str());
|
||||
stack = Stack()
|
||||
stack.addChild(listContainer)
|
||||
// stack.addChild(gridView)
|
||||
stack.addChild(empty)
|
||||
|
||||
val scroll = ScrolledWindow()
|
||||
scroll.setPolicy(PolicyType.NEVER, PolicyType.AUTOMATIC)
|
||||
scroll.child = stack
|
||||
|
||||
setDefaultSize(720, 360)
|
||||
title = "Inceptum"
|
||||
titlebar = header
|
||||
showMenubar = false
|
||||
child = scroll
|
||||
|
||||
generateWindowBody()
|
||||
//TODO DropTarget to add mods/instances
|
||||
|
||||
try {
|
||||
setupDirWatcher()
|
||||
} catch (e: IOException) {
|
||||
Log.error(
|
||||
"Could not set up watch service, live updates of the instance dir will be unavailable",
|
||||
e
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun setupDirWatcher() {
|
||||
val isw = InstanceListWatcher()
|
||||
addTickCallback { _, _ ->
|
||||
try {
|
||||
if (isw.poll()) generateWindowBody()
|
||||
} catch (e: IOException) {
|
||||
Log.error("Could not run update task", e)
|
||||
}
|
||||
GLib.SOURCE_CONTINUE
|
||||
}
|
||||
onCloseRequest {
|
||||
try {
|
||||
isw.close()
|
||||
} catch (ignored: IOException) {}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateWindowBody() {
|
||||
// listButton.visible = !InceptumConfig.listView
|
||||
// gridButton.visible = InceptumConfig.listView
|
||||
try {
|
||||
// Unbind then clear
|
||||
instanceListModel.clear()
|
||||
instanceList.clear()
|
||||
|
||||
// Add new entries
|
||||
instanceList.addAll(InstanceList.ordered())
|
||||
instanceListModel.addAll(instanceList.map { it.id }.toTypedArray())
|
||||
|
||||
// Choose view for this amount of entries
|
||||
// stack.visibleChild = if (InstanceList.isEmpty()) empty
|
||||
// else if (InceptumConfig.listView) listContainer
|
||||
// else gridView
|
||||
stack.visibleChild = if (InstanceList.isEmpty()) empty else listContainer
|
||||
|
||||
// This is called from a tick callback, so re-render
|
||||
stack.queueResize()
|
||||
stack.queueDraw()
|
||||
} catch (e: IOException) {
|
||||
Log.error("Could not generate window body", e)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,178 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window.create
|
||||
|
||||
import io.gitlab.jfronny.commons.StringFormatter
|
||||
import io.gitlab.jfronny.inceptum.common.InceptumConfig
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.KDropDown
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.KEntry
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.assistant.KAssistant
|
||||
import io.gitlab.jfronny.inceptum.gtk.schedule
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.markup
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.toTypedArray
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.FabricMetaApi
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.McApi
|
||||
import io.gitlab.jfronny.inceptum.launcher.model.fabric.FabricVersionLoaderInfo
|
||||
import io.gitlab.jfronny.inceptum.launcher.model.mojang.VersionsListInfo
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.InstanceNameTool
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.LoaderInfo
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.setup.SetupStepInfo
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.setup.Steps
|
||||
import org.gnome.glib.GLib
|
||||
import org.gnome.gtk.*
|
||||
|
||||
|
||||
class NewInstanceWindow(app: Application) : KAssistant(app) {
|
||||
companion object {
|
||||
private val VERSIONS = McApi.getVersions()
|
||||
}
|
||||
|
||||
init {
|
||||
var gameVersion: VersionsListInfo? = null
|
||||
var useFabric = false
|
||||
var fabricVersion: FabricVersionLoaderInfo? = null
|
||||
var name = "New Instance"
|
||||
|
||||
var failureMessage = "Unknown error, please look at the log!"
|
||||
var isFailure = false
|
||||
|
||||
page("Welcome", AssistantPageType.INTRO) {
|
||||
append(Label("This assistant will guide you through the process of setting up a Minecraft instance.\nTo begin, please choose the game version you want to use"))
|
||||
|
||||
val versions = VERSIONS.versions.stream()
|
||||
.filter { InceptumConfig.snapshots || it.type == "release" }
|
||||
.toTypedArray()
|
||||
val def = versions.withIndex().firstOrNull { it.value.id == VERSIONS.latest.release }?.index ?: 0
|
||||
gameVersion = versions[def]
|
||||
|
||||
append(KDropDown(versions, { it.id }, def).apply {
|
||||
onChange { gameVersion = versions[it] }
|
||||
})
|
||||
|
||||
setComplete(true)
|
||||
}
|
||||
page("Loader", AssistantPageType.CONTENT) {
|
||||
append(Label("Select a mod loader if you want to use mods in this instance. This can be changed later."))
|
||||
var lastGameVersion: VersionsListInfo? = null
|
||||
var versions = arrayOf<FabricVersionLoaderInfo>()
|
||||
var def = 0
|
||||
|
||||
val none = CheckButton.withLabel("None")
|
||||
val fabric = CheckButton.withLabel("Fabric")
|
||||
none.onToggled { useFabric = false }
|
||||
none.onToggled { useFabric = true }
|
||||
fabric.setGroup(none)
|
||||
append(none)
|
||||
val fabricVersionDropdown = KDropDown(versions, { it.loader.version }, def)
|
||||
fabricVersionDropdown.onChange { fabricVersion = versions[it] }
|
||||
append(Box(Orientation.HORIZONTAL, 8).apply {
|
||||
append(fabric)
|
||||
append(fabricVersionDropdown)
|
||||
})
|
||||
|
||||
onOpen {
|
||||
if (lastGameVersion == null || lastGameVersion != gameVersion) {
|
||||
versions = FabricMetaApi.getLoaderVersions(gameVersion!!).toTypedArray()
|
||||
def = versions.withIndex().firstOrNull { it.value.loader.stable }?.index ?: 0
|
||||
fabricVersionDropdown.updateOptions(versions, def)
|
||||
lastGameVersion = gameVersion
|
||||
none.active = true
|
||||
fabric.active = false
|
||||
useFabric = false
|
||||
}
|
||||
|
||||
if (versions.isEmpty()) {
|
||||
none.active = true
|
||||
fabric.active = false
|
||||
useFabric = false
|
||||
fabric.sensitive = false
|
||||
}
|
||||
}
|
||||
|
||||
setComplete(true)
|
||||
}
|
||||
page("Name", AssistantPageType.CONTENT) {
|
||||
append(Label(I18n["instance.settings.general.name.placeholder"]))
|
||||
val entry = KEntry(name)
|
||||
entry.placeholderText = I18n["instance.settings.general.name.placeholder"]
|
||||
entry.valign
|
||||
entry.onChange { name = InstanceNameTool.getNextValid(it) }
|
||||
append(entry)
|
||||
onOpen {
|
||||
name = InstanceNameTool.getDefaultName(gameVersion!!.id, useFabric)
|
||||
entry.text = name
|
||||
}
|
||||
|
||||
setComplete(true)
|
||||
}
|
||||
page("Creating", AssistantPageType.PROGRESS) {
|
||||
append(Label("Creating Instance"))
|
||||
val progress = ProgressBar()
|
||||
append(progress)
|
||||
val stage = Label("")
|
||||
append(stage)
|
||||
onOpen {
|
||||
commit()
|
||||
val pState = Steps.createProcessState()
|
||||
val state = SetupStepInfo(
|
||||
McApi.getVersionInfo(gameVersion),
|
||||
if (useFabric) LoaderInfo(fabricVersion!!.loader) else LoaderInfo.NONE,
|
||||
name,
|
||||
pState
|
||||
)
|
||||
var finished = false
|
||||
var cachedState: ProcessStateWatcherDialog.State? = null
|
||||
addTickCallback { widget, _ ->
|
||||
if (finished) return@addTickCallback GLib.SOURCE_REMOVE
|
||||
val nc = ProcessStateWatcherDialog.State(pState)
|
||||
if (nc != cachedState) {
|
||||
cachedState = nc
|
||||
stage.markup = cachedState!!.msg
|
||||
progress.fraction = cachedState!!.progress.coerceAtMost(1f).toDouble()
|
||||
widget.queueDraw()
|
||||
}
|
||||
GLib.SOURCE_CONTINUE
|
||||
}
|
||||
onClose { pState.cancel() }
|
||||
onCancel { pState.cancel() }
|
||||
pState.updateStep("Starting install process")
|
||||
Thread {
|
||||
try {
|
||||
for (step in Steps.STEPS) {
|
||||
if (state.isCancelled) {
|
||||
state.tryRemoveInstance()
|
||||
return@Thread
|
||||
}
|
||||
pState.incrementStep(step.name)
|
||||
step.execute(state)
|
||||
}
|
||||
state.clearSetupLock()
|
||||
} catch (e: Throwable) {
|
||||
pState.cancel()
|
||||
Log.error("Could not create instance")
|
||||
failureMessage = StringFormatter.toString(e)
|
||||
isFailure = true
|
||||
state.tryRemoveInstance()
|
||||
} finally {
|
||||
finished = true
|
||||
schedule { setComplete(true) }
|
||||
schedule { nextPage() }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
}
|
||||
page("Done", AssistantPageType.SUMMARY) {
|
||||
val status = Label("")
|
||||
onOpen {
|
||||
if (isFailure) {
|
||||
status.markup = "Something went wrong while creating the instance.\n\n$failureMessage"
|
||||
} else {
|
||||
status.markup = "The instance was successfully created. You can now launch it using the main menu"
|
||||
}
|
||||
}
|
||||
|
||||
setComplete(true)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window.dialog
|
||||
|
||||
import io.gitlab.jfronny.inceptum.common.Utils
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAccount
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAuthAPI
|
||||
import io.gitlab.jfronny.inceptum.launcher.api.account.MicrosoftAuthServer
|
||||
import org.gnome.gtk.*
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
||||
class MicrosoftLoginDialog(
|
||||
parent: Window?,
|
||||
account: MicrosoftAccount? = null,
|
||||
onClose: Runnable? = null
|
||||
) : MessageDialog(
|
||||
parent,
|
||||
flags(parent != null),
|
||||
MessageType.QUESTION,
|
||||
ButtonsType.CLOSE,
|
||||
I18n["auth.description"]
|
||||
) {
|
||||
constructor(parent: Window?, onClose: Runnable?) : this(parent, null, onClose)
|
||||
|
||||
init {
|
||||
title = I18n["auth.title"]
|
||||
val server = MicrosoftAuthServer(account)
|
||||
try {
|
||||
server.start()
|
||||
} catch (e: Exception) {
|
||||
Log.error("Could not start mc login server", e)
|
||||
}
|
||||
val finalize = Runnable {
|
||||
server.close()
|
||||
onClose?.run()
|
||||
}
|
||||
onResponse { responseId: Int ->
|
||||
when (ResponseType.of(responseId)) {
|
||||
ResponseType.CLOSE, ResponseType.CANCEL -> {
|
||||
finalize.run()
|
||||
close()
|
||||
}
|
||||
|
||||
ResponseType.DELETE_EVENT -> {
|
||||
finalize.run()
|
||||
destroy()
|
||||
}
|
||||
|
||||
else -> Log.error("Unexpected response type: $responseId")
|
||||
}
|
||||
}
|
||||
val btn = Button.withLabel(I18n["auth.open-browser"])
|
||||
(messageArea as Box).append(btn)
|
||||
btn.onClicked {
|
||||
try {
|
||||
Utils.openWebBrowser(URI(MicrosoftAuthAPI.MICROSOFT_LOGIN_URL))
|
||||
} catch (e: URISyntaxException) {
|
||||
Log.error("Could not open browser", e)
|
||||
}
|
||||
}
|
||||
onCloseRequest {
|
||||
finalize.run()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun flags(modal: Boolean): DialogFlags {
|
||||
var flags = DialogFlags.DESTROY_WITH_PARENT
|
||||
if (modal) flags = flags.or(DialogFlags.MODAL)
|
||||
return flags
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window.dialog
|
||||
|
||||
import io.gitlab.jfronny.commons.StringFormatter
|
||||
import io.gitlab.jfronny.commons.throwable.ThrowingRunnable
|
||||
import io.gitlab.jfronny.inceptum.gtk.GtkEnvBackend
|
||||
import io.gitlab.jfronny.inceptum.gtk.schedule
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.Log
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.markup
|
||||
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
|
||||
import org.gnome.glib.GLib
|
||||
import org.gnome.gtk.*
|
||||
|
||||
class ProcessStateWatcherDialog(
|
||||
parent: Window?,
|
||||
title: String,
|
||||
errorMessage: String,
|
||||
private val state: ProcessState,
|
||||
executor: ThrowingRunnable<*>
|
||||
) : MessageDialog(
|
||||
parent,
|
||||
DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT),
|
||||
MessageType.INFO,
|
||||
ButtonsType.NONE,
|
||||
null
|
||||
) {
|
||||
private var finished = false
|
||||
private var cachedState: State? = null
|
||||
|
||||
init {
|
||||
//TODO alternate UI: Only show progress bar by default, but have a dropdown to a "console" with the actual steps
|
||||
// this should make visualizing parallelized steps easier
|
||||
this.title = title
|
||||
addButton(I18n["cancel"], ResponseType.CANCEL.value)
|
||||
onResponse { responseId: Int ->
|
||||
when (ResponseType.of(responseId)) {
|
||||
ResponseType.CLOSE, ResponseType.CANCEL -> {
|
||||
state.cancel()
|
||||
close()
|
||||
}
|
||||
|
||||
ResponseType.DELETE_EVENT -> destroy()
|
||||
else -> Log.error("Unexpected response type: $responseId")
|
||||
}
|
||||
}
|
||||
onCloseRequest {
|
||||
if (finished) return@onCloseRequest false
|
||||
state.cancel()
|
||||
false
|
||||
}
|
||||
val progress = ProgressBar()
|
||||
(messageArea as Box).append(progress)
|
||||
addTickCallback { widget, _ ->
|
||||
if (finished) return@addTickCallback GLib.SOURCE_REMOVE
|
||||
val nc = State(state)
|
||||
if (nc != cachedState) {
|
||||
cachedState = nc
|
||||
markup = cachedState!!.msg
|
||||
progress.fraction = cachedState!!.progress.coerceAtMost(1f).toDouble()
|
||||
widget.queueDraw()
|
||||
}
|
||||
GLib.SOURCE_CONTINUE
|
||||
}
|
||||
Thread {
|
||||
try {
|
||||
executor.run()
|
||||
} catch (e: Throwable) {
|
||||
state.cancel()
|
||||
Log.error(errorMessage, e)
|
||||
GtkEnvBackend.simpleDialog(
|
||||
parent,
|
||||
StringFormatter.toString(e),
|
||||
errorMessage,
|
||||
null,
|
||||
null
|
||||
)
|
||||
} finally {
|
||||
finished = true
|
||||
schedule { close() }
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
data class State(val msg: String, val progress: Float) {
|
||||
constructor(source: ProcessState) : this(source.currentStep, source.progress)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun show(
|
||||
parent: Window?,
|
||||
title: String,
|
||||
errorMessage: String,
|
||||
state: ProcessState,
|
||||
executor: ThrowingRunnable<*>
|
||||
): ProcessStateWatcherDialog {
|
||||
val dialog = ProcessStateWatcherDialog(parent, title, errorMessage, state, executor)
|
||||
dialog.visible = true
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window.dialog
|
||||
|
||||
import org.gnome.gtk.*
|
||||
|
||||
class StringInputDialog(parent: Window?, flags: DialogFlags, type: MessageType, buttons: ButtonsType, message: String, value: String) : MessageDialog(parent, flags, type, buttons, message) {
|
||||
private val entry = Entry()
|
||||
|
||||
init {
|
||||
(messageArea as Box).append(entry)
|
||||
entry.text = value
|
||||
}
|
||||
|
||||
val input: String get() = entry.text
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
package io.gitlab.jfronny.inceptum.gtk.window.settings.instance
|
||||
|
||||
import io.gitlab.jfronny.inceptum.common.Utils
|
||||
import io.gitlab.jfronny.inceptum.gtk.control.settings.SectionedSettingsTab
|
||||
import io.gitlab.jfronny.inceptum.gtk.schedule
|
||||
import io.gitlab.jfronny.inceptum.gtk.util.I18n
|
||||
import io.gitlab.jfronny.inceptum.gtk.window.dialog.ProcessStateWatcherDialog
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.exporter.Exporter
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.exporter.Exporters
|
||||
import io.gitlab.jfronny.inceptum.launcher.system.instance.Instance
|
||||
import io.gitlab.jfronny.inceptum.launcher.util.ProcessState
|
||||
import org.gnome.gio.Cancellable
|
||||
import org.gnome.gtk.*
|
||||
import java.nio.file.Path
|
||||
|
||||
class ExportTab(window: InstanceSettingsWindow) : SectionedSettingsTab<InstanceSettingsWindow>(window) {
|
||||
private val instance: Instance = window.instance
|
||||
init {
|
||||
section(null) {
|
||||
row("instance.settings.export.version", "instance.settings.export.version.subtitle") {
|
||||
setEntry(instance.meta.instanceVersion) {
|
||||
instance.meta.instanceVersion = it
|
||||
instance.writeMeta()
|
||||
}
|
||||
}
|
||||
for (exporter in Exporters.EXPORTERS) {
|
||||
row("instance.settings.export.title", "instance.settings.export.subtitle", exporter.name, exporter.fileExtension) {
|
||||
setButton("instance.settings.export") {
|
||||
run {
|
||||
val dialog = FileDialog()
|
||||
dialog.title = I18n["instance.settings.export.dialog.title", exporter.name]
|
||||
val filters = AnyFilter()
|
||||
val filter = FileFilter()
|
||||
filter.name = exporter.name + " Pack"
|
||||
filter.addPattern("*." + exporter.fileExtension)
|
||||
filters.append(filter)
|
||||
dialog.filters = filters
|
||||
dialog.initialName = exporter.getDefaultFileName(instance)
|
||||
dialog.save(window, Cancellable()) { _, res, _ ->
|
||||
export(exporter, Path.of(dialog.saveFinish(res)?.path ?: return@save))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun export(exporter: Exporter<*>, path: Path) {
|
||||
val state = ProcessState(Exporters.STEP_COUNT, "Initializing...")
|
||||
ProcessStateWatcherDialog.show(
|
||||
window,
|
||||
I18n["instance.settings.export.dialog.title", exporter.name],
|
||||
I18n["instance.settings.export.dialog.error", instance.name],
|
||||
state
|
||||
) {
|
||||
exporter.generate(state, instance, path)
|
||||
schedule {
|
||||
val success = MessageDialog(
|
||||
window,
|
||||
DialogFlags.MODAL.or(DialogFlags.DESTROY_WITH_PARENT),
|
||||
MessageType.INFO,
|
||||
ButtonsType.NONE,
|
||||
I18n["instance.settings.export.dialog.success", instance.name, path.toString()]
|
||||
)
|
||||
success.title = I18n["instance.settings.export.dialog.success.title"]
|
||||
success.addButton(I18n["show"], ResponseType.OK.value)
|
||||
success.addButton(I18n["ok"], ResponseType.CANCEL.value)
|
||||
success.onResponse { responseId1: Int ->
|
||||
when (ResponseType.of(responseId1)) {
|
||||
ResponseType.OK -> {
|
||||
success.close()
|
||||
Utils.openFile(path.toFile())
|
||||
}
|
||||
|
||||
ResponseType.CLOSE, ResponseType.CANCEL -> success.close()
|
||||
ResponseType.DELETE_EVENT -> success.destroy()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
success.visible = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue