aboutsummaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorBryson Steck <brysonsteck@protonmail.com>2023-05-13 19:53:34 -0600
committerBryson Steck <brysonsteck@protonmail.com>2023-05-13 19:53:34 -0600
commit4388bc04152ea256858c38bdea1ff83bc9544898 (patch)
tree8f5a1a2ab7bb6e48502b7316e9f724a546a84254 /app/src/main
parentb92a9c1ec9b7e8a39ff098d25dfda517a3338721 (diff)
downloadServerCraft-4388bc04152ea256858c38bdea1ff83bc9544898.tar
ServerCraft-4388bc04152ea256858c38bdea1ff83bc9544898.tar.gz
ServerCraft-4388bc04152ea256858c38bdea1ff83bc9544898.tar.bz2
desktop not working, trying original package
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/kotlin/xyz/brysonsteck/serverfordummies/App.kt1
-rw-r--r--app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/InfoController.kt17
-rw-r--r--app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/PrimaryController.kt617
-rw-r--r--app/src/main/resources/xyz/brysonsteck/serverfordummies/info.fxml4
-rw-r--r--app/src/main/resources/xyz/brysonsteck/serverfordummies/primary.fxml2
5 files changed, 20 insertions, 621 deletions
diff --git a/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/App.kt b/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/App.kt
index 8a00744..0a3cab9 100644
--- a/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/App.kt
+++ b/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/App.kt
@@ -9,6 +9,7 @@ import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image
import javafx.stage.Stage;
+import java.awt.Desktop;
class App : Application() {
diff --git a/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/InfoController.kt b/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/InfoController.kt
index b91d3e2..5f98c83 100644
--- a/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/InfoController.kt
+++ b/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/InfoController.kt
@@ -27,19 +27,34 @@ class InfoController {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.BROWSE)) {
+ println("hi")
try {
when {
split[1].equals("email") -> {
+ println("email")
desktop.browse(URI("mailto:" + websites[split[0]]))
}
- else -> {
+ split[1].equals("website") -> {
+ println("website")
desktop.browse(URI(websites[split[0]]))
}
+ split[0].equals("source") -> {
+ println("source")
+ desktop.browse(URI(source))
+ }
+ split[0].equals("license") -> {
+ println("license")
+ desktop.browse(URI(license))
+ }
+ else -> {
+ println("unknown")
+ }
}
} catch (e: Exception) {
println(e)
}
}
+ println("done")
}
@FXML
diff --git a/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/PrimaryController.kt b/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/PrimaryController.kt
deleted file mode 100644
index d854138..0000000
--- a/app/src/main/kotlin/xyz/brysonsteck/serverfordummies/controllers/PrimaryController.kt
+++ /dev/null
@@ -1,617 +0,0 @@
-package xyz.brysonsteck.serverfordummies.controllers
-
-import kotlinx.coroutines.*
-import kotlinx.coroutines.javafx.JavaFx
-import org.rauschig.jarchivelib.*
-
-import java.io.File
-import java.io.IOException
-import java.io.BufferedReader
-import java.io.InputStreamReader
-import java.awt.Checkbox
-import java.awt.Desktop
-import java.util.Properties
-import java.net.URL
-import java.net.URI
-
-import javafx.beans.value.ChangeListener
-import javafx.beans.value.ObservableValue
-import javafx.concurrent.Task
-import javafx.beans.property.BooleanProperty
-import javafx.collections.FXCollections
-import javafx.fxml.FXML
-import javafx.fxml.FXMLLoader
-import javafx.geometry.Insets
-import javafx.scene.control.Button
-import javafx.scene.control.ChoiceBox
-import javafx.scene.control.Label
-import javafx.scene.control.TextField
-import javafx.scene.control.Spinner
-import javafx.scene.control.TitledPane
-import javafx.scene.control.ButtonBar
-import javafx.scene.control.CheckBox
-import javafx.scene.control.ProgressBar
-import javafx.scene.control.Hyperlink
-import javafx.scene.layout.Border
-import javafx.scene.layout.BorderStroke
-import javafx.scene.layout.GridPane
-import javafx.scene.layout.Pane
-import javafx.scene.layout.HBox
-import javafx.scene.layout.VBox
-import javafx.scene.text.TextAlignment
-import javafx.scene.text.Text
-import javafx.scene.Scene
-import javafx.scene.input.MouseEvent
-import javafx.scene.image.Image
-import javafx.scene.image.ImageView
-import javafx.stage.FileChooser
-import javafx.stage.FileChooser.ExtensionFilter
-import javafx.stage.DirectoryChooser
-import javafx.stage.Modality
-import javafx.stage.Stage
-import javafx.event.EventHandler
-import org.rauschig.jarchivelib.*
-
-import Download
-import xyz.brysonsteck.serverfordummies.server.Server
-import xyz.brysonsteck.serverfordummies.App
-
-class PrimaryController {
- @FXML
- lateinit private var currentDirectoryLabel: Label
- @FXML
- lateinit private var worldNameField: TextField
- @FXML
- lateinit private var seedField: TextField
- @FXML
- lateinit private var portSpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var difficultyBox: ChoiceBox<String>
- @FXML
- lateinit private var gamemodeBox: ChoiceBox<String>
- @FXML
- lateinit private var worldTypeBox: ChoiceBox<String>
- @FXML
- lateinit private var worldSettingsPane: HBox
- @FXML
- lateinit private var parentPane: Pane
- @FXML
- lateinit private var directoryPane: Pane
- @FXML
- lateinit private var buttonBar: ButtonBar
- @FXML
- lateinit private var flightCheckbox: CheckBox
- @FXML
- lateinit private var netherCheckbox: CheckBox
- @FXML
- lateinit private var structuresCheckbox: CheckBox
- @FXML
- lateinit private var pvpCheckbox: CheckBox
- @FXML
- lateinit private var whitelistCheckbox: CheckBox
- @FXML
- lateinit private var cmdBlocksCheckbox: CheckBox
- @FXML
- lateinit private var playerCountCheckbox: CheckBox
- @FXML
- lateinit private var maxPlayersSpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var maxSizeSpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var memorySpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var spawnSpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var simulationSpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var renderSpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var maxTickSpinner: Spinner<kotlin.Int>
- @FXML
- lateinit private var statusBar: Label
- @FXML
- lateinit private var progressBar: ProgressBar
- @FXML
- lateinit private var startButton: Button
- @FXML
- lateinit private var buildButton: Button
- @FXML
- lateinit private var defaultsButton: Button
-
- lateinit private var server: Server
- private var building = false
- private var directory = ""
- private var asyncResult = false
- private var started = false
-
- @FXML
- public fun initialize() {
- difficultyBox.items = FXCollections.observableArrayList(
- "Peaceful",
- "Easy",
- "Normal",
- "Hard",
- "Hardcore"
- )
- difficultyBox.value = "Normal"
- difficultyBox.selectionModel.selectedIndexProperty().addListener { _, _, new ->
- onChoiceBoxChange("difficulty", difficultyBox.items[new as Int])
- }
- gamemodeBox.items = FXCollections.observableArrayList(
- "Survival",
- "Creative",
- "Adventure",
- "Spectator"
- )
- gamemodeBox.value = "Survival"
- gamemodeBox.selectionModel.selectedIndexProperty().addListener { _, _, new ->
- onChoiceBoxChange("gamemode", gamemodeBox.items[new as Int])
- }
- worldTypeBox.items = FXCollections.observableArrayList(
- "Normal",
- "Superflat",
- "Large Biomes",
- "Amplified"
- )
- worldTypeBox.value = "Normal"
- worldTypeBox.selectionModel.selectedIndexProperty().addListener { _, _, new ->
- onChoiceBoxChange("world-type", worldTypeBox.items[new as Int])
- }
- }
-
- @FXML
- private fun onDirectoryButtonClick() {
- val dirChooser = DirectoryChooser()
- dirChooser.title = "Open a server directory"
- dirChooser.initialDirectory = File(System.getProperty("user.home"))
- val result = dirChooser.showDialog(null)
- if (result != null) {
- currentDirectoryLabel.text = result.absolutePath
- server = Server()
- val res = loadServerDir(result.absolutePath)
- if (res) {
- parentPane.isDisable = false
- worldSettingsPane.isDisable = false
- buildButton.isDisable = false
- defaultsButton.isDisable = false
- } else {
- currentDirectoryLabel.text = "<NONE>"
- parentPane.isDisable = true
- worldSettingsPane.isDisable = true
- startButton.isDisable = true
- buildButton.isDisable = true
- defaultsButton.isDisable = true
- }
- }
- }
-
- @FXML
- private fun onWorldNameChange() {
-
- }
-
- @FXML
- private fun onSeedChange() {
-
- }
-
- @FXML
- private fun onPortChange() {
-
- }
-
- @FXML
- private fun onCheckboxClick() {
-
- }
-
- @FXML
- private fun onSpinnerChange() {
-
- }
-
- private fun onChoiceBoxChange(box: String, selection: String) {
-
- }
-
- @FXML
- private fun onInfo() {
- val stage = Stage()
- val scene = Scene(FXMLLoader(App().javaClass.getResource("info.fxml")).load(), 398.0, 358.0)
- stage.icons.add(Image(App().javaClass.getResourceAsStream("app-256x256.png")))
- stage.setResizable(false)
- stage.initModality(Modality.APPLICATION_MODAL);
- stage.title = "About ServerCraft"
- stage.scene = scene
- stage.show()
- }
-
- @FXML
- private fun onBuild() {
- if (building) {
- building = false
- return;
- }
- building = true
- worldSettingsPane.isDisable = true
- directoryPane.isDisable = true
- parentPane.isDisable = true
- startButton.isDisable = true
- defaultsButton.isDisable = true
- buildButton.text = "Cancel Build"
-
- @Suppress("OPT_IN_USAGE")
- GlobalScope.launch(Dispatchers.Default) {
- progressBar.isVisible = true
- var javaFile = ""
- var archiver = ArchiverFactory.createArchiver(ArchiveFormat.TAR, CompressionType.GZIP)
- val os = System.getProperty("os.name").lowercase()
- when {
- os.contains("win") -> {
- javaFile = "openjdk-20.0.1_windows-x64_bin.zip"
- archiver = ArchiverFactory.createArchiver(ArchiveFormat.ZIP)
- }
- os.contains("linux") -> {
- javaFile = "openjdk-20.0.1_linux-x64_bin.tar.gz"
- }
- os.contains("mac") -> {
- javaFile = "openjdk-20.0.1_macos-x64_bin.tar.gz"
- }
- }
-
- // download files
- val downloads = mapOf(
- "Java 20" to "https://download.java.net/java/GA/jdk20.0.1/b4887098932d415489976708ad6d1a4b/9/GPL/${javaFile}",
- "BuildTools" to "https://hub.spigotmc.org/jenkins/job/BuildTools/lastSuccessfulBuild/artifact/target/BuildTools.jar",
- )
- val destinations = mapOf (
- "Java 20" to directory + "ServerForDummies" + File.separator + "Java" + File.separator,
- "BuildTools" to directory + "ServerForDummies" + File.separator + "Spigot" + File.separator
- )
- val spigotBuilt = File(destinations["BuildTools"]).exists()
- val javaExtracted = File(destinations["Java 20"] + "jdk-20.0.1").exists()
- destinations.forEach {
- File(it.value).mkdir()
- }
- downloads.forEach {
- if (it.key == "Java 20" && javaExtracted) {
- return@forEach
- }
- withContext(Dispatchers.JavaFx){
- statusBar.text = "Downloading ${it.key}..."
- progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS
- }
- val download = Download(URL(it.value), destinations[it.key]!!)
- download.start()
- while (download.status == Download.Status.DOWNLOADING) {
- var prog = (download.downloaded.toDouble() / download.contentLength.toDouble())
- // for whatever reason I need to print something to the screen in order for it to update the progress bar
- print("")
- if (prog >= 0.01) {
- withContext(Dispatchers.JavaFx) {progressBar.progress = prog}
- }
- if (!building) download.status = Download.Status.CANCELLED
- Thread.sleep(300)
- }
- }
-
- // extract java archive
- if (building && !javaExtracted) {
- withContext(Dispatchers.JavaFx) {
- progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS
- statusBar.text = "Extracting Java archive..."
- }
- var stream = archiver.stream(File(directory + "ServerForDummies" + File.separator + "Java" + File.separator + javaFile))
- val dest = File(directory + "ServerForDummies" + File.separator + "Java")
- var entries = 0.0
- while(stream.getNextEntry() != null && building) {
- entries++
- }
- stream = archiver.stream(File(directory + "ServerForDummies" + File.separator + "Java" + File.separator + javaFile))
- var entry = stream.getNextEntry()
- var currentEntry = 0.0
- do {
- withContext(Dispatchers.JavaFx) {progressBar.progress = currentEntry/entries}
- entry.extract(dest)
- entry = stream.getNextEntry()
- currentEntry++
- } while (entry != null && building)
- }
-
- if (building) {
- withContext(Dispatchers.JavaFx) {
- progressBar.progress = ProgressBar.INDETERMINATE_PROGRESS
- statusBar.text = "Building Minecraft Server..."
- }
- val builder = ProcessBuilder("java", "-jar", "BuildTools.jar", "--rev", "latest", "-o", ".." + File.separator + ".." + File.separator)
- builder.directory(File(directory + "ServerForDummies" + File.separator + "Spigot"))
- val proc = builder.start()
- val reader = InputStreamReader(proc.inputStream)
- val br = BufferedReader(reader)
- try {
- var line = br.readLine()
- var currentline = 0.0
- while (line != null) {
- if (!building) {
- proc.destroy()
- }
- println(line)
- line = br.readLine()
- currentline++
- if (currentline > 15) {
- withContext(Dispatchers.JavaFx) {progressBar.progress = if (spigotBuilt) {currentline/1100.0} else {currentline/14122.0} }
- }
- }
- } catch (e: IOException) {
- println("Stream closed")
- }
- }
-
- progressBar.isVisible = false
- withContext(Dispatchers.JavaFx){
- worldSettingsPane.isDisable = false
- directoryPane.isDisable = false
- parentPane.isDisable = false
- defaultsButton.isDisable = false
- buildButton.text = "Build Server"
- statusBar.text = if (building) {
- findServerJar()
- buildButton.text = "Rebuild Server"
- startButton.isDisable = false
- "Ready."
- } else {
- "Server build cancelled."
- }
- building = false;
- }
- }
- }
-
- @FXML
- private fun onStart() {
- if (started) {
- createDialog("warning", "You should only kill the server if\nabsolutely necessary. Data loss may occur.\nContinue anyway?", "Yes", "No", false)
- return;
- }
- if (!File(directory + "eula.txt").exists()) {
- val res = eulaDialog()
- if (res) {
- File(directory + "eula.txt").writeText("eula=true")
- } else {
- return;
- }
- }
- started = true
- statusBar.text = "The Minecraft Server is now running. Shutdown the server to unlock the settings."
- worldSettingsPane.isDisable = true
- directoryPane.isDisable = true
- parentPane.isDisable = true
- buildButton.isDisable = true
- defaultsButton.isDisable = true
- startButton.text = "Kill Server"
- @Suppress("OPT_IN_USAGE")
- GlobalScope.launch(Dispatchers.Default) {
- val builder = ProcessBuilder("java", "-jar", "${server.jar}")
- builder.directory(File(directory))
- val proc = builder.start()
- val reader = InputStreamReader(proc.inputStream)
- val br = BufferedReader(reader)
- try {
- var line = br.readLine()
- while (line != null) {
- if (asyncResult) {
- proc.destroy()
- }
- println(line);
- line = br.readLine()
- }
- } catch (e: IOException) {
- println("Stream closed")
- }
- withContext(Dispatchers.JavaFx) {
- statusBar.text = if (asyncResult) {
- asyncResult = false
- "Server killed."
- } else {
- "Server stopped."
- }
- worldSettingsPane.isDisable = false
- directoryPane.isDisable = false
- parentPane.isDisable = false
- buildButton.isDisable = false
- defaultsButton.isDisable = false
- startButton.text = "Start Server"
- started = false
- }
- }
- }
-
- private fun eulaDialog(): Boolean {
- var result = false
- val resources = this.javaClass.getResource("icons/warning.png")
- val dialog = Stage()
- dialog.setResizable(false)
- dialog.initModality(Modality.APPLICATION_MODAL);
- dialog.title = directory
- val scenePane = Pane()
- val dialogScene = Scene(scenePane, 400.0, 150.0);
- val imagePane = Pane()
- val icon = Image("$resources")
- imagePane.layoutX = 14.0
- imagePane.layoutY = 14.0
- imagePane.scaleX = 0.7
- imagePane.scaleY = 0.7
- imagePane.children.add(ImageView(icon))
- val label = Label("Do you agree to the terms of the Minecraft End User License Agreement?")
- label.isWrapText = true
- label.layoutX = 115.0
- label.layoutY = 40.0
- val buttonBar = ButtonBar()
- buttonBar.buttonOrder = "L+R"
- buttonBar.padding = Insets(10.0, 10.0, 10.0, 10.0)
- buttonBar.layoutX = 0.0
- buttonBar.layoutY = 107.0
- buttonBar.prefWidth = 400.0
- val noButton = Button("I Disagree")
- noButton.onMouseClicked = EventHandler<MouseEvent>() {
- result = false
- dialog.hide()
- }
- noButton.isDefaultButton = true
- val yesButton = Button("I Agree")
- yesButton.onMouseClicked = EventHandler<MouseEvent>() {
- result = true
- dialog.hide()
- }
- val eula = Button("View EULA")
- eula.onMouseClicked = EventHandler<MouseEvent>() {
- val desktop = Desktop.getDesktop()
- if (desktop.isSupported(Desktop.Action.BROWSE)) {
- try {
- desktop.browse(URI("https://account.mojang.com/documents/minecraft_eula"))
- } catch (e: Exception) {
- println(e)
- }
- }
- }
- ButtonBar.setButtonData(eula, ButtonBar.ButtonData.LEFT)
- ButtonBar.setButtonData(noButton, ButtonBar.ButtonData.RIGHT)
- ButtonBar.setButtonData(yesButton, ButtonBar.ButtonData.RIGHT)
- buttonBar.buttons.add(eula)
- buttonBar.buttons.add(noButton)
- buttonBar.buttons.add(yesButton)
- scenePane.children.addAll(imagePane, label, buttonBar)
- dialog.setScene(dialogScene);
- dialog.showAndWait();
- return result
- }
-
- private fun createDialog(type: String, msg: String, yes: String, no: String, hold: Boolean): Boolean {
- var result = false
- val resources = this.javaClass.getResource("icons/$type.png")
- val dialog = Stage()
- dialog.setResizable(false)
- dialog.initModality(Modality.APPLICATION_MODAL);
- dialog.title = directory
- val scenePane = Pane()
- val dialogScene = Scene(scenePane, 400.0, 150.0);
- val imagePane = Pane()
- val icon = Image("$resources")
- imagePane.layoutX = 14.0
- imagePane.layoutY = 14.0
- imagePane.scaleX = 0.7
- imagePane.scaleY = 0.7
- imagePane.children.add(ImageView(icon))
- val label = Label(msg)
- label.isWrapText = true
- label.layoutX = 115.0
- label.layoutY = if (type == "warning") {10.0} else {40.0}
- val buttonBar = ButtonBar()
- buttonBar.padding = Insets(10.0, 10.0, 10.0, 10.0)
- buttonBar.layoutX = 0.0
- buttonBar.layoutY = 107.0
- buttonBar.prefWidth = 400.0
- val noButton = Button(no)
- noButton.onMouseClicked = EventHandler<MouseEvent>() {
- if (hold) {
- result = false
- } else {
- asyncResult = false
- }
- dialog.hide()
- }
- val yesButton = Button(yes)
- yesButton.onMouseClicked = EventHandler<MouseEvent>() {
- if (hold) {
- result = true
- } else {
- asyncResult = true
- }
- dialog.hide()
- }
- yesButton.isDefaultButton = true
- buttonBar.buttons.add(noButton)
- buttonBar.buttons.add(yesButton)
- scenePane.children.addAll(imagePane, label, buttonBar)
- dialog.setScene(dialogScene);
- if (hold) {
- dialog.showAndWait();
- } else {
- dialog.show();
- }
- return result
- }
-
- private fun loadServerDir(dir: String): Boolean {
- directory = dir
- if (!File(directory).isDirectory) {
- return false;
- }
-
- if (directory[directory.length-1] != File.separatorChar)
- directory += File.separatorChar
-
- val hasDummy = File(directory + "ServerForDummies").isDirectory
- val hasProperties = File(directory + File.separator + "server.properties").isFile
- val hasServer = findServerJar()
-
- if (hasDummy && hasServer) {
- // server complete, just read jproperties
- statusBar.text = "Server found!"
- startButton.isDisable = false
- buildButton.text = "Rebuild Server"
- } else if (hasDummy && !hasServer && hasProperties) {
- // just needs to be built
- startButton.isDisable = true
- statusBar.text = "Server needs to be built before starting."
- } else if (!hasDummy && hasServer) {
- // server created externally
- val result = createDialog("warning", "This server directory was not created by \nServerForDummies. Errors may occur; copying\nthe world directories to a new folder may be\nsafer. Proceed anyway?", "Yes", "No", true)
- statusBar.text = "Ready."
- if (result) {
- startButton.isDisable = false
- }
- return result
- } else {
- // assume clean directory
- val result = createDialog("info", "There is no server in this directory.\nCreate one?", "Yes", "No", true)
- if (result) {
- File(directory + "ServerForDummies").mkdir()
- startButton.isDisable = true
- buildButton.text = "Build Server"
- }
- statusBar.text = "Ready."
- return result
- }
-
- return true;
- }
-
- private fun findServerJar(): Boolean {
- // search for spigot jar
- // major version
- for (i in 25 downTo 8) {
- // patch number
- for (j in 15 downTo 0) {
- var spigotFile: String = ""
- if (j == 0)
- spigotFile += "spigot-1.$i.jar"
- else
- spigotFile += "spigot-1.$i.$j.jar";
-
- if (File(directory + spigotFile).isFile) {
- server.jar = directory + spigotFile
- return true
- }
- }
- }
-
- // try vanilla server if no spigot server
- if (File(directory + "server.jar").isFile) {
- server.jar = directory + "server.jar"
- return true
- }
-
- return false
- }
-} \ No newline at end of file
diff --git a/app/src/main/resources/xyz/brysonsteck/serverfordummies/info.fxml b/app/src/main/resources/xyz/brysonsteck/serverfordummies/info.fxml
index 978bc5c..4c82385 100644
--- a/app/src/main/resources/xyz/brysonsteck/serverfordummies/info.fxml
+++ b/app/src/main/resources/xyz/brysonsteck/serverfordummies/info.fxml
@@ -48,8 +48,8 @@
<Insets bottom="13.0" left="13.0" right="13.0" top="13.0" />
</padding>
</Label>
- <Hyperlink fx:id="_license" layoutX="127.0" layoutY="101.0" text="License" />
- <Hyperlink fx:id="_source" layoutX="189.0" layoutY="101.0" text="Source Code" />
+ <Hyperlink fx:id="license" layoutX="127.0" layoutY="101.0" onAction="#openHyperlink" text="License" />
+ <Hyperlink fx:id="source" layoutX="189.0" layoutY="101.0" onAction="#openHyperlink" text="Source Code" />
</children>
<padding>
<Insets bottom="13.0" left="13.0" right="13.0" top="13.0" />
diff --git a/app/src/main/resources/xyz/brysonsteck/serverfordummies/primary.fxml b/app/src/main/resources/xyz/brysonsteck/serverfordummies/primary.fxml
index 2cedc4f..43ee1a0 100644
--- a/app/src/main/resources/xyz/brysonsteck/serverfordummies/primary.fxml
+++ b/app/src/main/resources/xyz/brysonsteck/serverfordummies/primary.fxml
@@ -20,7 +20,7 @@
<?import javafx.scene.layout.Pane?>
<?import javafx.scene.text.Font?>
-<Pane fx:id="primary" maxHeight="713.0" maxWidth="963.0" minHeight="713.0" minWidth="963.0" prefHeight="713.0" prefWidth="963.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="xyz.brysonsteck.serverfordummies.controllers.PrimaryController">
+<Pane fx:id="primary" maxHeight="713.0" maxWidth="963.0" minHeight="713.0" minWidth="963.0" prefHeight="713.0" prefWidth="963.0" xmlns="http://javafx.com/javafx/19" xmlns:fx="http://javafx.com/fxml/1" fx:controller="xyz.brysonsteck.serverfordummies.PrimaryController">
<children>
<HBox fx:id="directoryPane" prefHeight="39.0" prefWidth="963.0">
<children>