Initial work on distro load logic.

Added new FATAL view to display information when a distro load fails. This replaces
the overlay behavior used on v1. The fatal view will eventually do an update check
and allow the user to update the app. This solves a potential issue of a user using
a very outdated launcher version, and the distro failing as a result.

Added new wrapper classes to store the distribution in the redux store.
This commit is contained in:
Daniel Scalzi 2020-08-29 01:12:39 -04:00
parent dbc49f51dd
commit dc00e6104b
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
22 changed files with 1028 additions and 450 deletions

View File

@ -1,4 +1,4 @@
<p align="center"><img src="./app/assets/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p> <p align="center"><img src="./static/images/SealCircle.png" width="150px" height="150px" alt="aventium softworks"></p>
<h1 align="center">Helios Launcher</h1> <h1 align="center">Helios Launcher</h1>

6
package-lock.json generated
View File

@ -1445,6 +1445,12 @@
"integrity": "sha512-Ee0vt82qcg05OeJrQZ/YN+NQwaBCnAul1rVLYaMLPkwR5f44WC3BpBQNvn5Z3Axu9szaVOHqXEDBI+uAXAiyrg==", "integrity": "sha512-Ee0vt82qcg05OeJrQZ/YN+NQwaBCnAul1rVLYaMLPkwR5f44WC3BpBQNvn5Z3Axu9szaVOHqXEDBI+uAXAiyrg==",
"dev": true "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": { "@types/eslint-visitor-keys": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",

View File

@ -53,6 +53,7 @@
"@types/chai": "^4.2.12", "@types/chai": "^4.2.12",
"@types/chai-as-promised": "^7.1.3", "@types/chai-as-promised": "^7.1.3",
"@types/discord-rpc": "^3.0.4", "@types/discord-rpc": "^3.0.4",
"@types/electron-devtools-installer": "^2.2.0",
"@types/fs-extra": "^9.0.1", "@types/fs-extra": "^9.0.1",
"@types/jquery": "^3.5.1", "@types/jquery": "^3.5.1",
"@types/lodash": "^4.14.160", "@types/lodash": "^4.14.160",

View File

@ -1,14 +1,16 @@
import { IndexProcessor } from '../model/engine/IndexProcessor' import got from 'got'
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 { dirname, join } from 'path' import { dirname, join } from 'path'
import { VersionJson, AssetIndex, LibraryArtifact } from '../model/mojang/VersionJson' import { ensureDir, pathExists, readFile, readJson, writeFile } from 'fs-extra'
import { AssetGuardError } from '../model/engine/AssetGuardError'
import { Asset } from '../model/engine/Asset' import { Asset } from 'common/asset/model/engine/Asset'
import { isLibraryCompatible, getMojangOS } from 'common/util/MojangUtils' 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 { 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 VERSION_MANIFEST_ENDPOINT = 'https://launchermeta.mojang.com/mc/game/version_manifest.json'
public static readonly ASSET_RESOURCE_ENDPOINT = 'http://resources.download.minecraft.net' 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 versionJson!: VersionJson
private assetIndex!: AssetIndex private assetIndex!: AssetIndex
@ -24,26 +26,6 @@ export class MojangIndexProcessor extends IndexProcessor {
responseType: 'json' responseType: 'json'
}) })
private handleGotError<T>(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 private assetPath: string
constructor(commonDir: string, protected version: string) { constructor(commonDir: string, protected version: string) {
@ -148,7 +130,7 @@ export class MojangIndexProcessor extends IndexProcessor {
return res.body return res.body
} catch(error) { } 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<MojangVersionManifest>(MojangIndexProcessor.VERSION_MANIFEST_ENDPOINT) const res = await this.client.get<MojangVersionManifest>(MojangIndexProcessor.VERSION_MANIFEST_ENDPOINT)
return res.body return res.body
} catch(error) { } 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 progress tracker
// TODO type return object // TODO type return object
public async validate(): Promise<any> { public async validate(): Promise<{[category: string]: Asset[]}> {
const assets = await this.validateAssets(this.assetIndex) const assets = await this.validateAssets(this.assetIndex)
const libraries = await this.validateLibraries(this.versionJson) const libraries = await this.validateLibraries(this.versionJson)

View File

@ -5,6 +5,9 @@ import { LoggerUtil } from 'common/logging/loggerutil'
import { RestResponse, handleGotError, RestResponseStatus } from 'common/got/RestResponse' import { RestResponse, handleGotError, RestResponseStatus } from 'common/got/RestResponse'
import { pathExists, readFile, writeFile } from 'fs-extra' 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 { export class DistributionAPI {
private static readonly logger = LoggerUtil.getLogger('DistributionAPI') private static readonly logger = LoggerUtil.getLogger('DistributionAPI')

View File

@ -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<this.servers.length; i++) {
if(this.servers[i].rawServer.mainServer) {
return i
}
}
return 0
}
public getMainServer(): HeliosServer | null {
return this.mainServerIndex < this.servers.length ? this.servers[this.mainServerIndex] : null
}
public getServerById(id: string): HeliosServer | null {
return this.servers.find(s => 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<MavenComponents>
private readonly required: Readonly<Required<HeliosRequired>>
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<HeliosRequired> {
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<MavenComponents> {
return this.mavenComponents
}
public getRequired(): Readonly<Required<HeliosRequired>> {
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
}
}

View File

@ -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))
}
}

View File

@ -8,14 +8,12 @@ import isdev from '../common/util/isdev'
declare const __static: string declare const __static: string
const installExtensions = async () => { const installExtensions = async () => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const installer = require('electron-devtools-installer')
const forceDownload = !!process.env.UPGRADE_EXTENSIONS
const extensions = ['REACT_DEVELOPER_TOOLS', 'REDUX_DEVTOOLS']
return Promise.all( const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } = await import('electron-devtools-installer')
extensions.map(name => installer.default(installer[name], forceDownload)) const forceDownload = !!process.env.UPGRADE_EXTENSIONS
).catch(console.log) // eslint-disable-line no-console const extensions = [REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS]
return installExtension(extensions, forceDownload).catch(console.log) // eslint-disable-line no-console
} }
// Setup auto updater. // Setup auto updater.
@ -145,6 +143,12 @@ async function createWindow() {
win.removeMenu() win.removeMenu()
win.resizable = true 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') { if (process.env.NODE_ENV !== 'production') {
// Open DevTools, see https://github.com/electron/electron/issues/12438 for why we wait for dom-ready // Open DevTools, see https://github.com/electron/electron/issues/12438 for why we wait for dom-ready

View File

@ -8,17 +8,21 @@ import Landing from './landing/Landing'
import Login from './login/Login' import Login from './login/Login'
import Loader from './loader/Loader' import Loader from './loader/Loader'
import Settings from './settings/Settings' import Settings from './settings/Settings'
import Overlay from './overlay/Overlay'
import Fatal from './fatal/Fatal'
import { StoreType } from '../redux/store' import { StoreType } from '../redux/store'
import { CSSTransition } from 'react-transition-group' import { CSSTransition } from 'react-transition-group'
import { ViewActionDispatch } from '../redux/actions/viewActions' import { ViewActionDispatch } from '../redux/actions/viewActions'
import { throttle } from 'lodash' import { throttle } from 'lodash'
import { readdir } from 'fs-extra' import { readdir } from 'fs-extra'
import { join } from 'path' import { join } from 'path'
import Overlay from './overlay/Overlay' import { AppActionDispatch } from '../redux/actions/appActions'
import { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions' 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 { getServerStatus } from 'common/mojang/net/ServerStatusAPI'
import { Distribution } from 'helios-distribution-types'
import { HeliosDistribution } from 'common/distribution/DistributionFactory'
import './Application.css' import './Application.css'
@ -49,6 +53,7 @@ const mapState = (state: StoreType): Partial<ApplicationProps> => {
} }
} }
const mapDispatch = { const mapDispatch = {
...AppActionDispatch,
...ViewActionDispatch, ...ViewActionDispatch,
...OverlayActionDispatch ...OverlayActionDispatch
} }
@ -68,6 +73,8 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
} }
getViewElement(): JSX.Element { getViewElement(): JSX.Element {
// TODO debug remove
console.log('loading', this.props.currentView, this.state.workingView)
switch(this.state.workingView) { switch(this.state.workingView) {
case View.WELCOME: case View.WELCOME:
return <> return <>
@ -85,6 +92,12 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
return <> return <>
<Settings /> <Settings />
</> </>
case View.FATAL:
return <>
<Fatal />
</>
case View.NONE:
return <></>
} }
} }
@ -94,6 +107,8 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
} }
private updateWorkingView = throttle(() => { private updateWorkingView = throttle(() => {
// TODO debug remove
console.log('Setting to', this.props.currentView)
this.setState({ this.setState({
...this.state, ...this.state,
workingView: this.props.currentView workingView: this.props.currentView
@ -101,8 +116,14 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
}, 200) }, 200)
private finishLoad = (): void => {
if(this.props.currentView !== View.FATAL) {
setBackground(this.bkid)
}
this.showMain()
}
private showMain = (): void => { private showMain = (): void => {
setBackground(this.bkid)
this.setState({ this.setState({
...this.state, ...this.state,
showMain: true showMain: true
@ -113,23 +134,53 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
if(this.state.loading) { if(this.state.loading) {
const MIN_LOAD = 800 const MIN_LOAD = 800
const start = Date.now() const start = Date.now()
this.bkid = Math.floor((Math.random() * (await readdir(join(__static, 'images', 'backgrounds'))).length))
const endLoad = () => { // 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.setState({
...this.state, ...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 // TODO temp
setTimeout(() => { setTimeout(() => {
//this.props.setView(View.WELCOME) //this.props.setView(View.WELCOME)
this.props.pushGenericOverlay({ this.props.pushGenericOverlay({
title: 'Load Distribution', title: 'Load Distribution',
description: 'This is a test. Will load the distribution.', description: 'This is a test.',
dismissible: false, dismissible: false,
acknowledgeCallback: async () => { 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) const serverStatus = await getServerStatus(47, 'play.hypixel.net', 25565)
console.log(serverStatus) console.log(serverStatus)
} }
@ -206,7 +257,7 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
classNames="loader" classNames="loader"
unmountOnExit unmountOnExit
onEnter={this.initLoad} onEnter={this.initLoad}
onExited={this.showMain} onExited={this.finishLoad}
> >
<Loader /> <Loader />
</CSSTransition> </CSSTransition>

View File

@ -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);
}

View File

@ -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 (
<>
<div id="fatalContainer">
<div id="fatalContent">
<div id="fatalHeader">
<div id="fatalLeft">
<img id="fatalErrorImg" src="../images/SealCircleError.png"/>
</div>
<div id="fatalRight">
<span id="fatalErrorLabel">FATAL ERROR</span>
<span id="fatalErrorText">Failed to load Distribution Index</span>
</div>
</div>
<div id="fatalBody">
<h4>What Happened?</h4>
<p id="fatalDescription">
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.
</p>
{/* TODO When auto update is done, do a version check and auto/update here. */}
<div id="fatalChecklistContainer">
<ul>
<li>Ensure you are running the latest version of Helios Launcher.</li>
<li>Ensure you are connected to the internet.</li>
</ul>
</div>
<h4>Relaunch the application to try again.</h4>
<div id="fatalActionContainer">
<button onClick={openLatest} id="fatalAcknowledge">Latest Releaes</button>
<div id="fatalDismissWrapper">
<button onClick={closeHandler} id="fatalDismiss">Close Launcher</button>
</div>
</div>
</div>
</div>
</div>
</>
)
}
}

View File

@ -83,327 +83,6 @@
display: inline-flex; 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) * * Landing View (Top Styles) *

View File

@ -1,4 +1,5 @@
import * as React from 'react' import * as React from 'react'
import News from '../news/News'
import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus' import { MojangStatus, MojangStatusColor } from 'common/mojang/rest/internal/MojangStatus'
import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse' import { MojangResponse } from 'common/mojang/rest/internal/MojangResponse'
@ -99,12 +100,10 @@ export default class Landing extends React.Component<unknown, LandingState> {
const statuses: JSX.Element[] = [] const statuses: JSX.Element[] = []
for(const status of this.state.mojangStatuses.filter(s => s.essential === essential)) { for(const status of this.state.mojangStatuses.filter(s => s.essential === essential)) {
statuses.push( statuses.push(
<> <div className="mojangStatusContainer" key={status.service}>
<div className="mojangStatusContainer"> <span className="mojangStatusIcon" style={{color: MojangRestAPI.statusToHex(status.status)}}>&#8226;</span>
<span className="mojangStatusIcon" style={{color: MojangRestAPI.statusToHex(status.status)}}>&#8226;</span> <span className="mojangStatusName">{status.name}</span>
<span className="mojangStatusName">{status.name}</span> </div>
</div>
</>
) )
} }
return statuses return statuses
@ -270,59 +269,7 @@ export default class Landing extends React.Component<unknown, LandingState> {
</div> </div>
</div> </div>
</div> </div>
<div id="newsContainer"> <News />
<div id="newsContent" {...{article: '-1'}} style={{display: 'none'}}>
<div id="newsStatusContainer">
<div id="newsStatusContent">
<div id="newsTitleContainer">
<a id="newsArticleTitle" href="#">Lorem Ipsum</a>
</div>
<div id="newsMetaContainer">
<div id="newsArticleDateWrapper">
<span id="newsArticleDate">Mar 15, 44 BC, 9:14 AM</span>
</div>
<div id="newsArticleAuthorWrapper">
<span id="newsArticleAuthor">by Cicero</span>
</div>
<a href="#" id="newsArticleComments">0 Comments</a>
</div>
</div>
<div id="newsNavigationContainer">
<button id="newsNavigateLeft">
<svg id="newsNavigationLeftSVG" viewBox="0 0 24.87 13.97">
<polyline style={{transition: '0.25s ease'}} fill="none" stroke="#FFF" strokeWidth="2px" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</button>
<span id="newsNavigationStatus">1 of 1</span>
<button id="newsNavigateRight">
<svg id="newsNavigationRightSVG" viewBox="0 0 24.87 13.97">
<polyline style={{transition: '0.25s ease'}} fill="none" stroke="#FFF" strokeWidth="2px" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</button>
</div>
</div>
<div id="newsArticleContainer">
<div id="newsArticleContent">
<div id="newsArticleContentScrollable">
{/* Article Content */}
</div>
</div>
</div>
</div>
<div id="newsErrorContainer">
<div id="newsErrorLoading">
<span id="nELoadSpan" className="newsErrorContent">Checking for News..</span>
</div>
<div id="newsErrorFailed" style={{display: 'none'}}>
<span id="nEFailedSpan" className="newsErrorContent">Failed to Load News</span>
<button id="newsErrorRetry">Try Again</button>
</div>
<div id="newsErrorNone" style={{display: 'none'}}>
<span id="nENoneSpan" className="newsErrorContent">No News</span>
</div>
</div>
</div>
<script src="./assets/js/scripts/landing.js"></script>
</div> </div>

View File

@ -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;
}

View File

@ -0,0 +1,70 @@
import * as React from 'react'
import './News.css'
export default class News extends React.Component {
render(): JSX.Element {
return (
<>
<div id="newsContainer">
<div id="newsContent" {...{article: '-1'}} style={{display: 'none'}}>
<div id="newsStatusContainer">
<div id="newsStatusContent">
<div id="newsTitleContainer">
<a id="newsArticleTitle" href="#">Lorem Ipsum</a>
</div>
<div id="newsMetaContainer">
<div id="newsArticleDateWrapper">
<span id="newsArticleDate">Mar 15, 44 BC, 9:14 AM</span>
</div>
<div id="newsArticleAuthorWrapper">
<span id="newsArticleAuthor">by Cicero</span>
</div>
<a href="#" id="newsArticleComments">0 Comments</a>
</div>
</div>
<div id="newsNavigationContainer">
<button id="newsNavigateLeft">
<svg id="newsNavigationLeftSVG" viewBox="0 0 24.87 13.97">
<polyline style={{transition: '0.25s ease'}} fill="none" stroke="#FFF" strokeWidth="2px" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</button>
<span id="newsNavigationStatus">1 of 1</span>
<button id="newsNavigateRight">
<svg id="newsNavigationRightSVG" viewBox="0 0 24.87 13.97">
<polyline style={{transition: '0.25s ease'}} fill="none" stroke="#FFF" strokeWidth="2px" points="0.71 13.26 12.56 1.41 24.16 13.02"/>
</svg>
</button>
</div>
</div>
<div id="newsArticleContainer">
<div id="newsArticleContent">
<div id="newsArticleContentScrollable">
{/* Article Content */}
</div>
</div>
</div>
</div>
<div id="newsErrorContainer">
<div id="newsErrorLoading">
<span id="nELoadSpan" className="newsErrorContent">Checking for News..</span>
</div>
<div id="newsErrorFailed" style={{display: 'none'}}>
<span id="nEFailedSpan" className="newsErrorContent">Failed to Load News</span>
<button id="newsErrorRetry">Try Again</button>
</div>
<div id="newsErrorNone" style={{display: 'none'}}>
<span id="nENoneSpan" className="newsErrorContent">No News</span>
</div>
</div>
</div>
</>
)
}
}

View File

@ -1,11 +1,21 @@
import * as React from 'react' import * as React from 'react'
import * as ReactDOM from 'react-dom' import * as ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader' import { AppContainer } from 'react-hot-loader'
import { Provider } from 'react-redux'
// import { shell } from 'electron'
import store from './redux/store' import store from './redux/store'
import './index.css'
import Application from './components/Application' 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 // Create main element
const mainElement = document.createElement('div') const mainElement = document.createElement('div')

View File

@ -2,5 +2,7 @@ export enum View {
LANDING = 'LANDING', LANDING = 'LANDING',
WELCOME = 'WELCOME', WELCOME = 'WELCOME',
LOGIN = 'LOGIN', LOGIN = 'LOGIN',
SETTINGS = 'SETTINGS' SETTINGS = 'SETTINGS',
FATAL = 'FATAL',
NONE = 'NONE'
} }

View File

@ -1,19 +1,24 @@
import { Action } from 'redux' import { Action } from 'redux'
import { HeliosDistribution } from 'common/distribution/DistributionFactory'
export enum AppActionType { export enum AppActionType {
ChangeLoadState = 'SET_LOADING' SetDistribution = 'SET_DISTRIBUTION'
} }
// eslint-disable-next-line @typescript-eslint/no-empty-interface // eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface AppAction extends Action {} export interface AppAction extends Action {}
export interface ChangeLoadStateAction extends AppAction { export interface SetDistributionAction extends AppAction {
payload: boolean payload: HeliosDistribution
} }
export function setLoadingState(state: boolean): ChangeLoadStateAction { export function setDistribution(distribution: HeliosDistribution): SetDistributionAction {
return { return {
type: AppActionType.ChangeLoadState, type: AppActionType.SetDistribution,
payload: state payload: distribution
} }
} }
export const AppActionDispatch = {
setDistribution: (d: HeliosDistribution): SetDistributionAction => setDistribution(d)
}

View File

@ -1,21 +1,21 @@
import { ChangeLoadStateAction, AppActionType, AppAction } from '../actions/appActions' import { AppActionType, AppAction, SetDistributionAction } from '../actions/appActions'
import { Reducer } from 'redux' import { Reducer } from 'redux'
import { HeliosDistribution } from 'common/distribution/DistributionFactory'
export interface AppState { export interface AppState {
loading: boolean distribution: HeliosDistribution | null
} }
const defaultAppState: AppState = { const defaultAppState: AppState = {
loading: true distribution: null!
} }
// TODO remove loading from global state. Keeping as an example...
const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => { const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => {
switch(action.type) { switch(action.type) {
case AppActionType.ChangeLoadState: case AppActionType.SetDistribution:
return { return {
...state, ...state,
loading: (action as ChangeLoadStateAction).payload distribution: (action as SetDistributionAction).payload
} }
} }
return state return state

View File

@ -2,7 +2,7 @@ import { Reducer } from 'redux'
import { View } from '../../meta/Views' import { View } from '../../meta/Views'
import { ChangeViewAction, ViewActionType } from '../actions/viewActions' import { ChangeViewAction, ViewActionType } from '../actions/viewActions'
const defaultView = View.LANDING const defaultView = View.NONE
const ViewReducer: Reducer<View, ChangeViewAction> = (state = defaultView, action) => { const ViewReducer: Reducer<View, ChangeViewAction> = (state = defaultView, action) => {
switch(action.type) { switch(action.type) {

View File

@ -1,6 +1,8 @@
import { createStore } from 'redux' import { createStore, StoreEnhancer } from 'redux'
import reducer from './reducers' import reducer from './reducers'
export type StoreType = ReturnType<typeof reducer> export type StoreType = ReturnType<typeof reducer>
export default createStore(reducer) type Tmp = {__REDUX_DEVTOOLS_EXTENSION__?: () => StoreEnhancer}
export default createStore(reducer, (window as Tmp).__REDUX_DEVTOOLS_EXTENSION__ && (window as Tmp).__REDUX_DEVTOOLS_EXTENSION__!())

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB