Initial work on overlay system.

The overlay display is driven by an array in the global redux store. Overlay
content is now queueabale, so alerts can be asynchronously dispatched without
interferring with existing displayed content.

The server/account select overlay components are yet to be completed. Some usability
features also need to be implemented, such as keybinds for the acknowledge/dismiss
buttons.
This commit is contained in:
Daniel Scalzi 2020-06-12 20:58:07 -04:00
parent 691cf937fc
commit c670b7d88d
No known key found for this signature in database
GPG Key ID: D18EA3FB4B142A57
18 changed files with 819 additions and 60 deletions

View File

@ -2,6 +2,9 @@
height: calc(100vh - 22px); height: calc(100vh - 22px);
background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 100%); background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 100%);
} }
.appWrapper[overlay] {
filter: blur(3px) contrast(0.9) brightness(1.0);
}
.loader-enter { .loader-enter {
opacity: 0; opacity: 0;

View File

@ -8,14 +8,16 @@ 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 './Application.css'
import { StoreType } from '../redux/store' import { StoreType } from '../redux/store'
import { CSSTransition } from 'react-transition-group' import { CSSTransition } from 'react-transition-group'
import { setCurrentView } 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 { OverlayPushAction, OverlayActionDispatch } from '../redux/actions/overlayActions'
import './Application.css'
declare const __static: string declare const __static: string
@ -27,6 +29,7 @@ function setBackground(id: number) {
interface ApplicationProps { interface ApplicationProps {
currentView: View currentView: View
overlayQueue: OverlayPushAction<unknown>[]
} }
interface ApplicationState { interface ApplicationState {
@ -36,13 +39,15 @@ interface ApplicationState {
workingView: View workingView: View
} }
const mapState = (state: StoreType) => { const mapState = (state: StoreType): Partial<ApplicationProps> => {
return { return {
currentView: state.currentView currentView: state.currentView,
overlayQueue: state.overlayQueue
} }
} }
const mapDispatch = { const mapDispatch = {
setView: (x: View) => setCurrentView(x) ...ViewActionDispatch,
...OverlayActionDispatch
} }
class Application extends React.Component<ApplicationProps & typeof mapDispatch, ApplicationState> { class Application extends React.Component<ApplicationProps & typeof mapDispatch, ApplicationState> {
@ -81,6 +86,10 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
} }
} }
private hasOverlay = (): boolean => {
return this.props.overlayQueue.length > 0
}
private updateWorkingView = throttle(() => { private updateWorkingView = throttle(() => {
this.setState({ this.setState({
...this.state, ...this.state,
@ -109,7 +118,32 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
}) })
// TODO temp // TODO temp
setTimeout(() => { setTimeout(() => {
this.props.setView(View.WELCOME) //this.props.setView(View.WELCOME)
this.props.pushGenericOverlay({
title: 'Test Title',
description: 'Test Description',
dismissible: true
})
this.props.pushGenericOverlay({
title: 'Test Title 2',
description: 'Test Description',
dismissible: true
})
this.props.pushGenericOverlay({
title: 'Test Title 3',
description: 'Test Description',
dismissible: true
})
this.props.pushGenericOverlay({
title: 'Test Title 4',
description: 'Test Description',
dismissible: true
})
this.props.pushGenericOverlay({
title: 'Test Title IMPORTANT',
description: 'Test Description',
dismissible: true
}, true)
}, 5000) }, 5000)
} }
const diff = Date.now() - start const diff = Date.now() - start
@ -132,7 +166,7 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
classNames="appWrapper" classNames="appWrapper"
unmountOnExit unmountOnExit
> >
<div className="appWrapper"> <div className="appWrapper" {...(this.hasOverlay() ? {overlay: 'true'} : {})}>
<CSSTransition <CSSTransition
in={this.props.currentView == this.state.workingView} in={this.props.currentView == this.state.workingView}
appear={true} appear={true}
@ -146,6 +180,15 @@ class Application extends React.Component<ApplicationProps & typeof mapDispatch,
</div> </div>
</CSSTransition> </CSSTransition>
<CSSTransition
in={this.hasOverlay()}
appear={true}
timeout={500}
classNames="appWrapper"
unmountOnExit
>
<Overlay overlayQueue={this.props.overlayQueue} />
</CSSTransition>
<CSSTransition <CSSTransition
in={this.state.loading} in={this.state.loading}
appear={true} appear={true}

View File

@ -16,7 +16,7 @@
#landingContainer > #upper { #landingContainer > #upper {
position: relative; position: relative;
transition: top 2s ease; transition: top 2s ease;
top: 0px; top: 0;
height: 77%; height: 77%;
display: flex; display: flex;
} }
@ -46,7 +46,7 @@
#landingContainer > #lower > #left { #landingContainer > #lower > #left {
position: relative; position: relative;
transition: top 2s ease; transition: top 2s ease;
top: 0px; top: 0;
height: 100%; height: 100%;
width: 33%; width: 33%;
display: inline-flex; display: inline-flex;
@ -62,7 +62,7 @@
#landingContainer > #lower > #center { #landingContainer > #lower > #center {
position: relative; position: relative;
transition: top 2s ease; transition: top 2s ease;
top: 0px; top: 0;
height: 100%; height: 100%;
width: 34%; width: 34%;
display: inline-flex; display: inline-flex;
@ -77,7 +77,7 @@
#landingContainer > #lower > #right { #landingContainer > #lower > #right {
position: relative; position: relative;
transition: top 2s ease; transition: top 2s ease;
top: 0px; top: 0;
height: 100%; height: 100%;
width: 33%; width: 33%;
display: inline-flex; display: inline-flex;
@ -161,11 +161,11 @@
} }
#newsArticleTitle:hover, #newsArticleTitle:hover,
#newsArticleTitle:focus { #newsArticleTitle:focus {
text-shadow: 0px 0px 20px white; text-shadow: 0 0 20px white;
} }
#newsArticleTitle:active { #newsArticleTitle:active {
color: #c7c7c7; color: #c7c7c7;
text-shadow: 0px 0px 20px #c7c7c7; text-shadow: 0 0 20px #c7c7c7;
} }
/* News meta container. */ /* News meta container. */
@ -186,7 +186,7 @@
#newsArticleAuthor { #newsArticleAuthor {
display: inline-block; display: inline-block;
font-size: 10px; font-size: 10px;
padding: 0px 5px; padding: 0 5px;
font-weight: bold; font-weight: bold;
border-radius: 2px; border-radius: 2px;
} }
@ -226,7 +226,7 @@
#newsArticleContainer { #newsArticleContainer {
width: calc(100% - 25px); width: calc(100% - 25px);
height: 100%; height: 100%;
margin: 0px 0px 0px 25px; margin: 0 0 0 25px;
} }
/* Article content styles. */ /* Article content styles. */
@ -234,7 +234,7 @@
font-size: 12px; font-size: 12px;
overflow-y: scroll; overflow-y: scroll;
height: 100%; height: 100%;
padding: 0px 15px 0px 15px; padding: 0 15px 0 15px;
} }
#newsArticleContentScrollable img, #newsArticleContentScrollable img,
#newsArticleContentScrollable iframe { #newsArticleContentScrollable iframe {
@ -277,15 +277,15 @@
} }
.bbCodeSpoilerButton:hover, .bbCodeSpoilerButton:hover,
.bbCodeSpoilerButton:focus { .bbCodeSpoilerButton:focus {
text-shadow: 0px 0px 20px #ffffff, 0px 0px 20px #ffffff, 0px 0px 20px #ffffff; text-shadow: 0 0 20px #ffffff, 0 0 20px #ffffff, 0 0 20px #ffffff;
} }
.bbCodeSpoilerButton:active { .bbCodeSpoilerButton:active {
color: #c7c7c7; color: #c7c7c7;
text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
} }
.bbCodeSpoilerText { .bbCodeSpoilerText {
display: none; display: none;
padding: 15px 0px; padding: 15px 0;
border-bottom: 1px solid white; border-bottom: 1px solid white;
} }
@ -312,13 +312,13 @@
-webkit-user-select: none; -webkit-user-select: none;
position: absolute; position: absolute;
bottom: 15px; bottom: 15px;
right: 0px; right: 0;
} }
/* Navigation status span. */ /* Navigation status span. */
#newsNavigationStatus { #newsNavigationStatus {
font-size: 12px; font-size: 12px;
margin: 0px 15px; margin: 0 15px;
} }
/* Left and right navigation button styles. */ /* Left and right navigation button styles. */
@ -334,8 +334,8 @@
#newsNavigateLeft:focus #newsNavigationLeftSVG, #newsNavigateLeft:focus #newsNavigationLeftSVG,
#newsNavigateRight:hover #newsNavigationRightSVG, #newsNavigateRight:hover #newsNavigationRightSVG,
#newsNavigateRight:focus #newsNavigationRightSVG { #newsNavigateRight:focus #newsNavigationRightSVG {
filter: drop-shadow(0px 0px 2px #fff); filter: drop-shadow(0px 0 2px #fff);
-webkit-filter: drop-shadow(0px 0px 2px #fff); -webkit-filter: drop-shadow(0px 0 2px #fff);
} }
#newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine, #newsNavigateLeft:active #newsNavigationLeftSVG .arrowLine,
#newsNavigateRight:active #newsNavigationRightSVG .arrowLine { #newsNavigateRight:active #newsNavigationRightSVG .arrowLine {
@ -343,8 +343,8 @@
} }
#newsNavigateLeft:active #newsNavigationLeftSVG, #newsNavigateLeft:active #newsNavigationLeftSVG,
#newsNavigateRight:active #newsNavigationRightSVG { #newsNavigateRight:active #newsNavigationRightSVG {
filter: drop-shadow(0px 0px 2px #c7c7c7); filter: drop-shadow(0px 0 2px #c7c7c7);
-webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -webkit-filter: drop-shadow(0px 0 2px #c7c7c7);
} }
#newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine, #newsNavigateLeft:disabled #newsNavigationLeftSVG .arrowLine,
#newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine { #newsNavigateRight:disabled #newsNavigationRightSVG .arrowLine {
@ -397,11 +397,11 @@
} }
#newsErrorRetry:focus, #newsErrorRetry:focus,
#newsErrorRetry:hover { #newsErrorRetry:hover {
text-shadow: 0px 0px 20px white; text-shadow: 0 0 20px white;
} }
#newsErrorRetry:active { #newsErrorRetry:active {
color: #c7c7c7; color: #c7c7c7;
text-shadow: 0px 0px 20px #c7c7c7; text-shadow: 0 0 20px #c7c7c7;
} }
/******************************************************************************* /*******************************************************************************
@ -537,7 +537,7 @@
background: rgba(1, 2, 1, 0.5); background: rgba(1, 2, 1, 0.5);
height: 70px; height: 70px;
width: 70px; width: 70px;
box-shadow: 0px 0px 10px 0px rgb(0, 0, 0); box-shadow: 0 0 10px 0 rgb(0, 0, 0);
overflow: hidden; overflow: hidden;
position: relative; position: relative;
background-position: center; background-position: center;
@ -578,7 +578,7 @@
min-width: 135px; min-width: 135px;
font-weight: 900; font-weight: 900;
letter-spacing: 1px; letter-spacing: 1px;
text-shadow: 0px 0px 20px black; text-shadow: 0 0 20px black;
position: absolute; position: absolute;
right: 95px; right: 95px;
text-align: right; text-align: right;
@ -615,7 +615,7 @@
height: 1px; height: 1px;
width: 14px; width: 14px;
background: rgb(255, 255, 255); background: rgb(255, 255, 255);
margin: 10px 0px; margin: 10px 0;
} }
/* Social media icon shared styles. */ /* Social media icon shared styles. */
@ -642,7 +642,7 @@
.mediaButton { .mediaButton {
background: none; background: none;
border: none; border: none;
padding: 0px; padding: 0;
display: flex; display: flex;
align-items: center; align-items: center;
outline: none; outline: none;
@ -745,7 +745,7 @@
font-size: 9px; font-size: 9px;
letter-spacing: 1px; letter-spacing: 1px;
font-weight: bold; font-weight: bold;
text-shadow: 0px 0px 0px #bebcbb; text-shadow: 0 0 0 #bebcbb;
} }
/* Divider used on the bottom of the landing view. */ /* Divider used on the bottom of the landing view. */
@ -772,7 +772,7 @@
color: #949494; color: #949494;
font-size: 8px; font-size: 8px;
font-weight: 900; font-weight: 900;
text-shadow: 0px 0px 20px #949494; text-shadow: 0 0 20px #949494;
margin-left: 10px; margin-left: 10px;
} }
@ -809,7 +809,7 @@
bottom: calc(100% + 15px); bottom: calc(100% + 15px);
transform: translateX(-50%); transform: translateX(-50%);
margin-left: 50%; margin-left: 50%;
box-shadow: 0px 0px 20px rgb(0, 0, 0); box-shadow: 0 0 20px rgb(0, 0, 0);
cursor: default; cursor: default;
} }
#mojangStatusTooltip:after { #mojangStatusTooltip:after {
@ -840,7 +840,7 @@
#mojangStatusNEContainer { #mojangStatusNEContainer {
display: flex; display: flex;
align-items: center; align-items: center;
margin: 10px 0px; margin: 10px 0;
} }
/* White bar which surrounds the non essential service title. */ /* White bar which surrounds the non essential service title. */
@ -853,7 +853,7 @@
/* Non essential service title text. */ /* Non essential service title text. */
#mojangStatusNETitle { #mojangStatusNETitle {
font-size: 10px; font-size: 10px;
padding: 0px 3px; padding: 0 3px;
text-align: center; text-align: center;
letter-spacing: 1px; letter-spacing: 1px;
} }
@ -869,7 +869,7 @@
font-size: 10px; font-size: 10px;
letter-spacing: 1px; letter-spacing: 1px;
line-height: 12px; line-height: 12px;
padding: 6px 0px; padding: 6px 0;
} }
/* Displays the status of the mojang service. */ /* Displays the status of the mojang service. */
@ -892,24 +892,24 @@
} }
#newsButton:hover #newsButtonText, #newsButton:hover #newsButtonText,
#newsButton:focus #newsButtonText { #newsButton:focus #newsButtonText {
text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; text-shadow: 0 0 20px #fff, 0 0 20px #fff;
} }
#newsButton:active { #newsButton:active {
color: #c7c7c7; color: #c7c7c7;
text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
} }
#newsButton:hover #newsButtonSVG, #newsButton:hover #newsButtonSVG,
#newsButton:focus #newsButtonSVG { #newsButton:focus #newsButtonSVG {
filter: drop-shadow(0px 0px 2px #fff); filter: drop-shadow(0px 0 2px #fff);
-webkit-filter: drop-shadow(0px 0px 2px #fff); -webkit-filter: drop-shadow(0px 0 2px #fff);
} }
#newsButton:active #newsButtonSVG .arrowLine { #newsButton:active #newsButtonSVG .arrowLine {
stroke: #c7c7c7; stroke: #c7c7c7;
} }
#newsButton:active #newsButtonSVG { #newsButton:active #newsButtonSVG {
filter: drop-shadow(0px 0px 2px #c7c7c7); filter: drop-shadow(0px 0 2px #c7c7c7);
-webkit-filter: drop-shadow(0px 0px 2px #c7c7c7); -webkit-filter: drop-shadow(0px 0 2px #c7c7c7);
} }
#newsButton:disabled #newsButtonSVG .arrowLine { #newsButton:disabled #newsButtonSVG .arrowLine {
stroke: rgba(255, 255, 255, 0.75); stroke: rgba(255, 255, 255, 0.75);
@ -938,7 +938,7 @@
color: white; color: white;
font-weight: 900; font-weight: 900;
letter-spacing: 2px; letter-spacing: 2px;
text-shadow: 0px 0px 0px #bebcbb; text-shadow: 0 0 0 #bebcbb;
font-size: 11px; font-size: 11px;
line-height: 30px; line-height: 30px;
display: flex; display: flex;
@ -963,19 +963,19 @@
cursor: pointer; cursor: pointer;
font-weight: 900; font-weight: 900;
letter-spacing: 2px; letter-spacing: 2px;
text-shadow: 0px 0px 0px #bebcbb; text-shadow: 0 0 0 #bebcbb;
font-size: 20px; font-size: 20px;
padding: 0px; padding: 0;
transition: 0.25s ease; transition: 0.25s ease;
outline: none; outline: none;
} }
#launch_button:hover, #launch_button:hover,
#launch_button:focus { #launch_button:focus {
text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff; text-shadow: 0 0 20px #fff, 0 0 20px #fff;
} }
#launch_button:active { #launch_button:active {
color: #c7c7c7; color: #c7c7c7;
text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
} }
#launch_button:disabled { #launch_button:disabled {
color: #c7c7c7; color: #c7c7c7;
@ -999,7 +999,7 @@
#launch_progress_label { #launch_progress_label {
font-weight: 900; font-weight: 900;
letter-spacing: 1px; letter-spacing: 1px;
text-shadow: 0px 0px 0px #bebcbb; text-shadow: 0 0 0 #bebcbb;
font-size: 20px; font-size: 20px;
min-width: 53.21px; min-width: 53.21px;
max-width: 53.21px; max-width: 53.21px;
@ -1020,16 +1020,16 @@
outline: none; outline: none;
cursor: pointer; cursor: pointer;
line-height: 24px; line-height: 24px;
padding: 0px; padding: 0;
transition: 0.25s ease; transition: 0.25s ease;
} }
#server_selection_button:hover, #server_selection_button:hover,
#server_selection_button:focus { #server_selection_button:focus {
text-shadow: 0px 0px 20px #fff, 0px 0px 20px #fff, 0px 0px 20px #fff; text-shadow: 0 0 20px #fff, 0 0 20px #fff, 0 0 20px #fff;
} }
#server_selection_button:active { #server_selection_button:active {
color: #c7c7c7; color: #c7c7c7;
text-shadow: 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7, 0px 0px 20px #c7c7c7; text-shadow: 0 0 20px #c7c7c7, 0 0 20px #c7c7c7, 0 0 20px #c7c7c7;
} }
/* Progress bar styles. */ /* Progress bar styles. */

View File

@ -0,0 +1,12 @@
/* Overlay container, placed over the main div. */
#overlayContainer {
position: absolute;
z-index: 500;
top: 22px;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: calc(100% - 22px);
background: rgba(0, 0, 0, 0.50);
}

View File

@ -0,0 +1,79 @@
import * as React from 'react'
import GenericOverlay from './generic-overlay/GenericOverlay'
import ServerSelectOverlay from './server-select/ServerSelectOverlay'
import AccountSelectOverlay from './account-select/AccountSelectOverlay'
import { OverlayContent, PushGenericOverlayAction, PushServerSelectOverlayAction } from '../../redux/actions/overlayActions'
import { StoreType } from '../../redux/store'
import { OverlayPushAction } from '../../redux/actions/overlayActions'
import { connect } from 'react-redux'
import './Overlay.css'
interface OverlayProps {
overlayQueue: OverlayPushAction<unknown>[]
}
const mapState = (state: StoreType): Partial<OverlayProps> => {
return {
overlayQueue: state.overlayQueue
}
}
class Overlay extends React.Component<OverlayProps> {
private getGenericOverlay(action: PushGenericOverlayAction): JSX.Element {
return (
<>
<GenericOverlay
title={action.payload.title}
description={action.payload.description}
dismissible={action.payload.dismissible}
acknowledgeText={action.payload.acknowledgeText}
dismissText={action.payload.dismissText}
acknowledgeCallback={action.payload.acknowledgeCallback}
dismissCallback={action.payload.dismissCallback}
/>
</>
)
}
private getServerSelectOverlay(action: PushServerSelectOverlayAction): JSX.Element {
return (
<>
<ServerSelectOverlay
servers={action.payload.servers}
/>
</>
)
}
private getOverlayContent(): JSX.Element {
if(!this.props.overlayQueue || this.props.overlayQueue.length < 1) {
return (<></>)
}
const currentContent = this.props.overlayQueue[0]
switch(currentContent.overlayContent) {
case OverlayContent.GENERIC:
return this.getGenericOverlay(currentContent as PushGenericOverlayAction)
case OverlayContent.SERVER_SELECT:
return this.getServerSelectOverlay(currentContent as PushServerSelectOverlayAction)
case OverlayContent.ACCOUNT_SELECT:
return (
<>
<AccountSelectOverlay />
</>
)
}
}
render(): JSX.Element {
return <>
<div id="overlayContainer">
{this.getOverlayContent()}
</div>
</>
}
}
export default connect<unknown, unknown>(mapState)(Overlay)

View File

@ -0,0 +1,28 @@
import * as React from 'react'
import '../shared-select/SharedSelect.css'
export default class AccountSelectOverlay extends React.Component {
render(): JSX.Element {
return (
<>
<div id="accountSelectContent">
<span id="accountSelectHeader">Select an Account</span>
<div id="accountSelectList">
<div id="accountSelectListScrollable">
{/* Accounts populated here. */}
</div>
</div>
<div id="accountSelectActions">
<button id="accountSelectConfirm" className="overlayKeybindEnter" type="submit">Select</button>
<div id="accountSelectCancelWrapper">
<button id="accountSelectCancel" className="overlayKeybindEsc">Cancel</button>
</div>
</div>
</div>
</>
)
}
}

View File

@ -0,0 +1,104 @@
/* Main overlay content. */
#overlayContent {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
/*justify-content: space-between;*/
width: 300px;
/*height: 35%;*/
box-sizing: border-box;
padding: 15px 0px;
/* background-color: #424242; */
text-align: center;
}
/* Main overlay content anchor styles. */
#overlayContent a,
#overlayDismiss {
color: rgba(202, 202, 202, 0.75);
transition: 0.25s ease;
}
#overlayContent a:hover,
#overlayContent a:focus,
#overlayDismiss:focus {
color: rgba(255, 255, 255, 0.75);
}
#overlayContent a:active,
#overlayDismiss:active {
color: rgba(165, 165, 165, 0.75);
}
/* Add spacing between overlay content elements. */
#overlayContent > *:first-child {
margin-top: 0px !important;
}
#overlayContent > *:last-child {
margin-bottom: 0px !important;
}
#overlayContent > * {
margin: 8px 0px;
}
/* Overlay title styles. */
#overlayTitle {
font-family: 'Avenir Medium';
font-size: 20px;
font-weight: bold;
letter-spacing: 1px;
-webkit-user-select: initial;
}
/* Overlay description styles. */
#overlayDesc {
font-size: 12px;
font-weight: bold;
-webkit-user-select: initial;
}
/* Div which contains action buttons. */
#overlayActionContainer {
display: flex;
flex-direction: column;
justify-content: center;
}
/* Overlay acknowledge button styles. */
#overlayAcknowledge {
background: none;
border: 1px solid #ffffff;
color: white;
font-family: 'Avenir Medium';
font-weight: bold;
border-radius: 2px;
padding: 0px 8.1px;
cursor: pointer;
transition: 0.25s ease;
}
#overlayAcknowledge:hover,
#overlayAcknowledge:focus {
box-shadow: 0px 0px 10px 0px #fff;
outline: none;
}
#overlayAcknowledge:active {
border-color: rgba(255, 255, 255, 0.75);
color: rgba(255, 255, 255, 0.75);
}
/* Overlay dismiss option styles. */
#overlayDismiss {
font-weight: bold;
font-size: 10px;
text-decoration: none;
padding-top: 2.5px;
background: none;
border: none;
outline: none;
cursor: pointer;
}
#overlayDismiss:hover {
color: rgba(255, 255, 255, 0.75);
}
#overlayDismiss:active {
color: rgba(165, 165, 165, 0.75);
}

