Compare commits

...

102 Commits

Author SHA1 Message Date
Johannes Frohnmeyer aede030a7f
chore: refactor build scripts and update java-gi
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2024-03-01 21:25:11 +01:00
Johannes Frohnmeyer 3e70e9e4e1
chore: bump to JDK 21 (gava-gi seems to segfault again, figure that out later)
ci/woodpecker/manual/woodpecker Pipeline was successful Details
ci/woodpecker/manual/docs Pipeline failed Details
2023-12-01 08:01:02 +01:00
Johannes Frohnmeyer 4629d5f68e
fix: migrate components that I forgot when upgrading java-commons 2023-11-09 07:55:15 +01:00
Johannes Frohnmeyer 7dd1d4dee2
fix: prevent lag spike until MDS is completed when entering Mods tab. Still laggy as hell, but this should at least prevent ANRs or WL disconnects
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline failed Details
2023-11-08 09:20:08 +01:00
Johannes Frohnmeyer f4ed8a4bcb
chore: bump commons and update for new modules 2023-11-08 09:15:59 +01:00
Johannes Frohnmeyer af84674c26
chore: bump subrepo
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-10-27 16:41:42 +02:00
Johannes Frohnmeyer 4aa1f24461
fix: apparently the previous fix worked during testing but broke after the merge.
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
Somehow.
2023-10-21 17:50:19 +02:00
Johannes Frohnmeyer 9fd5d870cf
chore: clean up annotations and imports
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-10-21 17:48:23 +02:00
Johannes Frohnmeyer e61df73dd3
Merge branch 'gtk_mod_browsing'
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
# Conflicts:
#	build.gradle.kts
#	launcher-gtk/build.gradle.kts
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/GtkMain.kt
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/control/assistant/KAssistant.kt
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/util/UIExt.kt
#	launcher-gtk/src/main/kotlin/io/gitlab/jfronny/inceptum/gtk/window/create/NewInstanceWindow.kt
#	launcher-imgui/src/main/java/io/gitlab/jfronny/inceptum/imgui/window/AddModWindow.java
2023-10-21 17:41:20 +02:00
Johannes Frohnmeyer c94c8b59af
fix: prevent segfault by not exporting account menu.
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline failed Details
Why does this work? No idea. But it seems to fix the crash.
2023-10-21 17:38:55 +02:00
Johannes Frohnmeyer 475717b6b4
chore: use setVisible instead of show 2023-10-21 16:49:49 +02:00
Johannes Frohnmeyer 5cc650921b
fix: prevent instance not exiting setup stage when redownload cancelled 2023-10-21 15:50:35 +02:00
Johannes Frohnmeyer 5b79987dcf
chore: move logging in kotlin code to Log object 2023-10-21 13:35:02 +02:00
Johannes Frohnmeyer 502026bb22
fix: add quickAction suffix 2023-10-21 13:35:02 +02:00
Johannes Frohnmeyer 3746e30ec7
fix: wrap Entry.setText to prevent segfault for empty strings 2023-10-21 13:35:01 +02:00
Johannes Frohnmeyer ad033711f9
chore: basic mod browsing (untested due to GTK4 issues) 2023-10-21 13:35:01 +02:00
Johannes Frohnmeyer f1f2e95dd2
chore: bump javagi 2023-10-21 13:34:57 +02:00
Johannes Frohnmeyer 9e1c20737d
style: don't explicitly depend on gtk transitive dependencies
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-10-16 16:14:46 +02:00
Johannes Frohnmeyer 04d8121ca2
fix: update java-gi and, in doing so, fix segfault 2023-10-16 16:00:57 +02:00
Johannes Frohnmeyer f4157bae09
fix: correctly assign permissions
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-10-06 08:52:00 +02:00
Johannes Frohnmeyer c7eabc37d3
fix: handle run directory permissions correctly (inspired by signald)
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline failed Details
2023-10-06 08:33:53 +02:00
Johannes Frohnmeyer 0bd675fc7c
fix: prevent endlessly recursive serialization due to misinterpretation of checked method as getter
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-10-03 20:16:00 +02:00
Johannes Frohnmeyer bcd4e34f7a
fix: defensively use deleteIfExists to prevent races in Instance 2023-10-03 20:15:11 +02:00
Johannes Frohnmeyer 7a7d009e29
fix: use proper dir in ModrinthImporter 2023-10-03 20:14:41 +02:00
Johannes Frohnmeyer e14294fdd6
chore: perform more actions in the builder stage for simpleDialog
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 16:20:17 +02:00
Johannes Frohnmeyer 98cc37405a
fix: update AddModWindow for ModrinthApi change 2023-08-19 16:19:50 +02:00
Johannes Frohnmeyer b1b82c423a
fix: prevent segfault in simpleDialog
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 16:13:45 +02:00
Johannes Frohnmeyer 32a01547c0
build: get rid of toolchains
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 16:05:07 +02:00
Johannes Frohnmeyer e19d7cfb3b
chore: optimize imports
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-08-19 15:54:27 +02:00
Johannes Frohnmeyer d7ddde6c4d
chore: move logging in kotlin code to Log object
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-08-19 15:53:09 +02:00
Johannes Frohnmeyer f091cb7a2b
fix: add quickAction suffix
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-08-19 15:52:24 +02:00
Johannes Frohnmeyer 5faa505235
fix: wrap Entry.setText to prevent segfault for empty strings
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-08-19 15:32:42 +02:00
Johannes Frohnmeyer 8aa0555a2a
fix: use deleteIfExists to work around potential race condition 2023-08-19 15:32:09 +02:00
Johannes Frohnmeyer a7a135c598
chore: basic mod browsing (untested due to GTK4 issues) 2023-07-24 14:29:06 +02:00
Johannes Frohnmeyer 7f168ded43
chore: bump javagi 2023-07-15 22:15:14 +02:00
Johannes Frohnmeyer cbba1ab70a
support for testing offline via gradle-side reflection hacks and firejail
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-07-15 00:00:27 +02:00
Johannes Frohnmeyer ec67a80389
more cleanup
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-07-14 16:25:52 +02:00
Johannes Frohnmeyer 1759e5c3de
refactor SignalListItemFactory usage with custom wrapper 2023-07-14 16:19:37 +02:00
Johannes Frohnmeyer b8f30247ea
clean up gtk kt 2023-07-14 15:24:32 +02:00
Johannes Frohnmeyer 7805400e43
it's fixed in GTK 2023-07-14 15:10:23 +02:00
Johannes Frohnmeyer 55b9e5986f
use string list with IDs, fixes crash on remove, scaffold mods tab 2023-07-14 14:29:23 +02:00
Johannes Frohnmeyer 7cefa88dcb
fix: instance creation + dialog
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-06-30 23:39:05 +02:00
Johannes Frohnmeyer 8f46e4887f
Group 2023-06-30 23:22:20 +02:00
Johannes Frohnmeyer 1be0d68a56
Implement new instance window, bump java and GTK 2023-06-30 14:02:31 +02:00
Johannes Frohnmeyer e9f8af5617
Disable irrelevant check 2023-06-30 10:22:32 +02:00
Johannes Frohnmeyer 9d287c6521 I forgor
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-06-16 09:40:03 +02:00
Johannes Frohnmeyer 7c4461a275
Check if exists
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-06 10:12:30 +02:00
Johannes Frohnmeyer d8bd25b438
Centralize mv logic
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-06 10:06:42 +02:00
Johannes Frohnmeyer 218e12714d
InstanceMeta.checkArguments to centralize null checking + remove mutability from Arguments and kotlin lambda indy
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-05-06 09:54:40 +02:00
Johannes Frohnmeyer 3d73879bed
Use nio because that somehow makes things work
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-05 23:53:31 +02:00
Johannes Frohnmeyer d1c6b746f0
No install 2023-05-05 23:38:44 +02:00
Johannes Frohnmeyer 71e784fdff
Aptless
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-05 23:27:39 +02:00
Johannes Frohnmeyer 1db7bcde18
Does this works?
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-05 23:19:29 +02:00
Johannes Frohnmeyer 8f056468a7
Workaround
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-05 23:06:58 +02:00
Johannes Frohnmeyer 25874c40f7
Set -xe + jammy
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-05 22:41:23 +02:00
Johannes Frohnmeyer 3aaf7e3652
Try moving build_platform_jars gradle invocation to shell script
ci/woodpecker/push/docs Pipeline is pending Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-05-05 22:36:10 +02:00
Johannes Frohnmeyer 17e89ba829
GTK: JPMS + optimize imports
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-05-05 21:37:16 +02:00
Johannes Frohnmeyer a89f51aa5d
Initial port of launcher-gtk to kotlin. With this, manifold is completely gone from the codebase.
Might have introduced some new bugs + crashes, though.
2023-05-05 21:27:00 +02:00
Johannes Frohnmeyer 441a9b26b2
Remove manifold and add JPMS in every project except wrapper, launchwrapper and launcher-gtk
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-05-05 18:49:56 +02:00
Johannes Frohnmeyer 50e9db1fcc
Use jpackage for windows/linux distribution
ci/woodpecker/push/woodpecker Pipeline failed Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-05 15:27:25 +02:00
Johannes Frohnmeyer 6151f0e71e
Optimize imports
ci/woodpecker/push/woodpecker Pipeline was successful Details
ci/woodpecker/push/docs Pipeline was successful Details
2023-05-04 11:31:54 +02:00
Johannes Frohnmeyer 8d45fdff84
Bump other dependencies 2023-05-04 11:25:37 +02:00
Johannes Frohnmeyer 2cb852c5cb
Bump java-gi 2023-05-04 11:14:34 +02:00
Johannes Frohnmeyer fc256a1376
Support release candidates
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-03-12 22:07:07 +01:00
Johannes Frohnmeyer 1353de777e
Die URL
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-29 23:14:16 +01:00
Johannes Frohnmeyer 3f273bf6f8
Update copyright
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-29 18:39:17 +01:00
Johannes Frohnmeyer 7a80132ab0
GTK: Clean up leftovers
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-01-29 18:37:31 +01:00
Johannes Frohnmeyer c027885364
GTK: Start working on instance creation flow
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-01-29 18:34:33 +01:00
Johannes Frohnmeyer 4967410f51
GTK: Showcase configuration using broadwayd
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-29 17:19:02 +01:00
Johannes Frohnmeyer 14a23fdfed
GTK: Implement account management
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-29 16:33:59 +01:00
Johannes Frohnmeyer 7284193981
GTK: Start working on accounts UI and complete more menu entries
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-29 15:45:51 +01:00
Johannes Frohnmeyer eb9601d6cf
GTK: Memory Settings
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-29 12:39:46 +01:00
Johannes Frohnmeyer 5c9ce78ebf
GTK: Support search in IRow dropdown
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-29 11:16:18 +01:00
Johannes Frohnmeyer fe0c23b97b
GTK: Instance Settings: custom java version
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 21:05:41 +01:00
Johannes Frohnmeyer 94dbaadaf1
GTK: Some of the cleanest code ever to be written (ignoring all other code)
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 20:37:57 +01:00
Johannes Frohnmeyer cd3c8a1852
GTK: More work on settings screens
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 19:25:52 +01:00
Johannes Frohnmeyer 18c4953b36
GTK: More work on InstanceSettingsWindow
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 15:31:33 +01:00
Johannes Frohnmeyer 0cb3df331a
GTK: Fix instance list auto-updating
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 14:53:16 +01:00
Johannes Frohnmeyer 08bb13f994
GTK: Fix launching
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 14:15:17 +01:00
Johannes Frohnmeyer a93f2c8411
GTK: Implement instance exporting, break launching
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 13:26:50 +01:00
Johannes Frohnmeyer e15ef8c485
Use a "stable" java-gi version (0.3) and mention it in the README
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-28 10:02:01 +01:00
Johannes Frohnmeyer 2ca25a7bee
Some TODOs
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-24 18:51:41 +01:00
Johannes Frohnmeyer bdd86c7683
Start working on instance settings
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-24 18:25:18 +01:00
Johannes Frohnmeyer 442d462843
GTK: fix list view
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-22 19:15:09 +01:00
Johannes Frohnmeyer 37872e6c79
Bump commons + gson-compile. Introduces safe writing (no more half-written config files!)
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-22 16:17:43 +01:00
Johannes Frohnmeyer dff05af62f
More gi changes
ci/woodpecker/manual/docs Pipeline failed Details
ci/woodpecker/manual/woodpecker Pipeline was successful Details
2023-01-20 15:47:49 +01:00
Johannes Frohnmeyer 05a18765c9
Clean
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-01-15 11:23:02 +01:00
Johannes Frohnmeyer 36f462597a
Fix menu not interactable
ci/woodpecker/push/docs Pipeline failed Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-01-15 11:19:57 +01:00
Johannes Frohnmeyer c52f1f3350
Tweak main window
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2023-01-13 19:34:29 +01:00
Johannes Frohnmeyer d4a016771f
It werks
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2023-01-13 14:20:16 +01:00
Johannes Frohnmeyer 8af7c214d2
Make it compile against java-gi changes (and some patches) 2023-01-10 21:20:01 +01:00
Johannes Frohnmeyer 71faae3b9a
Update java-gi unibranch
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-12-19 14:07:37 +01:00
Johannes Frohnmeyer 379f02c41c
Try to fix
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-12-18 20:46:46 +01:00
Johannes Frohnmeyer be8252ce58
Initial work on porting to java-gi, currently broken
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-12-17 21:53:21 +01:00
Johannes Frohnmeyer fb56c9e922
Fix (don't use extension methods for now)
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-24 21:11:07 +01:00
Johannes Frohnmeyer 18810b255b
Refactor to use manifold and records for models
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-18 21:58:40 +01:00
Johannes Frohnmeyer 3370495207
Update scripts
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline failed Details
2022-11-13 12:41:09 +01:00
Johannes Frohnmeyer b0326536c7
Move tests for LambdaMetafactory to source tree
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-13 10:29:26 +01:00
Johannes Frohnmeyer eb6c2538b5
Revert "Revert "Revert "One last release on gitlab"""
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
2022-11-04 19:10:27 +01:00
Johannes Frohnmeyer 3563d7449b
Revert "Revert "One last release on gitlab""
This reverts commit 7dee85292c.
2022-11-04 18:49:50 +01:00
Johannes Frohnmeyer 7dee85292c
Revert "One last release on gitlab"
ci/woodpecker/push/docs Pipeline was successful Details
ci/woodpecker/push/woodpecker Pipeline was successful Details
This reverts commit d2b041ef59.
2022-11-04 18:16:31 +01:00
Johannes Frohnmeyer d2b041ef59
One last release on gitlab 2022-11-04 18:09:40 +01:00
234 changed files with 5394 additions and 2954 deletions

View File

@ -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*

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)
}

