diff --git a/bun.lockb b/bun.lockb index 6af9cff..0c10e67 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index ed09330..9ee7ef6 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "errorhandler": "^1.5.1", "express": "^4.19.2", "helmet": "^7.1.0", + "indexeddb-export-import": "^2.1.5", "morgan": "^1.10.0", "pako": "^2.1.0", "semver": "^7.6.3", diff --git a/src/articles/index.html b/src/articles/index.html index a519ce3..7586475 100644 --- a/src/articles/index.html +++ b/src/articles/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/console/index.html b/src/console/index.html new file mode 100644 index 0000000..c334d1e --- /dev/null +++ b/src/console/index.html @@ -0,0 +1,105 @@ + + + + + + MineXLauncher + + + + + + + + +
    + +
    +
    + MineXLauncher +
    + +
    +
    +
    +
    +
    +
    +
    + Join the MineXLauncher Discord + © 2024 MineXLauncher. All rights reserved. +
    +
    +
    + +
    + + diff --git a/src/home/archive/index.html b/src/home/archive/index.html index 4dc621c..b19621b 100644 --- a/src/home/archive/index.html +++ b/src/home/archive/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/home/clients/index.html b/src/home/clients/index.html index ea8b169..2958295 100644 --- a/src/home/clients/index.html +++ b/src/home/clients/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/home/downloads/index.html b/src/home/downloads/index.html index 27c7b84..097ea68 100644 --- a/src/home/downloads/index.html +++ b/src/home/downloads/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/home/game/index.html b/src/home/game/index.html index bbd718b..028e145 100644 --- a/src/home/game/index.html +++ b/src/home/game/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/mods/mods/index.html b/src/mods/mods/index.html index d0f0834..43348ef 100644 --- a/src/mods/mods/index.html +++ b/src/mods/mods/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/mods/resourcepacks/index.html b/src/mods/resourcepacks/index.html index 8cf1613..5687f70 100644 --- a/src/mods/resourcepacks/index.html +++ b/src/mods/resourcepacks/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/resources/scripts/eagler-launch/1.8.8/eaglerforge.ts b/src/resources/scripts/eagler-launch/1.8.8/eaglerforge.ts deleted file mode 100644 index 8395b01..0000000 --- a/src/resources/scripts/eagler-launch/1.8.8/eaglerforge.ts +++ /dev/null @@ -1,57 +0,0 @@ -// @ts-nocheck -window.addEventListener('load', () => { - const relayId = Math.floor(Math.random() * 3); - window.eaglercraftXOpts = { - container: 'game_frame', - assetsURI: `${window.location.pathname}/assets.epk`, - localesURI: `${window.location.pathname}/lang/`, - servers: [ - { addr: 'wss://mc.ricenetwork.xyz', name: 'Rice Network' }, - { addr: 'wss://mc.lamplifesteal.xyz', name: 'LampLifesteal' }, - { addr: 'wss://electronmc.club', name: 'Electron Network' }, - { addr: 'wss://play.brickmc.net', name: 'BrickMC' }, - ], - relays: [ - { - addr: 'wss://relay.deev.is/', - comment: 'lax1dude relay #1', - primary: relayId === 0, - }, - { - addr: 'wss://relay.lax1dude.net/', - comment: 'lax1dude relay #2', - primary: relayId === 1, - }, - { - addr: 'wss://relay.shhnowisnottheti.me/', - comment: 'ayunami relay #1', - primary: relayId === 2, - }, - ], - }; - - const storage = { - local: { - get: function (key: string) { - const item = localStorage.getItem('minexlauncher'); - if (item !== null) { - const json = JSON.parse(item); - if (json[key] !== undefined) { - return json[key]; - } else { - return null; - } - } else { - return null; - } - }, - }, - }; - - const urlParams = new URLSearchParams(window.location.search); - window.eaglercraftXOpts.joinServer = urlParams.get('server') ?? undefined; - window.eaglercraftXOpts.Mods = storage.local.get('mods') ?? []; - - history.replaceState({}, '', '/play'); - main(); -}); diff --git a/src/resources/scripts/main.ts b/src/resources/scripts/main.ts index 3b8a4b2..0bf22ae 100644 --- a/src/resources/scripts/main.ts +++ b/src/resources/scripts/main.ts @@ -1,5 +1,6 @@ import { gt, coerce } from 'semver'; import { inflate, deflate } from 'pako'; +import idbExportImport from 'indexeddb-export-import'; let selectedVersion: string | undefined = undefined; let articleAnimationLock = false; @@ -47,23 +48,78 @@ const versionSelector = { }, }; +function consoleLog( + type: 'debug' | 'log' | 'info' | 'warn' | 'error', + msg: string, +) { + const consoleElement = document.querySelector('.console'); + if (consoleElement) { + const messageElement = document.createElement('p'); + messageElement.classList.add(type); + messageElement.innerText = msg; + consoleElement.appendChild(messageElement); + storage.session.set( + 'console', + base64Gzip.compress(consoleElement.innerHTML), + ); + consoleElement.scroll(0, consoleElement.scrollHeight); + } +} + const game = { play: function (version?: string) { - if (version) { - document.body.style.display = 'none'; - storage.session.set('lastGame', version); - // @ts-expect-error - window.top.location.href = version; - } else if (selectedVersion) { - document.body.style.display = 'none'; - storage.session.set('lastGame', selectedVersion); - // @ts-expect-error - window.top.location.href = selectedVersion; - } else { + version = version ?? selectedVersion; + if (!version) { alert('Please select a version to play.'); return; } + document.body.style.display = 'none'; + storage.session.set('lastGame', selectedVersion); + + window.open(version, '_blank', 'popup=true'); + + /*window.top.instanceWindow = window.open(version); + window.top.instanceWindow.onload = function () { + window.top.instanceWindow.history.replaceState({}, '', '/play'); + window.top.instanceWindow.console.debug = (msg: string) => + consoleLog('debug', msg); + window.top.instanceWindow.console.log = (msg: string) => + consoleLog('log', msg); + window.top.instanceWindow.console.info = (msg: string) => + consoleLog('info', msg); + window.top.instanceWindow.console.warn = (msg: string) => + consoleLog('warn', msg); + window.top.instanceWindow.console.error = (msg: string) => + consoleLog('error', msg); + }; + const waitForCrash = setInterval(() => { + if (window.top.instanceWindow.closed) { + clearInterval(waitForCrash); + game.stop(); + } else { + window.top.instanceWindow.document + .querySelectorAll('div') + .forEach((element: HTMLElement) => { + if (element.innerText.includes('Game Crashed!')) { + clearInterval(waitForCrash); + game.stop(element.innerHTML); + } + }); + } + }, 50);*/ }, + /*stop: function (error?: string) { + if (window.top.instanceWindow) { + window.top.instanceWindow.onbeforeunload = null; + window.top.instanceWindow.close(); + if (error) { + consoleLog('error', '\n\nMineXLauncher: Game crashed with error:'); + consoleLog('error', error); + } else { + consoleLog('error', '\n\nMineXLauncher: Game process killed by user'); + } + } + },*/ select: function (path: string, name?: string) { selectedVersion = path; const selector = document.querySelector('.installations div .selector'); @@ -135,6 +191,18 @@ const navigate = { window.location.href = navUrl; }, }, + settings: { + general: function () { + const navUrl = '/settings/general/'; + storage.session.set('lastPage', navUrl); + window.location.href = navUrl; + }, + backups: function () { + const navUrl = '/settings/backups/'; + storage.session.set('lastPage', navUrl); + window.location.href = navUrl; + }, + }, articles: function () { const navUrl = '/articles/'; storage.session.set('lastPage', navUrl); @@ -155,11 +223,6 @@ const navigate = { storage.session.set('lastPage', navUrl); window.location.href = navUrl; }, - settings: function () { - const navUrl = '/settings/'; - storage.session.set('lastPage', navUrl); - window.location.href = navUrl; - }, }; const article = { @@ -346,6 +409,117 @@ const mods = { }, }; +const backups = { + export: async function () { + const exportData: { + cookies: Record; + localStorage: Record; + indexedDb: Record; + } = { + cookies: {}, + localStorage: { ...localStorage }, + indexedDb: {}, + }; + + document.cookie.split('; ').forEach((cookie) => { + const pair = cookie.split('='); + if (pair[0] !== undefined && pair[1] !== undefined) + exportData.cookies[pair[0]] = pair[1]; + }); + + for (const dbInfo of await indexedDB.databases()) { + if (dbInfo.name) + indexedDB.open(dbInfo.name).onsuccess = (event) => + (exportData.indexedDb[dbInfo.name ?? ''] = + idbExportImport.exportToJsonString( + (event.target as IDBOpenDBRequest).result, + )); + } + + const url = URL.createObjectURL( + new Blob([base64Gzip.compress(JSON.stringify(exportData))]), + ); + const a = document.createElement('a'); + a.href = url; + a.target = '_blank'; + a.download = `MineXLauncher_${new Date().toLocaleDateString('en-GB').replace(/\//g, '-')}.backup`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + }, + import: async function () { + await new Promise((resolve, reject) => { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.backup'; + fileInput.multiple = false; + fileInput.onchange = (event) => { + const file = (event.target as HTMLInputElement).files?.[0]; + if (file) resolve(file); + else reject(); + }; + fileInput.oncancel = () => reject(); + document.body.appendChild(fileInput); + fileInput.click(); + document.body.removeChild(fileInput); + }) + .then(async (file) => { + const importData = JSON.parse(base64Gzip.decompress(await file.text())); + + // @ts-expect-error + if (typeof cookieStore !== 'undefined') + // @ts-expect-error + await cookieStore.getAll().then((cookies: object[]) => + // @ts-expect-error + cookies.forEach((cookie) => cookieStore.delete(cookie)), + ); + else + document.cookie + .split('; ') + .forEach( + (cookie) => + (document.cookie = `${cookie.split('=')[0]}=; Path=/; Max-Age=0`), + ); + + localStorage.clear(); + + for (const dbInfo of await indexedDB.databases()) + if (dbInfo.name) indexedDB.deleteDatabase(dbInfo.name); + + for (const key in importData.localStorage) + if (importData.localStorage[key] !== undefined) + localStorage.setItem(key, importData.localStorage[key]); + + for (const key in importData.cookies) + document.cookie = `${key}=${importData.cookies[key]}; Max-Age=${365 * 24 * 60 * 60}; Path=/; SameSite=Lax; Secure`; + + if (Object.keys(importData.indexedDb).length > 0) + for (const dbName in importData.indexedDb) { + const dbRequest = indexedDB.open(dbName); + dbRequest.onsuccess = async (event) => { + const db = (event.target as IDBOpenDBRequest).result; + const transaction = db.transaction( + Array.from(db.objectStoreNames), + 'readwrite', + ); + + for (const storeName of Array.from(db.objectStoreNames)) + transaction.objectStore(storeName).clear(); + + transaction.oncomplete = () => + idbExportImport.importFromJsonString( + db, + importData.indexedDb[dbName], + ); + }; + } + window.top?.location.reload(); + }) + .catch((error) => console.error(error)); + }, +}; + const sw = { register: function (url: string) { if ('serviceWorker' in navigator) { @@ -365,40 +539,16 @@ const sw = { }; const base64Gzip = { - decode: function (base64: string) { - // Decode Base64 to binary string - const binaryString = atob(base64); - - // Convert binary string to Uint8Array - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - // Use pako to decompress the Uint8Array - const decompressed = inflate(bytes, { to: 'string' }); - - return decompressed; + compress: function (string: string): string { + return btoa( + String.fromCharCode(...deflate(new TextEncoder().encode(string))), + ); }, - encode: function (inputString: string) { - // Convert the input string to a Uint8Array - const encoder = new TextEncoder(); - const inputBytes = encoder.encode(inputString); - - // Use pako to compress the Uint8Array - const compressedBytes = deflate(inputBytes); - - // Convert the compressed Uint8Array to a binary string - let binaryString = ''; - for (const byte of compressedBytes) { - binaryString += String.fromCharCode(byte); - } - - // Encode the binary string to Base64 - const base64String = btoa(binaryString); - - return base64String; + decompress: function (string: string): string { + return inflate( + Uint8Array.from(atob(string), (c) => c.charCodeAt(0)), + { to: 'string' }, + ); }, }; @@ -435,6 +585,7 @@ if (window.location.pathname === '/') { } }); */ document.addEventListener('load', () => sw.register('/sw.js')); + window.addEventListener('beforeunload', () => game.stop()); window.addEventListener('beforeinstallprompt', (event) => { // @ts-expect-error @@ -500,7 +651,7 @@ if (window.location.pathname === '/') { } */ } -if (window.location.pathname === '/settings/') { +if (window.location.pathname === '/settings/general/') { document.addEventListener('DOMContentLoaded', async () => { const profileName = document.querySelector('.username'); const usernameInput = document.querySelector( @@ -731,5 +882,6 @@ if (window.location.hostname === null) { base64Gzip, article, downloadFile, + backups, ]); } diff --git a/src/resources/styles/themes/default.css b/src/resources/styles/themes/default.css index 9b37886..5f4635c 100644 --- a/src/resources/styles/themes/default.css +++ b/src/resources/styles/themes/default.css @@ -699,6 +699,10 @@ body { outline: none; } +.settings form[id='settings-backups'] { + padding-bottom: 60px; +} + .settings form button[type='submit'] { width: 100%; padding: 10px; diff --git a/src/servers/index.html b/src/servers/index.html index 4ebbdf6..7b0e9ec 100644 --- a/src/servers/index.html +++ b/src/servers/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings
  • diff --git a/src/settings/backups/index.html b/src/settings/backups/index.html new file mode 100644 index 0000000..32ff6c0 --- /dev/null +++ b/src/settings/backups/index.html @@ -0,0 +1,107 @@ + + + + + + MineXLauncher + + + + + + + +
    + +
    +
    + MineXLauncher +
    + +
    +
    +
      +
    • General
    • +
    • + Backups +
    • +
    +
    +
    +
    +
    +

    Backups

    +

    + Save a backup of your launcher data.
    Note: Backing up + worlds is not yet supported. Please export them from + Eaglercraft for now. +

    + + +
    +
    +
    +
    +
    +
    + Join the MineXLauncher Discord + © 2024 MineXLauncher. All rights reserved. +
    +
    +
    + +
    + + diff --git a/src/settings/index.html b/src/settings/general/index.html similarity index 84% rename from src/settings/index.html rename to src/settings/general/index.html index 4ebcb86..eb9b3b2 100644 --- a/src/settings/index.html +++ b/src/settings/general/index.html @@ -55,14 +55,21 @@ Tutorials -
  • +
  • Settings
  • - Settings +
    +
      +
    • + General +
    • +
    • Backups
    • +
    +
    @@ -72,15 +79,11 @@ + +
    - -
    diff --git a/src/updates/index.html b/src/updates/index.html index a4b3edc..abaaf80 100644 --- a/src/updates/index.html +++ b/src/updates/index.html @@ -55,7 +55,7 @@ Tutorials -
  • +
  • Settings