diff --git a/README.md b/README.md index 301a92e1..cc5150f8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -

aventium softworks

+

aventium softworks

Helios Launcher

diff --git a/package-lock.json b/package-lock.json index 0f6afe26..d9b0bff4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1445,6 +1445,12 @@ "integrity": "sha512-Ee0vt82qcg05OeJrQZ/YN+NQwaBCnAul1rVLYaMLPkwR5f44WC3BpBQNvn5Z3Axu9szaVOHqXEDBI+uAXAiyrg==", "dev": true }, + "@types/electron-devtools-installer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/electron-devtools-installer/-/electron-devtools-installer-2.2.0.tgz", + "integrity": "sha512-HJNxpaOXuykCK4rQ6FOMxAA0NLFYsf7FiPFGmab0iQmtVBHSAfxzy3MRFpLTTDDWbV0yD2YsHOQvdu8yCqtCfw==", + "dev": true + }, "@types/eslint-visitor-keys": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", diff --git a/package.json b/package.json index 01e12f7d..faa388a0 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "@types/chai": "^4.2.12", "@types/chai-as-promised": "^7.1.3", "@types/discord-rpc": "^3.0.4", + "@types/electron-devtools-installer": "^2.2.0", "@types/fs-extra": "^9.0.1", "@types/jquery": "^3.5.1", "@types/lodash": "^4.14.160", diff --git a/src/common/asset/processor/MojangIndexProcessor.ts b/src/common/asset/processor/MojangIndexProcessor.ts index 5cef57b8..1c4636fb 100644 --- a/src/common/asset/processor/MojangIndexProcessor.ts +++ b/src/common/asset/processor/MojangIndexProcessor.ts @@ -1,14 +1,16 @@ -import { IndexProcessor } from '../model/engine/IndexProcessor' -import got, { HTTPError, RequestError, ParseError, TimeoutError } from 'got' -import { LoggerUtil } from 'common/logging/loggerutil' -import { pathExists, readFile, ensureDir, writeFile, readJson } from 'fs-extra' -import { MojangVersionManifest } from '../model/mojang/VersionManifest' -import { calculateHash, getVersionJsonPath, validateLocalFile, getLibraryDir, getVersionJarPath } from 'common/util/FileUtils' +import got from 'got' import { dirname, join } from 'path' -import { VersionJson, AssetIndex, LibraryArtifact } from '../model/mojang/VersionJson' -import { AssetGuardError } from '../model/engine/AssetGuardError' -import { Asset } from '../model/engine/Asset' -import { isLibraryCompatible, getMojangOS } from 'common/util/MojangUtils' +import { ensureDir, pathExists, readFile, readJson, writeFile } from 'fs-extra' + +import { Asset } from 'common/asset/model/engine/Asset' +import { AssetGuardError } from 'common/asset/model/engine/AssetGuardError' +import { IndexProcessor } from 'common/asset/model/engine/IndexProcessor' +import { MojangVersionManifest } from 'common/asset/model/mojang/VersionManifest' +import { handleGotError } from 'common/got/RestResponse' +import { AssetIndex, LibraryArtifact, VersionJson } from 'common/asset/model/mojang/VersionJson' +import { calculateHash, getLibraryDir, getVersionJarPath, getVersionJsonPath, validateLocalFile } from 'common/util/FileUtils' +import { getMojangOS, isLibraryCompatible } from 'common/util/MojangUtils' +import { LoggerUtil } from 'common/logging/loggerutil' export class MojangIndexProcessor extends IndexProcessor { @@ -16,7 +18,7 @@ export class MojangIndexProcessor extends IndexProcessor { public static readonly VERSION_MANIFEST_ENDPOINT = 'https://launchermeta.mojang.com/mc/game/version_manifest.json' public static readonly ASSET_RESOURCE_ENDPOINT = 'http://resources.download.minecraft.net' - private readonly logger = LoggerUtil.getLogger('MojangIndexProcessor') + private static readonly logger = LoggerUtil.getLogger('MojangIndexProcessor') private versionJson!: VersionJson private assetIndex!: AssetIndex @@ -24,26 +26,6 @@ export class MojangIndexProcessor extends IndexProcessor { responseType: 'json' }) - private handleGotError(operation: string, error: RequestError, dataProvider: () => T): T { - if(error instanceof HTTPError) { - this.logger.error(`Error during ${operation} request (HTTP Response ${error.response.statusCode})`, error) - this.logger.debug('Response Details:') - this.logger.debug('Body:', error.response.body) - this.logger.debug('Headers:', error.response.headers) - } else if(Object.getPrototypeOf(error) instanceof RequestError) { - this.logger.error(`${operation} request recieved no response (${error.code}).`, error) - } else if(error instanceof TimeoutError) { - this.logger.error(`${operation} request timed out (${error.timings.phases.total}ms).`) - } else if(error instanceof ParseError) { - this.logger.error(`${operation} request recieved unexepected body (Parse Error).`) - } else { - // CacheError, ReadError, MaxRedirectsError, UnsupportedProtocolError, CancelError - this.logger.error(`Error during ${operation} request.`, error) - } - - return dataProvider() - } - private assetPath: string constructor(commonDir: string, protected version: string) { @@ -148,7 +130,7 @@ export class MojangIndexProcessor extends IndexProcessor { return res.body } catch(error) { - return this.handleGotError(url, error, () => null) + return handleGotError(url, error, MojangIndexProcessor.logger, () => null).data } } @@ -158,7 +140,7 @@ export class MojangIndexProcessor extends IndexProcessor { const res = await this.client.get(MojangIndexProcessor.VERSION_MANIFEST_ENDPOINT) return res.body } catch(error) { - return this.handleGotError('Load Mojang Version Manifest', error, () => null) + return handleGotError('Load Mojang Version Manifest', error, MojangIndexProcessor.logger, () => null).data } } @@ -187,7 +169,7 @@ export class MojangIndexProcessor extends IndexProcessor { // TODO progress tracker // TODO type return object - public async validate(): Promise { + public async validate(): Promise<{[category: string]: Asset[]}> { const assets = await this.validateAssets(this.assetIndex) const libraries = await this.validateLibraries(this.versionJson) diff --git a/src/common/distribution/distribution.ts b/src/common/distribution/DistributionAPI.ts similarity index 96% rename from src/common/distribution/distribution.ts rename to src/common/distribution/DistributionAPI.ts index 19c25798..9bc482ed 100644 --- a/src/common/distribution/distribution.ts +++ b/src/common/distribution/DistributionAPI.ts @@ -5,6 +5,9 @@ import { LoggerUtil } from 'common/logging/loggerutil' import { RestResponse, handleGotError, RestResponseStatus } from 'common/got/RestResponse' import { pathExists, readFile, writeFile } from 'fs-extra' +// TODO Option to check endpoint for hash of distro for local compare +// Useful if distro is large (MBs) + export class DistributionAPI { private static readonly logger = LoggerUtil.getLogger('DistributionAPI') diff --git a/src/common/distribution/DistributionFactory.ts b/src/common/distribution/DistributionFactory.ts new file mode 100644 index 00000000..b5a230df --- /dev/null +++ b/src/common/distribution/DistributionFactory.ts @@ -0,0 +1,195 @@ +import { Distribution, Server, Module, Type, Required as HeliosRequired } from 'helios-distribution-types' +import { MavenComponents, MavenUtil } from 'common/util/MavenUtil' +import { join } from 'path' + +export class HeliosDistribution { + + private mainServerIndex: number + + public readonly servers: HeliosServer[] + + constructor( + public readonly rawDistribution: Distribution + ) { + + this.servers = this.rawDistribution.servers.map(s => new HeliosServer(s)) + this.mainServerIndex = this.indexOfMainServer() + } + + private indexOfMainServer(): number { + for(let i=0; i s.rawServer.id === id) || null + } + +} + +export class HeliosServer { + + public readonly modules: HeliosModule[] + + constructor( + public readonly rawServer: Server + ) { + this.modules = rawServer.modules.map(m => new HeliosModule(m, rawServer.id)) + } + +} + +export class HeliosModule { + + public readonly subModules: HeliosModule[] + + private readonly mavenComponents: Readonly + private readonly required: Readonly> + private readonly localPath: string + + constructor( + public readonly rawModule: Module, + private readonly serverId: string + ) { + + this.mavenComponents = this.resolveMavenComponents() + this.required = this.resolveRequired() + this.localPath = this.resolveLocalPath() + + if(this.rawModule.subModules != null) { + this.subModules = this.rawModule.subModules.map(m => new HeliosModule(m, serverId)) + } else { + this.subModules = [] + } + + } + + private resolveMavenComponents(): MavenComponents { + + // Files need not have a maven identifier if they provide a path. + if(this.rawModule.type === Type.File && this.rawModule.artifact.path != null) { + return null! as MavenComponents + } + // Version Manifests never provide a maven identifier. + if(this.rawModule.type === Type.VersionManifest) { + return null! as MavenComponents + } + + const isMavenId = MavenUtil.isMavenIdentifier(this.rawModule.id) + + if(!isMavenId) { + if(this.rawModule.type !== Type.File) { + throw new Error(`Module ${this.rawModule.name} (${this.rawModule.id}) of type ${this.rawModule.type} must have a valid maven identifier!`) + } else { + throw new Error(`Module ${this.rawModule.name} (${this.rawModule.id}) of type ${this.rawModule.type} must either declare an artifact path or have a valid maven identifier!`) + } + } + + try { + return MavenUtil.getMavenComponents(this.rawModule.id) + } catch(err) { + throw new Error(`Failed to resolve maven components for module ${this.rawModule.name} (${this.rawModule.id}) of type ${this.rawModule.type}. Reason: ${err.message}`) + } + + } + + private resolveRequired(): Required { + if(this.rawModule.required == null) { + return { + value: true, + def: true + } + } else { + return { + value: this.rawModule.required.value ?? true, + def: this.rawModule.required.def ?? true + } + } + } + + private resolveLocalPath(): string { + + // Version Manifests have a pre-determined path. + if(this.rawModule.type === Type.VersionManifest) { + return join('TODO_COMMON_DIR', 'versions', this.rawModule.id, `${this.rawModule.id}.json`) + } + + const relativePath = this.rawModule.artifact.path ?? MavenUtil.mavenComponentsAsNormalizedPath( + this.mavenComponents.group, + this.mavenComponents.artifact, + this.mavenComponents.version, + this.mavenComponents.classifier, + this.mavenComponents.extension + ) + + switch (this.rawModule.type) { + case Type.Library: + case Type.Forge: + case Type.ForgeHosted: + case Type.LiteLoader: + return join('TODO_COMMON_DIR', 'libraries', relativePath) + case Type.ForgeMod: + case Type.LiteMod: + return join('TODO_COMMON_DIR', 'modstore', relativePath) + case Type.File: + default: + return join('TODO_INSTANCE_DIR', this.serverId, relativePath) + } + + } + + public hasMavenComponents(): boolean { + return this.mavenComponents != null + } + + public getMavenComponents(): Readonly { + return this.mavenComponents + } + + public getRequired(): Readonly> { + return this.required + } + + public getPath(): string { + return this.localPath + } + + public getMavenIdentifier(): string { + return MavenUtil.mavenComponentsToIdentifier( + this.mavenComponents.group, + this.mavenComponents.artifact, + this.mavenComponents.version, + this.mavenComponents.classifier, + this.mavenComponents.extension + ) + } + + public getExtensionlessMavenIdentifier(): string { + return MavenUtil.mavenComponentsToExtensionlessIdentifier( + this.mavenComponents.group, + this.mavenComponents.artifact, + this.mavenComponents.version, + this.mavenComponents.classifier + ) + } + + public getVersionlessMavenIdentifier(): string { + return MavenUtil.mavenComponentsToVersionlessIdentifier( + this.mavenComponents.group, + this.mavenComponents.artifact + ) + } + + public hasSubModules(): boolean { + return this.subModules.length > 0 + } + +} \ No newline at end of file diff --git a/src/common/util/MavenUtil.ts b/src/common/util/MavenUtil.ts new file mode 100644 index 00000000..b8298972 --- /dev/null +++ b/src/common/util/MavenUtil.ts @@ -0,0 +1,107 @@ +import { normalize } from 'path' +import { URL } from 'url' + +export interface MavenComponents { + group: string + artifact: string + version: string + classifier?: string + extension: string +} + +export class MavenUtil { + + public static readonly ID_REGEX = /(.+):(.+):([^@]+)()(?:@{1}(.+)$)?/ + public static readonly ID_REGEX_WITH_CLASSIFIER = /(.+):(.+):(?:([^@]+)(?:-([a-zA-Z]+)))(?:@{1}(.+)$)?/ + + public static mavenComponentsToIdentifier( + group: string, + artifact: string, + version: string, + classifier?: string, + extension?: string + ): string { + return `${group}:${artifact}:${version}${classifier != null ? `:${classifier}` : ''}${extension != null ? `@${extension}` : ''}` + } + + public static mavenComponentsToExtensionlessIdentifier( + group: string, + artifact: string, + version: string, + classifier?: string + ): string { + return MavenUtil.mavenComponentsToIdentifier(group, artifact, version, classifier) + } + + public static mavenComponentsToVersionlessIdentifier( + group: string, + artifact: string + ): string { + return `${group}:${artifact}` + } + + public static isMavenIdentifier(id: string): boolean { + return MavenUtil.ID_REGEX.test(id) || MavenUtil.ID_REGEX_WITH_CLASSIFIER.test(id) + } + + public static getMavenComponents(id: string, extension = 'jar'): MavenComponents { + if (!MavenUtil.isMavenIdentifier(id)) { + throw new Error('Id is not a maven identifier.') + } + + let result + + if (MavenUtil.ID_REGEX_WITH_CLASSIFIER.test(id)) { + result = MavenUtil.ID_REGEX_WITH_CLASSIFIER.exec(id) + } else { + result = MavenUtil.ID_REGEX.exec(id) + } + + if (result != null) { + return { + group: result[1], + artifact: result[2], + version: result[3], + classifier: result[4] || undefined, + extension: result[5] || extension + } + } + + throw new Error('Failed to process maven data.') + } + + public static mavenIdentifierAsPath(id: string, extension = 'jar'): string { + const tmp = MavenUtil.getMavenComponents(id, extension) + + return MavenUtil.mavenComponentsAsPath( + tmp.group, tmp.artifact, tmp.version, tmp.classifier, tmp.extension + ) + } + + public static mavenComponentsAsPath( + group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): string { + return `${group.replace(/\./g, '/')}/${artifact}/${version}/${artifact}-${version}${classifier != null ? `-${classifier}` : ''}.${extension}` + } + + public static mavenIdentifierToUrl(id: string, extension = 'jar'): URL { + return new URL(MavenUtil.mavenIdentifierAsPath(id, extension)) + } + + public static mavenComponentsToUrl( + group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): URL { + return new URL(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)) + } + + public static mavenIdentifierToPath(id: string, extension = 'jar'): string { + return normalize(MavenUtil.mavenIdentifierAsPath(id, extension)) + } + + public static mavenComponentsAsNormalizedPath( + group: string, artifact: string, version: string, classifier?: string, extension = 'jar' + ): string { + return normalize(MavenUtil.mavenComponentsAsPath(group, artifact, version, classifier, extension)) + } + +} \ No newline at end of file diff --git a/src/main/index.ts b/src/main/index.ts index 08aabac3..80d870f1 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -8,14 +8,12 @@ import isdev from '../common/util/isdev' declare const __static: string const installExtensions = async () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const installer = require('electron-devtools-installer') + + const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } = await import('electron-devtools-installer') const forceDownload = !!process.env.UPGRADE_EXTENSIONS - const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS'] + const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS] - return Promise.all( - extensions.map(name => installer.default(installer[name], forceDownload)) - ).catch(console.log) // eslint-disable-line no-console + return installExtension(extensions, forceDownload).catch(console.log) // eslint-disable-line no-console } // Setup auto updater. @@ -145,6 +143,12 @@ async function createWindow() { win.removeMenu() win.resizable = true + // win.webContents.on('new-window', (e, url) => { + // if(url != win!.webContents.getURL()) { + // e.preventDefault() + // shell.openExternal(url) + // } + // }) if (process.env.NODE_ENV !== 'production') { // Open DevTools, see https://github.com/electron/electron/issues/12438 for why we wait for dom-ready diff --git a/src/renderer/components/Application.tsx b/src/renderer/components/Application.tsx index 09ef8776..dac41944 100644 --- a/src/renderer/components/Application.tsx +++ b/src/renderer/components/Application.tsx @@ -8,17 +8,21 @@ import Landing from './landing/Landing' import Login from './login/Login' import Loader from './loader/Loader' import Settings from './settings/Settings' +import Overlay from './overlay/Overlay' +import Fatal from './fatal/Fatal' import { StoreType } from '../redux/store' import { CSSTransition } from 'react-transition-group' import { ViewActionDispatch } from '../redux/actions/viewActions' import { throttle } from 'lodash' import { readdir } from 'fs-extra' import { join } from 'path' -import Overlay from './overlay/Overlay' +import { AppActionDispatch } from '../redux/actions/appActions' import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions' -import { DistributionAPI } from 'common/distribution/distribution' +import { DistributionAPI } from 'common/distribution/DistributionAPI' import { getServerStatus } from 'common/mojang/net/ServerStatusAPI' +import { Distribution } from 'helios-distribution-types' +import { HeliosDistribution } from 'common/distribution/DistributionFactory' import './Application.css' @@ -49,6 +53,7 @@ const mapState = (state: StoreType): Partial => { } } const mapDispatch = { + ...AppActionDispatch, ...ViewActionDispatch, ...OverlayActionDispatch } @@ -68,6 +73,8 @@ class Application extends React.Component @@ -85,6 +92,12 @@ class Application extends React.Component + case View.FATAL: + return <> + + + case View.NONE: + return <> } } @@ -94,6 +107,8 @@ class Application extends React.Component { + // TODO debug remove + console.log('Setting to', this.props.currentView) this.setState({ ...this.state, workingView: this.props.currentView @@ -101,8 +116,14 @@ class Application extends React.Component { + if(this.props.currentView !== View.FATAL) { + setBackground(this.bkid) + } + this.showMain() + } + private showMain = (): void => { - setBackground(this.bkid) this.setState({ ...this.state, showMain: true @@ -113,23 +134,53 @@ class Application extends React.Component { + + // Initial distribution load. + const distroAPI = new DistributionAPI('C:\\Users\\user\\AppData\\Roaming\\Helios Launcher') + let rawDisto: Distribution + try { + rawDisto = await distroAPI.testLoad() + console.log('distro', distroAPI) + } catch(err) { + console.log('EXCEPTION IN DISTRO LOAD TODO TODO TODO', err) + rawDisto = null! + } + + // Fatal error + if(rawDisto == null) { + this.props.setView(View.FATAL) this.setState({ ...this.state, - loading: false + loading: false, + workingView: View.FATAL + }) + return + } else { + this.props.setDistribution(new HeliosDistribution(rawDisto)) + } + + // TODO Setup hook for distro refresh every ~ 5 mins. + + // Pick a background id. + this.bkid = Math.floor((Math.random() * (await readdir(join(__static, 'images', 'backgrounds'))).length)) + + const endLoad = () => { + // TODO determine correct view + // either welcome, landing, or login + this.props.setView(View.LANDING) + this.setState({ + ...this.state, + loading: false, + workingView: View.LANDING }) // TODO temp setTimeout(() => { //this.props.setView(View.WELCOME) this.props.pushGenericOverlay({ title: 'Load Distribution', - description: 'This is a test. Will load the distribution.', + description: 'This is a test.', dismissible: false, acknowledgeCallback: async () => { - const distro = new DistributionAPI('C:\\Users\\user\\AppData\\Roaming\\Helios Launcher') - const x = await distro.testLoad() - console.log(x) const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565) console.log(serverStatus) } @@ -206,7 +257,7 @@ class Application extends React.Component diff --git a/src/renderer/components/fatal/Fatal.css b/src/renderer/components/fatal/Fatal.css new file mode 100644 index 00000000..c3752ba5 --- /dev/null +++ b/src/renderer/components/fatal/Fatal.css @@ -0,0 +1,124 @@ +#fatalContainer { + position: relative; + display: flex; + justify-content: center; + align-items: center; + height: 100%; + width: 100%; +} + +#fatalContent { + display: flex; + flex-direction: column; + align-items: center; + top: 0; + height: 100%; + padding-top: 2.5rem; + box-sizing: border-box; + position: relative; +} + +#fatalHeader { + display: flex; + align-items: center; + justify-content: center; + padding: 0 0 1rem 0; + width: 70%; + margin-bottom: .5rem; + border-bottom: 0.0625rem solid rgba(126, 126, 126, 0.57); +} + +#fatalLeft { + display: flex; +} +#fatalErrorImg { + width: 3.125rem; +} + +#fatalRight { + display: flex; + flex-direction: column; + padding-left: 0.625rem; +} + +#fatalErrorLabel { + font-size: .75rem; + font-weight: bold; +} +#fatalErrorText { + font-size: 1.75rem; +} + +#fatalBody { + width: 65%; + display: flex; + flex-direction: column; + align-items: center; +} + +#fatalDescription { + text-align: justify; + font-size: 0.875rem; +} + +#fatalChecklistContainer { + width: 100%; + font-size: 0.875rem; +} + +/* Div which contains action buttons. */ +#fatalActionContainer { + display: flex; + flex-direction: column; + justify-content: center; + padding-top: 2rem; +} + +/* Fatal acknowledge button styles. */ +#fatalAcknowledge { + background: none; + border: 0.0625rem solid #ffffff; + color: white; + font-family: 'Avenir Medium'; + font-weight: bold; + border-radius: .125rem; + padding: 0 .6rem; + font-size: 1rem; + cursor: pointer; + transition: 0.25s ease; +} +#fatalAcknowledge:hover, +#fatalAcknowledge:focus { + box-shadow: 0 0 .625rem 0 #fff; + outline: none; +} +#fatalAcknowledge:active { + border-color: rgba(255, 255, 255, 0.75); + color: rgba(255, 255, 255, 0.75); +} + +#fatalDismissWrapper { + display: flex; + justify-content: center; +} + +/* Fatal dismiss option styles. */ +#fatalDismiss { + font-weight: bold; + font-size: 0.875rem; + text-decoration: none; + padding-top: 0.375rem; + background: none; + border: none; + outline: none; + cursor: pointer; + color: rgba(202, 202, 202, 0.75); + transition: 0.25s ease; +} +#fatalDismiss:hover, +#fatalDismiss:focus { + color: rgba(255, 255, 255, 0.75); +} +#fatalDismiss:active { + color: rgba(165, 165, 165, 0.75); +} \ No newline at end of file diff --git a/src/renderer/components/fatal/Fatal.tsx b/src/renderer/components/fatal/Fatal.tsx new file mode 100644 index 00000000..a3b42369 --- /dev/null +++ b/src/renderer/components/fatal/Fatal.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' +import { remote, shell } from 'electron' + +import './Fatal.css' + +function closeHandler() { + const window = remote.getCurrentWindow() + window.close() +} + +function openLatest() { + // TODO don't hardcode + shell.openExternal('https://github.com/dscalzi/HeliosLauncher/releases') +} + +export default class Fatal extends React.Component { + + render(): JSX.Element { + + return ( + <> +
+
+ +
+
+ +
+
+ FATAL ERROR + Failed to load Distribution Index +
+
+ +
+

