From 0ceb0b61b47d561085726fed2e0c80d0d3697ea5 Mon Sep 17 00:00:00 2001 From: Aidan Date: Tue, 8 Jul 2025 17:39:12 -0400 Subject: [PATCH] design improvements, stabilize everything, update readme --- README.md | 4 +- create_zip.sh | 30 +- module/customize.sh | 12 +- module/util/config.sh | 62 ++- module/webroot/package.json | 6 +- module/webroot/src/css/style.css | 401 ++++++++++++++++--- module/webroot/src/index.html | 320 +++++++++------ module/webroot/src/js/app.js | 655 +++++++++++++++++-------------- 8 files changed, 979 insertions(+), 511 deletions(-) diff --git a/README.md b/README.md index c15fe30..4b6695a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A KernelSU module which simplifies the installation of a keybox.xml file by fetching it from a server. > [!IMPORTANT] -> **This project is not hosted on GitHub. If you are looking to contribute or open an issue, please see my repo on [LibreCloud Git](https://git.pontusmail.org/aidan/beesrv).** +> **This project is not hosted on GitHub. If you are looking to contribute or open an issue, please see my repo on [p0ntus git](https://git.p0ntus.com/aidan/BeeSrv).** ## Module @@ -51,7 +51,7 @@ A `beebox.xml` file should be placed the `server/serve/` directory. You will hav Thank you to all of the people and projects I have come across while building this! Without you, this project wouldn't be a reality. -* [Re-Malwack by ZG089](https://github.com/ZG089/Re-Malwack) - +* [Re-Malwack by ZG089](https://github.com/ZG089/Re-Malwack) - This helped me so much while writing the module * [KernelSU Documentation](https://kernelsu.org/guide/module.html) - Very helpful resource for building a complete module diff --git a/create_zip.sh b/create_zip.sh index a34fd27..1a84f0a 100755 --- a/create_zip.sh +++ b/create_zip.sh @@ -17,15 +17,15 @@ if ! command -v bun &> /dev/null; then fi # Check if filename to be created already exists -if [ -f "BeeSrv-$version.zip" ]; then - echo "[i] BeeSrv-$version.zip already exists, would you like to overwrite it? (y/n)" +if [ -f "BeeSrv-$version.zip" ] || [ -f "BeeSrv-$version-debug.zip" ]; then + echo "[i] BeeSrv zip files already exist, would you like to overwrite them? (y/n)" read overwrite if [ "$overwrite" != "y" ]; then echo "[!] Aborting..." exit 1 else - rm -rf BeeSrv-$version.zip - echo "[✔] Overwriting BeeSrv-$version.zip..." + rm -rf BeeSrv-$version.zip BeeSrv-$version-debug.zip + echo "[✔] Overwriting existing zip files..." fi fi @@ -46,9 +46,9 @@ cp -r module tmp echo "[✔] Created working directory" # Clean any unnecessary files -rm -rf tmp/module/webroot/dist -rm -rf tmp/module/webroot/.gitignore -rm -rf tmp/module/webroot/package-lock.json +rm -rf tmp/webroot/dist +rm -rf tmp/webroot/.gitignore +rm -rf tmp/webroot/package-lock.json echo "[✔] Completed cleanup" # Build webroot @@ -83,15 +83,23 @@ echo "[✔] Moved built files to webroot" rm -rf webroot/dist echo "[✔] Completed cleanup" -# Create zip -echo "[i] Creating zip..." +echo "[i] Creating debug zip..." +zip -r ../BeeSrv-$version-debug.zip * +echo "[✔] Created debug zip" + +echo "[i] Removing eruda scripts for production version..." +sed -i '/ + + -
-

BeeSrv WebUI

-
+
+
+

BeeSrv WebUI

+
-
-

Made with ❤️ by ihatenodejs

-

Self-Check

- - - - - - - -
Internet Connection - -
+
+ - -

Module

- - - - - - - - - - - -
Version - - - Loading... - -
Debug Mode - - - Loading... - -
+ + + -

Join Us

-
- - - - - - - - - + + +
+ + + + + diff --git a/module/webroot/src/js/app.js b/module/webroot/src/js/app.js index b442fb3..21bca3c 100644 --- a/module/webroot/src/js/app.js +++ b/module/webroot/src/js/app.js @@ -3,6 +3,230 @@ import { exec } from "kernelsu" const modules_dir = "/data/adb/modules/BeeSrv" const persist_dir = "/data/adb/beesrv" +class ConfigManager { + constructor() { + this.state = { + server: { + value: '', + isEditing: false, + isLoading: false, + error: null, + isDirty: false + }, + email: { + value: '', + isEditing: false, + isLoading: false, + error: null, + isDirty: false + } + } + this.validators = { + server: (value) => { + if (!value.trim()) return 'Server URL cannot be empty' + try { + new URL(value.trim()) + return null + } catch { + return 'Please enter a valid URL' + } + }, + email: (value) => { + if (!value.trim()) return 'Email cannot be empty' + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(value.trim())) return 'Please enter a valid email address' + return null + } + } + this.init() + } + + init() { + this.setupEventListeners() + } + + setupEventListeners() { + document.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const target = e.target + if (target.id === 'serverInput' && this.state.server.isEditing) { + e.preventDefault() + this.save('server') + } + if (target.id === 'emailInput' && this.state.email.isEditing) { + e.preventDefault() + this.save('email') + } + } + }) + } + + updateState(field, updates) { + this.state[field] = { ...this.state[field], ...updates } + this.renderField(field) + } + + startEdit(field) { + if (this.state[field].isLoading) return + + hideConfigError() + this.updateState(field, { + isEditing: true, + error: null, + isDirty: false + }) + + setTimeout(() => { + const input = document.getElementById(`${field}Input`) + if (input) { + input.value = this.state[field].value + input.focus() + input.select() + } + }, 0) + } + + cancelEdit(field) { + if (this.state[field].isLoading) return + + this.updateState(field, { + isEditing: false, + error: null, + isDirty: false + }) + } + + async save(field) { + const input = document.getElementById(`${field}Input`) + if (!input) return + + const newValue = input.value.trim() + const validation = this.validators[field](newValue) + + if (validation) { + this.updateState(field, { error: validation }) + showConfigError(validation) + return + } + + this.updateState(field, { + isLoading: true, + error: null + }) + + try { + const flag = field === 'server' ? '-s' : '-e' + const sanitizedValue = newValue + .replace(/['"]/g, '') + .replace(/`/g, '') + .replace(/\$/g, '') + .trim() + const { errno, stderr, stdout } = await exec(`${modules_dir}/util/config.sh ${flag} "${sanitizedValue}"`) + if (errno !== 0) { + const debug = await getDebugMode() + const errorMsg = debug ? (stderr || `Command failed with exit code ${errno}`) : `Failed to update ${field} configuration` + this.updateState(field, { + isLoading: false, + error: errorMsg + }) + showConfigError(errorMsg) + } else { + if (stdout && stdout.includes('Success')) { + this.updateState(field, { + value: newValue, + isEditing: false, + isLoading: false, + error: null, + isDirty: false + }) + hideConfigError() + } else { + const errorMsg = `Configuration update may have failed - unexpected response` + this.updateState(field, { + isLoading: false, + error: errorMsg + }) + showConfigError(errorMsg) + } + } + } catch (error) { + const debug = await getDebugMode() + const errorMsg = debug ? error.toString() : `Error updating ${field} configuration` + this.updateState(field, { + isLoading: false, + error: errorMsg + }) + showConfigError(errorMsg) + } + } + + renderField(field) { + const state = this.state[field] + const displayElement = document.getElementById(`${field}-display`) + const editElement = document.getElementById(`${field}-edit`) + const textElement = document.getElementById(field) + const loaderElement = document.getElementById(`${field}Loader`) + const inputElement = document.getElementById(`${field}Input`) + const saveBtn = document.getElementById(`save${field.charAt(0).toUpperCase() + field.slice(1)}Btn`) + + if (!displayElement || !editElement || !textElement) return + + if (state.isEditing) { + displayElement.classList.add('hidden') + editElement.classList.remove('hidden') + } else { + displayElement.classList.remove('hidden') + editElement.classList.add('hidden') + } + + if (state.isLoading) { + loaderElement?.classList.remove('hidden') + textElement.textContent = 'Saving...' + if (saveBtn) saveBtn.disabled = true + } else { + loaderElement?.classList.add('hidden') + if (saveBtn) saveBtn.disabled = false + + if (state.error) { + textElement.textContent = 'Error' + } else if (state.value) { + textElement.textContent = state.value + } else { + textElement.textContent = 'Not set' + } + } + + if (inputElement && state.error) { + inputElement.classList.add('border-red-500') + inputElement.classList.remove('border-gray-600') + } else if (inputElement) { + inputElement.classList.remove('border-red-500') + inputElement.classList.add('border-gray-600') + } + } + + setValue(field, value) { + this.updateState(field, { value: value || 'Not set' }) + } + + setError(field, error) { + this.updateState(field, { + value: 'Error', + error: error + }) + } +} + +const configManager = new ConfigManager() + +window.startServerEdit = () => configManager.startEdit('server') +window.cancelServerEdit = () => configManager.cancelEdit('server') +window.saveServer = () => configManager.save('server') + +window.startEmailEdit = () => configManager.startEdit('email') +window.cancelEmailEdit = () => configManager.cancelEdit('email') +window.saveEmail = () => configManager.save('email') + function showError(message) { const errorBox = document.getElementById("errorBox") const errorMessage = document.getElementById("errorMessage") @@ -15,29 +239,117 @@ function hideError() { errorBox.classList.add("hidden") } +function showConfigError(message) { + const errorBox = document.getElementById("configErrorBox") + const errorMessage = document.getElementById("configErrorMessage") + errorMessage.textContent = message + errorBox.classList.remove("hidden") +} + +function hideConfigError() { + const errorBox = document.getElementById("configErrorBox") + errorBox.classList.add("hidden") +} + +window.switchToTab = function(tabId, clickedButton) { + if (tabId !== 'settings-tab') { + hideConfigError() + } + + document.getElementById('home-tab').classList.add('hidden') + document.getElementById('settings-tab').classList.add('hidden') + document.getElementById('support-tab').classList.add('hidden') + document.getElementById(tabId).classList.remove('hidden') + const navButtons = document.querySelectorAll('.nav-tab') + navButtons.forEach(btn => { + btn.classList.remove('text-blue-400', 'border-t-2', 'border-blue-400') + btn.classList.add('text-gray-400') + }) + clickedButton.classList.remove('text-gray-400') + clickedButton.classList.add('text-blue-400', 'border-t-2', 'border-blue-400') +} + +window.testConnection = async function() { + const testBtn = document.getElementById("testBtn") + const testBtnLoading = document.getElementById("testBtnLoading") + const connectionStatus = document.getElementById("connectionStatus") + const connectionError = document.getElementById("connectionError") + const checkConnectionBtn = document.getElementById("checkConnection") + + testBtn.classList.add("hidden") + testBtnLoading.classList.remove("hidden") + testBtnLoading.classList.add("flex") + connectionStatus.classList.add("hidden") + connectionStatus.classList.remove("flex") + connectionError.classList.add("hidden") + connectionError.classList.remove("flex") + checkConnectionBtn.disabled = true + hideError() + + try { + const response = await fetch('https://httpbin.org/get', { + method: 'GET', + signal: AbortSignal.timeout(5000) + }); + testBtnLoading.classList.add("hidden") + testBtnLoading.classList.remove("flex") + testBtn.classList.remove("hidden") + checkConnectionBtn.disabled = false + + if (response.ok) { + connectionStatus.classList.remove("hidden") + connectionStatus.classList.add("flex") + connectionError.classList.add("hidden") + connectionError.classList.remove("flex") + checkConnectionBtn.classList.add("hidden") + } else { + connectionStatus.classList.add("hidden") + connectionStatus.classList.remove("flex") + connectionError.classList.remove("hidden") + connectionError.classList.add("flex") + showError("No internet connection detected") + checkConnectionBtn.classList.add("hidden") + } + } catch (error) { + testBtnLoading.classList.add("hidden") + testBtnLoading.classList.remove("flex") + testBtn.classList.remove("hidden") + checkConnectionBtn.disabled = false + + connectionStatus.classList.add("hidden") + connectionStatus.classList.remove("flex") + connectionError.classList.remove("hidden") + connectionError.classList.add("flex") + + let errorMessage = "Connection check failed. Please try again later." + if (error.name === 'AbortError') { + errorMessage = 'Connection check timed out. Please try again.' + } else if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { + errorMessage = 'Unable to reach the internet. Please check your network connection.' + } + + showError(errorMessage) + checkConnectionBtn.classList.add("hidden") + } +} + async function getDebugMode() { - const { errno, stdout } = await exec(`cat ${persist_dir}/config.txt`) - if (errno !== 0) { - showError("Failed to read debug mode") + try { + const { errno, stdout } = await exec(`${modules_dir}/util/config.sh -d`) + if (errno !== 0) { + return false + } + return stdout.trim() === "true" + } catch (error) { return false } - const debug = stdout.split("\n").find(line => line.startsWith("DEBUG=")) - if (!debug) { - return false - } - return debug.split("=")[1] === "true" } async function getEmail() { try { - const { errno, stdout, stderr } = await exec(`cat ${persist_dir}/config.txt`) + const { errno, stdout } = await exec(`cat ${persist_dir}/config.txt`) if (errno !== 0) { - if (await getDebugMode() !== true) { - showError("Failed to read email configuration") - } else { - showError(stderr) - } - return "Unknown" + return "Not set" } const email = stdout.split("\n").find(line => line.startsWith("EMAIL=")) if (!email) { @@ -45,315 +357,70 @@ async function getEmail() { } return email.split("=")[1] } catch (error) { - showError("Error reading email configuration") - return "Unknown" + return "Error" } } - async function getVersion() { try { - const { errno, stdout, stderr } = await exec(`cat ${modules_dir}/module.prop`) + const { errno, stdout } = await exec(`${modules_dir}/util/config.sh -v`) if (errno !== 0) { - if (await getDebugMode() !== true) { - showError("Failed to read module version") - } else { - showError(stderr) - } return "Unknown" } - const version = stdout.split("\n").find(line => line.startsWith("version=")) - if (!version) { - showError("Module version not found") - return "Unknown" - } - return version.split("=")[1] + return stdout.trim() || "Unknown" } catch (error) { - showError("Error reading module version") - return "Unknown" + return "Error" } } async function getServer() { try { - const { errno, stdout, stderr } = await exec(`cat ${persist_dir}/config.txt`) + const { errno, stdout } = await exec(`cat ${persist_dir}/config.txt`) if (errno !== 0) { - if (await getDebugMode() !== true) { - showError("Failed to read server configuration") - } else { - showError(stderr) - } - return "Unknown" + return "Not set" } const server = stdout.split("\n").find(line => line.startsWith("SERVER=")) if (!server) { - showError("Server configuration not found") - return "Unknown" + return "Not set" } return server.split("=")[1] } catch (error) { - showError("Error reading server configuration") - return "Unknown" - } -} - -async function checkConnection() { - try { - const response = await fetch('https://httpbin.org/get', { - method: 'GET', - signal: AbortSignal.timeout(5000) - }); - - return response.ok; - } catch (error) { - if (error.name === 'AbortError') { - throw new Error('Connection check timed out. Please try again.'); - } else if (error.name === 'TypeError' && error.message.includes('Failed to fetch')) { - throw new Error('Unable to reach the internet. Please check your network connection.'); - } else { - throw new Error('Connection check failed. Please try again later.'); - } - } -} - -async function setEmail(email) { - try { - const { errno, stderr } = await exec(`${modules_dir}/util/config.sh -e "${email.replace(/['"]/g, '')}"`) - if (errno !== 0) { - if (await getDebugMode() !== true) { - showError("Failed to update email configuration") - } else { - showError(stderr) - } - return false - } - return true - } catch (error) { - if (await getDebugMode() !== true) { - showError("Error updating email configuration") - } else { - showError(error) - } - return false - } -} - -async function setServer(server) { - try { - const { errno, stderr } = await exec(`${modules_dir}/util/config.sh -s "${server.replace(/['"]/g, '')}"`) - if (errno !== 0) { - if (await getDebugMode() !== true) { - showError("Failed to update server configuration") - } else { - showError(stderr) - } - return false - } - return true - } catch (error) { - if (await getDebugMode() !== true) { - showError("Error updating server configuration") - } else { - showError(error) - } - return false + return "Error" } } document.addEventListener("DOMContentLoaded", async () => { - const versionText = document.getElementById("versionText") - const serverText = document.getElementById("serverText") - const emailText = document.getElementById("emailText") - const debugText = document.getElementById("debugText") - const versionLoader = document.getElementById("versionLoader") - const serverLoader = document.getElementById("serverLoader") - const emailLoader = document.getElementById("emailLoader") - const debugLoader = document.getElementById("debugLoader") - const emailInput = document.getElementById("emailInput") - const serverInput = document.getElementById("serverInput") - const editEmailBtn = document.getElementById("editEmailBtn") - const editServerBtn = document.getElementById("editServerBtn") - const saveEmailBtn = document.getElementById("saveEmailBtn") - const saveServerBtn = document.getElementById("saveServerBtn") - const cancelEmailBtn = document.getElementById("cancelEmailBtn") - const cancelServerBtn = document.getElementById("cancelServerBtn") + const [version, server, email, debug] = await Promise.allSettled([ + getVersion(), + getServer(), + getEmail(), + getDebugMode() + ]); - // Server editing - function startServerEditing() { - serverText.classList.add("hidden") - serverInput.classList.remove("hidden") - editServerBtn.classList.add("hidden") - saveServerBtn.classList.remove("hidden") - cancelServerBtn.classList.remove("hidden") - serverInput.value = serverText.textContent - serverInput.focus() + document.getElementById("versionLoader")?.classList.add("hidden"); + document.getElementById("serverLoader")?.classList.add("hidden"); + document.getElementById("emailLoader")?.classList.add("hidden"); + document.getElementById("debugLoader")?.classList.add("hidden"); + + const versionText = document.getElementById("version"); + if (versionText) { + versionText.textContent = version.status === "fulfilled" ? version.value : "Error"; } - function stopServerEditing() { - serverText.classList.remove("hidden") - serverInput.classList.add("hidden") - editServerBtn.classList.remove("hidden") - saveServerBtn.classList.add("hidden") - cancelServerBtn.classList.add("hidden") + const debugText = document.getElementById("debug"); + if (debugText) { + debugText.textContent = debug.status === "fulfilled" ? (debug.value ? "Enabled" : "Disabled") : "Error"; } - editServerBtn.addEventListener("click", startServerEditing) - - cancelServerBtn.addEventListener("click", stopServerEditing) - - saveServerBtn.addEventListener("click", async () => { - const newServer = serverInput.value.trim() - if (!newServer) { - showError("Server URL cannot be empty") - return - } - - serverLoader.classList.remove("hidden") - serverText.textContent = "Saving..." - stopServerEditing() - - const success = await setServer(newServer) - if (success) { - serverText.textContent = newServer - } else { - serverText.textContent = "Error" - } - serverLoader.classList.add("hidden") - }) - - // Handle enter button for server input - serverInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - saveServerBtn.click() - } else if (event.key === "Escape") { - cancelServerBtn.click() - } - }) - - // Email editing - function startEditing() { - emailText.classList.add("hidden") - emailInput.classList.remove("hidden") - editEmailBtn.classList.add("hidden") - saveEmailBtn.classList.remove("hidden") - cancelEmailBtn.classList.remove("hidden") - emailInput.value = emailText.textContent - emailInput.focus() + if (server.status === "fulfilled") { + configManager.setValue('server', server.value); + } else { + configManager.setError('server', 'Failed to load server configuration'); } - function stopEditing() { - emailText.classList.remove("hidden") - emailInput.classList.add("hidden") - editEmailBtn.classList.remove("hidden") - saveEmailBtn.classList.add("hidden") - cancelEmailBtn.classList.add("hidden") + if (email.status === "fulfilled") { + configManager.setValue('email', email.value); + } else { + configManager.setError('email', 'Failed to load email configuration'); } - - editEmailBtn.addEventListener("click", startEditing) - - cancelEmailBtn.addEventListener("click", stopEditing) - - saveEmailBtn.addEventListener("click", async () => { - const newEmail = emailInput.value.trim() - if (!newEmail) { - showError("Email cannot be empty") - return - } - - emailLoader.classList.remove("hidden") - emailText.textContent = "Saving..." - stopEditing() - - const success = await setEmail(newEmail) - if (success) { - emailText.textContent = newEmail - } else { - emailText.textContent = "Error" - } - emailLoader.classList.add("hidden") - }) - - // Handle enter button for email input - emailInput.addEventListener("keydown", (event) => { - if (event.key === "Enter") { - saveEmailBtn.click() - } else if (event.key === "Escape") { - cancelEmailBtn.click() - } - }) - - try { - const version = await getVersion() - const server = await getServer() - const email = await getEmail() - const debug = await getDebugMode() - versionLoader.classList.add("hidden") - serverLoader.classList.add("hidden") - emailLoader.classList.add("hidden") - debugLoader.classList.add("hidden") - versionText.textContent = version - serverText.textContent = server - emailText.textContent = email - debugText.textContent = debug ? "Enabled" : "Disabled" - } catch (error) { - versionLoader.classList.add("hidden") - serverLoader.classList.add("hidden") - emailLoader.classList.add("hidden") - debugLoader.classList.add("hidden") - versionText.textContent = "Error" - serverText.textContent = "Error" - emailText.textContent = "Error" - debugText.textContent = "Error" - } - - const checkConnectionBtn = document.getElementById("checkConnection") - const testBtn = document.getElementById("testBtn") - const testBtnLoading = document.getElementById("testBtnLoading") - const connectionStatus = document.getElementById("connectionStatus") - const connectionError = document.getElementById("connectionError") - - function resetButtonState() { - testBtnLoading.classList.add("hidden") - testBtn.classList.remove("hidden") - checkConnectionBtn.disabled = false - } - - function setLoadingState() { - testBtn.classList.add("hidden") - testBtnLoading.classList.remove("hidden") - connectionStatus.classList.add("hidden") - connectionError.classList.add("hidden") - checkConnectionBtn.disabled = true - hideError() - } - - function showSuccessState() { - connectionStatus.classList.remove("hidden") - connectionError.classList.add("hidden") - // Hide the button after successful test - checkConnectionBtn.classList.add("hidden") - } - - function showErrorState(message) { - connectionStatus.classList.add("hidden") - connectionError.classList.remove("hidden") - showError(message) - checkConnectionBtn.classList.add("hidden") - } - - checkConnectionBtn.addEventListener("click", async () => { - setLoadingState() - try { - const isConnected = await checkConnection() - resetButtonState() - if (isConnected) { - showSuccessState() - } else { - showErrorState("No internet connection detected") - } - } catch (error) { - resetButtonState() - showErrorState(error.message) - } - }) -}) +});