View File

@ -0,0 +1,67 @@
import * as React from 'react'
import { connect } from 'react-redux'
import { OverlayActionDispatch } from '../../../redux/actions/overlayActions'
import './GenericOverlay.css'
export interface GenericOverlayProps {
title: string
description: string
acknowledgeText?: string
dismissText?: string
dismissible: boolean
acknowledgeCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
dismissCallback?: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void
}
const mapDispatch = {
...OverlayActionDispatch
}
type InternalGenericOverlayProps = GenericOverlayProps & typeof mapDispatch
class GenericOverlay extends React.Component<InternalGenericOverlayProps> {
private getAcknowledgeText = (): string => {
return this.props.acknowledgeText || 'OK'
}
private getDismissText = (): string => {
return this.props.dismissText || 'Dismiss'
}
private onAcknowledgeClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
if(this.props.acknowledgeCallback) {
this.props.acknowledgeCallback(event)
}
this.props.popOverlayContent()
}
private onDismissClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>): void => {
if(this.props.dismissCallback) {
this.props.dismissCallback(event)
}
this.props.popOverlayContent()
}
render(): JSX.Element {
return <>
<div id="overlayContent">
<span id="overlayTitle">{this.props.title}</span>
<span id="overlayDesc">{this.props.description}</span>
<div id="overlayActionContainer">
<button onClick={this.onAcknowledgeClick} id="overlayAcknowledge" className="overlayKeybindEnter">{this.getAcknowledgeText()}</button>
<div id="overlayDismissWrapper">
{ this.props.dismissible
? <button onClick={this.onDismissClick} id="overlayDismiss" className="overlayKeybindEsc">{this.getDismissText()}</button>
: <></>
}
</div>
</div>
</div>
</>
}
}
export default connect<unknown, typeof mapDispatch>(undefined, mapDispatch)(GenericOverlay)

