diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..c1cd848 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,29 @@ +{ + "env": { + "browser": true, + "commonjs": true, + "es2021": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": 13 + }, + "rules": { + "no-undef": 0, + "no-unused-vars": 0, + "no-case-declarations": 0, + + "indent": [ + "error", + 2 + ], + "quotes": [ + "error", + "single" + ], + "semi": [ + "error", + "never" + ] + } +} diff --git a/.gitignore b/.gitignore index 2ccb550..66207a5 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ bin/ dist/ ext/ temp/ +gc*/ resources/js/neutralino.js resources/bg/official diff --git a/languages/en.json b/languages/en.json index a7ecafe..1fecb4d 100644 --- a/languages/en.json +++ b/languages/en.json @@ -63,5 +63,21 @@ "alertAuthNoRegister": "Authentication is disabled, no need to register!", "alertRegisterSuccess": "Registration successful!", + "downloadTitle": "Downloads", + "grassclipperTitle": "GrassClipper", + "grasscutterTitle": "Grasscutter", + "installerTitle": "Installer", + "installerSubtitle": "Installs proxy and other tools. Required for Grasscutter servers.", + "downloadStable": "Download Grasscutter Stable Build", + "stableSubtitle": "Install Grasscutter stable branch. This build usually has less bugs, but also less features.", + "downloadDev": "Download Grasscutter Development Build", + "downloadSubtitle": "Install Grasscutter development branch. This build sometimes has bugs, and is frequently updated. Use at your own risk.", + "downloadResources": "Download Grasscutter Resources", + "devSubtitle": "Downloads Grasscutter resources into the currently set Grasscutter folder. This should be done unless you plan on getting resources externally.", + + "gcScriptRunning": "Running...", + "stableInstall": "Download", + "devInstall": "Download", + "updateNotifText": "A new update is available! Newest version: " } diff --git a/languages/zh-tw.json b/languages/zh-tw.json index d551fb9..0d23c08 100644 --- a/languages/zh-tw.json +++ b/languages/zh-tw.json @@ -38,7 +38,7 @@ "proxyInstallDeny": "不用了,謝謝。", "gameFolderDialog": "選擇Genshin Impact game資料夾", - "grasscutterFileDialog": "選擇Grasscutter.jar檔案" + "grasscutterFileDialog": "選擇Grasscutter.jar檔案", "loggingInTo": "登錄至:", "registeringFor": "註冊至:", @@ -60,7 +60,7 @@ "alertUserTaken": "用戶名已被占用", "alertPassMismatch": "兩組密碼不一致", "alertAuthNoRegister": "未啟用認證,無需註冊!", - "alertRegisterSuccess": "註冊成功!" + "alertRegisterSuccess": "註冊成功!", "updateNotifText": "有新的GrassClipper更新可用! 最新版本: " } diff --git a/manifest.json b/manifest.json index 3df9a34..77d9e27 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,5 @@ { "applicationId": "js.grassclipper.app", - "version": "0.8.5", + "version": "0.9.0", "resourcesURL": "https://github.com/Grasscutters/GrassClipper/releases/latest/download/resources.neu" } \ No newline at end of file diff --git a/neutralino.config.json b/neutralino.config.json index 7abf636..0bdb3dd 100644 --- a/neutralino.config.json +++ b/neutralino.config.json @@ -1,6 +1,6 @@ { "applicationId": "js.grassclipper.app", - "version": "0.8.5", + "version": "0.9.0", "defaultMode": "window", "port": 0, "documentRoot": "/resources/", diff --git a/package.json b/package.json index 196bce7..764fed5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "grassclipper", - "version": "0.8.5", + "version": "0.9.0", "repository": "https://github.com/Grasscutters/GrassClipper.git", "author": "SpikeHD ", "license": "Apache-2.0", @@ -9,5 +9,8 @@ "build-win": ".\\build_win.cmd", "build-linux": "./build.sh", "build": "echo !! Run build-win or build-linux to build for your platform !!\n" + }, + "devDependencies": { + "eslint": "^8.14.0" } } diff --git a/resources/icons/download.svg b/resources/icons/download.svg new file mode 100644 index 0000000..8baf9ed --- /dev/null +++ b/resources/icons/download.svg @@ -0,0 +1,11 @@ + +Created with Fabric.js 1.7.22 + + + + + + + + + \ No newline at end of file diff --git a/resources/index.html b/resources/index.html index f59f5ea..bb71778 100644 --- a/resources/index.html +++ b/resources/index.html @@ -8,6 +8,7 @@ + @@ -114,6 +115,59 @@ + + + +
diff --git a/resources/js/authAlert.js b/resources/js/authAlert.js index c6b806e..8af7959 100644 --- a/resources/js/authAlert.js +++ b/resources/js/authAlert.js @@ -9,37 +9,37 @@ async function displayRegisterAlert(message, type, cooldown = null) { } function displayAlert(message, type, cooldown, name) { - const elm = document.getElementById(`${name}Alert`); - const text = document.getElementById(`${name}AlertText`); + const elm = document.getElementById(`${name}Alert`) + const text = document.getElementById(`${name}AlertText`) - elm.style.removeProperty('display'); + elm.style.removeProperty('display') // Remove classification classes - elm.classList.remove('error'); - elm.classList.remove('success'); - elm.classList.remove('warn'); + elm.classList.remove('error') + elm.classList.remove('success') + elm.classList.remove('warn') switch(type) { - case 'error': - elm.classList.add('error'); - break; + case 'error': + elm.classList.add('error') + break - case 'success': - elm.classList.add('success'); - break; + case 'success': + elm.classList.add('success') + break - case 'warn': - default: - elm.classList.add('warn'); - break; + case 'warn': + default: + elm.classList.add('warn') + break } - text.innerText = message; + text.innerText = message clearTimeout(alertTimeout) // Disappear after cooldown alertTimeout = setTimeout(() => { - elm.style.display = 'none'; + elm.style.display = 'none' }, cooldown || alertCooldown) } \ No newline at end of file diff --git a/resources/js/gcdownloader.js b/resources/js/gcdownloader.js new file mode 100644 index 0000000..e461cfd --- /dev/null +++ b/resources/js/gcdownloader.js @@ -0,0 +1,102 @@ +async function clearGCInstallation() { + Neutralino.os.execCommand('del /s /q "./gc"') +} + +async function setDownloadButtonsToLoading() { + const stableBtn = document.querySelector('#stableInstall') + const devBtn = document.querySelector('#devInstall') + + stableBtn.innerText = localeObj.gcScriptRunning || 'Running...' + + devBtn.innerText = localeObj.gcScriptRunning || 'Running...' + + // Set btns to disabled + stableBtn.disabled = true + stableBtn.classList.add('disabled') + + devBtn.disabled = true + devBtn.classList.add('disabled') +} + +async function resetDownloadButtons() { + const stableBtn = document.querySelector('#stableInstall') + const devBtn = document.querySelector('#devInstall') + + stableBtn.innerText = localeObj.stableInstall || 'Download' + devBtn.innerText = localeObj.devInstall || 'Download' + + // Set btns to enabled + stableBtn.disabled = false + stableBtn.classList.remove('disabled') + + devBtn.disabled = false + devBtn.classList.remove('disabled') +} + +async function downloadGC(branch) { + const config = await getCfg() + + // If we are pulling from a new branch, delete the old installation + if (config.grasscutterBranch !== branch) await clearGCInstallation() + + // Set current installation in config + config.grasscutterBranch = branch + + // Set gc path for people with launcher enabled + config.serverFolder = `${NL_CWD}/gc-${branch}/grasscutter.jar` + + // Enable server launcher + config.serverLaunchPanel = true + + Neutralino.storage.setData('config', JSON.stringify(config)) + + setDownloadButtonsToLoading() + + // Keystore for branch (since they can differ) + const keystoreUrl = `https://github.com/Grasscutters/Grasscutter/raw/${branch}/keystore.p12` + + // External service that allows un-authed artifact downloading + const artiUrl = `https://nightly.link/Grasscutters/Grasscutter/workflows/build/${branch}/Grasscutter.zip` + + // For data files + const dataFiles = await axios.get(`https://api.github.com/repos/Grasscutters/Grasscutter/contents/data?ref=${branch}`) + const dataList = dataFiles.data + .map(file => ({ path: file.path, filename: file.name })) + .map(o => ({ url: `https://raw.githubusercontent.com/Grasscutters/Grasscutter/${branch}/${o.path}`, filename: o.filename })) + + // For key files + const keyFiles = await axios.get(`https://api.github.com/repos/Grasscutters/Grasscutter/contents/keys?ref=${branch}`) + const keyList = keyFiles.data + .map(file => ({ path: file.path, filename: file.name })) + .map(o => ({ url: `https://raw.githubusercontent.com/Grasscutters/Grasscutter/${branch}/${o.path}`, filename: o.filename })) + + const serverFolderFixed = config.serverFolder.match(/.*\\|.*\//g, '')[0].replace(/\//g, '\\') + + // Ensure data and key folders exist + + await Neutralino.os.execCommand(`mkdir ${serverFolderFixed}\\data`) + await Neutralino.os.execCommand(`mkdir ${serverFolderFixed}\\keys`) + + // Download data files + for (const o of dataList) { + const folder = 'data' + await Neutralino.os.execCommand(`powershell Invoke-WebRequest -Uri ${o.url} -OutFile "${serverFolderFixed}\\${folder}\\${o.filename}"`) + } + + // Download key files + for (const o of keyList) { + const folder = 'keys' + await Neutralino.os.execCommand(`powershell Invoke-WebRequest -Uri ${o.url} -OutFile "${serverFolderFixed}\\${folder}\\${o.filename}"`) + } + + // Run installer + createCmdWindow(`.\\scripts\\gc_download.cmd ${artiUrl} ${keystoreUrl} ${branch}`) + + // Fix buttons + resetDownloadButtons() + + // Display folder after saving config + displayServerFolder() + enableServerButton() + displayServerLaunchSection() +} \ No newline at end of file diff --git a/resources/js/helpers.js b/resources/js/helpers.js index c0b31c0..8fe28c2 100644 --- a/resources/js/helpers.js +++ b/resources/js/helpers.js @@ -3,7 +3,7 @@ * * @returns {Promise} */ - async function getCfg() { +async function getCfg() { const defaultConf = { gameexe: '', serverFolder: '', @@ -12,6 +12,7 @@ serverLaunchPanel: false, language: 'en', useHttps: true, + grasscutterBranch: '', } const cfgStr = await Neutralino.storage.getData('config').catch(e => { // The data isn't set, so this is our first time opening @@ -31,7 +32,7 @@ * * @returns {Promise} */ - async function getFavIps() { +async function getFavIps() { const ipStr = await Neutralino.storage.getData('favorites').catch(e => { // The data isn't set, so this is our first time opening Neutralino.storage.setData('favorites', JSON.stringify([])) @@ -97,7 +98,7 @@ async function openGameFolder() { async function openGrasscutterFolder() { const config = await getCfg() - const folder = config.serverFolder.match(/.*\\/g, '')[0] + const folder = config.serverFolder.match(/.*\\|.*\//g, '')[0] openInExplorer(folder) } @@ -105,7 +106,7 @@ async function openGrasscutterFolder() { /** * Minimize the window */ - function minimizeWin() { +function minimizeWin() { console.log('min') Neutralino.window.minimize() } diff --git a/resources/js/index.js b/resources/js/index.js index 0a4e2ce..15523cc 100644 --- a/resources/js/index.js +++ b/resources/js/index.js @@ -1,6 +1,6 @@ -Neutralino.init(); +Neutralino.init() -let localeObj; +let localeObj const filesystem = Neutralino.filesystem const createCmdWindow = async (command) => { Neutralino.os.execCommand(`cmd.exe /c start "" ${command}`, { background: true }) @@ -31,7 +31,7 @@ async function enableButtons() { /** * Enable server launch button */ - async function enableServerButton() { +async function enableServerButton() { const serverBtn = document.querySelector('#serverLaunch') serverBtn.classList.remove('disabled') @@ -46,7 +46,7 @@ async function handleGameNotSet() { document.querySelector('#gamePath').innerHTML = localeObj.folderNotSet // Set official server background to default - document.querySelector('#firstPanel').style.backgroundImage = `url("../bg/private/default.png")` + document.querySelector('#firstPanel').style.backgroundImage = 'url("../bg/private/default.png")' const offBtn = document.querySelector('#playOfficial') const privBtn = document.querySelector('#playPrivate') @@ -86,7 +86,7 @@ async function displayGameFolder() { /** * Show the server folder under the select button */ - async function displayServerFolder() { +async function displayServerFolder() { const elm = document.querySelector('#serverPath') const config = await getCfg() @@ -106,7 +106,7 @@ async function setBackgroundImage() { const servImage = servImages[Math.floor(Math.random() * servImages.length)].entry // Set default image, it will change if the bg folder exists - document.querySelector('#firstPanel').style.backgroundImage = `url("https://webstatic.hoyoverse.com/upload/event/2020/11/04/7fd661b5184e1734f91f628b6f89a31f_7367318474207189623.png")` + document.querySelector('#firstPanel').style.backgroundImage = 'url("https://webstatic.hoyoverse.com/upload/event/2020/11/04/7fd661b5184e1734f91f628b6f89a31f_7367318474207189623.png")' // Set the private background image document.querySelector('#secondPanel').style.backgroundImage = `url("../bg/private/${privImage}")` @@ -246,6 +246,30 @@ async function handleFavoriteList() { } } +async function openDownloads() { + const downloads = document.querySelector('#downloadPanel') + const config = await getCfg() + + if (downloads.style.display === 'none') { + downloads.style.removeProperty('display') + } + + // Disable the resource download button if a serverFolder path is not set + if (!config.serverFolder) { + document.querySelector('#resourceInstall').disabled = true + document.querySelector('#resourceInstall').classList.add('disabled') + } else { + document.querySelector('#resourceInstall').disabled = false + document.querySelector('#resourceInstall').classList.remove('disabled') + } +} + +async function closeDownloads() { + const downloads = document.querySelector('#downloadPanel') + + downloads.style.display = 'none' +} + async function openSettings() { const settings = document.querySelector('#settingsPanel') const config = await getCfg() @@ -294,13 +318,13 @@ async function openLogin() { const config = await getCfg() const useHttps = config.useHttps - const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}`; + const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}` // Check if we even need to authenticate try { - const { data } = await axios.get(url + '/grasscutter/auth_status') + const { data } = await axios.get(url + '/authentication/type') - if (data?.message !== 'AUTH_ENABLED') { + if (!data.includes('GCAuthAuthenticationHandler')) { launchPrivate() return } @@ -309,7 +333,6 @@ async function openLogin() { return } - loginIpDisplay.innerText = ip registerIpDisplay.innerText = ip @@ -357,10 +380,10 @@ async function checkForUpdatesAndShow() { // Version mismatch? Update! if (manifest?.version !== NL_APPVERSION) { - subtitle.innerHTML = "New update available!" + subtitle.innerHTML = 'New update available!' updateBtn.classList.remove('disabled') } else { - subtitle.innerHTML = "You are on the latest version! :)" + subtitle.innerHTML = 'You are on the latest version! :)' updateBtn.classList.add('disabled') } } @@ -388,6 +411,8 @@ async function setGameExe() { ] }) + if (!gameExe[0]) return + // Set the folder in our configuration const config = await getCfg() @@ -409,6 +434,8 @@ async function setGrasscutterFolder() { ] }) + if (!folder[0]) return + // Set the folder in our configuration const config = await getCfg() diff --git a/resources/js/login.js b/resources/js/login.js index 5fd7f8e..0982456 100644 --- a/resources/js/login.js +++ b/resources/js/login.js @@ -2,99 +2,99 @@ * Toggle the login section */ async function setLoginSection() { - const title = document.getElementById('loginSectionTitle'); - const altTitle = document.getElementById('registerSectionTitle'); - const loginSection = document.getElementById('loginPopupContentBody'); - const registerSection = document.getElementById('registerPopupContentBody'); + const title = document.getElementById('loginSectionTitle') + const altTitle = document.getElementById('registerSectionTitle') + const loginSection = document.getElementById('loginPopupContentBody') + const registerSection = document.getElementById('registerPopupContentBody') title.classList.add('selectedTitle') altTitle.classList.remove('selectedTitle') - loginSection.style.removeProperty('display'); - registerSection.style.display = 'none'; + loginSection.style.removeProperty('display') + registerSection.style.display = 'none' } /** * Toggle the register section */ async function setRegisterSection(fromLogin = false) { - const title = document.getElementById('registerSectionTitle'); - const altTitle = document.getElementById('loginSectionTitle'); - const loginSection = document.getElementById('loginPopupContentBody'); - const registerSection = document.getElementById('registerPopupContentBody'); + const title = document.getElementById('registerSectionTitle') + const altTitle = document.getElementById('loginSectionTitle') + const loginSection = document.getElementById('loginPopupContentBody') + const registerSection = document.getElementById('registerPopupContentBody') title.classList.add('selectedTitle') altTitle.classList.remove('selectedTitle') - loginSection.style.display = 'none'; - registerSection.style.removeProperty('display'); + loginSection.style.display = 'none' + registerSection.style.removeProperty('display') if (fromLogin) { // Take the values from the login section and put them in the register section - const loginUsername = document.getElementById('loginUsername').value; - const loginPassword = document.getElementById('loginPassword').value; + const loginUsername = document.getElementById('loginUsername').value + const loginPassword = document.getElementById('loginPassword').value - document.getElementById('registerUsername').value = loginUsername; - document.getElementById('registerPassword').value = loginPassword; + document.getElementById('registerUsername').value = loginUsername + document.getElementById('registerPassword').value = loginPassword } } function parseJwt(token) { - const base64Url = token.split('.')[1]; - const base64 = base64Url.replace('-', '+').replace('_', '/'); - return JSON.parse(window.atob(base64)); + const base64Url = token.split('.')[1] + const base64 = base64Url.replace('-', '+').replace('_', '/') + return JSON.parse(window.atob(base64)) } /** * Attempt login and launch game */ async function login() { - const username = document.getElementById('loginUsername').value; - const password = document.getElementById('loginPassword').value; - const ip = document.getElementById('ip').value; - const port = document.getElementById('port').value || '443'; - const config = await getCfg(); - const useHttps = config.useHttps; - const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}`; + const username = document.getElementById('loginUsername').value + const password = document.getElementById('loginPassword').value + const ip = document.getElementById('ip').value + const port = document.getElementById('port').value || '443' + const config = await getCfg() + const useHttps = config.useHttps + const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}` const reqBody = { username, password, } - const { data } = await axios.post(url + '/grasscutter/login', reqBody) + const { data } = await axios.post(url + '/authentication/login', reqBody) switch(data.message) { - case 'INVALID_ACCOUNT': - displayLoginAlert(localeObj.alertInvalid || 'Invalid username or password', 'error'); - break; + case 'INVALID_ACCOUNT': + displayLoginAlert(localeObj.alertInvalid || 'Invalid username or password', 'error') + break - case 'NO_PASSWORD': - // No account password, create one with change password - displayLoginAlert(localeObj.alertNoPass || 'No password set, please change password', 'warn'); - break; + case 'NO_PASSWORD': + // No account password, create one with change password + displayLoginAlert(localeObj.alertNoPass || 'No password set, please change password', 'warn') + break - case 'UNKNOWN': - // Unknown error, contact server owner - displayLoginAlert(localeObj.alertUnknown || 'Unknown error, contact server owner', 'error'); - break; + case 'UNKNOWN': + // Unknown error, contact server owner + displayLoginAlert(localeObj.alertUnknown || 'Unknown error, contact server owner', 'error') + break - case undefined: - case null: - case 'AUTH_DISABLED': - // Authentication is disabled, we can just connect the user - displayLoginAlert(localeObj.alertAuthNoLogin || 'Authentication is disabled, no need to log in!', 'warn'); - launchPrivate(); - break; + case undefined: + case null: + case 'AUTH_DISABLED': + // Authentication is disabled, we can just connect the user + displayLoginAlert(localeObj.alertAuthNoLogin || 'Authentication is disabled, no need to log in!', 'warn') + launchPrivate() + break - default: - // Success! Copy the JWT token to their clipboard - const tkData = parseJwt(data.jwt) - await Neutralino.clipboard.writeText(tkData.token) + default: + // Success! Copy the JWT token to their clipboard + const tkData = parseJwt(data.jwt) + await Neutralino.clipboard.writeText(tkData.token) - displayLoginAlert(localeObj.alertLoginSuccess || 'Login successful! Token copied to clipboard. Paste this token into the username field of the game to log in.', 'success', 8000); - launchPrivate() - break; + displayLoginAlert(localeObj.alertLoginSuccess || 'Login successful! Token copied to clipboard. Paste this token into the username field of the game to log in.', 'success', 8000) + launchPrivate() + break } } @@ -102,14 +102,14 @@ async function login() { * Attempt registration, do not launch game */ async function register() { - const username = document.getElementById('registerUsername').value; - const password = document.getElementById('registerPassword').value; - const password_confirmation = document.getElementById('registerPasswordConfirm').value; - const ip = document.getElementById('ip').value; - const port = document.getElementById('port').value || '443'; - const config = await getCfg(); - const useHttps = config.useHttps; - const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}`; + const username = document.getElementById('registerUsername').value + const password = document.getElementById('registerPassword').value + const password_confirmation = document.getElementById('registerPasswordConfirm').value + const ip = document.getElementById('ip').value + const port = document.getElementById('port').value || '443' + const config = await getCfg() + const useHttps = config.useHttps + const url = `${useHttps ? 'https' : 'http'}://${ip}:${port}` const reqBody = { username, @@ -117,38 +117,38 @@ async function register() { password_confirmation } - const { data } = await axios.post(url + '/grasscutter/register', reqBody) + const { data } = await axios.post(url + '/authentication/register', reqBody) switch(data.message) { - case 'USERNAME_TAKEN': - // Username is taken - displayRegisterAlert(localeObj.alertUserTaken || 'Username is taken', 'error'); - break; + case 'USERNAME_TAKEN': + // Username is taken + displayRegisterAlert(localeObj.alertUserTaken || 'Username is taken', 'error') + break - case 'PASSWORD_MISMATCH': - // The password and password confirmation do not match - displayRegisterAlert(localStorage.alertPassMismatch || 'Password and password confirmation do not match', 'error'); - break; + case 'PASSWORD_MISMATCH': + // The password and password confirmation do not match + displayRegisterAlert(localStorage.alertPassMismatch || 'Password and password confirmation do not match', 'error') + break - case 'UNKNOWN': - // Unknown error, contact server owner - displayRegisterAlert(localeObj.alertUnknown || 'Unknown error, contact server owner', 'error'); - break; + case 'UNKNOWN': + // Unknown error, contact server owner + displayRegisterAlert(localeObj.alertUnknown || 'Unknown error, contact server owner', 'error') + break - case undefined: - case null: - case 'AUTH_DISABLED': - // Authentication is disabled, we can just connect the user - displayRegisterAlert(localeObj.alertAuthNoRegister || 'Authentication is disabled, no need to register!', 'warn'); - break; + case undefined: + case null: + case 'AUTH_DISABLED': + // Authentication is disabled, we can just connect the user + displayRegisterAlert(localeObj.alertAuthNoRegister || 'Authentication is disabled, no need to register!', 'warn') + break - default: - // Success!! Bring them to the login screen and auto-input their username - const loginUsername = document.getElementById('loginUsername'); - loginUsername.value = username; + default: + // Success!! Bring them to the login screen and auto-input their username + const loginUsername = document.getElementById('loginUsername') + loginUsername.value = username - setLoginSection(); - displayLoginAlert(localeObj.alertRegisterSuccess || 'Registration successful!', 'success', 5000); - break; + setLoginSection() + displayLoginAlert(localeObj.alertRegisterSuccess || 'Registration successful!', 'success', 5000) + break } } diff --git a/resources/js/onLoad.js b/resources/js/onLoad.js index a1e8773..2065d5e 100644 --- a/resources/js/onLoad.js +++ b/resources/js/onLoad.js @@ -3,11 +3,11 @@ * Every autofill, such as backgrounds and the game folder, * should be done here to ensure DOM contents are loaded. */ - document.addEventListener('DOMContentLoaded', async () => { - displayUpdate(); - setBackgroundImage(); - displayGameFolder(); - displayServerFolder(); +document.addEventListener('DOMContentLoaded', async () => { + displayUpdate() + setBackgroundImage() + displayGameFolder() + displayServerFolder() // Set title version document.querySelector('#version').innerHTML = NL_APPVERSION @@ -35,9 +35,10 @@ } // Exit favorites list and settings panel when clicking outside of it - window.addEventListener("click", function(e) { + window.addEventListener('click', function(e) { const favList = document.querySelector('#ipList') const settingsPanel = document.querySelector('#settingsPanel') + const downloadPanel = document.querySelector('#downloadPanel') // This will close the favorites list no matter what is clicked if (favList.style.display !== 'none') { @@ -46,25 +47,34 @@ } // This will close the settings panel no matter what is clicked - let settingCheckElm = e.target + let checkElm = e.target - while(settingCheckElm.tagName !== 'BODY') { - if (settingCheckElm.id === 'settingsPanel' - || settingCheckElm.id === 'settingsBtn') { + while(checkElm.tagName !== 'BODY') { + if (checkElm.id === 'settingsPanel' + || checkElm.id === 'settingsBtn') { return } - settingCheckElm = settingCheckElm.parentElement + if (checkElm.id === 'downloadPanel' || + checkElm.id === 'downloadBtn') { + return + } + + checkElm = checkElm.parentElement } // We travelled through the parents, so if we are at the body, we clicked outside of the settings panel - if (settingCheckElm.tagName === 'BODY') { + if (checkElm.tagName === 'BODY') { // This will close the settings panel only when something outside of it is clicked if (settingsPanel.style.display !== 'none') { settingsPanel.style.display = 'none' } + + if (downloadPanel.style.display !== 'none') { + downloadPanel.style.display = 'none' + } } - }); + }) // Ensure we do the translation at the very end, after everything else has loaded await doTranslation() diff --git a/resources/js/options.js b/resources/js/options.js index 58da7f6..5004108 100644 --- a/resources/js/options.js +++ b/resources/js/options.js @@ -72,7 +72,7 @@ async function handleLanguageChange(elm) { /** * Toggle the use of HTTPS */ - async function toggleHttps() { +async function toggleHttps() { const httpsCheckbox = document.querySelector('#httpsOption') const config = await getCfg() @@ -86,7 +86,7 @@ async function handleLanguageChange(elm) { * OR * Remove the current value of the IP input from the favorites list */ - async function setFavorite() { +async function setFavorite() { const ip = document.querySelector('#ip').value const port = document.querySelector('#port').value || '443' const ipArr = await getFavIps() diff --git a/resources/js/translation.js b/resources/js/translation.js index 986b03c..dd80b83 100644 --- a/resources/js/translation.js +++ b/resources/js/translation.js @@ -90,6 +90,21 @@ async function doTranslation() { set('loginPopupContentBodyBtnRegister', 'authRegisterBtn') set('noLoginBtn', 'launchWithoutAuth') + // Downloads section + set('downloadTitle', 'downloadTitle') + set('grassclipperTitle', 'grassclipperTitle') + set('grasscutterTitle', 'grasscutterTitle') + set('installerTitle', 'installerTitle') + set('installerSubtitle', 'installerSubtitle') + set('downloadStable', 'downloadStable') + set('stableSubtitle', 'stableSubtitle') + set('downloadDev', 'downloadDev') + set('devSubtitle', 'downloadSubtitle') + set('downloadResources', 'downloadResources') + set('devSubtitle', 'devSubtitle') + set('stableInstall', 'stableInstall') + set('devInstall', 'devInstall') + // update notification set('updateNotifText', 'updateNotifText') } \ No newline at end of file diff --git a/resources/js/windowDrag.js b/resources/js/windowDrag.js index b9aa3f5..642b0f3 100644 --- a/resources/js/windowDrag.js +++ b/resources/js/windowDrag.js @@ -1,25 +1,25 @@ // https://stackoverflow.com/questions/67971689/positioning-the-borderless-window-in-neutralino-js // had to use this since the in-built function breaks the close and minimize buttons -let dragging = false, ratio = 1, posX, posY; -let draggable; +let dragging = false, ratio = 1, posX, posY +let draggable document.addEventListener('DOMContentLoaded', async () => { - draggable = document.getElementById('controlBar'); + draggable = document.getElementById('controlBar') // Listen to hovers draggable.onmousedown = function (e) { ratio = window.devicePixelRatio - posX = e.pageX * ratio, posY = e.pageY * ratio; - dragging = true; + posX = e.pageX * ratio, posY = e.pageY * ratio + dragging = true } // Patch for monitors with scaling enabled, allows them to detach from the titlebar anywhere window.onmouseup = function (e) { - dragging = false; + dragging = false } document.onmousemove = function (e) { - if (dragging) Neutralino.window.move(e.screenX * ratio - posX, e.screenY * ratio - posY); + if (dragging) Neutralino.window.move(e.screenX * ratio - posX, e.screenY * ratio - posY) } }) \ No newline at end of file diff --git a/resources/style/index.css b/resources/style/index.css index ff31870..f738e24 100644 --- a/resources/style/index.css +++ b/resources/style/index.css @@ -11,6 +11,10 @@ a { color: #fff; } +img { + height: 20px; +} + .darken { filter: brightness(0.6); } @@ -157,6 +161,7 @@ a { #firstTimeNotice, #loginPanel, +#downloadPanel, #settingsPanel { display: block; position: absolute; @@ -172,18 +177,21 @@ a { font-family: system-ui; } +#downloadPanel, #settingsPanel { width: 35%; height: 80%; overflow: auto; } +#downloadTitle, #fullSettingsTitle { font-size: 1.5em; font-weight: bold; margin-bottom: 10px; } +#downloadPanelInner, #settingsPanelInner { display: flex; flex-direction: column; @@ -192,16 +200,23 @@ a { padding: 10px 10%; } +.downloadRow, .settingsRow { width: 100%; } +.downloadTitle, .settingTitle { font-size: 1.2em; font-weight: bold; margin-bottom: 10px; } +.downloadTitle { + margin: 6px; +} + +.downloadLabel, .settingLabel { display:inline-block; font-size: 1em; @@ -209,12 +224,14 @@ a { margin: 10px 0px; } +.downloadSubtitle, .settingSubtitle { color: rgb(165, 165, 165); font-size: 0.8em; font-weight: normal; } +.downloadSection, .settingSection { display: flex; flex-direction: row; @@ -222,10 +239,12 @@ a { justify-content: space-between; } +.downloadSection .smolBtn, .settingSection .smolBtn { height: 30px; } +#downloadTitleBar, #settingsTitleBar { display: flex; flex-direction: row; @@ -233,15 +252,18 @@ a { justify-content: space-between; } +#downloadClose, #settingsClose { display: inline-block; transition: filter 0.1s ease-in-out; } +#downloadClose img, #settingsClose img { height: 20px; } +#downloadClose:hover, #settingsClose:hover { filter: invert(85%) sepia(31%) saturate(560%) hue-rotate(329deg) brightness(100%) contrast(92%); cursor: pointer; diff --git a/scripts/gc_download.cmd b/scripts/gc_download.cmd new file mode 100644 index 0000000..6d822d2 --- /dev/null +++ b/scripts/gc_download.cmd @@ -0,0 +1,58 @@ +@echo off + +set KEYSTORE_URL=%1 +set ARTIFACT_URL=%2 +set BRANCH=%3 +set FOLDER_NAME=".\gc-%BRANCH%" +set FOLDER_NAME=%FOLDER_NAME:"=% + +title GC Download Script + +if not exist "%FOLDER_NAME%" mkdir "%FOLDER_NAME%" +if not exist ".\temp" mkdir ".\temp" + +echo Downloading Grasscutter prebuilt jar... + +:: Download the jar +powershell Invoke-WebRequest -Uri %KEYSTORE_URL% -OutFile "./temp/gcjar.zip" + +echo Extracting... + +:: Delete old file if there is one there +if exist "%FOLDER_NAME%\grasscutter.jar" del "%FOLDER_NAME%\grasscutter.jar" + +powershell Expand-Archive -Path "./temp/gcjar.zip" -DestinationPath "%FOLDER_NAME%" -Force + +:: Find the jar file name and rename it, just in case +for %%i in (%FOLDER_NAME%/*) do ( + :: If the extension is jar, rename the file + if %%~xi equ .jar rename "%FOLDER_NAME%\%%i" grasscutter.jar +) + +echo Downloading keystore.p12... + +:: Download the keystore.p12 file +powershell Invoke-WebRequest -Uri %ARTIFACT_URL% -OutFile "./%FOLDER_NAME%/keystore.p12" + +:: Check java version, this will automatically output some tuff +call .\scripts\javaver.cmd %BRANCH% + +:: Allow resource downloading to be optional, since it takes a while +set REPLY=y +set /p "REPLY=Download server resources? (This can take a while) [y|n]:" +if /i not "%reply%" == "y" goto :finish + +call .\scripts\resources_download.cmd %FOLDER_NAME% + +goto :finish + +:finish + :: Remove temp stuff + del /s /q "./temp" + + echo Done, latest Grasscutter %BRANCH% now downloaded in %FOLDER_NAME% + + pause + + taskkill /f /fi "WINDOWTITLE eq GC Download Script" + diff --git a/scripts/install.cmd b/scripts/install.cmd index 99e27c3..63fbd41 100644 --- a/scripts/install.cmd +++ b/scripts/install.cmd @@ -3,6 +3,8 @@ set ORIGIN=%1 set ORIGIN=%ORIGIN:"=% +title Grassclipper Installer + echo Downloading proxy server... :: Make sure we are in the right directory @@ -35,12 +37,12 @@ taskkill /f /im mitmdump.exe echo Adding ceritifcate... :: Ensure we are elevated for certs ->nul 2>&1 certutil -addstore root %USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer || ( +>nul 2>&1 certutil -addstore root "%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer" || ( echo ============================================================================================================ echo !! Certificate install failed !! echo. echo Please manually run this command as Administrator: - echo certutil -addstore root %USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer + echo certutil -addstore root "%USERPROFILE%\.mitmproxy\mitmproxy-ca-cert.cer" echo ============================================================================================================ ) @@ -48,4 +50,4 @@ echo Done! You can now open GrassClipper.exe! pause -exit /b \ No newline at end of file +taskkill /f /fi "WINDOWTITLE eq Grassclipper Installer" \ No newline at end of file diff --git a/scripts/javaver.cmd b/scripts/javaver.cmd new file mode 100644 index 0000000..c22093a --- /dev/null +++ b/scripts/javaver.cmd @@ -0,0 +1,70 @@ +@echo off + +set BRANCH=%1 + +echo Checking java version... + +where java >nul 2>nul +if %errorlevel%==1 ( + echo ======================================================================================= + echo No version of Java was found! + + if %BRANCH% EQU stable ( + echo To launch the stable branch server, you must install Java 8 + ) + + if %BRANCH% EQU development ( + echo To launch the development branch server, you must install Java 17 + ) + + echo ======================================================================================= + + exit /b +) + +:: https://stackoverflow.com/questions/5675459/how-to-get-java-version-from-batch-script +for /f "tokens=3" %%g in ('java -version 2^>^&1 ^| findstr /i "version"') do ( + @echo Output: %%g + set JAVAVER=%%g +) +set JAVAVER=%JAVAVER:"=% + +for /f "delims=. tokens=1-3" %%v in ("%JAVAVER%") do ( + set MAJOR=%%v + set MINOR=%%w + set BUILD=%%x +) + +if %BRANCH% EQU stable ( + :: Ensure java 8 + if %MAJOR% EQU 1 ( + if %MINOR% LSS 8 ( + echo ======================================================================================= + echo !! Java version is less than 8 !! + echo Please download Java 8 to ensure %BRANCH% branch server launches correctly. + echo ======================================================================================= + exit /b + ) + ) + + if %MAJOR% NEQ 1 ( + echo ======================================================================================= + echo !! Java version is not 8 !! + echo Please download Java 8 to ensure %BRANCH% branch server launches correctly. + echo ======================================================================================= + exit /b + ) +) + +if %BRANCH% EQU development ( + :: Ensure java 17 + if %MAJOR% LSS 17 ( + echo ======================================================================================= + echo !! Java version is less than 17 !! + echo Please download Java 17 to ensure %BRANCH% branch server launches correctly. + echo ======================================================================================= + exit /b + ) +) + +echo Java version is compatible \ No newline at end of file diff --git a/scripts/local_server_launch.cmd b/scripts/local_server_launch.cmd index e052d1a..75dde1f 100644 --- a/scripts/local_server_launch.cmd +++ b/scripts/local_server_launch.cmd @@ -3,15 +3,23 @@ set GRASSCUTTER_JAR=%1 set GRASSCUTTER_JAR=%GRASSCUTTER_JAR:"=% +title Grasscutter + :: Get folder the jar is in set "X=%GRASSCUTTER_JAR%" :l -if "%X:~-1%"=="\" goto al -set "X=%X:~0,-1%" -goto l + set IS_SLASH=false + + if "%X:~-1%"=="\" set IS_SLASH=true + if "%X:~-1%"=="/" set IS_SLASH=true + + if %IS_SLASH% equ true goto al + + set "X=%X:~0,-1%" + goto l :al -set "X=%X:~0,-1%" -set "GRASSCUTTER_ROOT=%X%" + set "X=%X:~0,-1%" + set "GRASSCUTTER_ROOT=%X%" echo Starting local Grasscutter server... diff --git a/scripts/private_server_launch.cmd b/scripts/private_server_launch.cmd index 0cc7b12..5fcbf1e 100644 --- a/scripts/private_server_launch.cmd +++ b/scripts/private_server_launch.cmd @@ -27,7 +27,7 @@ if "%ENABLE_KILLSWITCH%" EQU "true" ( :: Restart in elevated if need be >nul 2>&1 reg query "HKU\S-1-5-19" || ( set params = %*:"="""% - cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 %1 %2 %3 "%4" ""%cd%/../"" %6", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && taskkill /f /fi "WINDOWTITLE eq PS Launcher Script" && exit /b ) + cd /d "%~dp0" && ( if exist "%temp%\getadmin.vbs" del "%temp%\getadmin.vbs" ) && fsutil dirty query %systemdrive% 1>nul 2>nul || ( echo Set UAC = CreateObject^("Shell.Application"^) : UAC.ShellExecute "cmd.exe", "/k cd ""%~sdp0"" && %~s0 %1 %2 %3 "%4" ""%cd%"" %6", "", "runas", 1 >> "%temp%\getadmin.vbs" && "%temp%\getadmin.vbs" && taskkill /f /fi "WINDOWTITLE eq PS Launcher Script" && exit /b ) ) ) @@ -41,7 +41,7 @@ reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v Pr reg add "HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings" /v ProxyServer /d "127.0.0.1:8080" /f >nul 2>nul :: Start proxy server -start "Proxy Server" "%ORIGIN%/ext/mitmdump.exe" -s "%ORIGIN%/proxy/proxy.py" -k --allow-hosts ".*\.yuanshen\.com|.*\.mihoyo\.com|.*\.hoyoverse\.com" --ssl-insecure --set ip=%IP% --set port=%PORT% --set use_https=%USE_HTTPS% +start "Proxy Server" "%ORIGIN%\ext\mitmdump.exe" -s "%ORIGIN%/proxy/proxy.py" -k --allow-hosts ".*\.yuanshen\.com|.*\.mihoyo\.com|.*\.hoyoverse\.com" --ssl-insecure --set ip=%IP% --set port=%PORT% --set use_https=%USE_HTTPS% echo Opening %GAME_PATH% diff --git a/scripts/resources_download.cmd b/scripts/resources_download.cmd new file mode 100644 index 0000000..f1e1574 --- /dev/null +++ b/scripts/resources_download.cmd @@ -0,0 +1,29 @@ +@echo off + +set FOLDER_NAME=%1 +set FOLDER_NAME=%FOLDER_NAME:"=% + +if not exist ".\temp" mkdir ".\temp" +if not exist ".\resources" mkdir ".\resources" + +echo Downloading resources, this can take a while... + +:: Grab the giant ass resource zip +powershell Invoke-WebRequest -Uri https://github.com/Koko-boya/Grasscutter_Resources/archive/refs/heads/main.zip -OutFile "./temp/resources.zip" + +echo Extracting... + +:: Extract resources to the folder +powershell Expand-Archive -Path "./temp/resources.zip" -DestinationPath "%FOLDER_NAME%" -Force + +:: Delete old resources folder if there is one there +del /s /q "%FOLDER_NAME%\resources">nul + +echo Moving resources to folder... + +robocopy "%FOLDER_NAME%\Grasscutter_Resources-main\Resources" "%FOLDER_NAME%\resources" /E /MOVE>nul + +:: Delete straggling files +del /s /q "%FOLDER_NAME%\Grasscutter_Resources-main" + +echo Done, resources should be properly extracted \ No newline at end of file