2
buildSrc/java-offline Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
firejail --net=none "$G_ORIGINAL_EXECUTABLE" "$@"

View File

@ -1 +1,8 @@
rootProject.name="inceptum-conventions"
rootProject.name="inceptum-conventions"
dependencyResolutionManagement {
versionCatalogs {
create("libs") {
from(files("../gradle/libs.versions.toml"))
}
}
}

View File

@ -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
}

View File

@ -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

View File

@ -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> {

View File

@ -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")
}
}
}
}
}

View File

@ -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))
}
}
}

View File

@ -1,5 +1,5 @@
plugins {
id("inceptum.java-conventions")
id("inceptum.java")
}
publishing {

View File

@ -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)
}
}
}

View File

@ -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);
}
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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));

View File

@ -1,7 +0,0 @@
package io.gitlab.jfronny.inceptum.common;
public class OutdatedException extends RuntimeException {
public OutdatedException(String message) {
super(message);
}
}

View File

@ -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) {
}
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -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++);
}
};
}

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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 + '/';

View File

@ -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();

View File

@ -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) {
}

View File

@ -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) {
}

View File

@ -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;
}

52
gradle/libs.versions.toml Normal file
View File

@ -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"]

View File

@ -1,5 +1,5 @@
plugins {
id("inceptum.application-conventions")
inceptum.application
}
application {
@ -7,5 +7,5 @@ application {
}
dependencies {
implementation(project(":launcher"))
implementation(projects.launcher)
}

View File

@ -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;

View File

@ -47,6 +47,8 @@ public class CliMain {
try {
command.invoke();
} catch (Exception e) {
Utils.LOGGER.error("Could not execute command", e);
} finally {
LauncherEnv.terminate();
}

View File

@ -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);

View File

@ -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)));
}
}
}

View File

@ -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);
}
}

View File

@ -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());
}
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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()

View File

@ -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;
}

View File

@ -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)
}
}
}
}
}

View File

@ -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;
}

View File

@ -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") }

View File

@ -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);
}
};
}
}

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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"));
}
}

View File

@ -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
});
}
}

View File

@ -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
});
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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));
}
}

View File

@ -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());
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}

View File

@ -1,7 +0,0 @@
package io.gitlab.jfronny.inceptum.gtk.window;
public class NewInstanceWindow {
public static void createAndShow() {
//TODO
}
}

View File

@ -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")
}
}
}

View File

@ -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)
}
}

View File

@ -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) }
}
}

View File

@ -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)
}
}
}

View File

@ -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 -> {}
}
}
}

View File

@ -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)
}

View File

@ -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
)
}

View File

@ -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())
}
}

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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() }
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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
)
}

View File

@ -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 }
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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))
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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)
}

View File

@ -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 }

View File

@ -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

View File

@ -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
}
}
}

View File

@ -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)

View File

@ -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

View File

@ -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, "&amp;")
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)

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}
}

View File

@ -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
}

View File

@ -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