View File

@ -0,0 +1,33 @@
import * as React from 'react'
import '../shared-select/SharedSelect.css'
import { Server } from 'helios-distribution-types'
export interface ServerSelectOverlayProps {
servers: Server[]
}
export default class ServerSelectOverlay extends React.Component<ServerSelectOverlayProps> {
render(): JSX.Element {
return (
<>
<div id="serverSelectContent">
<span id="serverSelectHeader">Available Servers</span>
<div id="serverSelectList">
<div id="serverSelectListScrollable">
{/* Server listings populated here. */}
</div>
</div>
<div id="serverSelectActions">
<button id="serverSelectConfirm" className="overlayKeybindEnter" type="submit">Select</button>
<div id="serverSelectCancelWrapper">
<button id="serverSelectCancel" className="overlayKeybindEsc">Cancel</button>
</div>
</div>
</div>
</>
)
}
}

View File

@ -0,0 +1,284 @@
/* * *
* Overlay View (Server + Account Selection Content)
* * */
/* Server selection content container. */
#serverSelectContent,
#accountSelectContent {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 75%;
}
/* Server selection header. */
#serverSelectHeader,
#accountSelectHeader {
font-family: 'Avenir Medium';
font-size: 20px;
font-weight: bold;
color: #fff;
margin-bottom: 25px;
}
/* Wrapper div for the list of available servers. */
#serverSelectList,
#accountSelectList {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-height: 65%;
min-height: 40%;
}
/* Scrollable div which lists the available servers. */
#serverSelectListScrollable,
#accountSelectListScrollable {
padding: 0px 5px;
overflow-y: scroll;
}
#serverSelectListScrollable::-webkit-scrollbar,
#accountSelectListScrollable::-webkit-scrollbar {
width: 2px;
}
#serverSelectListScrollable::-webkit-scrollbar-track,
#accountSelectListScrollable::-webkit-scrollbar-track {
display: none;
}
#serverSelectListScrollable::-webkit-scrollbar-thumb,
#accountSelectListScrollable::-webkit-scrollbar-thumb {
border-radius: 10px;
box-shadow: inset 0 0 10px rgba(255, 255, 255, 0.50);
}
/* Content container for a server listing. */
.serverListing {
border: none;
padding: 0px;
width: 375px;
min-height: 60px;
display: flex;
justify-content: flex-start;
align-items: center;
opacity: 0.6;
transition: 0.25s ease;
cursor: pointer;
position: relative;
background: rgba(131, 131, 131, 0.25);
}
.serverListing[selected] {
cursor: default;
opacity: 1.0;
}
.serverListing:hover,
.serverListing:focus {
outline: none;
opacity: 1.0;
}
.accountListing {
color: white;
border: 1px solid rgba(126, 126, 126, 0.57);
border-radius: 3px;
padding: 5px 45px;
width: 250px;
display: flex;
justify-content: flex-start;
align-items: center;
opacity: 0.6;
transition: 0.25s ease;
cursor: pointer;
position: relative;
background: rgba(0, 0, 0, 0.25);
}
.accountListing[selected] {
cursor: default;
opacity: 1.0;
}
.accountListing:hover,
.accountListing:focus {
outline: none;
opacity: 1.0;
}
.accountListingName {
display: flex;
height: 100%;
width: 100%;
padding-left: 10px;
}
/* Add spacing between server listings. */
#serverSelectListScrollable > .serverListing:not(:first-child):not(:last-child),
#accountSelectListScrollable > .accountListing:not(:first-child):not(:last-child) {
margin: 5px 0px;
}
#serverSelectListScrollable > .serverListing:first-child,
#accountSelectListScrollable > .accountListing:first-child {
margin-bottom: 5px;
}
#serverSelectListScrollable > .serverListing:last-child,
#accountSelectListScrollable > .accountListing:last-child {
margin-top: 5px;
}
/* Server listing image. */
.serverListingImg {
margin: 0px 10px 0px 5px;
border: 1px solid #fff;
height: 50px;
width: 50px;
}
/* Content container for the server listing's details. */
.serverListingDetails {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: space-between;
height: 50px;
}
/* The name of the server listing. */
.serverListingName {
font-size: 14px;
font-weight: bold;
}
/* Description for the server listing. */
.serverListingDescription {
font-size: 10px;
line-height: 10px;
font-weight: bold;
}
/* Content container for the server listing's information. */
.serverListingInfo {
width: 100%;
display: flex;
justify-content: flex-start;
}
/* The minecraft version of the server listing. */
.serverListingVersion {
font-size: 10px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
line-height: 12px;
height: 12px;
border-radius: 2px;
background: rgba(31, 140, 11, 0.8);
padding: 0px 2px;
}
/* The revision version of the server's manifest. */
.serverListingRevision {
color: #969696;
font-size: 10px;
line-height: 12px;
padding: 0px 5px;
}
/* Star which indicates the default (main) server. */
.serverListingStarWrapper {
display: flex;
align-items: center;
cursor: pointer;
height: 12px;
position: relative;
}
/* Tooltip which displays when hovering over the star. */
.serverListingStarTooltip {
visibility: hidden;
opacity: 0;
width: 65px;
background-color: rgba(0, 0, 0, 0.40);
text-align: center;
border-radius: 4px;
position: absolute;
z-index: 1;
left: 130%;
font-size: 10px;
transition: visibility 0s linear 0.25s, opacity 0.25s ease;
}
.serverListingStarTooltip::after {
content: " ";
position: absolute;
top: 50%;
right: 100%; /* To the left of the tooltip */
margin-top: -5px;
border-width: 5px;
border-style: solid;
border-color: transparent rgba(0, 0, 0, 0.40) transparent transparent;
}
.serverListingStarWrapper:hover .serverListingStarTooltip {
visibility: visible;
opacity: 1;
transition-delay:0s;
}
/* Content container which contains the server select actions. */
#serverSelectActions,
#accountSelectActions {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
margin-top: 25px;
}
/* Server selection confirm button styles. */
#serverSelectConfirm,
#accountSelectConfirm {
background: none;
border: 1px solid #ffffff;
color: white;
font-family: 'Avenir Medium';
font-weight: bold;
border-radius: 2px;
padding: 0px 8.1px;
cursor: pointer;
transition: 0.25s ease;
min-height: 20.67px;
}
#serverSelectConfirm:hover,
#serverSelectConfirm:focus,
#accountSelectConfirm:hover,
#accountSelectConfirm:focus {
box-shadow: 0px 0px 10px 0px #fff;
outline: none;
}
#serverSelectConfirm:active,
#accountSelectConfirm:active {
border-color: rgba(255, 255, 255, 0.75);
color: rgba(255, 255, 255, 0.75);
}
/* Server selection cancel button styles. */
#serverSelectCancel,
#accountSelectCancel {
font-weight: bold;
font-size: 10px;
text-decoration: none;
padding-top: 2.5px;
color: rgba(202, 202, 202, 0.75);
transition: 0.25s ease;
background: none;
border: none;
outline: none;
cursor: pointer;
}
#serverSelectCancel:hover,
#serverSelectCancel:focus,
#accountSelectCancel:hover,
#accountSelectCancel:focus {
color: rgba(255, 255, 255, 0.75);
}
#serverSelectCancel:active,
#accountSelectCancel:active {
color: rgba(165, 165, 165, 0.75);
}

