mirror of
https://github.com/dscalzi/HeliosLauncher.git
synced 2024-12-22 03:32:12 -08:00
Add loading UI, add view transition animations.
Framework is in place to run initial load on app startup. These routines will be called in Application's initLoad function. The loader will run for a min of 800ms to prevent it from looking odd on the UI. This can be reduced/changed later if this turns out to not be a concern. Added an app reducer to redux. The loading state was initially going to be there. On further inspection, it seemed better to have it as a state variable in the Application component. It remains in the store for now as an example. The pattern is valid and will be used for other proprties. Added animations for view transitions. On v1, this was done with jquery. Here, we are using react-transition-group. Got it working with a clever trick to store a workingView and use that for rendering. When the currentView is changed by the redux store, the fade out transition will start. Once the fade out completes, the workingView reference will be updated to the new view, triggering the fade in.
This commit is contained in:
parent
18fbfe4289
commit
d9394432d2
3
build.js
3
build.js
@ -57,7 +57,8 @@ builder.build({
|
||||
'!{dist,.gitignore,.vscode,docs,dev-app-update.yml,.travis.yml,.nvmrc,.eslintrc.json,build.js}'
|
||||
],
|
||||
extraResources: [
|
||||
'libraries'
|
||||
'libraries',
|
||||
'static'
|
||||
],
|
||||
asar: true
|
||||
}
|
||||
|
54
package-lock.json
generated
54
package-lock.json
generated
@ -1686,6 +1686,12 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash": {
|
||||
"version": "4.14.152",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.152.tgz",
|
||||
"integrity": "sha512-Vwf9YF2x1GE3WNeUMjT5bTHa2DqgUo87ocdgTScupY2JclZ5Nn7W2RLM/N0+oreexUk8uaVugR81NnTY/jNNXg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
|
||||
@ -1740,6 +1746,15 @@
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.0.tgz",
|
||||
"integrity": "sha512-/QfLHGpu+2fQOqQaXh8MG9q03bFENooTb/it4jr5kKaZlDQfWvjqWZg48AwzPVMBHlRuTRAY7hRHCEOXz5kV6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/request": {
|
||||
"version": "2.48.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.5.tgz",
|
||||
@ -4464,6 +4479,33 @@
|
||||
"utila": "~0.4"
|
||||
}
|
||||
},
|
||||
"dom-helpers": {
|
||||
"version": "5.1.4",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
|
||||
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^2.6.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.9.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
|
||||
"integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.13.4"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
|
||||
"integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz",
|
||||
@ -10831,6 +10873,18 @@
|
||||
"react-is": "^16.9.0"
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
|
||||
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.5.5",
|
||||
"dom-helpers": "^5.0.1",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"read-config-file": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-config-file/-/read-config-file-6.0.0.tgz",
|
||||
|
@ -37,6 +37,7 @@
|
||||
"github-syntax-dark": "^0.5.0",
|
||||
"got": "^11.1.4",
|
||||
"jquery": "^3.5.1",
|
||||
"lodash": "^4.17.15",
|
||||
"moment": "^2.26.0",
|
||||
"request": "^2.88.2",
|
||||
"semver": "^7.3.2",
|
||||
@ -53,11 +54,13 @@
|
||||
"@types/discord-rpc": "^3.0.4",
|
||||
"@types/fs-extra": "^9.0.1",
|
||||
"@types/jquery": "^3.3.38",
|
||||
"@types/lodash": "^4.14.152",
|
||||
"@types/mocha": "^7.0.2",
|
||||
"@types/node": "^12.12.42",
|
||||
"@types/react": "^16.9.35",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-redux": "^7.1.9",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/request": "^2.48.5",
|
||||
"@types/tar-fs": "^2.0.0",
|
||||
"@types/triple-beam": "^1.3.1",
|
||||
@ -79,6 +82,7 @@
|
||||
"react-dom": "^16.13.0",
|
||||
"react-hot-loader": "^4.12.21",
|
||||
"react-redux": "^7.2.0",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"redux": "^4.0.5",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-node": "^8.10.1",
|
||||
|
@ -2,3 +2,37 @@
|
||||
height: calc(100vh - 22px);
|
||||
background: linear-gradient(to top, rgba(0, 0, 0, 0.75) 0%, rgba(0, 0, 0, 0) 100%);
|
||||
}
|
||||
|
||||
.loader-enter {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
.loader-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
.loader-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
.loader-exit-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.9);
|
||||
transition: opacity 300ms, transform 300ms;
|
||||
}
|
||||
|
||||
.appWrapper-enter {
|
||||
opacity: 0;
|
||||
}
|
||||
.appWrapper-enter-active {
|
||||
opacity: 1;
|
||||
transition: opacity 500ms, transform 500ms;
|
||||
}
|
||||
.appWrapper-exit {
|
||||
opacity: 1;
|
||||
}
|
||||
.appWrapper-exit-active {
|
||||
opacity: 0;
|
||||
transition: opacity 500ms, transform 500ms;
|
||||
}
|
@ -6,18 +6,61 @@ import { connect } from 'react-redux'
|
||||
import { View } from '../meta/Views'
|
||||
import Landing from './landing/Landing'
|
||||
import Login from './login/Login'
|
||||
import Loader from './loader/Loader'
|
||||
import Settings from './settings/Settings'
|
||||
|
||||
import './Application.css'
|
||||
import { StoreType } from '../redux/store'
|
||||
import { CSSTransition } from 'react-transition-group'
|
||||
import { setCurrentView } from '../redux/actions/viewActions'
|
||||
import { throttle } from 'lodash'
|
||||
import { readdir } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
|
||||
type ApplicationProps = {
|
||||
declare const __static: string
|
||||
|
||||
function setBackground(id: number) {
|
||||
import(`../../../static/images/backgrounds/${id}.jpg`).then(mdl => {
|
||||
document.body.style.backgroundImage = `url('${mdl.default}')`
|
||||
})
|
||||
}
|
||||
|
||||
interface ApplicationProps {
|
||||
currentView: View
|
||||
}
|
||||
|
||||
class Application extends React.Component<ApplicationProps> {
|
||||
interface ApplicationState {
|
||||
loading: boolean
|
||||
showMain: boolean
|
||||
renderMain: boolean
|
||||
workingView: View
|
||||
}
|
||||
|
||||
const mapState = (state: StoreType) => {
|
||||
return {
|
||||
currentView: state.currentView
|
||||
}
|
||||
}
|
||||
const mapDispatch = {
|
||||
setView: (x: View) => setCurrentView(x)
|
||||
}
|
||||
|
||||
class Application extends React.Component<ApplicationProps & typeof mapDispatch, ApplicationState> {
|
||||
|
||||
private bkid!: number
|
||||
|
||||
constructor(props: ApplicationProps & typeof mapDispatch) {
|
||||
super(props)
|
||||
this.state = {
|
||||
loading: true,
|
||||
showMain: false,
|
||||
renderMain: false,
|
||||
workingView: props.currentView
|
||||
}
|
||||
}
|
||||
|
||||
getViewElement(): JSX.Element {
|
||||
switch(this.props.currentView) {
|
||||
switch(this.state.workingView) {
|
||||
case View.WELCOME:
|
||||
return <>
|
||||
<Welcome />
|
||||
@ -38,21 +81,86 @@ class Application extends React.Component<ApplicationProps> {
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
private updateWorkingView = throttle(() => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
workingView: this.props.currentView
|
||||
})
|
||||
|
||||
}, 200)
|
||||
|
||||
private showMain = (): void => {
|
||||
setBackground(this.bkid)
|
||||
this.setState({
|
||||
...this.state,
|
||||
showMain: true
|
||||
})
|
||||
}
|
||||
|
||||
private initLoad = async (): Promise<void> => {
|
||||
if(this.state.loading) {
|
||||
const MIN_LOAD = 800
|
||||
const start = Date.now()
|
||||
this.bkid = Math.floor((Math.random() * (await readdir(join(__static, 'images', 'backgrounds'))).length))
|
||||
const endLoad = () => {
|
||||
this.setState({
|
||||
...this.state,
|
||||
loading: false
|
||||
})
|
||||
// TODO temp
|
||||
setTimeout(() => {
|
||||
this.props.setView(View.WELCOME)
|
||||
}, 5000)
|
||||
}
|
||||
const diff = Date.now() - start
|
||||
if(diff < MIN_LOAD) {
|
||||
setTimeout(endLoad, MIN_LOAD-diff)
|
||||
} else {
|
||||
endLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<Frame />
|
||||
<CSSTransition
|
||||
in={this.state.showMain}
|
||||
appear={true}
|
||||
timeout={500}
|
||||
classNames="appWrapper"
|
||||
unmountOnExit
|
||||
>
|
||||
<div className="appWrapper">
|
||||
<CSSTransition
|
||||
in={this.props.currentView == this.state.workingView}
|
||||
appear={true}
|
||||
timeout={500}
|
||||
classNames="appWrapper"
|
||||
unmountOnExit
|
||||
onExited={this.updateWorkingView}
|
||||
>
|
||||
{this.getViewElement()}
|
||||
</CSSTransition>
|
||||
|
||||
</div>
|
||||
</CSSTransition>
|
||||
<CSSTransition
|
||||
in={this.state.loading}
|
||||
appear={true}
|
||||
timeout={300}
|
||||
classNames="loader"
|
||||
unmountOnExit
|
||||
onEnter={this.initLoad}
|
||||
onExited={this.showMain}
|
||||
>
|
||||
<Loader />
|
||||
</CSSTransition>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const connected = connect((state: any) => ({
|
||||
currentView: state.currentView
|
||||
}), undefined)(Application)
|
||||
|
||||
export default hot(connected)
|
||||
export default hot(connect<unknown, typeof mapDispatch>(mapState, mapDispatch)(Application))
|
62
src/renderer/components/loader/Loader.css
Normal file
62
src/renderer/components/loader/Loader.css
Normal file
@ -0,0 +1,62 @@
|
||||
/*******************************************************************************
|
||||
* *
|
||||
* Loading Element (app.ejs) *
|
||||
* *
|
||||
******************************************************************************/
|
||||
|
||||
/* Loading container, placed above everything. */
|
||||
#loadingContainer {
|
||||
position: absolute;
|
||||
z-index: 400;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: calc(100% - 22px);
|
||||
}
|
||||
|
||||
/* Loading content container. */
|
||||
#loadingContent {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Spinner container. */
|
||||
#loadSpinnerContainer {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Stationary image for the spinner. */
|
||||
#loadCenterImage {
|
||||
position: absolute;
|
||||
width: 277px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Rotating image for the spinner. */
|
||||
#loadSpinnerImage {
|
||||
width: 280px;
|
||||
height: auto;
|
||||
z-index: 400;
|
||||
}
|
||||
|
||||
/* Rotating animation for the spinner. */
|
||||
@keyframes rotating {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Class which is applied when the spinner image is spinning. */
|
||||
.rotating {
|
||||
animation: rotating 10s linear infinite;
|
||||
}
|
23
src/renderer/components/loader/Loader.tsx
Normal file
23
src/renderer/components/loader/Loader.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import './Loader.css'
|
||||
|
||||
import LoadingSeal from '../../../../static/images/LoadingSeal.png'
|
||||
import LoadingText from '../../../../static/images/LoadingText.png'
|
||||
|
||||
export default class Loader extends React.Component {
|
||||
|
||||
render(): JSX.Element {
|
||||
return <>
|
||||
<div id="loadingContainer">
|
||||
<div id="loadingContent">
|
||||
<div id="loadSpinnerContainer">
|
||||
<img id="loadCenterImage" src={LoadingSeal} />
|
||||
<img id="loadSpinnerImage" className="rotating" src={LoadingText} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
}
|
@ -19,7 +19,6 @@
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: filter 0.25s ease;
|
||||
background: rgba(0, 0, 0, 0.50);
|
||||
}
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: rgba(0, 0, 0, 0.50);
|
||||
}
|
||||
|
||||
#welcomeContent {
|
||||
|
@ -28,6 +28,8 @@
|
||||
/* TODO: Temp for development */
|
||||
body {
|
||||
/* background-image: url('../../static/images/backgrounds/3.jpg'); */
|
||||
transition: background-image 1s ease;
|
||||
background-image: url('data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEAYABgAAD//gA+Q1JFQVRPUjogZ2QtanBlZyB2MS4wICh1c2luZyBJSkcgSlBFRyB2ODApLCBkZWZhdWx0IHF1YWxpdHkK/9sAQwAIBgYHBgUIBwcHCQkICgwUDQwLCwwZEhMPFB0aHx4dGhwcICQuJyAiLCMcHCg3KSwwMTQ0NB8nOT04MjwuMzQy/9sAQwEJCQkMCwwYDQ0YMiEcITIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy/8AAEQgAPwBwAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/aAAwDAQACEQMRAD8A8VooopDCiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKACiiigAooooAKKKKAP/9k=');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
|
@ -6,19 +6,6 @@ import './index.css'
|
||||
|
||||
import Application from './components/Application'
|
||||
import { Provider } from 'react-redux'
|
||||
import { readdirSync } from 'fs-extra'
|
||||
import { join } from 'path'
|
||||
|
||||
declare const __static: string
|
||||
|
||||
function setBackground(id: number) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const bk = require('../../static/images/backgrounds/' + id + '.jpg')
|
||||
document.body.style.backgroundImage = `url('${bk.default}')`
|
||||
}
|
||||
|
||||
const id = Math.floor((Math.random() * readdirSync(join(__static, 'images', 'backgrounds')).length))
|
||||
setBackground(id)
|
||||
|
||||
// Create main element
|
||||
const mainElement = document.createElement('div')
|
||||
|
19
src/renderer/redux/actions/appActions.ts
Normal file
19
src/renderer/redux/actions/appActions.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { Action } from 'redux'
|
||||
|
||||
export enum AppActionType {
|
||||
ChangeLoadState = 'SET_LOADING'
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface AppAction extends Action {}
|
||||
|
||||
export interface ChangeLoadStateAction extends AppAction {
|
||||
payload: boolean
|
||||
}
|
||||
|
||||
export function setLoadingState(state: boolean): ChangeLoadStateAction {
|
||||
return {
|
||||
type: AppActionType.ChangeLoadState,
|
||||
payload: state
|
||||
}
|
||||
}
|
24
src/renderer/redux/reducers/appReducer.ts
Normal file
24
src/renderer/redux/reducers/appReducer.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { ChangeLoadStateAction, AppActionType, AppAction } from '../actions/appActions'
|
||||
import { Reducer } from 'redux'
|
||||
|
||||
export interface AppState {
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const defaultAppState: AppState = {
|
||||
loading: true
|
||||
}
|
||||
|
||||
// TODO remove loading from global state. Keeping as an example...
|
||||
const AppReducer: Reducer<AppState, AppAction> = (state = defaultAppState, action) => {
|
||||
switch(action.type) {
|
||||
case AppActionType.ChangeLoadState:
|
||||
return {
|
||||
...state,
|
||||
loading: (action as ChangeLoadStateAction).payload
|
||||
}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
export default AppReducer
|
@ -1,6 +1,8 @@
|
||||
import { combineReducers } from 'redux'
|
||||
import ViewReducer from './viewReducer'
|
||||
import AppReducer from './appReducer'
|
||||
|
||||
export default combineReducers({
|
||||
currentView: ViewReducer
|
||||
currentView: ViewReducer,
|
||||
app: AppReducer
|
||||
})
|
@ -1,4 +1,6 @@
|
||||
import { createStore } from 'redux'
|
||||
import reducer from './reducers'
|
||||
|
||||
export type StoreType = ReturnType<typeof reducer>
|
||||
|
||||
export default createStore(reducer)
|
Before Width: | Height: | Size: 244 KiB After Width: | Height: | Size: 244 KiB |
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
6
typings/redux-augment.d.ts
vendored
Normal file
6
typings/redux-augment.d.ts
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
import { StoreType } from '../src/renderer/redux/store'
|
||||
|
||||
declare module 'react-redux' {
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
export interface DefaultRootState extends StoreType {}
|
||||
}
|
16
typings/static-import.d.ts
vendored
Normal file
16
typings/static-import.d.ts
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
declare module '*.png' {
|
||||
const value: any
|
||||
export default value
|
||||
}
|
||||
|
||||
declare module '*.jpg' {
|
||||
const value: any
|
||||
export default value
|
||||
}
|
||||
|
||||
declare module '*.gif' {
|
||||
const value: any
|
||||
export default value
|
||||
}
|
Loading…
Reference in New Issue
Block a user