What Happened?

+

+ A connection could not be established to our servers to download the distribution index. No local copies were available to load. + The distribution index is an essential file which provides the latest server information. The launcher is unable to start without it. +

+ + {/* TODO When auto update is done, do a version check and auto/update here. */} + +
+
    +
  • Ensure you are running the latest version of Helios Launcher.
  • +
  • Ensure you are connected to the internet.
  • +
+
+ +

Relaunch the application to try again.

+ +
+ +
+ +
+
+ +
+ +
+
+ + + ) + + } + +} \ No newline at end of file diff --git a/src/renderer/components/landing/Landing.css b/src/renderer/components/landing/Landing.css index 0a80c265..578870f8 100644 --- a/src/renderer/components/landing/Landing.css +++ b/src/renderer/components/landing/Landing.css @@ -83,327 +83,6 @@ display: inline-flex; } -/******************************************************************************* - * * - * Landing View (News Styles) * - * * - ******************************************************************************/ - -/* Main container. */ -#newsContainer { - position: absolute; - top: 100%; - height: 100%; - width: 100%; - transition: top 2s ease; - display: flex; - align-items: flex-end; - justify-content: center; -} - -/* News content container. */ -#newsContent { - height: 82vh; - width: 100%; - display: flex; - -webkit-user-select: initial; - position: relative; -} - -/* Drop shadow displayed when content is scrolled out of view. */ -#newsContent:before { - content: ''; - background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); - width: 100%; - height: 5px; - position: absolute; - opacity: 0; - transition: opacity 0.25s ease; -} -#newsContent[scrolled]:before { - opacity: 1; -} - -/* News article status container (left). */ -#newsStatusContainer { - width: calc(30% - 60px); - height: calc(100% - 30px); - padding: 15px 15px 15px 45px; - display: flex; - flex-direction: column; - justify-content: space-between; - position: relative; -} - -/* News status content. */ -#newsStatusContent { - display: flex; - flex-direction: column; - align-items: flex-end; -} - -/* News title wrapper. */ -#newsTitleContainer { - display: flex; - max-width: 90%; -} - -/* News article title styles. */ -#newsArticleTitle { - font-size: 18px; - font-weight: bold; - font-family: 'Avenir Medium'; - color: white; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleTitle:hover, -#newsArticleTitle:focus { - text-shadow: 0 0 20px white; -} -#newsArticleTitle:active { - color: #c7c7c7; - text-shadow: 0 0 20px #c7c7c7; -} - -/* News meta container. */ -#newsMetaContainer { - display: flex; - flex-direction: column; -} - -/* Date and author wrappers. */ -#newsArticleDateWrapper, -#newsArticleAuthorWrapper { - display: flex; - justify-content: flex-end; -} - -/* Date and author shared styles. */ -#newsArticleDate, -#newsArticleAuthor { - display: inline-block; - font-size: 10px; - padding: 0 5px; - font-weight: bold; - border-radius: 2px; -} - -/* Date styles. */ -#newsArticleDate { - background: white; - color: black; - margin-top: 5px; -} - -/* Author styles. */ -#newsArticleAuthor { - background: #a02d2a; -} - -/* News article comments styles. */ -#newsArticleComments { - margin-top: 5px; - display: inline-block; - font-size: 10px; - color: #ffffff; - text-decoration: none; - transition: 0.25s ease; - outline: none; - text-align: right; -} -#newsArticleComments:focus, -#newsArticleComments:hover { - color: #e0e0e0; -} -#newsArticleComments:active { - color: #c7c7c7; -} - -/* Article content container (right). */ -#newsArticleContainer { - width: calc(100% - 25px); - height: 100%; - margin: 0 0 0 25px; -} - -/* Article content styles. */ -#newsArticleContentScrollable { - font-size: 12px; - overflow-y: scroll; - height: 100%; - padding: 0 15px 0 15px; -} -#newsArticleContentScrollable img, -#newsArticleContentScrollable iframe { - max-width: 95%; - display: block; - margin: 0 auto; -} -#newsArticleContentScrollable a { - color: rgba(202, 202, 202, 0.75); - transition: 0.25s ease; - outline: none; -} -#newsArticleContentScrollable a:hover, -#newsArticleContentScrollable a:focus { - color: rgba(255, 255, 255, 0.75); -} -#newsArticleContentScrollable a:active { - color: rgba(165, 165, 165, 0.75); -} -#newsArticleContentScrollable::-webkit-scrollbar { - width: 2px; -} -#newsArticleContentScrollable::-webkit-scrollbar-track { - display: none; -} -#newsArticleContentScrollable::-webkit-scrollbar-thumb { - border-radius: 10px; - box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); -} -.bbCodeSpoilerButton { - background: none; - border: none; - outline: none; - cursor: pointer; - font-size: 16px; - transition: 0.25s ease; - width: 100%; - border-bottom: 1px solid white; - padding-bottom: 15px; -} -.bbCodeSpoilerButton:hover, -.bbCodeSpoilerButton:focus { - text-shadow: 0 0 20px #ffffff, 0 0 20px #ffffff, 0 0 20px #ffffff; -} -.bbCodeSpoilerButton:active { - color: #c7c7c7; - text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7, 0 0 20px #c7c7c7; -} -.bbCodeSpoilerText { - display: none; - padding: 15px 0; - border-bottom: 1px solid white; -} - - -#newsArticleContentWrapper { - width: 80%; -} - -.newsArticleSpacerTop { - height: 15px; -} - -/* Div to add spacing at the end of a news article. */ -.newsArticleSpacerBot { - height: 30px; -} - -/* News navigation container. */ -#newsNavigationContainer { - display: flex; - justify-content: center; - align-items: center; - margin-bottom: 10px; - -webkit-user-select: none; - position: absolute; - bottom: 15px; - right: 0; -} - -/* Navigation status span. */ -#newsNavigationStatus { - font-size: 12px; - margin: 0 15px; -} - -/* Left and right navigation button styles. */ -#newsNavigateLeft, -#newsNavigateRight { - background: none; - border: none; - outline: none; - height: 20px; - cursor: pointer; -} -#newsNavigateLeft:hover #newsNavigationLeftSVG, -#newsNavigateLeft:focus #newsNavigationLeftSVG, -#newsNavigateRight:hover #newsNavigationRightSVG, -#newsNavigateRight:focus #newsNavigationRightSVG { - filter: drop-shadow(0px 0 2px #fff); - -webkit-filter: drop-shadow(0px 0 2px #fff); -} -#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:active #newsNavigationRightSVG .arrowLine { - stroke: #c7c7c7; -} -#newsNavigateLeft:active #newsNavigationLeftSVG, -#newsNavigateRight:active #newsNavigationRightSVG { - filter: drop-shadow(0px 0 2px #c7c7c7); - -webkit-filter: drop-shadow(0px 0 2px #c7c7c7); -} -#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine, -#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine { - stroke: rgba(255, 255, 255, 0.75); -} -#newsNavigationLeftSVG { - transform: rotate(-90deg); - width: 15px; -} -#newsNavigationRightSVG { - transform: rotate(90deg); - width: 15px; -} - -/* News error (message) container. */ -#newsErrorContainer { - height: 100%; - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} -#newsErrorFailed { - display: flex; - align-items: center; - flex-direction: column; - justify-content: center; -} - -/* News error content (message). */ -.newsErrorContent { - font-size: 20px; -} -#newsErrorLoading { - display: flex; - width: 168.92px; -} -#nELoadSpan { - white-space: pre; -} -/* News error retry button styles. */ -#newsErrorRetry { - font-size: 12px; - font-weight: bold; - cursor: pointer; - background: none; - border: none; - outline: none; - transition: 0.25s ease; -} -#newsErrorRetry:focus, -#newsErrorRetry:hover { - text-shadow: 0 0 20px white; -} -#newsErrorRetry:active { - color: #c7c7c7; - text-shadow: 0 0 20px #c7c7c7; -} - /******************************************************************************* * * * Landing View (Top Styles) * diff --git a/src/renderer/components/landing/Landing.tsx b/src/renderer/components/landing/Landing.tsx index c9eb8178..100cf8a7 100644 --- a/src/renderer/components/landing/Landing.tsx +++ b/src/renderer/components/landing/Landing.tsx @@ -1,4 +1,5 @@ import * as React from 'react' +import News from '../news/News' import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus' import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse' @@ -99,12 +100,10 @@ export default class Landing extends React.Component { const statuses: JSX.Element[] = [] for(const status of this.state.mojangStatuses.filter(s => s.essential === essential)) { statuses.push( - <> -
- - {status.name} -
- +
+ + {status.name} +
) } return statuses @@ -270,59 +269,7 @@ export default class Landing extends React.Component { -
-
-
-
- -
-
- Mar 15, 44 BC, 9:14 AM -
-
- by Cicero -
- 0 Comments -
-
-
- - 1 of 1 - -
-
-
-
-
- {/* Article Content */} -
-
-
-
-
-
- Checking for News.. -
-
- Failed to Load News - -
-
- No News -
-
-
- + diff --git a/src/renderer/components/news/News.css b/src/renderer/components/news/News.css new file mode 100644 index 00000000..ca8567aa --- /dev/null +++ b/src/renderer/components/news/News.css @@ -0,0 +1,320 @@ +/******************************************************************************* + * * + * Landing View (News Styles) * + * * + ******************************************************************************/ + +/* Main container. */ +#newsContainer { + position: absolute; + top: 100%; + height: 100%; + width: 100%; + transition: top 2s ease; + display: flex; + align-items: flex-end; + justify-content: center; +} + +/* News content container. */ +#newsContent { + height: 82vh; + width: 100%; + display: flex; + -webkit-user-select: initial; + position: relative; +} + +/* Drop shadow displayed when content is scrolled out of view. */ +#newsContent:before { + content: ''; + background: linear-gradient(rgba(0, 0, 0, 0.25), transparent); + width: 100%; + height: 5px; + position: absolute; + opacity: 0; + transition: opacity 0.25s ease; +} +#newsContent[scrolled]:before { + opacity: 1; +} + +/* News article status container (left). */ +#newsStatusContainer { + width: calc(30% - 60px); + height: calc(100% - 30px); + padding: 15px 15px 15px 45px; + display: flex; + flex-direction: column; + justify-content: space-between; + position: relative; +} + +/* News status content. */ +#newsStatusContent { + display: flex; + flex-direction: column; + align-items: flex-end; +} + +/* News title wrapper. */ +#newsTitleContainer { + display: flex; + max-width: 90%; +} + +/* News article title styles. */ +#newsArticleTitle { + font-size: 18px; + font-weight: bold; + font-family: 'Avenir Medium'; + color: white; + text-decoration: none; + transition: 0.25s ease; + outline: none; + text-align: right; +} +#newsArticleTitle:hover, +#newsArticleTitle:focus { + text-shadow: 0 0 20px white; +} +#newsArticleTitle:active { + color: #c7c7c7; + text-shadow: 0 0 20px #c7c7c7; +} + +/* News meta container. */ +#newsMetaContainer { + display: flex; + flex-direction: column; +} + +/* Date and author wrappers. */ +#newsArticleDateWrapper, +#newsArticleAuthorWrapper { + display: flex; + justify-content: flex-end; +} + +/* Date and author shared styles. */ +#newsArticleDate, +#newsArticleAuthor { + display: inline-block; + font-size: 10px; + padding: 0 5px; + font-weight: bold; + border-radius: 2px; +} + +/* Date styles. */ +#newsArticleDate { + background: white; + color: black; + margin-top: 5px; +} + +/* Author styles. */ +#newsArticleAuthor { + background: #a02d2a; +} + +/* News article comments styles. */ +#newsArticleComments { + margin-top: 5px; + display: inline-block; + font-size: 10px; + color: #ffffff; + text-decoration: none; + transition: 0.25s ease; + outline: none; + text-align: right; +} +#newsArticleComments:focus, +#newsArticleComments:hover { + color: #e0e0e0; +} +#newsArticleComments:active { + color: #c7c7c7; +} + +/* Article content container (right). */ +#newsArticleContainer { + width: calc(100% - 25px); + height: 100%; + margin: 0 0 0 25px; +} + +/* Article content styles. */ +#newsArticleContentScrollable { + font-size: 12px; + overflow-y: scroll; + height: 100%; + padding: 0 15px 0 15px; +} +#newsArticleContentScrollable img, +#newsArticleContentScrollable iframe { + max-width: 95%; + display: block; + margin: 0 auto; +} +#newsArticleContentScrollable a { + color: rgba(202, 202, 202, 0.75); + transition: 0.25s ease; + outline: none; +} +#newsArticleContentScrollable a:hover, +#newsArticleContentScrollable a:focus { + color: rgba(255, 255, 255, 0.75); +} +#newsArticleContentScrollable a:active { + color: rgba(165, 165, 165, 0.75); +} +#newsArticleContentScrollable::-webkit-scrollbar { + width: 2px; +} +#newsArticleContentScrollable::-webkit-scrollbar-track { + display: none; +} +#newsArticleContentScrollable::-webkit-scrollbar-thumb { + border-radius: 10px; + box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50); +} +.bbCodeSpoilerButton { + background: none; + border: none; + outline: none; + cursor: pointer; + font-size: 16px; + transition: 0.25s ease; + width: 100%; + border-bottom: 1px solid white; + padding-bottom: 15px; +} +.bbCodeSpoilerButton:hover, +.bbCodeSpoilerButton:focus { + text-shadow: 0 0 20px #ffffff, 0 0 20px #ffffff, 0 0 20px #ffffff; +} +.bbCodeSpoilerButton:active { + color: #c7c7c7; + text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7, 0 0 20px #c7c7c7; +} +.bbCodeSpoilerText { + display: none; + padding: 15px 0; + border-bottom: 1px solid white; +} + + +#newsArticleContentWrapper { + width: 80%; +} + +.newsArticleSpacerTop { + height: 15px; +} + +/* Div to add spacing at the end of a news article. */ +.newsArticleSpacerBot { + height: 30px; +} + +/* News navigation container. */ +#newsNavigationContainer { + display: flex; + justify-content: center; + align-items: center; + margin-bottom: 10px; + -webkit-user-select: none; + position: absolute; + bottom: 15px; + right: 0; +} + +/* Navigation status span. */ +#newsNavigationStatus { + font-size: 12px; + margin: 0 15px; +} + +/* Left and right navigation button styles. */ +#newsNavigateLeft, +#newsNavigateRight { + background: none; + border: none; + outline: none; + height: 20px; + cursor: pointer; +} +#newsNavigateLeft:hover #newsNavigationLeftSVG, +#newsNavigateLeft:focus #newsNavigationLeftSVG, +#newsNavigateRight:hover #newsNavigationRightSVG, +#newsNavigateRight:focus #newsNavigationRightSVG { + filter: drop-shadow(0px 0 2px #fff); + -webkit-filter: drop-shadow(0px 0 2px #fff); +} +#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine, +#newsNavigateRight:active #newsNavigationRightSVG .arrowLine { + stroke: #c7c7c7; +} +#newsNavigateLeft:active #newsNavigationLeftSVG, +#newsNavigateRight:active #newsNavigationRightSVG { + filter: drop-shadow(0px 0 2px #c7c7c7); + -webkit-filter: drop-shadow(0px 0 2px #c7c7c7); +} +#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine, +#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine { + stroke: rgba(255, 255, 255, 0.75); +} +#newsNavigationLeftSVG { + transform: rotate(-90deg); + width: 15px; +} +#newsNavigationRightSVG { + transform: rotate(90deg); + width: 15px; +} + +/* News error (message) container. */ +#newsErrorContainer { + height: 100%; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; +} +#newsErrorFailed { + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; +} + +/* News error content (message). */ +.newsErrorContent { + font-size: 20px; +} +#newsErrorLoading { + display: flex; + width: 168.92px; +} +#nELoadSpan { + white-space: pre; +} +/* News error retry button styles. */ +#newsErrorRetry { + font-size: 12px; + font-weight: bold; + cursor: pointer; + background: none; + border: none; + outline: none; + transition: 0.25s ease; +} +#newsErrorRetry:focus, +#newsErrorRetry:hover { + text-shadow: 0 0 20px white; +} +#newsErrorRetry:active { + color: #c7c7c7; + text-shadow: 0 0 20px #c7c7c7; +} \ No newline at end of file diff --git a/src/renderer/components/news/News.tsx b/src/renderer/components/news/News.tsx new file mode 100644 index 00000000..22237d32 --- /dev/null +++ b/src/renderer/components/news/News.tsx @@ -0,0 +1,70 @@ +import * as React from 'react' + +import './News.css' + +export default class News extends React.Component { + + + render(): JSX.Element { + + return ( + <> +
+
+
+
+ +
+
+ Mar 15, 44 BC, 9:14 AM +
+
+ by Cicero +
+ 0 Comments +
+
+
+ + 1 of 1 + +
+
+
+
+
+ {/* Article Content */} +
+
+
+
+
+
+ Checking for News.. +
+
+ Failed to Load News + +
+
+ No News +
+
+
+ + + ) + + } + +} \ No newline at end of file diff --git a/src/renderer/index.tsx b/src/renderer/index.tsx index 6f20ddd8..568bbefa 100644 --- a/src/renderer/index.tsx +++ b/src/renderer/index.tsx @@ -1,11 +1,21 @@ import * as React from 'react' import * as ReactDOM from 'react-dom' import { AppContainer } from 'react-hot-loader' +import { Provider } from 'react-redux' +// import { shell } from 'electron' import store from './redux/store' -import './index.css' import Application from './components/Application' -import { Provider } from 'react-redux' + +import './index.css' + + +// document.addEventListener('click', (event: MouseEvent) => { +// if ((event.target as HTMLElement)?.tagName === 'A' && (event.target as HTMLAnchorElement)?.href.startsWith('http')) { +// event.preventDefault() +// shell.openExternal((event.target as HTMLAnchorElement).href) +// } +// }) // Create main element const mainElement = document.createElement('div') diff --git a/src/renderer/meta/Views.ts b/src/renderer/meta/Views.ts index 8e627ebb..b8df0777 100644 --- a/src/renderer/meta/Views.ts +++ b/src/renderer/meta/Views.ts @@ -2,5 +2,7 @@ export enum View { LANDING = 'LANDING', WELCOME = 'WELCOME', LOGIN = 'LOGIN', - SETTINGS = 'SETTINGS' + SETTINGS = 'SETTINGS', + FATAL = 'FATAL', + NONE = 'NONE' } \ No newline at end of file diff --git a/src/renderer/redux/actions/appActions.ts b/src/renderer/redux/actions/appActions.ts index d83de66f..ad6a6ec7 100644 --- a/src/renderer/redux/actions/appActions.ts +++ b/src/renderer/redux/actions/appActions.ts @@ -1,19 +1,24 @@ import { Action } from 'redux' +import { HeliosDistribution } from 'common/distribution/DistributionFactory' export enum AppActionType { - ChangeLoadState = 'SET_LOADING' + SetDistribution = 'SET_DISTRIBUTION' } // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface AppAction extends Action {} -export interface ChangeLoadStateAction extends AppAction { - payload: boolean +export interface SetDistributionAction extends AppAction { + payload: HeliosDistribution } -export function setLoadingState(state: boolean): ChangeLoadStateAction { +export function setDistribution(distribution: HeliosDistribution): SetDistributionAction { return { - type: AppActionType.ChangeLoadState, - payload: state + type: AppActionType.SetDistribution, + payload: distribution } +} + +export const AppActionDispatch = { + setDistribution: (d: HeliosDistribution): SetDistributionAction => setDistribution(d) } \ No newline at end of file diff --git a/src/renderer/redux/reducers/appReducer.ts b/src/renderer/redux/reducers/appReducer.ts index c08e3cd7..cddb6f7c 100644 --- a/src/renderer/redux/reducers/appReducer.ts +++ b/src/renderer/redux/reducers/appReducer.ts @@ -1,21 +1,21 @@ -import { ChangeLoadStateAction, AppActionType, AppAction } from '../actions/appActions' +import { AppActionType, AppAction, SetDistributionAction } from '../actions/appActions' import { Reducer } from 'redux' +import { HeliosDistribution } from 'common/distribution/DistributionFactory' export interface AppState { - loading: boolean + distribution: HeliosDistribution | null } const defaultAppState: AppState = { - loading: true + distribution: null! } -// TODO remove loading from global state. Keeping as an example... const AppReducer: Reducer = (state = defaultAppState, action) => { switch(action.type) { - case AppActionType.ChangeLoadState: + case AppActionType.SetDistribution: return { ...state, - loading: (action as ChangeLoadStateAction).payload + distribution: (action as SetDistributionAction).payload } } return state diff --git a/src/renderer/redux/reducers/viewReducer.ts b/src/renderer/redux/reducers/viewReducer.ts index f279784f..9d734c38 100644 --- a/src/renderer/redux/reducers/viewReducer.ts +++ b/src/renderer/redux/reducers/viewReducer.ts @@ -2,7 +2,7 @@ import { Reducer } from 'redux' import { View } from '../../meta/Views' import { ChangeViewAction, ViewActionType } from '../actions/viewActions' -const defaultView = View.LANDING +const defaultView = View.NONE const ViewReducer: Reducer = (state = defaultView, action) => { switch(action.type) { diff --git a/src/renderer/redux/store.ts b/src/renderer/redux/store.ts index 8d11c98e..30fd5b70 100644 --- a/src/renderer/redux/store.ts +++ b/src/renderer/redux/store.ts @@ -1,6 +1,8 @@ -import { createStore } from 'redux' +import { createStore, StoreEnhancer } from 'redux' import reducer from './reducers' export type StoreType = ReturnType -export default createStore(reducer) \ No newline at end of file +type Tmp = {__REDUX_DEVTOOLS_EXTENSION__?: () => StoreEnhancer} + +export default createStore(reducer, (window as Tmp).__REDUX_DEVTOOLS_EXTENSION__ && (window as Tmp).__REDUX_DEVTOOLS_EXTENSION__!()) \ No newline at end of file diff --git a/static/images/SealCircleError.png b/static/images/SealCircleError.png new file mode 100644 index 00000000..b2d92192 Binary files /dev/null and b/static/images/SealCircleError.png differ