View File

@ -15,7 +15,10 @@ document.body.appendChild(mainElement)
ReactDOM.render( ReactDOM.render(
<AppContainer> <AppContainer>
<Provider store={store}> <Provider store={store}>
<Application currentView={store.getState().currentView} /> <Application
currentView={store.getState().currentView}
overlayQueue={store.getState().overlayQueue}
/>
</Provider> </Provider>
</AppContainer>, </AppContainer>,
mainElement mainElement

View File

@ -0,0 +1,66 @@
import { Action } from 'redux'
import { GenericOverlayProps } from 'src/renderer/components/overlay/generic-overlay/GenericOverlay'
import { ServerSelectOverlayProps } from 'src/renderer/components/overlay/server-select/ServerSelectOverlay'
export enum OverlayContent {
ACCOUNT_SELECT,
SERVER_SELECT,
GENERIC
}
export enum OverlayActionType {
PushContent = 'PUSH_CONTENT',
PopContent = 'POP_CONTENT'
}
// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface OverlayAction extends Action {}
export interface OverlayPushAction<T> extends OverlayAction {
overlayContent: OverlayContent
timestamp: number
showNext: boolean
payload: T
}
export interface PushGenericOverlayAction extends OverlayPushAction<GenericOverlayProps> {
overlayContent: OverlayContent.GENERIC
payload: GenericOverlayProps
}
export interface PushServerSelectOverlayAction extends OverlayPushAction<ServerSelectOverlayProps> {
overlayContent: OverlayContent.SERVER_SELECT
payload: ServerSelectOverlayProps
}
export function pushGenericOverlay(options: GenericOverlayProps, showNext = false): PushGenericOverlayAction {
return {
type: OverlayActionType.PushContent,
overlayContent: OverlayContent.GENERIC,
timestamp: Date.now(),
showNext,
payload: options
}
}
export function pushServerSelectOverlay(options: ServerSelectOverlayProps, showNext = false): PushServerSelectOverlayAction {
return {
type: OverlayActionType.PushContent,
overlayContent: OverlayContent.SERVER_SELECT,
timestamp: Date.now(),
showNext,
payload: options
}
}
export function popOverlayContent(): OverlayAction {
return {
type: OverlayActionType.PopContent
}
}
export const OverlayActionDispatch = {
pushGenericOverlay: (options: GenericOverlayProps, showNext = false): PushGenericOverlayAction => pushGenericOverlay(options, showNext),
pushServerSelectOverlay: (options: ServerSelectOverlayProps, showNext = false): PushServerSelectOverlayAction => pushServerSelectOverlay(options, showNext),
popOverlayContent: (): OverlayAction => popOverlayContent()
}

View File

@ -14,4 +14,8 @@ export function setCurrentView(view: View): ChangeViewAction {
type: ViewActionType.ChangeView, type: ViewActionType.ChangeView,
payload: view payload: view
} }
}
export const ViewActionDispatch = {
setView: (x: View): ChangeViewAction => setCurrentView(x)
} }

