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:
parent
dbc49f51dd
commit
dc00e6104b
@ -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>
|
||||
|
||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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<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
|
||||
|
||||
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<MojangVersionManifest>(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<any> {
|
||||
public async validate(): Promise<{[category: string]: Asset[]}> {
|
||||
|
||||
const assets = await this.validateAssets(this.assetIndex)
|
||||
const libraries = await this.validateLibraries(this.versionJson)
|
||||
|
@ -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')
|
195
src/common/distribution/DistributionFactory.ts
Normal file
195
src/common/distribution/DistributionFactory.ts
Normal 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
|
||||
}
|
||||
|
||||
}
|
107
src/common/util/MavenUtil.ts
Normal file
107
src/common/util/MavenUtil.ts
Normal 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))
|
||||
}
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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<ApplicationProps> => {
|
||||
}
|
||||
}
|
||||
const mapDispatch = {
|
||||
...AppActionDispatch,
|
||||
...ViewActionDispatch,
|
||||
...OverlayActionDispatch
|
||||
}
|
||||
@ -68,6 +73,8 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
|
||||
}
|
||||
|
||||
getViewElement(): JSX.Element {
|
||||
// TODO debug remove
|
||||
console.log('loading', this.props.currentView, this.state.workingView)
|
||||
switch(this.state.workingView) {
|
||||
case View.WELCOME:
|
||||
return <>
|
||||
@ -85,6 +92,12 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
|
||||
return <>
|
||||
<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(() => {
|
||||
// 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<ApplicationProps & typeof mapDispatch,
|
||||
|
||||
}, 200)
|
||||
|
||||
private finishLoad = (): void => {
|
||||
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<ApplicationProps & typeof mapDispatch,
|
||||
if(this.state.loading) {
|
||||
const MIN_LOAD = 800
|
||||
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.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<ApplicationProps & typeof mapDispatch,
|
||||
classNames="loader"
|
||||
unmountOnExit
|
||||
onEnter={this.initLoad}
|
||||
onExited={this.showMain}
|
||||
onExited={this.finishLoad}
|
||||
>
|
||||
<Loader />
|
||||
</CSSTransition>
|
||||
|
124
src/renderer/components/fatal/Fatal.css
Normal file
124
src/renderer/components/fatal/Fatal.css
Normal 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);
|
||||
}
|
70
src/renderer/components/fatal/Fatal.tsx
Normal file
70
src/renderer/components/fatal/Fatal.tsx
Normal 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>
|
||||
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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) *
|
||||
|
@ -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<unknown, LandingState> {
|
||||
const statuses: JSX.Element[] = []
|
||||
for(const status of this.state.mojangStatuses.filter(s => s.essential === essential)) {
|
||||
statuses.push(
|
||||
<>
|
||||
<div className="mojangStatusContainer">
|
||||
<span className="mojangStatusIcon" style={{color: MojangRestAPI.statusToHex(status.status)}}>•</span>
|
||||
<span className="mojangStatusName">{status.name}</span>
|
||||
</div>
|
||||
</>
|
||||
<div className="mojangStatusContainer" key={status.service}>
|
||||
<span className="mojangStatusIcon" style={{color: MojangRestAPI.statusToHex(status.status)}}>•</span>
|
||||
<span className="mojangStatusName">{status.name}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return statuses
|
||||
@ -270,59 +269,7 @@ export default class Landing extends React.Component<unknown, LandingState> {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<script src="./assets/js/scripts/landing.js"></script>
|
||||
<News />
|
||||
</div>
|
||||
|
||||
|
||||
|
320
src/renderer/components/news/News.css
Normal file
320
src/renderer/components/news/News.css
Normal 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;
|
||||
}
|
70
src/renderer/components/news/News.tsx
Normal file
70
src/renderer/components/news/News.tsx
Normal 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>
|
||||
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -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')
|
||||
|
@ -2,5 +2,7 @@ export enum View {
|
||||
LANDING = 'LANDING',
|
||||
WELCOME = 'WELCOME',
|
||||
LOGIN = 'LOGIN',
|
||||
SETTINGS = 'SETTINGS'
|
||||
SETTINGS = 'SETTINGS',
|
||||
FATAL = 'FATAL',
|
||||
NONE = 'NONE'
|
||||
}
|
@ -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)
|
||||
}
|
@ -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<AppState, AppAction> = (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
|
||||
|
@ -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<View, ChangeViewAction> = (state = defaultView, action) => {
|
||||
switch(action.type) {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { createStore } from 'redux'
|
||||
import { createStore, StoreEnhancer } from 'redux'
|
||||
import reducer from './reducers'
|
||||
|
||||
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__!())
|
BIN
static/images/SealCircleError.png
Normal file
BIN
static/images/SealCircleError.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 150 KiB |
Loading…
Reference in New Issue
Block a user