Merge branch 'master' of https://github.com/dscalzi/HeliosLauncher into dscalzi-master
This commit is contained in:
commit
3a301b237b
29
.github/workflows/build.yml
vendored
Normal file
29
.github/workflows/build.yml
vendored
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
name: Build/release
|
||||||
|
|
||||||
|
on: push
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out Git repository
|
||||||
|
uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install Node.js, NPM and Yarn
|
||||||
|
uses: actions/setup-node@v1
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
|
||||||
|
- name: Build/release Electron app
|
||||||
|
uses: samuelmeuli/action-electron-builder@v1
|
||||||
|
with:
|
||||||
|
github_token: ${{ secrets.github_token }}
|
||||||
|
|
||||||
|
# If the commit is tagged with a version (e.g. "v1.0.0"),
|
||||||
|
# release the app after building
|
||||||
|
release: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
45
.travis.yml
45
.travis.yml
@ -1,45 +0,0 @@
|
|||||||
matrix:
|
|
||||||
include:
|
|
||||||
- os: osx
|
|
||||||
osx_image: xcode11.3
|
|
||||||
language: node_js
|
|
||||||
node_js: "12"
|
|
||||||
env:
|
|
||||||
- ELECTRON_CACHE=$HOME/.cache/electron
|
|
||||||
- ELECTRON_BUILDER_CACHE=$HOME/.cache/electron-builder
|
|
||||||
- ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true
|
|
||||||
- CSC_IDENTITY_AUTO_DISCOVERY=false
|
|
||||||
|
|
||||||
- os: linux
|
|
||||||
services: docker
|
|
||||||
language: generic
|
|
||||||
node_js: "12"
|
|
||||||
env:
|
|
||||||
- ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- node_modules
|
|
||||||
- $HOME/.cache/electron
|
|
||||||
- $HOME/.cache/electron-builder
|
|
||||||
|
|
||||||
script:
|
|
||||||
- |
|
|
||||||
if [ "$TRAVIS_OS_NAME" == "linux" ]; then
|
|
||||||
ENVS=`env | grep -iE '(DEBUG|NODE_|ELECTRON_|YARN_|NPM_|CI|CIRCLE|TRAVIS|APPVEYOR_|CSC_|_TOKEN|_KEY|AWS_|STRIP|BUILD_)' | sed -n '/^[^\t]/s/=.*//p' | sed '/^$/d' | sed 's/^/-e /g' | tr '\n' ' '`
|
|
||||||
docker run $ENVS --rm \
|
|
||||||
-v ${PWD}:/project \
|
|
||||||
-v ~/.cache/electron:/root/.cache/electron \
|
|
||||||
-v ~/.cache/electron-builder:/root/.cache/electron-builder \
|
|
||||||
electronuserland/builder:wine \
|
|
||||||
/bin/bash -c "node -v && npm ci && npm run cilinux"
|
|
||||||
else
|
|
||||||
npm run cidarwin
|
|
||||||
fi
|
|
||||||
|
|
||||||
before_cache:
|
|
||||||
- rm -rf $HOME/.cache/electron-builder/wine
|
|
||||||
|
|
||||||
branches:
|
|
||||||
except:
|
|
||||||
- "/^v\\d+\\.\\d+\\.\\d+$/"
|
|
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2017-2021 Daniel D. Scalzi
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
28
README.md
28
README.md
@ -53,9 +53,10 @@ If you download from the [Releases](https://github.com/dscalzi/HeliosLauncher/re
|
|||||||
|
|
||||||
| Platform | File |
|
| Platform | File |
|
||||||
| -------- | ---- |
|
| -------- | ---- |
|
||||||
| Windows x64 | `helioslauncher-setup-VERSION.exe` |
|
| Windows x64 | `Helios-Launcher-setup-VERSION.exe` |
|
||||||
| macOS | `helioslauncher-VERSION.dmg` |
|
| macOS x64 | `Helios-Launcher-setup-VERSION.dmg` |
|
||||||
| Linux x64 | `helioslauncher-VERSION-x86_64.AppImage` |
|
| macOS arm64 | `Helios-Launcher-setup-VERSION-arm64.dmg` |
|
||||||
|
| Linux x64 | `Helios-Launcher-setup-VERSION.AppImage` |
|
||||||
|
|
||||||
## Console
|
## Console
|
||||||
|
|
||||||
@ -76,11 +77,13 @@ If you want to export the console output, simply right click anywhere on the con
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
|
This section details the setup of a basic developmentment environment.
|
||||||
|
|
||||||
### Getting Started
|
### Getting Started
|
||||||
|
|
||||||
**System Requirements**
|
**System Requirements**
|
||||||
|
|
||||||
* [Node.js][nodejs] v12
|
* [Node.js][nodejs] v14
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -137,13 +140,9 @@ Paste the following into `.vscode/launch.json`
|
|||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"cwd": "${workspaceFolder}",
|
"cwd": "${workspaceFolder}",
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
"program": "${workspaceFolder}/node_modules/electron/cli.js",
|
||||||
"windows": {
|
|
||||||
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
|
||||||
},
|
|
||||||
"args" : ["."],
|
"args" : ["."],
|
||||||
"console": "integratedTerminal",
|
"outputCapture": "std"
|
||||||
"protocol": "inspector"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Debug Renderer Process",
|
"name": "Debug Renderer Process",
|
||||||
@ -179,14 +178,7 @@ Note that you **cannot** open the DevTools window while using this debug configu
|
|||||||
|
|
||||||
### Note on Third-Party Usage
|
### Note on Third-Party Usage
|
||||||
|
|
||||||
You may use this software in your own project so long as the following conditions are met.
|
Please give credit to the original author and provide a link to the original source. This is free software, please do at least this much.
|
||||||
|
|
||||||
* Credit is expressly given to the original authors (Daniel Scalzi).
|
|
||||||
* Include a link to the original source on the launcher's About page.
|
|
||||||
* Credit the authors and provide a link to the original source in any publications or download pages.
|
|
||||||
* The source code remain **public** as a fork of this repository.
|
|
||||||
|
|
||||||
We reserve the right to update these conditions at any time, please check back periodically.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -6,8 +6,6 @@ if(target == null){
|
|||||||
}
|
}
|
||||||
let tracker = new target(...(process.argv.splice(3)))
|
let tracker = new target(...(process.argv.splice(3)))
|
||||||
|
|
||||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'
|
|
||||||
|
|
||||||
//const tracker = new AssetGuard(process.argv[2], process.argv[3])
|
//const tracker = new AssetGuard(process.argv[2], process.argv[3])
|
||||||
console.log('AssetExec Started')
|
console.log('AssetExec Started')
|
||||||
|
|
||||||
|
@ -266,7 +266,11 @@ class JavaGuard extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the last open JDK binary. Uses https://api.adoptopenjdk.net/
|
* Fetch the last open JDK binary.
|
||||||
|
*
|
||||||
|
* HOTFIX: Uses Corretto 8 for macOS.
|
||||||
|
* See: https://github.com/dscalzi/HeliosLauncher/issues/70
|
||||||
|
* See: https://github.com/AdoptOpenJDK/openjdk-support/issues/101
|
||||||
*
|
*
|
||||||
* @param {string} major The major version of Java to fetch.
|
* @param {string} major The major version of Java to fetch.
|
||||||
*
|
*
|
||||||
@ -274,6 +278,15 @@ class JavaGuard extends EventEmitter {
|
|||||||
*/
|
*/
|
||||||
static _latestOpenJDK(major = '8'){
|
static _latestOpenJDK(major = '8'){
|
||||||
|
|
||||||
|
if(process.platform === 'darwin') {
|
||||||
|
return this._latestCorretto(major)
|
||||||
|
} else {
|
||||||
|
return this._latestAdoptOpenJDK(major)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static _latestAdoptOpenJDK(major) {
|
||||||
|
|
||||||
const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform)
|
const sanitizedOS = process.platform === 'win32' ? 'windows' : (process.platform === 'darwin' ? 'mac' : process.platform)
|
||||||
|
|
||||||
const url = `https://api.adoptopenjdk.net/v2/latestAssets/nightly/openjdk${major}?os=${sanitizedOS}&arch=x64&heap_size=normal&openjdk_impl=hotspot&type=jre`
|
const url = `https://api.adoptopenjdk.net/v2/latestAssets/nightly/openjdk${major}?os=${sanitizedOS}&arch=x64&heap_size=normal&openjdk_impl=hotspot&type=jre`
|
||||||
@ -291,6 +304,48 @@ class JavaGuard extends EventEmitter {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
static _latestCorretto(major) {
|
||||||
|
|
||||||
|
let sanitizedOS, ext
|
||||||
|
|
||||||
|
switch(process.platform) {
|
||||||
|
case 'win32':
|
||||||
|
sanitizedOS = 'windows'
|
||||||
|
ext = 'zip'
|
||||||
|
break
|
||||||
|
case 'darwin':
|
||||||
|
sanitizedOS = 'macos'
|
||||||
|
ext = 'tar.gz'
|
||||||
|
break
|
||||||
|
case 'linux':
|
||||||
|
sanitizedOS = 'linux'
|
||||||
|
ext = 'tar.gz'
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
sanitizedOS = process.platform
|
||||||
|
ext = 'tar.gz'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `https://corretto.aws/downloads/latest/amazon-corretto-${major}-x64-${sanitizedOS}-jdk.${ext}`
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
request.head({url, json: true}, (err, resp) => {
|
||||||
|
if(!err && resp.statusCode === 200){
|
||||||
|
resolve({
|
||||||
|
uri: url,
|
||||||
|
size: parseInt(resp.headers['content-length']),
|
||||||
|
name: url.substr(url.lastIndexOf('/')+1)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
resolve(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -455,6 +510,11 @@ class JavaGuard extends EventEmitter {
|
|||||||
} */
|
} */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Space included so we get only the vendor.
|
||||||
|
} else if(props[i].lastIndexOf('java.vendor ') > -1) {
|
||||||
|
let vendorName = props[i].split('=')[1].trim()
|
||||||
|
console.log(props[i].trim())
|
||||||
|
meta.vendor = vendorName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,51 +709,26 @@ class JavaGuard extends EventEmitter {
|
|||||||
* @returns {Promise.<Set.<string>>} A promise which resolves to a set of the discovered
|
* @returns {Promise.<Set.<string>>} A promise which resolves to a set of the discovered
|
||||||
* root JVM folders.
|
* root JVM folders.
|
||||||
*/
|
*/
|
||||||
static _scanFileSystem(scanDir){
|
static async _scanFileSystem(scanDir){
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
|
|
||||||
fs.exists(scanDir, (e) => {
|
|
||||||
|
|
||||||
let res = new Set()
|
let res = new Set()
|
||||||
|
|
||||||
if(e){
|
if(await fs.pathExists(scanDir)) {
|
||||||
fs.readdir(scanDir, (err, files) => {
|
|
||||||
if(err){
|
|
||||||
resolve(res)
|
|
||||||
console.log(err)
|
|
||||||
} else {
|
|
||||||
let pathsDone = 0
|
|
||||||
|
|
||||||
|
const files = await fs.readdir(scanDir)
|
||||||
for(let i=0; i<files.length; i++){
|
for(let i=0; i<files.length; i++){
|
||||||
|
|
||||||
const combinedPath = path.join(scanDir, files[i])
|
const combinedPath = path.join(scanDir, files[i])
|
||||||
const execPath = JavaGuard.javaExecFromRoot(combinedPath)
|
const execPath = JavaGuard.javaExecFromRoot(combinedPath)
|
||||||
|
|
||||||
fs.exists(execPath, (v) => {
|
if(await fs.pathExists(execPath)) {
|
||||||
|
|
||||||
if(v){
|
|
||||||
res.add(combinedPath)
|
res.add(combinedPath)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
++pathsDone
|
|
||||||
|
|
||||||
if(pathsDone === files.length){
|
|
||||||
resolve(res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
return res
|
||||||
}
|
|
||||||
if(pathsDone === files.length){
|
|
||||||
resolve(res)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
resolve(res)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -799,9 +834,13 @@ class JavaGuard extends EventEmitter {
|
|||||||
|
|
||||||
// Get possible paths from the registry.
|
// Get possible paths from the registry.
|
||||||
let pathSet1 = await JavaGuard._scanRegistry()
|
let pathSet1 = await JavaGuard._scanRegistry()
|
||||||
if(pathSet1.length === 0){
|
if(pathSet1.size === 0){
|
||||||
// Do a manual file system scan of program files.
|
// Do a manual file system scan of program files.
|
||||||
pathSet1 = JavaGuard._scanFileSystem('C:\\Program Files\\Java')
|
pathSet1 = new Set([
|
||||||
|
...pathSet1,
|
||||||
|
...(await JavaGuard._scanFileSystem('C:\\Program Files\\Java')),
|
||||||
|
...(await JavaGuard._scanFileSystem('C:\\Program Files\\AdoptOpenJDK'))
|
||||||
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get possible paths from the data directory.
|
// Get possible paths from the data directory.
|
||||||
@ -1292,7 +1331,7 @@ class AssetGuard extends EventEmitter {
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
|
||||||
//Asset constants
|
//Asset constants
|
||||||
const resourceURL = 'http://resources.download.minecraft.net/'
|
const resourceURL = 'https://resources.download.minecraft.net/'
|
||||||
const localPath = path.join(self.commonPath, 'assets')
|
const localPath = path.join(self.commonPath, 'assets')
|
||||||
const objectPath = path.join(localPath, 'objects')
|
const objectPath = path.join(localPath, 'objects')
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ const sysRoot = process.env.APPDATA || (process.platform == 'darwin' ? process.e
|
|||||||
const dataPath = path.join(sysRoot, '.avalonConcordialauncher')
|
const dataPath = path.join(sysRoot, '.avalonConcordialauncher')
|
||||||
|
|
||||||
// Forked processes do not have access to electron, so we have this workaround.
|
// Forked processes do not have access to electron, so we have this workaround.
|
||||||
const launcherDir = process.env.CONFIG_DIRECT_PATH || require('electron').remote.app.getPath('userData')
|
const launcherDir = process.env.CONFIG_DIRECT_PATH || require('@electron/remote').app.getPath('userData')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve the absolute path of the launcher directory.
|
* Retrieve the absolute path of the launcher directory.
|
||||||
|
@ -551,17 +551,21 @@ exports.pullRemote = function(){
|
|||||||
data = DistroIndex.fromJSON(JSON.parse(body))
|
data = DistroIndex.fromJSON(JSON.parse(body))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
reject(e)
|
reject(e)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fs.writeFile(distroDest, body, 'utf-8', (err) => {
|
fs.writeFile(distroDest, body, 'utf-8', (err) => {
|
||||||
if(!err){
|
if(!err){
|
||||||
resolve(data)
|
resolve(data)
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
reject(err)
|
reject(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
reject(error)
|
reject(error)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -576,8 +580,10 @@ exports.pullLocal = function(){
|
|||||||
if(!err){
|
if(!err){
|
||||||
data = DistroIndex.fromJSON(JSON.parse(d))
|
data = DistroIndex.fromJSON(JSON.parse(d))
|
||||||
resolve(data)
|
resolve(data)
|
||||||
|
return
|
||||||
} else {
|
} else {
|
||||||
reject(err)
|
reject(err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -92,14 +92,17 @@ exports.addDropinMods = function(files, modsdir) {
|
|||||||
* @param {string} modsDir The path to the mods directory.
|
* @param {string} modsDir The path to the mods directory.
|
||||||
* @param {string} fullName The fullName of the discovered mod to delete.
|
* @param {string} fullName The fullName of the discovered mod to delete.
|
||||||
*
|
*
|
||||||
* @returns {boolean} True if the mod was deleted, otherwise false.
|
* @returns {Promise.<boolean>} True if the mod was deleted, otherwise false.
|
||||||
*/
|
*/
|
||||||
exports.deleteDropinMod = function(modsDir, fullName){
|
exports.deleteDropinMod = async function(modsDir, fullName){
|
||||||
const res = shell.moveItemToTrash(path.join(modsDir, fullName))
|
try {
|
||||||
if(!res){
|
await shell.trashItem(path.join(modsDir, fullName))
|
||||||
|
return true
|
||||||
|
} catch(error) {
|
||||||
shell.beep()
|
shell.beep()
|
||||||
|
console.error('Error deleting drop-in mod.', error)
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
return res
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,7 +17,7 @@ const minecraftAgent = {
|
|||||||
const authpath = 'https://authserver.mojang.com'
|
const authpath = 'https://authserver.mojang.com'
|
||||||
const statuses = [
|
const statuses = [
|
||||||
{
|
{
|
||||||
service: 'sessionserver.mojang.com',
|
service: 'session.minecraft.net',
|
||||||
status: 'grey',
|
status: 'grey',
|
||||||
name: 'Multiplayer Session Service',
|
name: 'Multiplayer Session Service',
|
||||||
essential: true
|
essential: true
|
||||||
|
@ -23,6 +23,7 @@ class ProcessBuilder {
|
|||||||
this.forgeData = forgeData
|
this.forgeData = forgeData
|
||||||
this.authUser = authUser
|
this.authUser = authUser
|
||||||
this.launcherVersion = launcherVersion
|
this.launcherVersion = launcherVersion
|
||||||
|
this.forgeModListFile = path.join(this.gameDir, 'forgeMods.list') // 1.13+
|
||||||
this.fmlDir = path.join(this.gameDir, 'forgeModList.json')
|
this.fmlDir = path.join(this.gameDir, 'forgeModList.json')
|
||||||
this.llDir = path.join(this.gameDir, 'liteloaderModList.json')
|
this.llDir = path.join(this.gameDir, 'liteloaderModList.json')
|
||||||
this.libPath = path.join(this.commonDir, 'libraries')
|
this.libPath = path.join(this.commonDir, 'libraries')
|
||||||
@ -44,9 +45,9 @@ class ProcessBuilder {
|
|||||||
|
|
||||||
// Mod list below 1.13
|
// Mod list below 1.13
|
||||||
if(!Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
|
if(!Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
|
||||||
this.constructModList('forge', modObj.fMods, true)
|
this.constructJSONModList('forge', modObj.fMods, true)
|
||||||
if(this.usingLiteLoader){
|
if(this.usingLiteLoader){
|
||||||
this.constructModList('liteloader', modObj.lMods, true)
|
this.constructJSONModList('liteloader', modObj.lMods, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,7 +55,8 @@ class ProcessBuilder {
|
|||||||
let args = this.constructJVMArguments(uberModArr, tempNativePath)
|
let args = this.constructJVMArguments(uberModArr, tempNativePath)
|
||||||
|
|
||||||
if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
|
if(Util.mcVersionAtLeast('1.13', this.server.getMinecraftVersion())){
|
||||||
args = args.concat(this.constructModArguments(modObj.fMods))
|
//args = args.concat(this.constructModArguments(modObj.fMods))
|
||||||
|
args = args.concat(this.constructModList(modObj.fMods))
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.log('Launch Arguments:', args)
|
logger.log('Launch Arguments:', args)
|
||||||
@ -224,7 +226,7 @@ class ProcessBuilder {
|
|||||||
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
||||||
* @param {boolean} save Optional. Whether or not we should save the mod list file.
|
* @param {boolean} save Optional. Whether or not we should save the mod list file.
|
||||||
*/
|
*/
|
||||||
constructModList(type, mods, save = false){
|
constructJSONModList(type, mods, save = false){
|
||||||
const modList = {
|
const modList = {
|
||||||
repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + path.join(this.commonDir, 'modstore')
|
repositoryRoot: ((type === 'forge' && this._requiresAbsolute()) ? 'absolute:' : '') + path.join(this.commonDir, 'modstore')
|
||||||
}
|
}
|
||||||
@ -249,22 +251,46 @@ class ProcessBuilder {
|
|||||||
return modList
|
return modList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Construct the mod argument list for forge 1.13
|
||||||
|
// *
|
||||||
|
// * @param {Array.<Object>} mods An array of mods to add to the mod list.
|
||||||
|
// */
|
||||||
|
// constructModArguments(mods){
|
||||||
|
// const argStr = mods.map(mod => {
|
||||||
|
// return mod.getExtensionlessID()
|
||||||
|
// }).join(',')
|
||||||
|
|
||||||
|
// if(argStr){
|
||||||
|
// return [
|
||||||
|
// '--fml.mavenRoots',
|
||||||
|
// path.join('..', '..', 'common', 'modstore'),
|
||||||
|
// '--fml.mods',
|
||||||
|
// argStr
|
||||||
|
// ]
|
||||||
|
// } else {
|
||||||
|
// return []
|
||||||
|
// }
|
||||||
|
|
||||||
|
// }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct the mod argument list for forge 1.13
|
* Construct the mod argument list for forge 1.13
|
||||||
*
|
*
|
||||||
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
* @param {Array.<Object>} mods An array of mods to add to the mod list.
|
||||||
*/
|
*/
|
||||||
constructModArguments(mods){
|
constructModList(mods) {
|
||||||
const argStr = mods.map(mod => {
|
const writeBuffer = mods.map(mod => {
|
||||||
return mod.getExtensionlessID()
|
return mod.getExtensionlessID()
|
||||||
}).join(',')
|
}).join('\n')
|
||||||
|
|
||||||
if(argStr){
|
if(writeBuffer) {
|
||||||
|
fs.writeFileSync(this.forgeModListFile, writeBuffer, 'UTF-8')
|
||||||
return [
|
return [
|
||||||
'--fml.mavenRoots',
|
'--fml.mavenRoots',
|
||||||
path.join('..', '..', 'common', 'modstore'),
|
path.join('..', '..', 'common', 'modstore'),
|
||||||
'--fml.mods',
|
'--fml.modLists',
|
||||||
argStr
|
this.forgeModListFile
|
||||||
]
|
]
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
|
@ -130,7 +130,7 @@ function updateSelectedAccount(authUser){
|
|||||||
username = authUser.displayName
|
username = authUser.displayName
|
||||||
}
|
}
|
||||||
if(authUser.uuid != null){
|
if(authUser.uuid != null){
|
||||||
document.getElementById('avatarContainer').style.backgroundImage = `url('https://crafatar.com/renders/body/${authUser.uuid}')`
|
document.getElementById('avatarContainer').style.backgroundImage = `url('https://mc-heads.net/body/${authUser.uuid}/right')`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
user_text.innerHTML = username
|
user_text.innerHTML = username
|
||||||
@ -466,7 +466,6 @@ let proc
|
|||||||
let hasRPC = false
|
let hasRPC = false
|
||||||
// Joined server regex
|
// Joined server regex
|
||||||
// Change this if your server uses something different.
|
// Change this if your server uses something different.
|
||||||
const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/
|
|
||||||
const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/
|
const GAME_JOINED_REGEX = /\[.+\]: Sound engine started/
|
||||||
const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/
|
const GAME_LAUNCH_REGEX = /^\[.+\]: (?:MinecraftForge .+ Initialized|ModLauncher .+ starting: .+)$/
|
||||||
const MIN_LINGER = 5000
|
const MIN_LINGER = 5000
|
||||||
@ -649,6 +648,9 @@ function dlAsync(login = true){
|
|||||||
let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion())
|
let pb = new ProcessBuilder(serv, versionData, forgeData, authUser, remote.app.getVersion())
|
||||||
setLaunchDetails('Launching game..')
|
setLaunchDetails('Launching game..')
|
||||||
|
|
||||||
|
// const SERVER_JOINED_REGEX = /\[.+\]: \[CHAT\] [a-zA-Z0-9_]{1,16} joined the game/
|
||||||
|
const SERVER_JOINED_REGEX = new RegExp(`\\[.+\\]: \\[CHAT\\] ${authUser.displayName} joined the game`)
|
||||||
|
|
||||||
const onLoadComplete = () => {
|
const onLoadComplete = () => {
|
||||||
toggleLaunchArea(false)
|
toggleLaunchArea(false)
|
||||||
if(hasRPC){
|
if(hasRPC){
|
||||||
|
@ -299,7 +299,7 @@ function populateAccountListings(){
|
|||||||
let htmlString = ''
|
let htmlString = ''
|
||||||
for(let i=0; i<accounts.length; i++){
|
for(let i=0; i<accounts.length; i++){
|
||||||
htmlString += `<button class="accountListing" uuid="${accounts[i].uuid}" ${i===0 ? 'selected' : ''}>
|
htmlString += `<button class="accountListing" uuid="${accounts[i].uuid}" ${i===0 ? 'selected' : ''}>
|
||||||
<img src="https://crafatar.com/renders/head/${accounts[i].uuid}?scale=2&default=MHF_Steve&overlay">
|
<img src="https://mc-heads.net/head/${accounts[i].uuid}/40">
|
||||||
<div class="accountListingName">${accounts[i].displayName}</div>
|
<div class="accountListingName">${accounts[i].displayName}</div>
|
||||||
</button>`
|
</button>`
|
||||||
}
|
}
|
||||||
|
@ -444,7 +444,7 @@ function populateAuthAccounts(){
|
|||||||
const acc = authAccounts[val]
|
const acc = authAccounts[val]
|
||||||
authAccountStr += `<div class="settingsAuthAccount" uuid="${acc.uuid}">
|
authAccountStr += `<div class="settingsAuthAccount" uuid="${acc.uuid}">
|
||||||
<div class="settingsAuthAccountLeft">
|
<div class="settingsAuthAccountLeft">
|
||||||
<img class="settingsAuthAccountImage" alt="${acc.displayName}" src="https://crafatar.com/renders/body/${acc.uuid}?scale=3&default=MHF_Steve&overlay">
|
<img class="settingsAuthAccountImage" alt="${acc.displayName}" src="https://mc-heads.net/body/${acc.uuid}/60">
|
||||||
</div>
|
</div>
|
||||||
<div class="settingsAuthAccountRight">
|
<div class="settingsAuthAccountRight">
|
||||||
<div class="settingsAuthAccountDetails">
|
<div class="settingsAuthAccountDetails">
|
||||||
@ -687,9 +687,9 @@ function resolveDropinModsForUI(){
|
|||||||
function bindDropinModsRemoveButton(){
|
function bindDropinModsRemoveButton(){
|
||||||
const sEls = settingsModsContainer.querySelectorAll('[remmod]')
|
const sEls = settingsModsContainer.querySelectorAll('[remmod]')
|
||||||
Array.from(sEls).map((v, index, arr) => {
|
Array.from(sEls).map((v, index, arr) => {
|
||||||
v.onclick = () => {
|
v.onclick = async () => {
|
||||||
const fullName = v.getAttribute('remmod')
|
const fullName = v.getAttribute('remmod')
|
||||||
const res = DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName)
|
const res = await DropinModUtil.deleteDropinMod(CACHE_SETTINGS_MODS_DIR, fullName)
|
||||||
if(res){
|
if(res){
|
||||||
document.getElementById(fullName).remove()
|
document.getElementById(fullName).remove()
|
||||||
} else {
|
} else {
|
||||||
@ -1139,10 +1139,11 @@ function populateJavaExecDetails(execPath){
|
|||||||
const jg = new JavaGuard(DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion())
|
const jg = new JavaGuard(DistroManager.getDistribution().getServer(ConfigManager.getSelectedServer()).getMinecraftVersion())
|
||||||
jg._validateJavaBinary(execPath).then(v => {
|
jg._validateJavaBinary(execPath).then(v => {
|
||||||
if(v.valid){
|
if(v.valid){
|
||||||
|
const vendor = v.vendor != null ? ` (${v.vendor})` : ''
|
||||||
if(v.version.major < 9) {
|
if(v.version.major < 9) {
|
||||||
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})`
|
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major} Update ${v.version.update} (x${v.arch})${vendor}`
|
||||||
} else {
|
} else {
|
||||||
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})`
|
settingsJavaExecDetails.innerHTML = `Selected: Java ${v.version.major}.${v.version.minor}.${v.version.revision} (x${v.arch})${vendor}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
settingsJavaExecDetails.innerHTML = 'Invalid Selection'
|
settingsJavaExecDetails.innerHTML = 'Invalid Selection'
|
||||||
|
@ -6,7 +6,8 @@
|
|||||||
*/
|
*/
|
||||||
// Requirements
|
// Requirements
|
||||||
const $ = require('jquery')
|
const $ = require('jquery')
|
||||||
const {ipcRenderer, remote, shell, webFrame} = require('electron')
|
const {ipcRenderer, shell, webFrame} = require('electron')
|
||||||
|
const remote = require('@electron/remote')
|
||||||
const isDev = require('./assets/js/isdev')
|
const isDev = require('./assets/js/isdev')
|
||||||
const LoggerUtil = require('./assets/js/loggerutil')
|
const LoggerUtil = require('./assets/js/loggerutil')
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ if(!isDev){
|
|||||||
|
|
||||||
if(process.platform === 'darwin'){
|
if(process.platform === 'darwin'){
|
||||||
info.darwindownload = `https://github.com/ASCIIcat/AvalonConcordiaLauncher/releases/download/v${info.version}/avalonconcordialauncher-setup-${info.version}.dmg`
|
info.darwindownload = `https://github.com/ASCIIcat/AvalonConcordiaLauncher/releases/download/v${info.version}/avalonconcordialauncher-setup-${info.version}.dmg`
|
||||||
|
|
||||||
showUpdateUI(info)
|
showUpdateUI(info)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
<div id="externalMedia">
|
<div id="externalMedia">
|
||||||
<div class="mediaContainer">
|
<div class="mediaContainer">
|
||||||
<a href="https://avalonconcordia.com/" class="mediaURL" id="linkURL">
|
<a href="https://avalonconcordia.com/" class="mediaURL" id="linkURL">
|
||||||
|
|
||||||
<svg id="linkSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
<svg id="linkSVG" class="mediaSVG" viewBox="35.34 34.3575 70.68 68.71500">
|
||||||
<g>
|
<g>
|
||||||
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
|
<path d="M75.37,65.51a3.85,3.85,0,0,0-1.73.42,8.22,8.22,0,0,1,.94,3.76A8.36,8.36,0,0,1,66.23,78H46.37a8.35,8.35,0,1,1,0-16.7h9.18a21.51,21.51,0,0,1,6.65-8.72H46.37a17.07,17.07,0,1,0,0,34.15H66.23A17,17,0,0,0,82.77,65.51Z"/>
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="loginOptions">
|
<div id="loginOptions">
|
||||||
<span class="loginSpanDim">
|
<span class="loginSpanDim">
|
||||||
<a href="https://my.minecraft.net/en-us/password/forgot/">forgot password?</a>
|
<a href="https://minecraft.net/password/forgot/">forgot password?</a>
|
||||||
</span>
|
</span>
|
||||||
<label id="checkmarkContainer">
|
<label id="checkmarkContainer">
|
||||||
<input id="loginRememberOption" type="checkbox" checked>
|
<input id="loginRememberOption" type="checkbox" checked>
|
||||||
@ -54,7 +54,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<div id="loginDisclaimer">
|
<div id="loginDisclaimer">
|
||||||
<span class="loginSpanDim" id="loginRegisterSpan">
|
<span class="loginSpanDim" id="loginRegisterSpan">
|
||||||
<a href="https://minecraft.net/en-us/store/minecraft/">Need an Account?</a>
|
<a href="https://minecraft.net/store/minecraft-java-edition/">Need an Account?</a>
|
||||||
</span>
|
</span>
|
||||||
<p class="loginDisclaimerText">Your password is sent directly to mojang and never stored.</p>
|
<p class="loginDisclaimerText">Your password is sent directly to mojang and never stored.</p>
|
||||||
<p class="loginDisclaimerText">Avalon Concordia is not affiliated with Mojang AB.</p>
|
<p class="loginDisclaimerText">Avalon Concordia is not affiliated with Mojang AB.</p>
|
||||||
|
51
electron-builder.yml
Normal file
51
electron-builder.yml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
appId: 'helioslauncher'
|
||||||
|
productName: 'Helios Launcher'
|
||||||
|
artifactName: '${productName}-setup-${version}.${ext}'
|
||||||
|
|
||||||
|
copyright: 'Copyright © 2018-2021 Daniel Scalzi'
|
||||||
|
|
||||||
|
asar: true
|
||||||
|
compression: 'maximum'
|
||||||
|
|
||||||
|
files:
|
||||||
|
- '!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.travis.yml,.nvmrc,.eslintrc.json,build.js}'
|
||||||
|
|
||||||
|
extraResources:
|
||||||
|
- 'libraries'
|
||||||
|
|
||||||
|
# Windows Configuration
|
||||||
|
win:
|
||||||
|
target:
|
||||||
|
- target: 'nsis'
|
||||||
|
arch: 'x64'
|
||||||
|
|
||||||
|
# Windows Installer Configuration
|
||||||
|
nsis:
|
||||||
|
oneClick: false
|
||||||
|
perMachine: false
|
||||||
|
allowElevation: true
|
||||||
|
allowToChangeInstallationDirectory: true
|
||||||
|
|
||||||
|
# macOS Configuration
|
||||||
|
mac:
|
||||||
|
target:
|
||||||
|
- target: 'dmg'
|
||||||
|
arch:
|
||||||
|
- 'x64'
|
||||||
|
- 'arm64'
|
||||||
|
artifactName: '${productName}-setup-${version}-${arch}.${ext}'
|
||||||
|
category: 'public.app-category.games'
|
||||||
|
|
||||||
|
# Linux Configuration
|
||||||
|
linux:
|
||||||
|
target: 'AppImage'
|
||||||
|
maintainer: 'Daniel Scalzi'
|
||||||
|
vendor: 'Daniel Scalzi'
|
||||||
|
synopsis: 'Modded Minecraft Launcher'
|
||||||
|
description: 'Custom launcher which allows users to join modded servers. All mods, configurations, and updates are handled automatically.'
|
||||||
|
category: 'Game'
|
||||||
|
|
||||||
|
|
||||||
|
directories:
|
||||||
|
buildResources: 'build'
|
||||||
|
output: 'dist'
|
13
index.js
13
index.js
@ -1,3 +1,5 @@
|
|||||||
|
require('@electron/remote/main').initialize()
|
||||||
|
|
||||||
// Requirements
|
// Requirements
|
||||||
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
|
const { app, BrowserWindow, ipcMain, Menu } = require('electron')
|
||||||
const autoUpdater = require('electron-updater').autoUpdater
|
const autoUpdater = require('electron-updater').autoUpdater
|
||||||
@ -6,7 +8,7 @@ const fs = require('fs')
|
|||||||
const isDev = require('./app/assets/js/isdev')
|
const isDev = require('./app/assets/js/isdev')
|
||||||
const path = require('path')
|
const path = require('path')
|
||||||
const semver = require('semver')
|
const semver = require('semver')
|
||||||
const url = require('url')
|
const { pathToFileURL } = require('url')
|
||||||
|
|
||||||
// Setup auto updater.
|
// Setup auto updater.
|
||||||
function initAutoUpdater(event, data) {
|
function initAutoUpdater(event, data) {
|
||||||
@ -103,19 +105,14 @@ function createWindow() {
|
|||||||
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'),
|
preload: path.join(__dirname, 'app', 'assets', 'js', 'preloader.js'),
|
||||||
nodeIntegration: true,
|
nodeIntegration: true,
|
||||||
contextIsolation: false,
|
contextIsolation: false,
|
||||||
enableRemoteModule: true,
|
enableRemoteModule: true
|
||||||
worldSafeExecuteJavaScript: false
|
|
||||||
},
|
},
|
||||||
backgroundColor: '#171614'
|
backgroundColor: '#171614'
|
||||||
})
|
})
|
||||||
|
|
||||||
ejse.data('bkid', Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)))
|
ejse.data('bkid', Math.floor((Math.random() * fs.readdirSync(path.join(__dirname, 'app', 'assets', 'images', 'backgrounds')).length)))
|
||||||
|
|
||||||
win.loadURL(url.format({
|
win.loadURL(pathToFileURL(path.join(__dirname, 'app', 'app.ejs')).toString())
|
||||||
pathname: path.join(__dirname, 'app', 'app.ejs'),
|
|
||||||
protocol: 'file:',
|
|
||||||
slashes: true
|
|
||||||
}))
|
|
||||||
|
|
||||||
/*win.once('ready-to-show', () => {
|
/*win.once('ready-to-show', () => {
|
||||||
win.show()
|
win.show()
|
||||||
|
Binary file not shown.
1944
package-lock.json
generated
1944
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "avalonconcordialauncher",
|
"name": "avalonconcordialauncher",
|
||||||
"version": "1.0.1",
|
"version": "1.8.0",
|
||||||
"productName": "Avalon Concordia Launcher",
|
"productName": "Avalon Concordia Launcher",
|
||||||
"description": "Modded Minecraft Launcher",
|
"description": "Modded Minecraft Launcher",
|
||||||
"author": "Jay aka ASCIIcat (https://github.com/ASCIIcat/)",
|
"author": "Jay aka ASCIIcat (https://github.com/ASCIIcat/)",
|
||||||
@ -13,37 +13,35 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "electron .",
|
"start": "electron .",
|
||||||
"cilinux": "node build.js WINDOWS && node build.js LINUX",
|
"dist": "electron-builder build",
|
||||||
"cidarwin": "node build.js MAC",
|
"dist:win": "npm run dist -- -w",
|
||||||
"dist": "cross-env ELECTRON_BUILDER_ALLOW_UNRESOLVED_DEPENDENCIES=true node build.js",
|
"dist:mac": "npm run dist -- -m",
|
||||||
"dist:win": "npm run dist -- WINDOWS",
|
"dist:linux": "npm run dist -- -l",
|
||||||
"dist:mac": "npm run dist -- MAC",
|
|
||||||
"dist:linux": "npm run dist -- LINUX",
|
|
||||||
"lint": "eslint --config .eslintrc.json ."
|
"lint": "eslint --config .eslintrc.json ."
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12.x.x"
|
"node": "14.x.x"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.4.16",
|
"@electron/remote": "^1.2.0",
|
||||||
|
"adm-zip": "^0.5.5",
|
||||||
"async": "^3.2.0",
|
"async": "^3.2.0",
|
||||||
"discord-rpc": "^3.1.3",
|
"discord-rpc": "^3.2.0",
|
||||||
"ejs": "^3.1.3",
|
"ejs": "^3.1.6",
|
||||||
"ejs-electron": "^2.1.1",
|
"ejs-electron": "^2.1.1",
|
||||||
"electron-updater": "^4.3.4",
|
"electron-updater": "^4.3.9",
|
||||||
"fs-extra": "^9.0.1",
|
"fs-extra": "^10.0.0",
|
||||||
"github-syntax-dark": "^0.5.0",
|
"github-syntax-dark": "^0.5.0",
|
||||||
"jquery": "^3.5.1",
|
"jquery": "^3.6.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.5",
|
||||||
"tar-fs": "^2.1.0",
|
"tar-fs": "^2.1.1",
|
||||||
"winreg": "^1.2.4"
|
"winreg": "^1.2.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^7.0.2",
|
"electron": "^13.1.4",
|
||||||
"electron": "^9.2.0",
|
"electron-builder": "^22.11.7",
|
||||||
"electron-builder": "^22.8.0",
|
"eslint": "^7.29.0"
|
||||||
"eslint": "^7.6.0"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
Loading…
Reference in New Issue
Block a user