View File

@ -1,8 +1,10 @@
import { combineReducers } from 'redux' import { combineReducers } from 'redux'
import ViewReducer from './viewReducer' import ViewReducer from './viewReducer'
import AppReducer from './appReducer' import AppReducer from './appReducer'
import OverlayReducer from './overlayReducer'
export default combineReducers({ export default combineReducers({
currentView: ViewReducer, currentView: ViewReducer,
overlayQueue: OverlayReducer,
app: AppReducer app: AppReducer
}) })

View File

@ -0,0 +1,30 @@
import { Reducer } from 'redux'
import { OverlayAction, OverlayActionType, OverlayPushAction } from '../actions/overlayActions'
const defaultOverlayQueue: OverlayPushAction<unknown>[] = []
const OverlayReducer: Reducer<OverlayPushAction<unknown>[], OverlayAction> = (state = defaultOverlayQueue, action) => {
switch(action.type) {
case OverlayActionType.PushContent:
if((action as OverlayPushAction<unknown>).showNext && state.length > 0) {
return [
state[0],
action as OverlayPushAction<unknown>,
...state.slice(1)
]
} else {
return [
...state,
action as OverlayPushAction<unknown>
]
}
case OverlayActionType.PopContent:
return [
...state.slice(1)
]
}
return state
}
export default OverlayReducer

