mirror of
https://github.com/dscalzi/HeliosLauncher.git
synced 2024-12-22 11:42:14 -08:00
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>
|
<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==",
|
"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",
|
||||||
|
@ -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",
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
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
|
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
|
||||||
|
@ -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 showMain = (): void => {
|
private finishLoad = (): void => {
|
||||||
|
if(this.props.currentView !== View.FATAL) {
|
||||||
setBackground(this.bkid)
|
setBackground(this.bkid)
|
||||||
|
}
|
||||||
|
this.showMain()
|
||||||
|
}
|
||||||
|
|
||||||
|
private showMain = (): void => {
|
||||||
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>
|
||||||
|
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;
|
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) *
|
||||||
|
@ -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)}}>•</span>
|
<span className="mojangStatusIcon" style={{color: MojangRestAPI.statusToHex(status.status)}}>•</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>
|
||||||
|
|
||||||
|
|
||||||
|
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 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')
|
||||||
|
@ -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'
|
||||||
}
|
}
|
@ -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)
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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) {
|
||||||
|
@ -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__!())
|
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