+
@@ -101,7 +102,7 @@ const SimpleModal = forwardRef((props, ref) => {
);
});
-const styles = {
+const modalStyles = {
dialog: {
padding: "1.5em",
border: "none",
diff --git a/front-end/src/components/Switch.js b/front-end/src/components/Switch.js
new file mode 100644
index 0000000..997289c
--- /dev/null
+++ b/front-end/src/components/Switch.js
@@ -0,0 +1,28 @@
+import React from 'react';
+import styles from '../styles/Switch.module.css';
+
+const Switch = ({ checked, onChange }) => {
+ const handleChange = (e) => {
+ // Only allow changes if not in null state
+ if (checked !== null) {
+ onChange(e.target.checked);
+ }
+ };
+
+ const isDisabled = checked === null;
+ const isChecked = checked === true;
+
+ return (
+
+ );
+};
+
+export default Switch;
diff --git a/front-end/src/components/Tabs/Tab.js b/front-end/src/components/Tabs/Tab.js
new file mode 100644
index 0000000..57ff8c2
--- /dev/null
+++ b/front-end/src/components/Tabs/Tab.js
@@ -0,0 +1,14 @@
+import styles from './Tabs.module.css';
+
+function Tab({ label, value, active, onClick }) {
+ return (
+
+ );
+}
+
+export default Tab;
diff --git a/front-end/src/components/Tabs/TabPanel.js b/front-end/src/components/Tabs/TabPanel.js
new file mode 100644
index 0000000..4ec3cec
--- /dev/null
+++ b/front-end/src/components/Tabs/TabPanel.js
@@ -0,0 +1,11 @@
+import styles from './Tabs.module.css';
+
+function TabPanel({ value, children }) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default TabPanel;
diff --git a/front-end/src/components/Tabs/Tabs.js b/front-end/src/components/Tabs/Tabs.js
new file mode 100644
index 0000000..3e53285
--- /dev/null
+++ b/front-end/src/components/Tabs/Tabs.js
@@ -0,0 +1,31 @@
+import styles from './Tabs.module.css';
+import { useState } from 'react';
+import Tab from './Tab';
+import TabPanel from './TabPanel';
+
+function Tabs({ children, labels }) {
+ const [activeTab, setActiveTab] = useState(labels[0].value);
+
+ return (
+
+
+ {labels.map(tab => (
+ setActiveTab(tab.value)}
+ />
+ ))}
+
+
+ {children.map(child =>
+ child.props.value === activeTab ? child : null
+ )}
+
+
+ );
+}
+
+export default Tabs;
diff --git a/front-end/src/components/Tabs/Tabs.module.css b/front-end/src/components/Tabs/Tabs.module.css
new file mode 100644
index 0000000..b5978e9
--- /dev/null
+++ b/front-end/src/components/Tabs/Tabs.module.css
@@ -0,0 +1,41 @@
+.tabsContainer {
+ width: 100%;
+}
+
+.tabHeader {
+ display: flex;
+ justify-content: center;
+ margin-bottom: 10px;
+}
+
+.tabButton {
+ background: none;
+ border: none;
+ font-size: 16px;
+ padding: 10px 20px;
+ cursor: pointer;
+ border-bottom: 2px solid transparent;
+ transition: all 0.2s ease-in-out;
+}
+
+.tabButton:hover {
+ color: #d63384;
+}
+
+.tabButtonActive {
+ border-color: #d63384;
+ font-weight: bold;
+}
+
+.tabPanels {
+ padding: 10px;
+}
+
+.tabPanel {
+ animation: fadeIn 0.3s ease;
+}
+
+@keyframes fadeIn {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
diff --git a/front-end/src/components/VersionTab.js b/front-end/src/components/VersionTab.js
new file mode 100644
index 0000000..0ad398c
--- /dev/null
+++ b/front-end/src/components/VersionTab.js
@@ -0,0 +1,138 @@
+import React, { useState, useEffect } from 'react';
+import { Card, CardContainer } from './card';
+import { useServerData } from '../utils/serverDataContext';
+import styles from '../styles/VersionTab.module.css';
+
+const VersionTab = () => {
+ const data = useServerData();
+ const [loading, setLoading] = useState(false);
+ const [selectedPlatform, setSelectedPlatform] = useState(data.platform || 'vanilla');
+ const [selectedVersion, setSelectedVersion] = useState(data.version || '');
+ const [availableVersions, setAvailableVersions] = useState([]);
+
+ const platforms = ['vanilla', 'paper', 'fabric', 'forge'];
+
+ useEffect(() => {
+ const fetchVersions = async () => {
+ try {
+ let versionsUrl;
+ switch (selectedPlatform) {
+ case 'vanilla':
+ versionsUrl = 'https://launchermeta.mojang.com/mc/game/version_manifest.json';
+ const vanillaResponse = await fetch(versionsUrl);
+ const vanillaData = await vanillaResponse.json();
+ const vanillaVersions = vanillaData.versions.map(v => v.id).filter(v => !v.includes('snapshot'));
+ setAvailableVersions(vanillaVersions);
+ if (!data.version) setSelectedVersion(vanillaVersions[0]);
+ break;
+ case 'paper':
+ versionsUrl = 'https://api.papermc.io/v2/projects/paper';
+ const paperResponse = await fetch(versionsUrl);
+ const paperData = await paperResponse.json();
+ setAvailableVersions(paperData.versions);
+ if (!data.version) setSelectedVersion(paperData.versions[0]);
+ break;
+ case 'fabric':
+ versionsUrl = 'https://meta.fabricmc.net/v2/versions/game';
+ const fabricResponse = await fetch(versionsUrl);
+ const fabricData = await fabricResponse.json();
+ const fabricVersions = fabricData.map(v => v.version).filter(v => !v.includes('snapshot'));
+ setAvailableVersions(fabricVersions);
+ if (!data.version) setSelectedVersion(fabricVersions[0]);
+ break;
+ case 'forge':
+ versionsUrl = 'https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json';
+ const forgeResponse = await fetch(versionsUrl);
+ const forgeData = await forgeResponse.json();
+ const forgeVersions = Object.keys(forgeData);
+ setAvailableVersions(forgeVersions);
+ if (!data.version) setSelectedVersion(forgeVersions[0]);
+ break;
+ }
+ } catch (error) {
+ console.error('Error fetching versions:', error);
+ setAvailableVersions([]);
+ }
+ };
+
+ fetchVersions();
+ }, [selectedPlatform, data.version]);
+
+ useEffect(() => {
+ if (availableVersions.length > 0 && !availableVersions.includes(selectedVersion)) {
+ setSelectedVersion(availableVersions[0]);
+ }
+ }, [availableVersions, selectedVersion]);
+
+ const handleDownload = async () => {
+ setLoading(true);
+ try {
+ const response = await fetch(
+ `http://${localStorage.getItem('ipAddress')}:${localStorage.getItem('port')}/installations/download/${selectedPlatform}/${selectedVersion}`,
+ { method: 'PUT' }
+ );
+ if (!response.ok) {
+ throw new Error('Failed to download server');
+ }
+ } catch (error) {
+ console.error('Error downloading server:', error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Current Platform: {data.platform || 'Not installed'}
+
Current Version: {data.version || 'Not installed'}
+
+
+
+
+ );
+};
+
+export default VersionTab;
\ No newline at end of file
diff --git a/front-end/src/components/card.js b/front-end/src/components/card.js
new file mode 100644
index 0000000..d83169c
--- /dev/null
+++ b/front-end/src/components/card.js
@@ -0,0 +1,40 @@
+
+import React from 'react';
+import styles from '../styles/Card.module.css';
+
+export const Card = ({ title, children }) => {
+ return (
+
+ {title &&
{title}
}
+
+ {children}
+
+
+ );
+};
+
+export const CardContainer = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const CardItem = ({ label, value }) => {
+ return (
+
+ {label}
+ {value}
+
+ );
+};
+
+export const CardGrid = ({ children }) => {
+ return (
+
+ {children}
+
+ );
+};
diff --git a/front-end/src/index.js b/front-end/src/index.js
index f045733..0cd8e78 100644
--- a/front-end/src/index.js
+++ b/front-end/src/index.js
@@ -6,24 +6,34 @@ import App from './App';
import { useState } from 'react';
import { Box, TextField, Button } from '@mui/material';
+// Read URL parameters and apply them to localStorage
+const urlParams = new URLSearchParams(window.location.search);
+const ipFromURL = urlParams.get('ip');
+const portFromURL = urlParams.get('port');
+if (ipFromURL && portFromURL) {
+ localStorage.setItem('ipAddress', ipFromURL);
+ localStorage.setItem('port', portFromURL);
-const IpForm = () => {
+ // Optional: Clean the URL after applying params
+ const cleanURL = new URL(window.location);
+ cleanURL.search = '';
+ window.history.replaceState({}, document.title, cleanURL.toString());
+}
+const IpForm = () => {
+ const [checkingURL, setCheckingURL] = useState(false);
+ const [ipAddress, setIpAddress] = useState('');
+ const [port, setPort] = useState('');
async function checkURL(ipAddress, port) {
try {
setCheckingURL(true);
const isIPv4 = ipAddress !== "localhost" ? /^(\d{1,3}\.){3}\d{1,3}$/.test(ipAddress) : "localhost";
- // console.log(`http://${isIPv4 ? ipAddress : `[${ipAddress}]`}:${port}/ping`);
const response = await fetch(`http://${isIPv4 ? ipAddress : `[${ipAddress}]`}:${port}/ping`);
setCheckingURL(false);
- if (response.ok) {
- return true;
- } else {
- return false;
- }
+ return response.ok;
} catch (error) {
console.error(error);
setCheckingURL(false);
@@ -31,21 +41,20 @@ const IpForm = () => {
}
}
-
- const [checkingURL, setCheckingURL] = useState(false);
- const [ipAddress, setIpAddress] = useState('');
- const [port, setPort] = useState('');
-
const handleSubmit = async (e) => {
e.preventDefault();
- if (await checkURL(ipAddress, port)){
+ if (await checkURL(ipAddress, port)) {
+ if(ipAddress === 'localhost') {
+ const publicIp = await fetch('https://ifconfig.me/ip')
+ const result = await checkURL(publicIp, port);
+ if(result === 200)
+ ipAddress = publicIp;
+ }
localStorage.setItem('ipAddress', ipAddress);
localStorage.setItem('port', port);
window.location.reload();
- }
- else{
+ } else {
alert("Invalid IP Address or Port, try again");
-
}
};
@@ -78,20 +87,16 @@ const IpForm = () => {
Submit
- )
-}
-
-
+ );
+};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
- {(!localStorage.getItem('ipAddress') || !localStorage.getItem('port')) ? (
-
- ) : (
-
- )}
+ {(!localStorage.getItem('ipAddress') || !localStorage.getItem('port')) ? (
+
+ ) : (
+
+ )}
);
-
-
diff --git a/front-end/src/server.properties.json b/front-end/src/server.properties.json
new file mode 100644
index 0000000..f057c9c
--- /dev/null
+++ b/front-end/src/server.properties.json
@@ -0,0 +1,249 @@
+
+{
+ "server-properties": {
+ "accepts-transfers": {
+ "default": false,
+ "description": "Controls whether server accepts player transfers"
+ },
+ "allow-flight": {
+ "default": false,
+ "description": "Allows players to fly in survival mode if set to true"
+ },
+ "allow-nether": {
+ "default": true,
+ "description": "Allows players to travel to the Nether"
+ },
+ "broadcast-console-to-ops": {
+ "default": true,
+ "description": "Send console output to all online operators"
+ },
+ "broadcast-rcon-to-ops": {
+ "default": true,
+ "description": "Send RCON output to all online operators"
+ },
+ "bug-report-link": {
+ "default": "",
+ "description": "Link for submitting bug reports"
+ },
+ "difficulty": {
+ "default": "easy",
+ "description": "Sets game difficulty (easy/normal/hard/peaceful)"
+ },
+ "enable-command-block": {
+ "default": false,
+ "description": "Enables command blocks"
+ },
+ "enable-jmx-monitoring": {
+ "default": false,
+ "description": "Enables JMX monitoring"
+ },
+ "enable-query": {
+ "default": false,
+ "description": "Enables GameSpy4 protocol server listener"
+ },
+ "enable-rcon": {
+ "default": false,
+ "description": "Enables remote access to server console"
+ },
+ "enable-status": {
+ "default": true,
+ "description": "Makes server appear in server list"
+ },
+ "enforce-secure-profile": {
+ "default": true,
+ "description": "Enforces secure profile settings"
+ },
+ "enforce-whitelist": {
+ "default": false,
+ "description": "Enforces whitelist on the server"
+ },
+ "entity-broadcast-range-percentage": {
+ "default": 100,
+ "description": "Controls how far entities are broadcast to players"
+ },
+ "force-gamemode": {
+ "default": false,
+ "description": "Forces players to join in the default game mode"
+ },
+ "function-permission-level": {
+ "default": 2,
+ "description": "Permission level for function commands"
+ },
+ "gamemode": {
+ "default": "survival",
+ "description": "Default game mode for new players"
+ },
+ "generate-structures": {
+ "default": true,
+ "description": "Defines whether structures generate in world"
+ },
+ "generator-settings": {
+ "default": "{}",
+ "description": "Settings used to customize world generation"
+ },
+ "hardcore": {
+ "default": false,
+ "description": "If true, players are banned upon death"
+ },
+ "hide-online-players": {
+ "default": false,
+ "description": "Hides online players in server status"
+ },
+ "initial-disabled-packs": {
+ "default": "",
+ "description": "Data packs disabled by default"
+ },
+ "initial-enabled-packs": {
+ "default": "vanilla",
+ "description": "Data packs enabled by default"
+ },
+ "level-name": {
+ "default": "world",
+ "description": "The name of the world"
+ },
+ "level-seed": {
+ "default": "",
+ "description": "Seed for world generation"
+ },
+ "level-type": {
+ "default": "minecraft:normal",
+ "description": "Defines the type of world to generate"
+ },
+ "log-ips": {
+ "default": true,
+ "description": "Log player IP addresses"
+ },
+ "max-chained-neighbor-updates": {
+ "default": 1000000,
+ "description": "Maximum number of chained neighbor updates"
+ },
+ "max-players": {
+ "default": 20,
+ "description": "Maximum number of players allowed"
+ },
+ "max-tick-time": {
+ "default": 60000,
+ "description": "Maximum milliseconds per tick before server timeout"
+ },
+ "max-world-size": {
+ "default": 29999984,
+ "description": "Maximum world size in blocks"
+ },
+ "motd": {
+ "default": "A Minecraft Server",
+ "description": "Message displayed in server list"
+ },
+ "network-compression-threshold": {
+ "default": 256,
+ "description": "Limits network compression threshold in bytes"
+ },
+ "online-mode": {
+ "default": true,
+ "description": "Requires players to be authenticated with Minecraft account"
+ },
+ "op-permission-level": {
+ "default": 4,
+ "description": "Sets permission level for operators"
+ },
+ "pause-when-empty-seconds": {
+ "default": 60,
+ "description": "Time in seconds before pausing empty server"
+ },
+ "player-idle-timeout": {
+ "default": 0,
+ "description": "Time in minutes before idle players are kicked"
+ },
+ "prevent-proxy-connections": {
+ "default": false,
+ "description": "Prevents connections through proxies"
+ },
+ "pvp": {
+ "default": true,
+ "description": "Enables player vs player combat"
+ },
+ "query.port": {
+ "default": 25565,
+ "description": "Port for GameSpy4 protocol server listener"
+ },
+ "rate-limit": {
+ "default": 0,
+ "description": "Sets the rate limit for connections"
+ },
+ "rcon.password": {
+ "default": "",
+ "description": "Password for RCON"
+ },
+ "rcon.port": {
+ "default": 25575,
+ "description": "Port for RCON"
+ },
+ "region-file-compression": {
+ "default": "deflate",
+ "description": "Compression type for region files"
+ },
+ "require-resource-pack": {
+ "default": false,
+ "description": "Forces clients to use server resource pack"
+ },
+ "resource-pack": {
+ "default": "",
+ "description": "URL to resource pack"
+ },
+ "resource-pack-id": {
+ "default": "",
+ "description": "Unique identifier for resource pack"
+ },
+ "resource-pack-prompt": {
+ "default": "",
+ "description": "Message shown when resource pack is offered"
+ },
+ "resource-pack-sha1": {
+ "default": "",
+ "description": "SHA1 hash of resource pack"
+ },
+ "server-ip": {
+ "default": "",
+ "description": "IP address to bind server to"
+ },
+ "server-port": {
+ "default": 25565,
+ "description": "Port the server listens on"
+ },
+ "simulation-distance": {
+ "default": 10,
+ "description": "Maximum simulation distance in chunks"
+ },
+ "spawn-monsters": {
+ "default": true,
+ "description": "Determines if monsters can spawn"
+ },
+ "spawn-protection": {
+ "default": 16,
+ "description": "Radius of spawn area protection"
+ },
+ "sync-chunk-writes": {
+ "default": true,
+ "description": "Enables synchronous chunk writes"
+ },
+ "text-filtering-config": {
+ "default": "",
+ "description": "Configuration for text filtering"
+ },
+ "text-filtering-version": {
+ "default": 0,
+ "description": "Version of text filtering to use"
+ },
+ "use-native-transport": {
+ "default": true,
+ "description": "Uses native transport if available"
+ },
+ "view-distance": {
+ "default": 10,
+ "description": "Maximum view distance in chunks"
+ },
+ "white-list": {
+ "default": false,
+ "description": "Enables server whitelist"
+ }
+ }
+}
diff --git a/front-end/src/styles/Card.module.css b/front-end/src/styles/Card.module.css
new file mode 100644
index 0000000..a8c359d
--- /dev/null
+++ b/front-end/src/styles/Card.module.css
@@ -0,0 +1,62 @@
+
+.card {
+
+ background: var(--code-bg-light);
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ padding: 20px;
+ margin-bottom: 20px;
+}
+
+.cardTitle {
+ font-size: 1.5rem;
+ margin-bottom: 15px;
+ color: #333;
+}
+
+.cardContent {
+ width: 100%;
+}
+
+.cardContainer {
+ width: 100%;
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 10px;
+ overflow-y: auto;
+
+}
+
+.cardItem {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 0;
+ border-bottom: 1px solid var(--secondary);
+}
+
+.cardLabel {
+ font-weight: 500;
+ color: #0c0c0c;
+}
+
+.cardValue {
+ color: var(--code-bg);
+}
+
+.cardGrid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ color: #0c0c0c;
+ gap: 20px;
+ width: 100%;
+}
+
+.truncatedText {
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ max-width: 100px; /* or whatever fits your layout */
+ display: inline-block;
+ vertical-align: bottom;
+}
\ No newline at end of file
diff --git a/front-end/src/styles/Console.module.css b/front-end/src/styles/Console.module.css
index 619e691..0b15a55 100644
--- a/front-end/src/styles/Console.module.css
+++ b/front-end/src/styles/Console.module.css
@@ -1,5 +1,5 @@
.viewbox{
- background-color: var(--secondary);
+ background-color: var(--code-bg);
width: 100%;
padding-top: 1em;
@@ -17,7 +17,7 @@
}
.consoleTextBox{
- background-color: var(--secondary);
+ background-color: var(--code-bg);
width: 100%;
padding-left: 1em;
@@ -35,14 +35,14 @@
.consoleInputField{
- background-color: var(--light-secondary);
+ background-color: var(--code-bg-light);
padding: 0.25em 0.25em 0.25em 0.25em;
border-radius: 1em;
width: 95%;
color: white;
}
.consoleInputButton{
- background-color: var(--light-secondary);
+ background-color: var(--code-bg-light);
padding: 0.25em 0.25em 0.25em 0.25em;
border-radius: 1em;
color: black;
diff --git a/front-end/src/styles/InfoTab.module.css b/front-end/src/styles/InfoTab.module.css
index 208aad2..133bda4 100644
--- a/front-end/src/styles/InfoTab.module.css
+++ b/front-end/src/styles/InfoTab.module.css
@@ -1,4 +1,6 @@
.card{
background-color: var(--element);
color: var(--text);
-}
\ No newline at end of file
+}
+
+
diff --git a/front-end/src/styles/Navbar.module.css b/front-end/src/styles/Navbar.module.css
new file mode 100644
index 0000000..4cf3f49
--- /dev/null
+++ b/front-end/src/styles/Navbar.module.css
@@ -0,0 +1,61 @@
+.navbar {
+ background-color: rgb(36, 36, 36);
+ width: 100%;
+ position: absolute;
+ top: 0;
+ padding: 0.5rem 1rem;
+ box-sizing: border-box;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 1rem;
+}
+
+.shareButton {
+ background-color: transparent;
+ border: 1px solid rgba(255, 255, 255, 0.1); /* A light color, similar to bootstrap's 'light' */
+ color: #f8f9fa;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ transition: color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out;
+ font-family: inherit; /* Ensure button inherits font */
+}
+
+.shareButton:hover {
+ color: #212529; /* A dark color for text on hover */
+ background-color: #f8f9fa;
+ border-color: #f8f9fa;
+}
+
+.shareButton:focus {
+ outline: 0;
+ box-shadow: 0 0 0 0.2rem rgba(248, 249, 250, 0.5);
+}
+
+.ipDisplay {
+ color: #f8f9fa;
+ background-color: rgba(0, 0, 0, 0.2);
+ padding: 0.375rem 0.75rem;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ transition: background-color 0.15s ease-in-out;
+ display: flex;
+ align-items: center;
+ font-family: inherit;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+.ipDisplay:hover {
+ background-color: rgba(0, 0, 0, 0.4);
+}
+
+.ipDisplay code {
+ font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
+ background-color: rgba(0, 0, 0, 0.4);
+ padding: 0.2rem 0.4rem;
+ border-radius: 0.2rem;
+ margin-left: 0.5rem;
+}
diff --git a/front-end/src/styles/PropertiesTab.module.css b/front-end/src/styles/PropertiesTab.module.css
new file mode 100644
index 0000000..3558d92
--- /dev/null
+++ b/front-end/src/styles/PropertiesTab.module.css
@@ -0,0 +1,129 @@
+.container {
+ padding: 0;
+ margin: 0;
+ overflow-y: auto;
+ width: 100%;
+
+}
+
+.property {
+ padding: 5px;
+ border-radius: 8px;
+ background-color: var(--code-accent);
+ margin-bottom: 10px;
+}
+
+.propertyHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 6px;
+}
+
+.propertyName {
+ font-weight: 600;
+ font-size: 1rem;
+ color: #333;
+ margin-bottom: 8px;
+}
+
+.propertyDescription {
+ font-size: 0.6rem;
+ color: var(--code-text);
+ line-height: 1.3;
+ margin-bottom: 6px;
+}
+
+.container::-webkit-scrollbar {
+ width: 8px;
+}
+
+.container::-webkit-scrollbar-track {
+ background: #f1f1f1;
+ border-radius: 4px;
+}
+
+.container::-webkit-scrollbar-thumb {
+ background: #007bff;
+ border-radius: 4px;
+}
+
+.container::-webkit-scrollbar-thumb:hover {
+ background: #0056b3;
+}
+
+.property {
+ display: flex;
+ flex-direction: column;
+ padding: 10px;
+ background-color: var(--code-accent);
+ border-radius: 4px;
+}
+
+.propertyHeader {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+}
+
+.propertyName {
+ font-weight: 500;
+}
+
+.select {
+ position: relative;
+ display: inline-block;
+ width: 120px;
+ height: 34px;
+ padding: 0 10px;
+ border: 1px solid black;
+ border-radius: 4px;
+ background-color: var(--code-accent);
+ cursor: pointer;
+ transition: .4s;
+}
+
+.select:focus {
+ outline: none;
+ border-color: #2196F3;
+ box-shadow: 0 0 1px #2196F3;
+}
+
+.numberInput {
+ position: relative;
+ display: inline-block;
+ width: 80px;
+ height: 34px;
+ padding: 0 10px;
+ border: 1px solid black;
+ border-radius: 4px;
+ background-color: var(--code-accent);
+ transition: .4s;
+}
+
+.numberInput:focus {
+ outline: none;
+ border-color: #2196F3;
+ box-shadow: 0 0 1px #2196F3;
+}
+
+.container {
+ padding: 20px;
+}
+
+.advancedProperties {
+ margin-top: 20px;
+}
+
+.advancedProperties summary {
+ cursor: pointer;
+ padding: 10px;
+ color: #333;
+ background-color: var(--code-accent);
+ border-radius: 4px;
+ font-weight: 500;
+}
+
+.advancedProperties summary:hover {
+ background-color: var(--code-accent);
+}
diff --git a/front-end/src/styles/ServerWindow.module.css b/front-end/src/styles/ServerWindow.module.css
index ffcc0e7..43987bf 100644
--- a/front-end/src/styles/ServerWindow.module.css
+++ b/front-end/src/styles/ServerWindow.module.css
@@ -5,6 +5,7 @@
justify-content: center;
min-width: 50em;
max-width: 60em;
+ max-height: 50em;
gap: 1em;
width: 100%; /* Make the sub-container take up the full width of its parent */
padding: 2em 2em 2em 2em;
@@ -14,25 +15,26 @@
display: flex;
min-height: 37em;
color: aliceblue;
- background-color: var(--secondary);
+ background-color: var(--code-bg);
outline: 0.25em solid var(--dark-third);
border-radius: 1em;
margin-left: 2em;
min-width: 55em;
-
+ max-height: 90%;
+ overflow-y: auto;
}
.container {
display: flex;
- min-height: 80%;
+ max-height: 43em;
border-radius: 0.5em;
align-items: center;
justify-content: center;
flex-direction: row;
width:100%;
- height: 100%;
+ overflow: hidden;
gap: 3px;
outline: 1px solid;
outline-color: var(--primary);
diff --git a/front-end/src/styles/StartStopBtn.module.css b/front-end/src/styles/StartStopBtn.module.css
new file mode 100644
index 0000000..50c2e87
--- /dev/null
+++ b/front-end/src/styles/StartStopBtn.module.css
@@ -0,0 +1,95 @@
+.button {
+ position: relative;
+ display: inline-block;
+ margin: 20px;
+ width: 100%;
+ font-family: Helvetica, sans-serif;
+ font-weight: bold;
+ font-size: 36px;
+ text-align: center;
+ border: none;
+ padding: 20px 40px;
+ color: white;
+ text-shadow: 0px 1px 0px #000;
+ border-radius: 5px;
+ transition: background-color 0.3s;
+}
+
+.button:active {
+ top: 10px;
+}
+
+.button:after {
+ content: "";
+ height: 100%;
+ width: 100%;
+ padding: 4px;
+ position: absolute;
+ bottom: -15px;
+ left: -4px;
+ z-index: -1;
+ border-radius: 5px;
+}
+
+.success {
+ background-color: #28a745;
+ cursor: pointer;
+ color: white;
+ box-shadow: inset 0 1px 0 #4caf50, 0 10px 0 #1e7e34;
+}
+
+.success:active {
+ background-color: #218838;
+ box-shadow: inset 0 1px 0 #4caf50, inset 0 -3px 0 #1e7e34;
+}
+
+.success:after {
+ background-color: #145523;
+}
+
+.danger {
+ background-color: #dc3545;
+ cursor: pointer;
+ color: white;
+ box-shadow: inset 0 1px 0 #e4606d, 0 10px 0 #bd2130;
+}
+
+.danger:active {
+ background-color: #c82333;
+ box-shadow: inset 0 1px 0 #e4606d, inset 0 -3px 0 #bd2130;
+}
+
+.danger:after {
+ background-color: #921925;
+}
+.warning {
+ background-color: #f0ad4e; /* Bootstrap warning base */
+ cursor: not-allowed;
+ color: white;
+ box-shadow: inset 0 1px 0 #ffc878, 0 10px 0 #d98c0a;
+}
+
+.warning:active {
+ background-color: #ec971f;
+ box-shadow: inset 0 1px 0 #ffc878, inset 0 -3px 0 #d98c0a;
+}
+
+.warning:after {
+ background-color: #b76b00;
+}
+
+.secondary {
+ background-color: #6c757d;
+ cursor: not-allowed;
+ color: white;
+ box-shadow: inset 0 1px 0 #adb5bd, 0 10px 0 #545b62;
+}
+
+.secondary:active {
+ background-color: #5a6268;
+ box-shadow: inset 0 1px 0 #adb5bd, inset 0 -3px 0 #545b62;
+}
+
+.secondary:after {
+ background-color: #343a40;
+}
diff --git a/front-end/src/styles/Switch.module.css b/front-end/src/styles/Switch.module.css
new file mode 100644
index 0000000..12f89e3
--- /dev/null
+++ b/front-end/src/styles/Switch.module.css
@@ -0,0 +1,70 @@
+.switch {
+ position: relative;
+ display: inline-block;
+ width: 60px;
+ height: 34px;
+}
+
+.switch input {
+ opacity: 0;
+ width: 0;
+ height: 0;
+}
+
+.slider {
+ position: absolute;
+ cursor: pointer;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background-color: #ff4444;
+ transition: .4s;
+ border-radius: 34px;
+}
+
+.slider:before {
+ position: absolute;
+ content: "✕";
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #ff4444;
+ height: 26px;
+ width: 26px;
+ left: 4px;
+ bottom: 4px;
+ background-color: white;
+ transition: .4s;
+ border-radius: 50%;
+}
+
+input:checked + .slider {
+ background-color: #4CAF50;
+}
+
+input:focus + .slider {
+ box-shadow: 0 0 1px #4CAF50;
+}
+
+input:checked + .slider:before {
+ content: "✓";
+ color: #4CAF50;
+ transform: translateX(26px);
+}
+
+/* Disabled/null state styles */
+.switch.disabled .slider {
+ background-color: #cccccc;
+ cursor: not-allowed;
+}
+
+.switch.disabled .slider:before {
+ content: "—";
+ color: #888888;
+ transform: translateX(13px); /* Center the slider */
+}
+
+.switch.disabled .slider:hover {
+ background-color: #cccccc; /* Prevent hover effects */
+}
diff --git a/front-end/src/styles/VersionTab.module.css b/front-end/src/styles/VersionTab.module.css
new file mode 100644
index 0000000..b7585dc
--- /dev/null
+++ b/front-end/src/styles/VersionTab.module.css
@@ -0,0 +1,73 @@
+
+/* VersionTab.module.css */
+.container {
+ width: 100%;
+ height: 100%;
+}
+
+.versionSelector {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.selectorGroup {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.selectorGroup label {
+ font-weight: 500;
+ color: #333;
+}
+
+.select {
+ padding: 8px 12px;
+ border-radius: 4px;
+ border: 1px solid #000000;
+ background-color: var(--code-accent);
+ font-size: 14px;
+ width: 100%;
+}
+
+.select:disabled {
+ background-color: #2c2c2c;
+ cursor: not-allowed;
+}
+
+.downloadButton {
+ padding: 10px 20px;
+ background-color: #4CAF50;
+ color: var(--code-bg-light);
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+ transition: background-color 0.3s;
+}
+
+.downloadButton:hover {
+ background-color: #45a049;
+}
+
+.downloadButton:disabled {
+ background-color: #cccccc;
+ cursor: not-allowed;
+}
+
+.currentVersion {
+ margin-top: 20px;
+ padding-top: 20px;
+ border-top: 1px solid black;
+}
+
+.currentVersion p {
+ margin: 8px 0;
+ color: #666;
+}
+
+.currentVersion span {
+ font-weight: 500;
+ color: #333;
+}
diff --git a/front-end/src/styles/theme.css b/front-end/src/styles/theme.css
index 9407d2a..61488d9 100644
--- a/front-end/src/styles/theme.css
+++ b/front-end/src/styles/theme.css
@@ -8,6 +8,10 @@
--element: #8BB174;
--light-element: #B5CA8D;
--text-color: #000000;
+ --code:#d63384;
+ --code-bg: #381e2b;
+ --code-bg-light:#61384d;
+ --code-accent: #8b4e6d;
}
[data-theme='dark'] {
diff --git a/front-end/src/utils/monitor.js b/front-end/src/utils/monitor.js
deleted file mode 100644
index cab2173..0000000
--- a/front-end/src/utils/monitor.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useState, useEffect } from "react";
-
-export const useServerStatus = () => {
- const [serverStatus, setServerStatus] = useState(false);
-
- useEffect(() => {
- const interval = setInterval(async () => {
- try {
- const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/check-server`);
- const data = await response.json();
- setServerStatus(data);
- } catch (error) {
- console.error("Failed to fetch server status:", error);
- setServerStatus(false);
- }
- }, 2000);
-
- return () => clearInterval(interval);
- }, []);
-
- return serverStatus;
-};
-
-export const getServerStatus = async () => {
- try {
- const response = await fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/server/check-server`);
- const data = await response.json();
- return data;
- } catch (error) {
- console.error("Failed to fetch server status:", error);
- return false;
- }
-}
\ No newline at end of file
diff --git a/front-end/src/utils/serverDataContext.js b/front-end/src/utils/serverDataContext.js
new file mode 100644
index 0000000..ca3beff
--- /dev/null
+++ b/front-end/src/utils/serverDataContext.js
@@ -0,0 +1,179 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+
+const ServerDataContext = createContext(null);
+export const ServerDataProvider = ({ children }) => {
+ const [data, setData] = useState({
+ memoryUsage: {
+ cpu: "0%",
+ usedMB: "0",
+ },
+ platform: null,
+ version: null,
+ directorySizeMB: null,
+ uptime: null,
+ allow_nether: null,
+ broadcast_console_to_ops: null,
+ broadcast_rcon_to_ops: null,
+ difficulty: null,
+ enable_command_block: null,
+ enable_jmx_monitoring: null,
+ enable_rcon: null,
+ enable_status: null,
+ enforce_whitelist: null,
+ entity_broadcast_range_percentage: null,
+ force_gamemode: null,
+ function_permission_level: null,
+ gamemode: null,
+ generate_structures: null,
+ hardcore: null,
+ hide_online_players: null,
+ level_name: null,
+ level_seed: null,
+ level_type: null,
+ max_players: null,
+ max_tick_time: null,
+ max_world_size: null,
+ motd: null,
+ network_compression_threshold: null,
+ online_mode: null,
+ op_permission_level: null,
+ player_idle_timeout: null,
+ prevent_proxy_connections: null,
+ pvp: null,
+ query_port: null,
+ rate_limit: null,
+ rcon_password: null,
+ rcon_port: null,
+ require_resource_pack: null,
+ resource_pack: null,
+ resource_pack_prompt: null,
+ resource_pack_sha1: null,
+ server_ip: null,
+ server_port: null,
+ simulation_distance: null,
+ spawn_animals: null,
+ spawn_monsters: null,
+ spawn_npcs: null,
+ spawn_protection: null,
+ sync_chunk_writes: null,
+ text_filtering_config: null,
+ use_native_transport: null,
+ view_distance: null,
+ white_list: null
+ });
+
+ const toSnakeCase = str => str.replace(/-/g, '_').replace(/\./g, '_');
+
+
+ useEffect(() => {
+ console.log("Updated context data:", data);
+ }, [data]);
+
+ const normalizeKeys = obj =>
+ Object.fromEntries(
+ Object.entries(obj).map(([key, value]) => [toSnakeCase(key), value])
+ );
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const [infores, propertiesResRaw] = await Promise.all([
+ fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/info/`).then(res => res.json()),
+ fetch(`http://${localStorage.getItem("ipAddress")}:${localStorage.getItem("port")}/properties/`).then(res => res.json()),
+ ]);
+
+ const propertiesRes = normalizeKeys(propertiesResRaw);
+
+
+ // console.log("infores:", infores);
+ // console.log("propertiesResRaw:", propertiesResRaw);
+ // console.log("normalized propertiesRes:", propertiesRes);
+ // console.log("data:", data);
+
+ setData(prev => ({
+ ...prev,
+ ...infores,
+ ...propertiesRes
+ }));
+ } catch (error) {
+ console.error("Fetch error:", error);
+ const resetAllToNull = () => {
+ setData({
+ memoryUsage: {
+ cpu: "0%",
+ usedMB: "0",
+ },
+ platform: null,
+ version: null,
+ directorySizeMB: 0,
+ uptime: null,
+ allow_nether: null,
+ broadcast_console_to_ops: null,
+ broadcast_rcon_to_ops: null,
+ difficulty: null,
+ enable_command_block: null,
+ enable_jmx_monitoring: null,
+ enable_rcon: null,
+ enable_status: null,
+ enforce_whitelist: null,
+ entity_broadcast_range_percentage: null,
+ force_gamemode: null,
+ function_permission_level: null,
+ gamemode: null,
+ generate_structures: null,
+ hardcore: null,
+ hide_online_players: null,
+ level_name: null,
+ level_seed: null,
+ level_type: null,
+ max_players: null,
+ max_tick_time: null,
+ max_world_size: null,
+ motd: null,
+ network_compression_threshold: null,
+ online_mode: null,
+ op_permission_level: null,
+ player_idle_timeout: null,
+ prevent_proxy_connections: null,
+ pvp: null,
+ query_port: null,
+ rate_limit: null,
+ rcon_password: null,
+ rcon_port: null,
+ require_resource_pack: null,
+ resource_pack: null,
+ resource_pack_prompt: null,
+ resource_pack_sha1: null,
+ server_ip: null,
+ server_port: null,
+ simulation_distance: null,
+ spawn_animals: null,
+ spawn_monsters: null,
+ spawn_npcs: null,
+ spawn_protection: null,
+ sync_chunk_writes: null,
+ text_filtering_config: null,
+ use_native_transport: null,
+ view_distance: null,
+ white_list: null
+ });
+ };
+ resetAllToNull();
+ }
+ };
+
+
+ fetchData();
+ const interval = setInterval(fetchData, 2500);
+
+ return () => clearInterval(interval);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useServerData = () => useContext(ServerDataContext);
diff --git a/installer.bat b/installer.bat
new file mode 100644
index 0000000..1e09423
--- /dev/null
+++ b/installer.bat
@@ -0,0 +1,40 @@
+@echo off
+setlocal
+echo === Running setup scripts... ===
+
+REM Navigate to the scripts\windows directory
+cd scripts\windows || (
+ echo ❌ Error: scripts\windows not found
+ pause
+ exit /b 1
+)
+
+REM === Check and set up Minecraft server ===
+echo [1/4] Running check_minecraft_server.bat...
+call check_minecraft_server.bat
+IF ERRORLEVEL 1 (
+ echo ❌ check_minecraft_server.bat failed
+ pause
+ goto :EOF
+)
+
+REM === Install dependencies ===
+echo [2/4] Running install_dependencies.bat...
+call install_dependencies.bat
+IF ERRORLEVEL 1 (
+ echo ❌ install_dependencies.bat failed
+ pause
+ goto :EOF
+)
+
+REM Return to project root (2 levels up)
+cd ..\.. || (
+ echo ❌ Failed to return to project root
+ pause
+ goto :EOF
+)
+
+echo ✅ All scripts completed successfully.
+:EOF
+pause
+exit /b
\ No newline at end of file
diff --git a/installer.exe b/installer.exe
new file mode 100755
index 0000000..e4f0775
Binary files /dev/null and b/installer.exe differ
diff --git a/installer.py b/installer.py
new file mode 100644
index 0000000..c341788
--- /dev/null
+++ b/installer.py
@@ -0,0 +1,333 @@
+import tkinter as tk
+from tkinter import ttk, scrolledtext, messagebox
+import subprocess
+import os
+import sys
+import threading
+import json
+import urllib.request
+from pathlib import Path
+import ctypes
+import platform
+
+class InstallerGUI:
+ def __init__(self, root):
+ self.root = root
+ self.servers_running = False
+ self.server_processes = []
+ self.root.title("Navarch Installer & Server Manager")
+ self.root.geometry("800x600")
+ self.root.resizable(True, True)
+
+ if self.is_windows() and not self.is_admin():
+ self.request_admin()
+ return
+
+ self.setup_ui()
+
+ def is_windows(self):
+ return platform.system() == "Windows"
+
+ def is_linux(self):
+ return platform.system() == "Linux"
+
+ def is_admin(self):
+ if self.is_windows():
+ try:
+ return ctypes.windll.shell32.IsUserAnAdmin()
+ except:
+ return False
+ elif self.is_linux():
+ return os.geteuid() == 0
+ return True
+
+ def request_admin(self):
+ if self.is_windows():
+ try:
+ ctypes.windll.shell32.ShellExecuteW(
+ None, "runas", sys.executable, " ".join(sys.argv), None, 1
+ )
+ except:
+ messagebox.showerror("Error", "Administrator privileges required!")
+ finally:
+ self.root.quit()
+ elif self.is_linux():
+ messagebox.showerror("Error", "Please run this script as root using: sudo python3 installer.py")
+ self.root.quit()
+
+ def setup_ui(self):
+ main_frame = ttk.Frame(self.root, padding="10")
+ main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S))
+
+ self.root.columnconfigure(0, weight=1)
+ self.root.rowconfigure(0, weight=1)
+ main_frame.columnconfigure(1, weight=1)
+ main_frame.rowconfigure(4, weight=1)
+
+ title_label = ttk.Label(main_frame, text="Navarch Installer & Server Manager",
+ font=("Arial", 16, "bold"))
+ title_label.grid(row=0, column=0, columnspan=2, pady=(0, 20))
+
+ buttons_frame = ttk.Frame(main_frame)
+ buttons_frame.grid(row=1, column=0, columnspan=2, pady=(0, 10), sticky=(tk.W, tk.E))
+ for i in range(3):
+ buttons_frame.columnconfigure(i, weight=1)
+
+ self.install_btn = ttk.Button(buttons_frame, text="🔧 Install Dependencies",
+ command=self.run_installer, width=20)
+ self.install_btn.grid(row=0, column=0, padx=(0, 5), sticky=(tk.W, tk.E))
+
+ self.start_btn = ttk.Button(buttons_frame, text="🚀 Start Navarch", command=self.toggle_servers)
+ self.start_btn.grid(row=0, column=1, padx=5, sticky=(tk.W, tk.E))
+
+ self.close_btn = ttk.Button(buttons_frame, text="❌ Close", command=self.close_app)
+ self.close_btn.grid(row=0, column=2, padx=(5, 0), sticky=(tk.W, tk.E))
+
+ self.progress = ttk.Progressbar(main_frame, mode='indeterminate')
+ self.progress.grid(row=2, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10))
+
+ self.status_label = ttk.Label(main_frame, text="Ready to install")
+ self.status_label.grid(row=3, column=0, columnspan=2, pady=(0, 10))
+
+ self.output_text = scrolledtext.ScrolledText(main_frame, height=20, width=80)
+ self.output_text.grid(row=4, column=0, columnspan=2, sticky=(tk.W, tk.E, tk.N, tk.S))
+
+ self.output_text.tag_configure("success", foreground="green")
+ self.output_text.tag_configure("error", foreground="red")
+ self.output_text.tag_configure("info", foreground="blue")
+ self.output_text.tag_configure("warning", foreground="orange")
+
+ def log_output(self, message, tag="normal"):
+ self.output_text.insert(tk.END, f"{message}\n", tag)
+ self.output_text.see(tk.END)
+ self.root.update_idletasks()
+
+ def run_command(self, command, cwd=None):
+ try:
+ result = subprocess.run(
+ command,
+ shell=True,
+ capture_output=True,
+ text=True,
+ cwd=cwd
+ )
+ return result.returncode == 0, result.stdout, result.stderr
+ except Exception as e:
+ return False, "", str(e)
+
+ def run_and_log(self, command, success_msg, error_msg):
+ success, _, stderr = self.run_command(command)
+ if success:
+ self.log_output(f"✅ {success_msg}", "success")
+ else:
+ self.log_output(f"❌ {error_msg}: {stderr}", "error")
+ return success
+
+ def check_node_js(self):
+ self.log_output("Checking for Node.js...", "info")
+ success, stdout, _ = self.run_command("node --version")
+ if success:
+ self.log_output(f"✅ Node.js is installed: {stdout.strip()}", "success")
+ return True
+ else:
+ self.log_output("❌ Node.js is not installed", "error")
+ return False
+
+ def install_node_js(self):
+ if self.is_windows():
+ self.log_output("Installing Node.js...", "info")
+ return self.run_and_log(
+ "winget install --silent --accept-package-agreements --accept-source-agreements OpenJS.NodeJS",
+ "Node.js installed successfully",
+ "Failed to install Node.js"
+ )
+ else:
+ self.log_output("Installing Node.js for Linux...", "info")
+ return self.run_and_log(
+ "sudo pacman -Sy --noconfirm nodejs npm",
+ "Node.js and NPM installed",
+ "Failed to install Node.js on Linux"
+ )
+
+ def check_java(self):
+ self.log_output("Checking for Java JDK...", "info")
+ success, stdout, _ = self.run_command("java --version")
+ if success:
+ for line in stdout.splitlines():
+ if 'version' in line.lower():
+ try:
+ version_str = line.split()[1].strip('"')
+ major_version = int(version_str.split('.')[0])
+ if major_version >= 21:
+ self.log_output(f"✅ Java JDK {version_str} is installed", "success")
+ return True
+ else:
+ self.log_output(f"⚠️ Java version {version_str} is too old (need 21+)", "warning")
+ return False
+ except:
+ pass
+ self.log_output("❌ Java JDK 21+ is not installed", "error")
+ return False
+
+ def install_java(self):
+ if self.is_windows():
+ self.log_output("Installing OpenJDK 21...", "info")
+ return self.run_and_log(
+ "winget install EclipseAdoptium.Temurin.21.JDK --silent --accept-package-agreements --accept-source-agreements",
+ "OpenJDK 21 installed successfully",
+ "Failed to install OpenJDK 21"
+ )
+ else:
+ self.log_output("Installing OpenJDK 21 for Linux...", "info")
+ return self.run_and_log(
+ "sudo pacman -Sy --noconfirm jdk-openjdk",
+ "OpenJDK installed",
+ "Failed to install OpenJDK 21 on Linux"
+ )
+
+ def check_minecraft_server(self):
+ self.log_output("Checking for Minecraft server JAR...", "info")
+ server_path = Path("server/server.jar")
+
+ if server_path.exists():
+ self.log_output("✅ server.jar already exists", "success")
+ return True
+
+ self.log_output("server.jar not found. Setting up Minecraft server...", "warning")
+ Path("server").mkdir(exist_ok=True)
+
+ try:
+ with urllib.request.urlopen("https://launchermeta.mojang.com/mc/game/version_manifest.json") as response:
+ version_data = json.loads(response.read().decode())
+
+ latest_version = version_data["latest"]["release"]
+ version_info = next(v for v in version_data["versions"] if v["id"] == latest_version)
+
+ with urllib.request.urlopen(version_info["url"]) as response:
+ version_json = json.loads(response.read().decode())
+
+ server_jar_url = version_json["downloads"]["server"]["url"]
+ urllib.request.urlretrieve(server_jar_url, server_path)
+
+ if server_path.exists():
+ self.log_output("✅ Minecraft server.jar downloaded successfully", "success")
+ return True
+ except Exception as e:
+ self.log_output(f"❌ Failed to download server.jar: {str(e)}", "error")
+ return False
+
+ def install_npm_dependencies(self):
+ self.log_output("Installing NPM dependencies...", "info")
+
+ for name, path in [("front-end", "front-end"), ("back-end", "api")]:
+ if not Path(f"{path}/node_modules").exists():
+ self.log_output(f"Installing {name} dependencies...", "info")
+ success, _, stderr = self.run_command("npm install", cwd=path)
+ if not success:
+ self.log_output(f"❌ Failed to install {name} dependencies: {stderr}", "error")
+ return False
+ self.log_output(f"✅ {name.capitalize()} dependencies installed", "success")
+ else:
+ self.log_output(f"✅ {name.capitalize()} dependencies already installed", "success")
+
+ self.log_output("Installing serve globally...", "info")
+ success, _, stderr = self.run_command("npm install -g serve")
+ if success:
+ self.log_output("✅ Serve installed globally", "success")
+ else:
+ self.log_output(f"⚠️ Warning: Failed to install serve globally: {stderr}", "warning")
+ if self.is_linux():
+ messagebox.showwarning(
+ "Permission Error",
+ "Could not install 'serve' globally due to permissions. You can install it manually with:\n\n sudo npm install -g serve"
+ )
+
+ return True
+
+ def run_installer_thread(self):
+ try:
+ self.progress.start()
+ self.status_label.config(text="Installing dependencies...")
+ self.install_btn.config(state="disabled")
+
+ self.log_output("=== Running setup scripts... ===", "info")
+
+ if not self.check_node_js() and not self.install_node_js():
+ raise Exception("Failed to install Node.js")
+
+ if not self.check_java() and not self.install_java():
+ raise Exception("Failed to install Java JDK")
+
+ if not self.check_minecraft_server():
+ raise Exception("Failed to setup Minecraft server")
+
+ if not self.install_npm_dependencies():
+ raise Exception("Failed to install NPM dependencies")
+
+ self.log_output("✅ All scripts completed successfully!", "success")
+ self.status_label.config(text="Installation completed successfully")
+ messagebox.showinfo("Success", "Installation completed successfully!")
+
+ except Exception as e:
+ self.log_output(f"❌ Installation failed: {str(e)}", "error")
+ self.status_label.config(text="Installation failed")
+ messagebox.showerror("Error", f"Installation failed: {str(e)}")
+ finally:
+ self.progress.stop()
+ self.install_btn.config(state="normal")
+
+ def run_installer(self):
+ threading.Thread(target=self.run_installer_thread, daemon=True).start()
+
+ def toggle_servers(self):
+ if self.servers_running:
+ self.stop_servers()
+ else:
+ threading.Thread(target=self.start_servers_thread, daemon=True).start()
+
+ def start_servers_thread(self):
+ try:
+ self.progress.start()
+ self.status_label.config(text="Starting servers...")
+ self.start_btn.config(state="disabled")
+
+ self.log_output("🚀 Starting backend server...", "info")
+ backend_proc = subprocess.Popen("npm start", cwd="api", shell=True)
+
+ self.log_output("🚀 Starting frontend server...", "info")
+ frontend_proc = subprocess.Popen("serve -s build", cwd="front-end", shell=True)
+
+ self.server_processes = [backend_proc, frontend_proc]
+ self.servers_running = True
+
+ self.start_btn.config(text="🛑 Stop Navarch")
+ self.status_label.config(text="Servers running")
+ self.log_output("✅ Servers started", "success")
+ except Exception as e:
+ self.log_output(f"❌ Failed to start servers: {str(e)}", "error")
+ self.status_label.config(text="Server start failed")
+ finally:
+ self.progress.stop()
+ self.start_btn.config(state="normal")
+
+ def stop_servers(self):
+ self.log_output("Stopping servers...", "info")
+ for proc in self.server_processes:
+ if proc.poll() is None:
+ proc.terminate()
+ self.server_processes.clear()
+ self.servers_running = False
+ self.start_btn.config(text="🚀 Start Navarch")
+ self.status_label.config(text="Servers stopped")
+ self.log_output("✅ Servers stopped", "success")
+
+ def close_app(self):
+ if self.servers_running:
+ self.stop_servers()
+ self.root.destroy()
+
+
+if __name__ == '__main__':
+ root = tk.Tk()
+ app = InstallerGUI(root)
+ root.mainloop()
diff --git a/run_windows.bat b/run_windows.bat
deleted file mode 100644
index 5644349..0000000
--- a/run_windows.bat
+++ /dev/null
@@ -1,45 +0,0 @@
-@echo off
-setlocal
-echo Running setup scripts...
-
-REM Navigate to the appropriate scripts directory
-cd scripts\windows || (
- echo scripts\windows not found
- exit /b 1
-)
-
-REM Check and set up Minecraft server
-call check_minecraft_server.bat
-IF ERRORLEVEL 1 (
- echo check_minecraft_server.bat failed
- exit /b 1
-)
-
-REM Run dependency installation script
-call install_dependencies.bat
-IF ERRORLEVEL 1 (
- echo install_dependencies.bat failed
- exit /b 1
-)
-
-REM Run npm install script
-call npm_install.bat
-IF ERRORLEVEL 1 (
- echo npm_install.bat failed
- exit /b 1
-)
-
-REM Start servers
-call start_servers.bat
-IF ERRORLEVEL 1 (
- echo start_servers.bat failed
- exit /b 1
-)
-
-cd ..\.. || (
- echo Failed to return to project root
- exit /b 1
-)
-
-echo All scripts completed successfully.
-pause
diff --git a/scripts/linux_debian/npm_install.sh b/scripts/linux_debian/npm_install.sh
index 802103a..7f486d0 100755
--- a/scripts/linux_debian/npm_install.sh
+++ b/scripts/linux_debian/npm_install.sh
@@ -1,22 +1,22 @@
-#!/bin/bash
+@echo off
-cd ..
-cd ..
+echo Starting backend server...
-# Check if front-end dependencies are missing
-if [ ! -d "front-end/node_modules" ]; then
- echo "Installing front-end dependencies..."
- cd front-end
- npm install
- cd ..
-fi
+start cmd /k "cd ..\..\api && npm start"
-# Check if back-end dependencies are missing
-if [ ! -d "api/node_modules" ]; then
- echo "Installing back-end dependencies..."
- cd api
- npm install
- cd ..
-fi
-
-cd scripts
+echo === Checking frontend ===
+if exist "..\..\front-end\build" (
+ echo Frontend build folder exists. Serving...
+ start "" cmd /k "cd ..\..\front-end\build && npx serve"
+) else (
+ echo Frontend build folder not found. Building...
+ pushd ..\..\front-end
+ npm run build
+ popd
+ if exist "..\..\front-end\build" (
+ echo Build complete. Serving frontend...
+ start "" cmd /k "cd ..\..\front-end\build && npx serve"
+ ) else (
+ echo Frontend build failed.
+ )
+)
diff --git a/scripts/linux_debian/start_servers.sh b/scripts/linux_debian/start_servers.sh
index d0a9c70..8c66cb3 100755
--- a/scripts/linux_debian/start_servers.sh
+++ b/scripts/linux_debian/start_servers.sh
@@ -1,7 +1,7 @@
#!/bin/bash
# List of common terminal emulators
-TERMINALS=("gnome-terminal" "xfce4-terminal" "konsole" "xterm" "lxterminal" "tilix" "mate-terminal" "kitty")
+TERMINALS=("gnome-terminal" "xfce4-terminal" "konsole" "xterm" "lxterminal" "tilix" "mate-terminal" "kitty" "alacritty" "wezterm")
# Find a supported terminal
for term in "${TERMINALS[@]}"; do
@@ -19,5 +19,21 @@ fi
echo "Starting backend server..."
"$TERMINAL" -- bash -c "cd ../../api && npm start; exec bash" &
-echo "Starting frontend server..."
-"$TERMINAL" -- bash -c "cd ../../front-end && npm start; exec bash" &
+echo "=== Checking frontend build ==="
+if [ -d "../../front-end/build" ]; then
+ echo "Frontend build folder exists. Serving..."
+ "$TERMINAL" -- bash -c "cd ../../front-end/build && npx serve; exec bash" &
+else
+ echo "Frontend build folder not found. Building..."
+ (
+ cd ../../front-end || exit 1
+ npm run build
+ )
+
+ if [ -d "../../front-end/build" ]; then
+ echo "Build complete. Serving frontend..."
+ "$TERMINAL" -- bash -c "cd ../../front-end/build && npx serve; exec bash" &
+ else
+ echo "Frontend build failed."
+ fi
+fi
\ No newline at end of file
diff --git a/scripts/windows/check_minecraft_server.bat b/scripts/windows/check_minecraft_server.bat
index 695f675..07b73d7 100644
--- a/scripts/windows/check_minecraft_server.bat
+++ b/scripts/windows/check_minecraft_server.bat
@@ -6,27 +6,16 @@ IF NOT EXIST "..\..\server\server.jar" (
REM Create server directory
mkdir "..\..\server"
- IF ERRORLEVEL 1 (
- echo Failed to create server directory
- exit /b 1
- )
echo Fetching latest Minecraft version info...
- powershell -Command ^
- "$versionData = Invoke-RestMethod 'https://launchermeta.mojang.com/mc/game/version_manifest.json'; ^
- $latest = $versionData.latest.release; ^
- $versionInfo = $versionData.versions | Where-Object { $_.id -eq $latest }; ^
- if (-not $versionInfo) { Write-Error 'Could not find version info'; exit 1 }; ^
- $versionJson = Invoke-RestMethod $versionInfo.url; ^
- $serverJarUrl = $versionJson.downloads.server.url; ^
- echo Latest version: $latest; ^
- echo Downloading server.jar from $serverJarUrl...; ^
- Invoke-WebRequest $serverJarUrl -OutFile '..\..\server\server.jar'; ^
- echo Minecraft server.jar downloaded successfully." ^
- || (
+ powershell -Command "$versionData = Invoke-RestMethod 'https://launchermeta.mojang.com/mc/game/version_manifest.json'; $latest = $versionData.latest.release; $versionInfo = $versionData.versions | Where-Object { $_.id -eq $latest }; if (-not $versionInfo) { Write-Error 'Could not find version info'; exit 1 }; $versionJson = Invoke-RestMethod $versionInfo.url; $serverJarUrl = $versionJson.downloads.server.url; echo Latest version: $latest; echo Downloading server.jar from $serverJarUrl...; Invoke-WebRequest $serverJarUrl -OutFile '..\..\server\server.jar'; if (-not (Test-Path '..\..\server\server.jar')) { Write-Error 'Download failed'; exit 1 }"
+
+ IF ERRORLEVEL 1 (
echo Failed to fetch or download server.jar
exit /b 1
)
+
+ echo Minecraft server.jar downloaded successfully.
) ELSE (
echo server.jar already exists.
-)
+)
\ No newline at end of file
diff --git a/scripts/windows/install_dependencies.bat b/scripts/windows/install_dependencies.bat
index 6053622..ad22dbc 100644
--- a/scripts/windows/install_dependencies.bat
+++ b/scripts/windows/install_dependencies.bat
@@ -1,65 +1,60 @@
@echo off
+setlocal enabledelayedexpansion
echo Checking dependencies...
+echo.
+:: -------------------------------
:: Check Node.js
+:: -------------------------------
+echo Checking for Node.js...
where node >nul 2>&1
if %ERRORLEVEL% EQU 0 (
- echo Node.js is installed
+ echo Node.js is installed.
node --version
) else (
echo Node.js is not installed. Installing...
- curl -o node-installer.msi https://nodejs.org/dist/v20.10.0/node-v20.10.0-x64.msi
- msiexec /i node-installer.msi /qn
- del node-installer.msi
- echo Node.js has been installed
+ winget install --silent --accept-package-agreements --accept-source-agreements OpenJS.NodeJS
)
+echo.
-:: Check NPM
-where npm >nul 2>&1
+:: -------------------------------
+:: Check Java JDK (21+)
+:: -------------------------------
+echo Checking for JDK...
+where java >nul 2>&1
if %ERRORLEVEL% EQU 0 (
- echo NPM is installed
- npm --version
+ echo JDK is installed.
+ java -version
) else (
- echo NPM is not installed. Installing...
- :: NPM comes with Node.js, but if somehow it's missing
- curl -o npm.ps1 https://npmjs.org/install.ps1
- powershell.exe -ExecutionPolicy Bypass -File npm.ps1
- del npm.ps1
- echo NPM has been installed
-)
+ for /f "tokens=3" %%i in ('java --version 2^>^&1 ^| findstr /i "version"') do set JAVA_VERSION=%%i
+ set JAVA_VERSION=!JAVA_VERSION:"=!
+ for /f "delims=. tokens=1" %%a in ("!JAVA_VERSION!") do set JAVA_MAJOR=%%a
-:: Check OpenJDK 21
-where java >nul 2>&1
-if %ERRORLEVEL% EQU 0 (
- for /f "tokens=3" %%i in ('java -version 2^>^&1 ^| findstr /i "version"') do set JAVA_VERSION=%%i
- set JAVA_VERSION=%JAVA_VERSION:"=%
- for /f "delims=. tokens=1" %%a in ("%JAVA_VERSION%") do set JAVA_MAJOR=%%a
if !JAVA_MAJOR! GEQ 21 (
- echo OpenJDK 21 or above is installed
- java -version
+ echo Java JDK !JAVA_VERSION! is installed.
+ java --version
) else (
- echo OpenJDK 21 or above is not installed. Installing...
- curl -o jdk21.zip https://download.java.net/java/GA/jdk21.0.1/415e3f918a1f4062a0074a2794853d0d/12/GPL/openjdk-21.0.1_windows-x64_bin.zip
- powershell.exe -Command "Expand-Archive -Path jdk21.zip -DestinationPath 'C:\Program Files\Java'"
- del jdk21.zip
- :: Set JAVA_HOME environment variable
- setx JAVA_HOME "C:\Program Files\Java\jdk-21.0.1" /M
- :: Add to PATH
- setx PATH "%PATH%;%JAVA_HOME%\bin" /M
- echo OpenJDK 21 has been installed
+ echo Detected Java version: !JAVA_VERSION!
+ echo Java JDK 21 or above is not installed.
+ call :InstallJava
)
+)
+goto :eof
+
+:: -------------------------------
+:: Java Install Function
+:: -------------------------------
+:InstallJava
+echo Installing OpenJDK 21...
+start /wait "" winget install EclipseAdoptium.Temurin.21.JDK --silent --accept-package-agreements --accept-source-agreements
+if %ERRORLEVEL% EQU 0 (
+ echo OpenJDK 21 has been installed successfully.
) else (
- echo Java is not installed. Installing...
- curl -o jdk21.zip https://download.java.net/java/GA/jdk21.0.1/415e3f918a1f4062a0074a2794853d0d/12/GPL/openjdk-21.0.1_windows-x64_bin.zip
- powershell.exe -Command "Expand-Archive -Path jdk21.zip -DestinationPath 'C:\Program Files\Java'"
- del jdk21.zip
- :: Set JAVA_HOME environment variable
- setx JAVA_HOME "C:\Program Files\Java\jdk-21.0.1" /M
- :: Add to PATH
- setx PATH "%PATH%;%JAVA_HOME%\bin" /M
- echo OpenJDK 21 has been installed
+ echo Failed to install OpenJDK 21.
)
+goto :eof
+
@REM :: Check OpenJDK 17
@REM where java >nul 2>&1
diff --git a/scripts/windows/npm_install.bat b/scripts/windows/npm_install.bat
index 898ca1e..7feee94 100644
--- a/scripts/windows/npm_install.bat
+++ b/scripts/windows/npm_install.bat
@@ -1,4 +1,5 @@
cd ..
+cd ..
if not exist "front-end\node_modules" (
echo Installing front-end dependencies...
@@ -9,9 +10,12 @@ if not exist "front-end\node_modules" (
if not exist "api\node_modules" (
echo Installing back-end dependencies...
- cd back-end
+ cd api
call npm install
cd ..
)
+call npm install -g serve
+
cd scripts
+cd windows
\ No newline at end of file
diff --git a/scripts/windows/start_servers.bat b/scripts/windows/start_servers.bat
deleted file mode 100644
index 6870993..0000000
--- a/scripts/windows/start_servers.bat
+++ /dev/null
@@ -1,9 +0,0 @@
-@echo off
-
-echo Starting backend server...
-
-start cmd /k "cd ..\..\api && npm start"
-
-echo Starting frontend server...
-
-start cmd /k "cd ..\..\front-end && npm start"
\ No newline at end of file
diff --git a/start_servers.bat b/start_servers.bat
new file mode 100644
index 0000000..3599736
--- /dev/null
+++ b/start_servers.bat
@@ -0,0 +1,66 @@
+@echo off
+setlocal enabledelayedexpansion
+
+:: Check if Node.js is installed
+where node >nul 2>&1
+if NOT %ERRORLEVEL% EQU 0 (
+ echo ❌ Node.js is not installed.
+ echo Please run installer.bat to install required packages.
+ pause
+ goto :eof
+)
+
+echo === Checking backend dependencies ===
+if not exist "api\node_modules" (
+ echo ⚙️ Backend dependencies missing. Running npm_install.bat...
+ pushd scripts\windows
+ call npm_install.bat
+ if ERRORLEVEL 1 (
+ echo ❌ npm_install.bat failed for backend
+ pause
+ popd
+ goto :eof
+ )
+ popd
+)
+
+echo === Checking frontend dependencies ===
+if not exist "front-end\node_modules" (
+ echo ⚙️ Frontend dependencies missing. Running npm_install.bat...
+ pushd scripts\windows
+ call npm_install.bat
+ if ERRORLEVEL 1 (
+ echo ❌ npm_install.bat failed for frontend
+ pause
+ popd
+ goto :eof
+ )
+ popd
+)
+
+echo ✅ All dependencies installed.
+
+echo Starting backend server...
+start "" cmd /k "cd api && npm start"
+
+echo === Checking frontend ===
+if exist "front-end\build" (
+ echo 🟢 Frontend build folder exists. Serving...
+ start "" cmd /k "cd front-end\build && npx serve"
+) else (
+ echo 🔧 Frontend build folder not found. Building...
+ pushd front-end
+ npm run build
+ popd
+ if exist "front-end\build" (
+ echo ✅ Build complete. Serving frontend...
+ start "" cmd /k "cd front-end\build && npx serve"
+ ) else (
+ echo ❌ Frontend build failed.
+ pause
+ )
+)
+
+echo ✅ You can now close this window.
+pause
+endlocal
\ No newline at end of file