View File

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

View File

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import nock from 'nock' import nock from 'nock'
import { URL } from 'url' import { URL } from 'url'
import { MojangIndexProcessor } from 'common/asset/processor/MojangIndexProcessor' import { MojangIndexProcessor } from 'common/asset/processor/MojangIndexProcessor'

View File

@ -1,8 +1,8 @@
import { Mojang } from "common/mojang/mojang" import { Mojang } from 'common/mojang/mojang'
import { expect } from 'chai' import { expect } from 'chai'
import nock from 'nock' import nock from 'nock'
import { Session } from "common/mojang/model/auth/Session" import { Session } from 'common/mojang/model/auth/Session'
import { MojangResponseCode } from "common/mojang/model/internal/Response" import { MojangResponseCode } from 'common/mojang/model/internal/Response'
function expectMojangResponse(res: any, responseCode: MojangResponseCode, negate = false) { function expectMojangResponse(res: any, responseCode: MojangResponseCode, negate = false) {
expect(res).to.not.be.an('error') expect(res).to.not.be.an('error')
@ -29,7 +29,7 @@ describe('Mojang Errors', () => {
.get('/check') .get('/check')
.reply(500, 'Service temprarily offline.') .reply(500, 'Service temprarily offline.')
const res = await Mojang.status(); const res = await Mojang.status()
expectMojangResponse(res, MojangResponseCode.SUCCESS, true) expectMojangResponse(res, MojangResponseCode.SUCCESS, true)
expect(res.data).to.be.an('array') expect(res.data).to.be.an('array')
expect(res.data).to.deep.equal(defStatusHack) expect(res.data).to.deep.equal(defStatusHack)
@ -65,7 +65,7 @@ describe('Mojang Status', () => {
.get('/check') .get('/check')
.reply(200, defStatusHack) .reply(200, defStatusHack)
const res = await Mojang.status(); const res = await Mojang.status()
expectMojangResponse(res, MojangResponseCode.SUCCESS) expectMojangResponse(res, MojangResponseCode.SUCCESS)
expect(res.data).to.be.an('array') expect(res.data).to.be.an('array')
expect(res.data).to.deep.equal(defStatusHack) expect(res.data).to.deep.equal(defStatusHack)