Add new places; add music component; add audio module; add touch events; fixes

This commit is contained in:
obergodmar
2020-06-29 18:55:33 +03:00
parent 0b52c73cab
commit dea845a76f
53 changed files with 570 additions and 188 deletions
+35 -52
View File
@@ -1,28 +1,26 @@
import * as React from 'react' import * as React from 'react'
import { KeyboardEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' import { KeyboardEvent, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'
import Sound from '../modules/sound'
import { PanelComponent, PreviewComponent, SettingsComponent, ViewComponent } from '../components' import { MusicComponent, PanelComponent, PreviewComponent, SettingsComponent, ViewComponent } from '../components'
import places from '../assets' import places from '../assets'
import { delay, UI_MUSIC_VOLUME, UI_SOUND_VOLUME } from '../utils' import { delay, soundLoad, UI_SOUND_VOLUME } from '../utils'
import { useSettings } from '../hooks' import { useSettings } from '../hooks'
import PanelOpenAudio from '../assets/audio/sound/panel-open.ogg' import PanelOpenAudio from '../assets/audio/panel-open.ogg'
import PanelCloseAudio from '../assets/audio/sound/panel-close.ogg' import PanelCloseAudio from '../assets/audio/panel-close.ogg'
import SettingsOpenAudio from '../assets/audio/sound/menu-open.ogg' import SettingsOpenAudio from '../assets/audio/menu-open.ogg'
import SettingsCloseAudio from '../assets/audio/sound/menu-close.ogg' import SettingsCloseAudio from '../assets/audio/menu-close.ogg'
import CheckBoxOnAudio from '../assets/audio/sound/check-box-on.ogg' import CheckBoxOnAudio from '../assets/audio/check-box-on.ogg'
import CheckBoxOffAudio from '../assets/audio/sound/check-box-off.ogg' import CheckBoxOffAudio from '../assets/audio/check-box-off.ogg'
import StormwindParkMusic1 from '../assets/audio/music/stormwind-park-music-1.mp3' import Sound from '../modules/sound'
import StormwindParkMusic2 from '../assets/audio/music/stormwind-park-music-2.mp3'
import './style.scss' import './style.scss'
export default function App() { export default function App() {
const {settings} = useSettings() const {settings: {uiSound, musicVolume, language}} = useSettings()
const [isSettingsShown, setSettingsShown] = useState(false) const [isSettingsShown, setSettingsShown] = useState(false)
const [isLoading, setLoading] = useState(false) const [isLoading, setLoading] = useState(false)
const [isPlaying, setPlaying] = useState(false) const [isPlaying, setPlaying] = useState(false)
@@ -31,12 +29,6 @@ export default function App() {
const [activePlace, setActivePlace] = useState(0) const [activePlace, setActivePlace] = useState(0)
const [activeView, setActiveView] = useState(0) const [activeView, setActiveView] = useState(0)
const soundLoad = (soundFile: string, soundVolume: number) => {
const sound = new Sound(soundFile)
sound.setVolume(soundVolume)
return sound
}
const panelOpenSound = useMemo(() => soundLoad(PanelOpenAudio, UI_SOUND_VOLUME), []) const panelOpenSound = useMemo(() => soundLoad(PanelOpenAudio, UI_SOUND_VOLUME), [])
const panelCloseSound = useMemo(() => soundLoad(PanelCloseAudio, UI_SOUND_VOLUME), []) const panelCloseSound = useMemo(() => soundLoad(PanelCloseAudio, UI_SOUND_VOLUME), [])
const settingsOpenSound = useMemo(() => soundLoad(SettingsOpenAudio, UI_SOUND_VOLUME), []) const settingsOpenSound = useMemo(() => soundLoad(SettingsOpenAudio, UI_SOUND_VOLUME), [])
@@ -44,9 +36,7 @@ export default function App() {
const checkboxOnSound = useMemo(() => soundLoad(CheckBoxOnAudio, UI_SOUND_VOLUME), []) const checkboxOnSound = useMemo(() => soundLoad(CheckBoxOnAudio, UI_SOUND_VOLUME), [])
const checkboxOffSound = useMemo(() => soundLoad(CheckBoxOffAudio, UI_SOUND_VOLUME), []) const checkboxOffSound = useMemo(() => soundLoad(CheckBoxOffAudio, UI_SOUND_VOLUME), [])
const StormwindMusic1 = useMemo(() => soundLoad(StormwindParkMusic1, UI_MUSIC_VOLUME), []) const [currentPlaying, setCurrentPlaying] = useState<Sound>()
const StormwindMusic2 = useMemo(() => soundLoad(StormwindParkMusic2, UI_MUSIC_VOLUME), [])
const [currentPlaying, setCurrentPlaying] = useState(StormwindMusic1)
const app = useRef<HTMLDivElement>(null) const app = useRef<HTMLDivElement>(null)
@@ -60,20 +50,20 @@ export default function App() {
setBottomPanelShown(!isBottomPanelShown) setBottomPanelShown(!isBottomPanelShown)
}, [isBottomPanelShown]) }, [isBottomPanelShown])
const handleLeftPreviewClick = useCallback((value: number) => { const delayedChange = useCallback((fn: (value: number) => void, value: number) => {
setActivePlace(value)
}, [])
const handleBottomPreviewClick = (value: number) => {
if (isLoading) { if (isLoading) {
return return
} }
setActiveView(value) fn(value)
setLoading(true) setLoading(true)
delay().then(() => { delay().then(() => {
setLoading(false) setLoading(false)
}) })
} }, [isLoading])
const handleLeftPreviewClick = (value: number) => delayedChange(setActivePlace, value)
const handleBottomPreviewClick = (value: number) => delayedChange(setActiveView, value)
useEffect(() => { useEffect(() => {
if (app && app.current) { if (app && app.current) {
@@ -82,40 +72,24 @@ export default function App() {
}, [app]) }, [app])
useLayoutEffect(() => { useLayoutEffect(() => {
document.title = settings.language['place.stormwind-park'] document.title = language[`place.${places[activePlace].name}` as keyof typeof language]
}, [settings.language]) }, [activePlace, language])
useEffect(() => { useEffect(() => {
StormwindMusic1.audio.onplay = () => { if (!currentPlaying) {
setCurrentPlaying(StormwindMusic1) return
setPlaying(true)
} }
StormwindMusic2.audio.onplay = () => { currentPlaying.setVolume(musicVolume)
setCurrentPlaying(StormwindMusic2) }, [currentPlaying, musicVolume])
setPlaying(true)
}
StormwindMusic1.audio.onended = () => {
StormwindMusic2.playMusic()
}
StormwindMusic2.audio.onended = () => {
StormwindMusic1.playMusic()
}
return () => {
StormwindMusic1.audio.onplay = null
StormwindMusic2.audio.onplay = null
StormwindMusic1.audio.onended = null
StormwindMusic2.audio.onended = null
}
}, [StormwindMusic1, StormwindMusic2])
const appClick = () => currentPlaying.playMusic() const appClick = () => currentPlaying && currentPlaying.playMusic()
const openCloseSettings = () => { const openCloseSettings = () => {
setSettingsShown(!isSettingsShown) setSettingsShown(!isSettingsShown)
if (app && app.current) { if (app && app.current) {
app.current.focus() app.current.focus()
} }
if (!settings.uiSound) { if (!uiSound) {
return return
} }
if (isSettingsShown) { if (isSettingsShown) {
@@ -136,6 +110,9 @@ export default function App() {
openCloseSettings() openCloseSettings()
break break
case 32: case 32:
if (!currentPlaying) {
return
}
if (isPlaying) { if (isPlaying) {
currentPlaying.pause() currentPlaying.pause()
setPlaying(false) setPlaying(false)
@@ -164,6 +141,7 @@ export default function App() {
> >
{places.map((place, index) => ( {places.map((place, index) => (
<PreviewComponent <PreviewComponent
name={`place.${place.name}`}
isLoading={isLoading} isLoading={isLoading}
key={index} key={index}
value={index} value={index}
@@ -198,6 +176,11 @@ export default function App() {
/> />
) )
} }
<MusicComponent
music={places[activePlace].music}
setPlaying={setPlaying}
setCurrentPlaying={setCurrentPlaying}
/>
</div> </div>
) )
} }
+3 -2
View File
@@ -1,6 +1,7 @@
@import "../assets/assets"; @import "../assets/assets";
$hoverColor: 0 0 4px 2px rgba(173, 154, 32, 0.75); $hoverColor: rgba(173, 154, 32, 0.75);
$hoverBox: 0 0 4px 2px $hoverColor;
$previewHeight: 180px; $previewHeight: 180px;
$previewWidth: 320px; $previewWidth: 320px;
$font: 'Arial Narrow'; $font: 'Arial Narrow';
@@ -59,7 +60,7 @@ button {
&:focus { &:focus {
outline: none; outline: none;
box-shadow: $hoverColor; box-shadow: $hoverBox;
} }
&:active { &:active {
+3
View File
@@ -22,3 +22,6 @@ $selectBorder: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAAAgCAYAAA
$selectArrow: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADFUlEQVR4nO2Xz0sUYRjHPzpjvFYKg//B3Iou65ILggdNiBY8JHTyEqyJIEXkIVDpICV08NKPg6IEgaeoTgmBPwhhZa1FiHCRYuwi0SFGhHDAXafDzszOvDvujBp18YGXYd7nfef7eZ/nnXeegVP7z1Z3SL8G6H9RxwDMWABCRQe6KwAi4LewguMj/BQ9gAXnGjBVunfFk5n+zECiNQFqUABLEhBBv3a+EYBsLkd7KkXh6yZjo+PTjjsUwpsLDAiVqaHBjD00mLGFii1UEWioBFqUP7u6aD989MAGpoABR8czfwqSiqLcGLx18z7A7MwsidaEQZ0AX/72D4IRaKgPRqBOPfAWlM3lTIoks6uLLC19YGx0/DHwCsi7g+QUQAmev5jlameXAZj7RUw/QDPb0gQlcHfm7CUPYPj2sD75dDLf3nEl+frlXFmtGJxdBfDt+xYA75eXTCBPUd69AqQwOuaM26r0vMMYvnune/LZE7JruZApYRGINt0B8EO4UaraYKtrudDX77gAGqBtrHR1V7rKe+Bix/yC468pGAmghCNplaulFT5mPceOdY50ZzNCtTRAsyo5NgH2fu8hVIHW1IRQBVYxuInjREAOeVj+3X739DR9raZFAWiANjchvJAL6eABmF/eZWvlgg6w50RA7ygsRIkD1Ef4TcDsG7HyEeOYX971mt5RyHPcCAj56HV2d9+IxcyESO5JR3GjCN5n7ll5wFAUpSzuLrEhHCAqAi6ACRj9EZHoC4rHehviAMSCOI74UQBqQrjixMz7kQAURfG3Kgj/yr28++fXlb8VQm0Mff5xjmJXxOgfsUzAPGrYTwrgQpiAFrZqyQ47uGIB6NSoYGIIeHXll41CPIDtnz/o7Unz5u18EsiX1FKy1oxSsVTziZ8+r+u919IUNjeqvgOyaZRLpqlUW8LuvZ62gaoSq6ohNcnf25O2U5cTNipTqLVLMvAVpam2xABAS0vNFFabVPH82jHJra9PUy7DFigGUxr2X6ADSdz8Re0SSfCQ8YYDYMjjwwA0qiuek1rsz/Op/XP7A/OiM7z5QXCQAAAAAElFTkSuQmCC'); $selectArrow: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAADFUlEQVR4nO2Xz0sUYRjHPzpjvFYKg//B3Iou65ILggdNiBY8JHTyEqyJIEXkIVDpICV08NKPg6IEgaeoTgmBPwhhZa1FiHCRYuwi0SFGhHDAXafDzszOvDvujBp18YGXYd7nfef7eZ/nnXeegVP7z1Z3SL8G6H9RxwDMWABCRQe6KwAi4LewguMj/BQ9gAXnGjBVunfFk5n+zECiNQFqUABLEhBBv3a+EYBsLkd7KkXh6yZjo+PTjjsUwpsLDAiVqaHBjD00mLGFii1UEWioBFqUP7u6aD989MAGpoABR8czfwqSiqLcGLx18z7A7MwsidaEQZ0AX/72D4IRaKgPRqBOPfAWlM3lTIoks6uLLC19YGx0/DHwCsi7g+QUQAmev5jlameXAZj7RUw/QDPb0gQlcHfm7CUPYPj2sD75dDLf3nEl+frlXFmtGJxdBfDt+xYA75eXTCBPUd69AqQwOuaM26r0vMMYvnune/LZE7JruZApYRGINt0B8EO4UaraYKtrudDX77gAGqBtrHR1V7rKe+Bix/yC468pGAmghCNplaulFT5mPceOdY50ZzNCtTRAsyo5NgH2fu8hVIHW1IRQBVYxuInjREAOeVj+3X739DR9raZFAWiANjchvJAL6eABmF/eZWvlgg6w50RA7ygsRIkD1Ef4TcDsG7HyEeOYX971mt5RyHPcCAj56HV2d9+IxcyESO5JR3GjCN5n7ll5wFAUpSzuLrEhHCAqAi6ACRj9EZHoC4rHehviAMSCOI74UQBqQrjixMz7kQAURfG3Kgj/yr28++fXlb8VQm0Mff5xjmJXxOgfsUzAPGrYTwrgQpiAFrZqyQ47uGIB6NSoYGIIeHXll41CPIDtnz/o7Unz5u18EsiX1FKy1oxSsVTziZ8+r+u919IUNjeqvgOyaZRLpqlUW8LuvZ62gaoSq6ohNcnf25O2U5cTNipTqLVLMvAVpam2xABAS0vNFFabVPH82jHJra9PUy7DFigGUxr2X6ADSdz8Re0SSfCQ8YYDYMjjwwA0qiuek1rsz/Op/XP7A/OiM7z5QXCQAAAAAElFTkSuQmCC');
$selectArrowClick: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACvElEQVR4nO2XTU8TURSGH+gEh1SS3uCGbWM0LmuVRsSVbqxxQXeSuBHS9Be4wbggkrBhbxtZkeDCvwALEoV00XSDMSAZEhJTTSTDBi39sC7mozO3d4YpYNzwJieTOffeOc+cuXPmDFzqP2sgwC+A5AXGMQAzGoBGEnh0ZoBWIMCafZTC+eUETxdmZ/J3bqdA0/0z6nX/ue4fF1eHAdjcKjNxL8OXrzu8mpsv2cNKCHctkEejWCjMdAqFmQ4a/Rt+29xa77xZeN0BikDejqNUOhaLLZ4ruAJAglgE0t6ggzJFfEDn7bvlrqPVp8nSYOLBQ1K3bvY+cBSuXWMPgBvJ6wA0G+2gjCnV/mPNPzg48Pk/bpWV8xVMPpmJREL5+gQpHo8DiMn7k2L1/eqp8wMBdo29CmDSct9fIR19oNJR8Akx/Ww6vfohHKIHoD3Y9D5L+e6TNoAXwrTNkHyiWq26jkF9JBpAiAQg1ku6LwN1LSGevPjujPf1uKIAeO9WAGKj4t+UmYxvnlM9I4OcBqBKedhcJ7hj/QPo3dIrADH33B+83WgiRkC/ArWfUC4fsl7qluP9ozEx+7IGQHyo5vqHAwB6CpFHJmAurPQO/G5A/QTGrlnnG5W2a3bwyBkIA3AuZMgQ9ZMuhFfzxSaEfHrPChAKERD8wgFciHqra0fH0P7VlILXHeO4Mda90rB6F5wG0AOxJGXCzoxBn7u/H4BAiKVzBof+KqH34sbSilv5IgXf/ryt9Pdk4NuPGrmpLFiNg0CzMS3z1n3HVMEFgD4UAyD3OMv+7k4Yn7soDxQz46lObiprdTQROp4wyz3NdjJ3Ux00imi9LZncFbtNaWY8lQcYHY1ShT2SuqLDI5NytVoCKsAaLX9TqvovSGKl36rt8i5Rt93BstYbNoAhr1cBCKJ/gKLqzG/Jpf65/gJs6UOU7v5t9gAAAABJRU5ErkJggg=='); $selectArrowClick: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACvElEQVR4nO2XTU8TURSGH+gEh1SS3uCGbWM0LmuVRsSVbqxxQXeSuBHS9Be4wbggkrBhbxtZkeDCvwALEoV00XSDMSAZEhJTTSTDBi39sC7mozO3d4YpYNzwJieTOffeOc+cuXPmDFzqP2sgwC+A5AXGMQAzGoBGEnh0ZoBWIMCafZTC+eUETxdmZ/J3bqdA0/0z6nX/ue4fF1eHAdjcKjNxL8OXrzu8mpsv2cNKCHctkEejWCjMdAqFmQ4a/Rt+29xa77xZeN0BikDejqNUOhaLLZ4ruAJAglgE0t6ggzJFfEDn7bvlrqPVp8nSYOLBQ1K3bvY+cBSuXWMPgBvJ6wA0G+2gjCnV/mPNPzg48Pk/bpWV8xVMPpmJREL5+gQpHo8DiMn7k2L1/eqp8wMBdo29CmDSct9fIR19oNJR8Akx/Ww6vfohHKIHoD3Y9D5L+e6TNoAXwrTNkHyiWq26jkF9JBpAiAQg1ku6LwN1LSGevPjujPf1uKIAeO9WAGKj4t+UmYxvnlM9I4OcBqBKedhcJ7hj/QPo3dIrADH33B+83WgiRkC/ArWfUC4fsl7qluP9ozEx+7IGQHyo5vqHAwB6CpFHJmAurPQO/G5A/QTGrlnnG5W2a3bwyBkIA3AuZMgQ9ZMuhFfzxSaEfHrPChAKERD8wgFciHqra0fH0P7VlILXHeO4Mda90rB6F5wG0AOxJGXCzoxBn7u/H4BAiKVzBof+KqH34sbSilv5IgXf/ryt9Pdk4NuPGrmpLFiNg0CzMS3z1n3HVMEFgD4UAyD3OMv+7k4Yn7soDxQz46lObiprdTQROp4wyz3NdjJ3Ux00imi9LZncFbtNaWY8lQcYHY1ShT2SuqLDI5NytVoCKsAaLX9TqvovSGKl36rt8i5Rt93BstYbNoAhr1cBCKJ/gKLqzG/Jpf65/gJs6UOU7v5t9gAAAABJRU5ErkJggg==');
$radio: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpAAACsklEQVR4nO2XT0gUURzHPytb7MIWM+nBgwcnVnDAS9EhPRgadKibgeCCh/BgsGiEp0A6yEAULGYhtYcOBWugrDe9hCtZLHTZETYaQXE7BAqBO9DBOQy9DmO7rjOrY5tp0Rcewzx+v9/7/X+/F+D4IWrkD9TCXFfj4X89gl6baqvqGRVjxajJ2ycRLgeoraqIx+Olf7leZuntEp1XOtHGNPEvOqEEtU0V6dm0UFvV8oqWV3o2LRqbGv3VbFASBCWBU+P78Ygal1+5nihFU21TxfWububfZAB3uqtRpyy0Rxrx4TibXzarZ0JQEoP9EQb7zyDJKhPPDSaSRsV5exR1Y2gOgFBEBsB60HGgDbtltl9up6O9A1mWySxmyCxmPGk9e4BXmhtrRkCNqiI+HPdi2SWxbPzFaxtgG6zrPQBMJA1RReFKDM3BhxQA9ukwAKF72f2csBsl4xPjCQC0MQ2AzGLGdX7FLbBaKBwoffLJ5IE0P40f7I+wrvdw/sIsd26rfpR3wX7/4pf4EuMJRu6OoI1pjN4fpbur25Ou7AAbGuobEHX7lFUQsu+yyDtpWQ1FswjAtmXx8HEWghLmVx2C0uEtudSLbX3DMjd8syiKAsDG5gaFnaAWqgTXVQLmlnl4Jffg5YzNwnSIq70WYJObl0mmtmuW6xf5fJ5YX4yp11MADNwaIKfnPGldDlCiCtI5Sax8WqlsglVmAxdsM/BqRhIAC9Mh5EiYZGqbZMoC2/R3hT69UWqC2JbzfXbTFysQyH/MC4BYX4xwKExOz6Ev6+DRf1xGSg1Omha3nDQOfC+TtERbWF1b9TcQOVdgGdWNP6pReK9cTzrXptKsCKWpsbxxyunCxaLjEH1Z/92D0LG+BTyZlWZFAEhnI1DnVIlpmhQ+F45iCjx5DvjD+P8aPE78AL19BiaDra8aAAAAAElFTkSuQmCC'); $radio: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAAAQCAYAAACm53kpAAACsklEQVR4nO2XT0gUURzHPytb7MIWM+nBgwcnVnDAS9EhPRgadKibgeCCh/BgsGiEp0A6yEAULGYhtYcOBWugrDe9hCtZLHTZETYaQXE7BAqBO9DBOQy9DmO7rjOrY5tp0Rcewzx+v9/7/X+/F+D4IWrkD9TCXFfj4X89gl6baqvqGRVjxajJ2ycRLgeoraqIx+Olf7leZuntEp1XOtHGNPEvOqEEtU0V6dm0UFvV8oqWV3o2LRqbGv3VbFASBCWBU+P78Ygal1+5nihFU21TxfWububfZAB3uqtRpyy0Rxrx4TibXzarZ0JQEoP9EQb7zyDJKhPPDSaSRsV5exR1Y2gOgFBEBsB60HGgDbtltl9up6O9A1mWySxmyCxmPGk9e4BXmhtrRkCNqiI+HPdi2SWxbPzFaxtgG6zrPQBMJA1RReFKDM3BhxQA9ukwAKF72f2csBsl4xPjCQC0MQ2AzGLGdX7FLbBaKBwoffLJ5IE0P40f7I+wrvdw/sIsd26rfpR3wX7/4pf4EuMJRu6OoI1pjN4fpbur25Ou7AAbGuobEHX7lFUQsu+yyDtpWQ1FswjAtmXx8HEWghLmVx2C0uEtudSLbX3DMjd8syiKAsDG5gaFnaAWqgTXVQLmlnl4Jffg5YzNwnSIq70WYJObl0mmtmuW6xf5fJ5YX4yp11MADNwaIKfnPGldDlCiCtI5Sax8WqlsglVmAxdsM/BqRhIAC9Mh5EiYZGqbZMoC2/R3hT69UWqC2JbzfXbTFysQyH/MC4BYX4xwKExOz6Ev6+DRf1xGSg1Omha3nDQOfC+TtERbWF1b9TcQOVdgGdWNP6pReK9cTzrXptKsCKWpsbxxyunCxaLjEH1Z/92D0LG+BTyZlWZFAEhnI1DnVIlpmhQ+F45iCjx5DvjD+P8aPE78AL19BiaDra8aAAAAAElFTkSuQmCC');
$rangeBorder: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIoAAAAQCAYAAADJeudBAAAABmJLR0QAKAAqADbykokVAAABBUlEQVRo3u3asW2DQBiG4RfkghG49qp4hLjGA3BI3gNdEUWurYjCyh5I2APAHqQxc9CRBmySULhJxfdUCKh+vbqTThfw8AYkiDw0QAGwmUfic69Q5O78eZ4ei808kt3rjuyQdRqRVGVlgWQWC7XP/VCV1QDcgFpjWr0auFVlNfjcD0AdAhhjppWkA/aa0+rtgS47ZJ0xBoBw4QeRPy2Emoc8Q6HI86G0batJyKKpDa0ooq1H/i8UnaHIYgshQBzHuNRZwCoWGRuwLnU2jmMAAsYj/OP7MWm/Wi7Xi47wBZc6u33Zcvo4NUATjO9/xNL3vSa1YlEUMY8EKILZd10zkN/u1wy+AWM3S3inoVC2AAAAAElFTkSuQmCC');
$range: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAB+ElEQVR4nO2VP2gTURzHP6kOJ1nurR1vcwghOaHERRuuoygGOjQIFeUQddOhq5OtZBShXdO0EIl26ImQs+Io2kBSsVtwu3a6Gxo5muEc8g4zONy7kizmA8e993jv9/vy4/cHZsz437l0gbcCmJfrcNoCDOAWYMq1Lz9l5lI6twCz862zLkVY8nziAkTsfOPlhg3QarbsMRFiGgJE7DxGihBpBKjmgAGY7ic3zGQyxpK1RBAEVJYrLqMc6AOeikGVCAjA1PSSWFldsx49eUbh2lVCNBpNxwJdgG6SIgpJMYH1ldW1qPX+Y6Tppbaml9qgtxtNJ2o0nQj0OCkTo1wFlds3qd5/4QLW3Ts3rIXSglVdrrqqdlILGOfd3peLPAfgsuJ98cFxCINjCsU8AOHgBAjwfh0DwcQECGSj6XR6vH5Ts54+fi7DHlB7VbO6va6yc4BMwntGNpu1B4OBD7pRKObtBw/vARCeBXR7Xerb9bfAIeDKfyIS94HhcOgBPmjzJ95p6HmnRrGY5/D7V+rbdZdR/QvpPHEvSCog/Gs0NMqL1+0r2hzh7zPOh+fkcjnj6MeRC+wDP1GYjqpV4AP+weeDrfHDnd2dLUZdsE/KqaiCAdjAZnmxHAGbcp9qGiZNwn+JiKefzyjx+iltpSYuzYn1/hkzZkyFP182nWrkPhvCAAAAAElFTkSuQmCC');
$rangeBackground: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAFUlEQVR4nGNkYGDoYMADmPBJDh8FAJNoAJjpM54wAAAAAElFTkSuQmCC');
+10
View File
@@ -0,0 +1,10 @@
import preview from './preview'
import view from './view'
import music from './music'
export default {
name: 'halls-of-valor',
music,
view,
preview
}
+5
View File
@@ -0,0 +1,5 @@
import HallsOfValorMusic1 from './halls-of-valor-music-1.mp3'
export default [
HallsOfValorMusic1
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

@@ -0,0 +1,11 @@
import HallsOfValor1 from './halls-of-valor-1.jpg'
import HallsOfValor2 from './halls-of-valor-2.jpg'
import HallsOfValor3 from './halls-of-valor-3.jpg'
import HallsOfValor4 from './halls-of-valor-4.jpg'
export default [
HallsOfValor1,
HallsOfValor2,
HallsOfValor3,
HallsOfValor4
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 322 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 279 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

+11
View File
@@ -0,0 +1,11 @@
import HallsOfValor1 from './halls-of-valor-1.jpg'
import HallsOfValor2 from './halls-of-valor-2.jpg'
import HallsOfValor3 from './halls-of-valor-3.jpg'
import HallsOfValor4 from './halls-of-valor-4.jpg'
export default [
HallsOfValor1,
HallsOfValor2,
HallsOfValor3,
HallsOfValor4
]
+6 -4
View File
File diff suppressed because one or more lines are too long
+5 -2
View File
@@ -1,7 +1,10 @@
import preview from './preview' import preview from './preview'
import view from './view' import view from './view'
import music from './music'
export default { export default {
preview, name: 'stormwind-park',
view music,
view,
preview
} }
+7
View File
@@ -0,0 +1,7 @@
import StormwindMusic1 from './stormwind-park-music-1.mp3'
import StormwindMusic2 from './stormwind-park-music-2.mp3'
export default [
StormwindMusic1,
StormwindMusic2
]
Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 67 KiB

@@ -0,0 +1,25 @@
@import "../../app/style";
$borderWidth: 12px;
$borderOutset: 8px;
.header {
font-family: $font;
font-size: $fontSize;
text-shadow: $fontShadow;
letter-spacing: $fontSpacing;
color: $fontColor;
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-bottom: 2px;
border: $borderWidth double $fontColor;
border-image: $border 15 15 15 15 fill;
border-image-outset: $borderOutset;
border-image-repeat: stretch stretch;
background-image: $backgroundTexture;
background-repeat: repeat;
}
@@ -0,0 +1,15 @@
import * as React from 'react'
import './bordered-header.scss'
interface Props {
children: React.ReactNode
}
export const BorderedHeader = ({children}: Props) => (
<div className='header'>
{children}
</div>
)
BorderedHeader.displayName = 'BorderedHeader'
@@ -8,7 +8,7 @@
&:focus { &:focus {
outline: none; outline: none;
box-shadow: inset $hoverColor; box-shadow: inset $hoverBox;
} }
&--checked { &--checked {
+3
View File
@@ -2,5 +2,8 @@ export { ViewComponent } from './view-component/view-component'
export { SelectComponent } from './select-component/select-component' export { SelectComponent } from './select-component/select-component'
export { PanelComponent } from './panel-component/panel-component' export { PanelComponent } from './panel-component/panel-component'
export { PreviewComponent } from './preview-component/preview-component' export { PreviewComponent } from './preview-component/preview-component'
export { RangeComponent } from './range-component/range-component'
export { SettingsComponent } from './settings-component/settings-component' export { SettingsComponent } from './settings-component/settings-component'
export { CheckBoxComponent } from './checkbox-component/checkbox-component' export { CheckBoxComponent } from './checkbox-component/checkbox-component'
export { BorderedHeader } from './bordered-header/bordered-header'
export { MusicComponent } from './music-component/music-component'
@@ -0,0 +1,42 @@
import { useEffect, useMemo } from 'react'
import { randomNumber, soundLoad, UI_MUSIC_VOLUME } from '../../utils'
import Sound from '../../modules/sound'
interface Props {
music: string[]
setPlaying: (value: boolean) => void
setCurrentPlaying: (value: Sound | undefined) => void
}
export const MusicComponent = ({ music, setPlaying, setCurrentPlaying }: Props) => {
const musicArray = useMemo(() => (
music.map(sound => soundLoad(sound, UI_MUSIC_VOLUME))
), [music])
useEffect(() => {
musicArray.forEach(sound => {
sound.audio.onplay = () => {
setPlaying(true)
setCurrentPlaying(sound)
}
sound.audio.onended = () => {
musicArray[randomNumber(0, musicArray.length)].playMusic()
}
})
setCurrentPlaying(musicArray[randomNumber(0, musicArray.length)])
return () => {
musicArray.forEach(({ audio }) => {
audio.onplay = null
audio.onended = null
audio.pause()
audio.currentTime = 0
})
setCurrentPlaying(undefined)
}
}, [musicArray, setPlaying, setCurrentPlaying])
return null
}
MusicComponent.displayName = 'MusicComponent'
@@ -2,7 +2,6 @@
$panelWidth: $previewWidth + 40px; $panelWidth: $previewWidth + 40px;
$panelHeight: $previewHeight + 40px; $panelHeight: $previewHeight + 40px;
$panelBorderSize: 8px; $panelBorderSize: 8px;
.panel { .panel {
@@ -33,7 +32,7 @@ $panelBorderSize: 8px;
border-top: $panelBorderSize double $fontColor; border-top: $panelBorderSize double $fontColor;
border-image: $borderTop 16 32 16 32; border-image: $borderTop 16 32 16 32;
border-image-outset: $panelBorderSize - 2px 0 0 0; border-image-outset: $panelBorderSize - 2px 0 0 0;
border-image-width: $panelBorderSize*2 0 0 100%; border-image-width: $panelBorderSize * 2 0 0 100%;
border-image-repeat: round round; border-image-repeat: round round;
.panel-content { .panel-content {
@@ -1,7 +1,17 @@
import * as React from 'react' import * as React from 'react'
import { FocusEvent, MouseEvent, useEffect, useMemo, useRef, useState, WheelEvent } from 'react' import {
FocusEvent,
MouseEvent,
TouchEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
WheelEvent
} from 'react'
import { PREVIEW_HEIGHT, PREVIEW_WIDTH } from '../../utils' import { ANIMATION_DURATION, debounce, PREVIEW_HEIGHT, PREVIEW_WIDTH } from '../../utils'
import { useSettings } from '../../hooks' import { useSettings } from '../../hooks'
import './panel-component.scss' import './panel-component.scss'
@@ -28,19 +38,12 @@ export const PanelComponent = ({
const {settings: {language, uiSound}} = useSettings() const {settings: {language, uiSound}} = useSettings()
const [isDrag, setDrag] = useState(false) const [isDrag, setDrag] = useState(false)
const [trackMouse, setTrackMouse] = useState(0) const [trackMouse, setTrackMouse] = useState(0)
const [position, setPosition] = useState(0)
const [lastPosition, setLastPosition] = useState(0) const [lastPosition, setLastPosition] = useState(0)
const panel = useRef<HTMLInputElement>(null) const panel = useRef<HTMLInputElement>(null)
let position = 0
const isBottom = useMemo(() => orientation === 'bottom', [orientation]) const isBottom = useMemo(() => orientation === 'bottom', [orientation])
useEffect(() => {
if (!isShown && panel.current) {
panel.current.style.transform = 'unset'
setLastPosition(0)
}
}, [isShown])
const handleClick = (event: MouseEvent) => { const handleClick = (event: MouseEvent) => {
event.preventDefault() event.preventDefault()
setShown() setShown()
@@ -54,14 +57,63 @@ export const PanelComponent = ({
} }
} }
const resizePanel = useCallback((animate = true) => {
if (!panel.current) {
return
}
if (animate) {
panel.current.style.transition = `transform 0.5s`
}
panel.current.style.transform = `unset`
setTrackMouse(0)
setPosition(0)
setLastPosition(0)
}, [panel])
useEffect(() => {
let timeout: NodeJS.Timeout
const handleResize = debounce(() => {
resizePanel()
timeout = setTimeout(() => {
if (!panel || !panel.current) {
return
}
panel.current.style.transition = 'unset'
}, ANIMATION_DURATION)
}, 100)
window.addEventListener('resize', handleResize)
return () => {
if (timeout) {
clearTimeout(timeout)
}
window.removeEventListener('resize', handleResize)
}
}, [panel, resizePanel])
useEffect(() => {
if (!isShown) {
resizePanel(false)
}
}, [isShown, resizePanel])
const handleDragScroll = (e: MouseEvent) => { const handleDragScroll = (e: MouseEvent) => {
if (!isDrag) { if (!isDrag) {
return return
} }
const {innerWidth, innerHeight} = window
const windowSize = isBottom ? innerWidth : innerHeight
const containerSize = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 15)
if (!(containerSize > windowSize)) {
return
}
const overflow = Math.abs(containerSize - windowSize)
const {clientX, clientY} = e const {clientX, clientY} = e
const value = isBottom ? clientX : clientY const value = isBottom ? clientX : clientY
position = trackMouse - value + lastPosition const diff = trackMouse - value + lastPosition
if (Math.abs(diff) > overflow + 40 || diff < 0) {
return
}
setPosition(diff)
changePosition() changePosition()
} }
@@ -69,39 +121,71 @@ export const PanelComponent = ({
if (!panel.current) { if (!panel.current) {
return return
} }
const {innerHeight, innerWidth} = window
const overflowWindow = isBottom ? innerWidth : innerHeight
const overflowContainer = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 10)
const overflow = overflowContainer > overflowWindow ? overflowContainer : overflowWindow
if (Math.abs(position) > overflow) {
position = -position
}
panel.current.style.transform = `translate${isBottom ? 'X' : 'Y'}(${-position}px)` panel.current.style.transform = `translate${isBottom ? 'X' : 'Y'}(${-position}px)`
} }
const handlePress = (e: MouseEvent) => { const handleMouseDown = (e: MouseEvent) => {
e.preventDefault() e.nativeEvent.stopImmediatePropagation()
setTrackMouse(isBottom ? e.clientX : e.clientY) setTrackMouse(isBottom ? e.clientX : e.clientY)
setDrag(true) setDrag(true)
} }
const handleFree = (e: MouseEvent | FocusEvent) => { const handleTouchstart = (e: TouchEvent) => {
e.preventDefault() const {touches} = e
e.nativeEvent.stopImmediatePropagation()
setTrackMouse(isBottom ? touches[0].clientX : touches[0].clientY)
setDrag(true)
}
const handleTouchMove = (e: TouchEvent) => {
const {touches} = e
const {innerWidth, innerHeight} = window
const windowSize = isBottom ? innerWidth : innerHeight
const containerSize = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 15)
if (!(containerSize > windowSize)) {
return
}
const overflow = Math.abs(containerSize - windowSize)
const {clientX, clientY} = touches[0]
const value = isBottom ? clientX : clientY
const diff = trackMouse - value + lastPosition
if (Math.abs(diff) > overflow + 40 || diff < 0) {
return
}
setPosition(diff)
changePosition()
}
const handleFree = (e: MouseEvent | FocusEvent | TouchEvent) => {
e.nativeEvent.stopImmediatePropagation()
setDrag(false) setDrag(false)
setLastPosition(position) setLastPosition(position)
} }
const handleScroll = (e: WheelEvent) => { const handleScroll = (e: WheelEvent) => {
const {deltaY} = e const {deltaY} = e
const value = deltaY > 0 ? 50 : -50 const {innerWidth, innerHeight} = window
position = value + lastPosition const windowSize = isBottom ? innerWidth : innerHeight
const containerSize = itemsCount * ((isBottom ? PREVIEW_WIDTH : PREVIEW_HEIGHT) + 15)
if (!(containerSize > windowSize)) {
return
}
const overflow = Math.abs(containerSize - windowSize)
const diff = (deltaY > 0 ? 80 : -80) + lastPosition
if (Math.abs(diff) > overflow + 40 || diff < 0) {
return
}
setPosition(diff)
changePosition() changePosition()
setLastPosition(position) setLastPosition(position)
} }
return ( return (
<div <div
onMouseDown={handlePress} onMouseDown={handleMouseDown}
onTouchStart={handleTouchstart}
onTouchMove={handleTouchMove}
onTouchEnd={handleFree}
onMouseUp={handleFree} onMouseUp={handleFree}
onMouseMove={handleDragScroll} onMouseMove={handleDragScroll}
onMouseLeave={handleFree} onMouseLeave={handleFree}
@@ -3,8 +3,12 @@
$previewBorderWidth: 10px; $previewBorderWidth: 10px;
$plugSize: 90px; $plugSize: 90px;
$previewNameWidth: 250px;
$previewNameHeight: 25px;
.preview { .preview {
z-index: 4; z-index: 4;
position: relative;
cursor: $cursorInteract, auto; cursor: $cursorInteract, auto;
min-width: $previewWidth; min-width: $previewWidth;
min-height: $previewHeight; min-height: $previewHeight;
@@ -19,6 +23,15 @@ $plugSize: 90px;
opacity: 1; opacity: 1;
transition: opacity $transitionDuration $transitionType; transition: opacity $transitionDuration $transitionType;
&-name {
position: absolute;
width: $previewNameWidth;
height: $previewNameHeight;
bottom: -$previewNameHeight + 5px;
left: 50%;
transform: translateX(-50%);
}
&--not-loaded { &--not-loaded {
background-size: $plugSize; background-size: $plugSize;
} }
@@ -28,6 +41,12 @@ $plugSize: 90px;
} }
&:hover { &:hover {
box-shadow: $hoverColor; box-shadow: $hoverBox;
.preview-name {
box-shadow: -4px 4px 4px 2px $hoverColor,
4px 0 4px 2px $hoverColor;
}
} }
} }
@@ -1,17 +1,21 @@
import * as React from 'react' import * as React from 'react'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useSettings } from '../../hooks'
import { Plug } from '../../assets' import { Plug } from '../../assets'
import './preview-component.scss' import './preview-component.scss'
import { BorderedHeader } from '..'
interface Props { interface Props {
name?: string
isLoading: boolean isLoading: boolean
src: string src: string
value: number value: number
handleChange: (value: number) => void handleChange: (value: number) => void
} }
export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) => { export const PreviewComponent = ({name = '', src, value, handleChange, isLoading}: Props) => {
const {settings: {language}} = useSettings()
const [isLoaded, setLoaded] = useState(false) const [isLoaded, setLoaded] = useState(false)
const image = useMemo(() => { const image = useMemo(() => {
setLoaded(false) setLoaded(false)
@@ -39,6 +43,7 @@ export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) =
onContextMenu={handleClick} onContextMenu={handleClick}
onClick={handleClick} onClick={handleClick}
style={{ style={{
margin: `${name ? '10px 5px' : '5px'}`,
backgroundImage: `url(${isLoaded ? image.src : Plug})` backgroundImage: `url(${isLoaded ? image.src : Plug})`
}} }}
className={ className={
@@ -48,7 +53,15 @@ export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) =
? 'preview--loading' ? 'preview--loading'
: ''}` : ''}`
} }
/> >
{name && (
<div className='preview-name'>
<BorderedHeader>
{language[name as keyof typeof language]}
</BorderedHeader>
</div>
)}
</div>
) )
} }
@@ -0,0 +1,32 @@
@import "../../app/style";
$rangeWidth: 90px;
$rangeHeight: 3px;
.range {
position: relative;
height: $rangeHeight;
width: $rangeWidth;
border: 3px double $fontColor;
border-image: $rangeBorder 5 5 5 5;
background: $rangeBackground center repeat;
border-image-width: 4px;
border-image-outset: 2px 0;
border-image-repeat: round round;
border-radius: 6px;
&-stick {
position: absolute;
width: 32px;
height: 32px;
background-image: $range;
left: 0;
top: 50%;
transform: translate(0, -50%);
}
&:focus {
outline: none;
box-shadow: $hoverBox;
}
}
@@ -0,0 +1,111 @@
import * as React from 'react'
import { KeyboardEvent, useCallback, useEffect, useRef, useState, WheelEvent } from 'react'
import './range-component.scss'
interface Props {
handleChange: (value: number) => void
defaultValue: number
}
const MAX = 55
export const RangeComponent = ({handleChange, defaultValue}: Props) => {
const [isPressed, setPressed] = useState(false)
const [position, setPosition] = useState(defaultValue * MAX)
const stick = useRef<HTMLDivElement>(null)
const handleKeyDown = (e: KeyboardEvent) => {
if (!stick || !stick.current || !stick.current.parentNode) {
return
}
const {width} = (stick.current.parentNode as HTMLDivElement).getBoundingClientRect()
switch (e.keyCode) {
case 37:
if (position - 5 < 0) {
return
}
setPosition(position - 5)
handleChange(position / MAX)
break
case 39:
if (position + 5 > width - 35) {
return
}
setPosition(position + 5)
handleChange(position / MAX)
break
}
}
const handleMouseDown = useCallback(() => {
setPressed(true)
}, [])
const handleMouseUp = useCallback(() => {
setPressed(false)
}, [])
const handleMouseMove = useCallback((e: MouseEvent) => {
if (!isPressed) {
return
}
if (!stick || !stick.current || !stick.current.parentNode) {
return
}
const {width, left} = (stick.current.parentNode as HTMLDivElement).getBoundingClientRect()
const {clientX} = e
const diff = clientX - left
if (diff > width - 35 || diff < 0) {
return
}
setPosition(diff)
handleChange(diff / MAX)
}, [isPressed, stick, handleChange])
const handleScroll = (e: WheelEvent) => {
if (!stick || !stick.current || !stick.current.parentNode) {
return
}
const range = stick.current.parentNode as HTMLDivElement
const {width} = range.getBoundingClientRect()
range.focus()
const {deltaY} = e
const value = position + (deltaY > 0 ? -5 : 5)
if (value > width - 35 || value < 0) {
return
}
setPosition(value)
handleChange(value / MAX)
}
useEffect(() => {
window.addEventListener('mousemove', handleMouseMove)
window.addEventListener('mouseup', handleMouseUp)
return () => {
window.removeEventListener('mousemove', handleMouseMove)
window.removeEventListener('mouseup', handleMouseUp)
}
}, [handleMouseMove, handleMouseUp])
return (
<div
tabIndex={0}
onKeyDown={handleKeyDown}
onMouseDown={handleMouseDown}
onWheel={handleScroll}
className='range'
>
<div
ref={stick}
style={{
left: `${position}px`
}}
className='range-stick'
/>
</div>
)
}
RangeComponent.displayName = 'RangeComponent'
@@ -36,7 +36,7 @@ $dropDownOutset: $DropDownBorder / 2;
background-repeat: repeat; background-repeat: repeat;
} }
&:active { &:active:not(.select--opened) {
.select-arrow { .select-arrow {
background-image: $selectArrowClick; background-image: $selectArrowClick;
} }
@@ -44,7 +44,7 @@ $dropDownOutset: $DropDownBorder / 2;
&:focus { &:focus {
outline: none; outline: none;
box-shadow: inset $hoverColor; box-shadow: inset $hoverBox;
} }
&-arrow { &-arrow {
@@ -54,6 +54,7 @@ $dropDownOutset: $DropDownBorder / 2;
} }
&-drop-down { &-drop-down {
z-index: 7;
position: absolute; position: absolute;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -81,7 +82,7 @@ $dropDownOutset: $DropDownBorder / 2;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
line-height: 17px; height: 20px;
&:hover { &:hover {
background: linear-gradient(90deg, background: linear-gradient(90deg,
@@ -91,6 +92,10 @@ $dropDownOutset: $DropDownBorder / 2;
rgba(173, 154, 32, 0.1) 100%); rgba(173, 154, 32, 0.1) 100%);
} }
&:active {
align-items: flex-end;
}
&:focus { &:focus {
outline: none; outline: none;
background: linear-gradient(90deg, background: linear-gradient(90deg,
@@ -1,5 +1,5 @@
import * as React from 'react' import * as React from 'react'
import { FocusEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react' import { FocusEvent, KeyboardEvent, useRef, useState } from 'react'
import './select-component.scss' import './select-component.scss'
@@ -14,12 +14,7 @@ export const SelectComponent = ({children, options, current, handleChange}: Prop
const [isSelectShown, setSelectShown] = useState(false) const [isSelectShown, setSelectShown] = useState(false)
const dropDownRef = useRef<HTMLDivElement>(null) const dropDownRef = useRef<HTMLDivElement>(null)
const handleSelectClick = (e: MouseEvent) => { const handleSelectClick = () => setSelectShown(!isSelectShown)
if (dropDownRef && dropDownRef.current && dropDownRef.current.contains(e.target as Node)) {
return
}
setSelectShown(!isSelectShown)
}
const handleBlur = (e: FocusEvent) => { const handleBlur = (e: FocusEvent) => {
if (!dropDownRef || !dropDownRef.current) { if (!dropDownRef || !dropDownRef.current) {
@@ -36,9 +31,6 @@ export const SelectComponent = ({children, options, current, handleChange}: Prop
if (e.keyCode !== 13 && e.keyCode !== 32) { if (e.keyCode !== 13 && e.keyCode !== 32) {
return return
} }
if (dropDownRef && dropDownRef.current && dropDownRef.current.contains(e.target as Node)) {
return
}
setSelectShown(!isSelectShown) setSelectShown(!isSelectShown)
} }
@@ -57,7 +49,7 @@ export const SelectComponent = ({children, options, current, handleChange}: Prop
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
onBlur={handleBlur} onBlur={handleBlur}
tabIndex={0} tabIndex={0}
className='select' className={`select ${isSelectShown ? 'select--opened' : ''}`}
> >
{children} {children}
<div className='select-arrow'/> <div className='select-arrow'/>
@@ -29,23 +29,11 @@ $settingsHeaderHeight: 25px;
&-header { &-header {
z-index: 6; z-index: 6;
position: absolute; position: absolute;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding-bottom: 2px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
color: $fontColor;
top: -$settingsHeaderHeight; top: -$settingsHeaderHeight;
width: $settingsHeaderWidth; width: $settingsHeaderWidth;
height: $settingsHeaderHeight; height: $settingsHeaderHeight;
border: $settingsBorderWidth double $fontColor;
border-image: $border 15 15 15 15 fill;
border-image-outset: $settingsBorderOutset;
border-image-repeat: stretch stretch;
background-image: $backgroundTexture;
background-repeat: repeat;
} }
&-content { &-content {
@@ -4,12 +4,10 @@ import ru from '../../locales/ru.json'
import en from '../../locales/en.json' import en from '../../locales/en.json'
import { useSettings } from '../../hooks' import { useSettings } from '../../hooks'
import { Settings } from '../../settings-context' import { Settings } from '../../settings-context'
import { CheckBoxComponent, SelectComponent } from '..' import { BorderedHeader, CheckBoxComponent, RangeComponent, SelectComponent } from '..'
import './settings-component.scss' import './settings-component.scss'
type languageValue = keyof typeof ru;
interface Props { interface Props {
closeSettings: () => void closeSettings: () => void
checkboxOnSoundPlay: (volume?: number) => void checkboxOnSoundPlay: (volume?: number) => void
@@ -46,29 +44,51 @@ export const SettingsComponent = ({closeSettings, checkboxOnSoundPlay, checkboxO
checkboxOnSoundPlay() checkboxOnSoundPlay()
} }
const renderOption = (option: keyof Settings, value: boolean | string[] = []) => { const handleChangeRange = (value: number) => {
checkboxOnSoundPlay()
saveSettings!({...settings, musicVolume: value})
}
const chooseOption = (option: keyof Settings) => {
const {language} = settings const {language} = settings
const valueName = `ui.${option}` as languageValue switch (typeof settings[option]) {
case 'boolean':
return (
<CheckBoxComponent
handleClick={handleCheckboxClick}
optionName={option}
value={settings[option] as boolean}
/>
)
case 'object':
return (
<SelectComponent
handleChange={handleChangeLanguage}
current={language['ui.language']}
options={settings[option] as []}
>
{language['ui.language']}
</SelectComponent>
)
case 'number':
return (
<RangeComponent
defaultValue={settings[option] as number}
handleChange={handleChangeRange}
/>
)
}
}
const renderOption = (option: keyof Settings) => {
const {language} = settings
const valueName = `ui.${option}` as keyof typeof language
return ( return (
<div className='settings-option'> <div className='settings-option'>
<div className='settings-option-name'> <div className='settings-option-name'>
{language[valueName]} {language[valueName]}
</div> </div>
{typeof value !== 'boolean' ? ( {chooseOption(option)}
<SelectComponent
handleChange={handleChangeLanguage}
current={language['ui.language']}
options={settings[option] as []}
>
{language['ui.language']}
</SelectComponent>
) : (
<CheckBoxComponent
handleClick={handleCheckboxClick}
optionName={option}
value={value}
/>
)}
</div> </div>
) )
} }
@@ -76,11 +96,14 @@ export const SettingsComponent = ({closeSettings, checkboxOnSoundPlay, checkboxO
return ( return (
<div className='settings'> <div className='settings'>
<div className='settings-header'> <div className='settings-header'>
{settings.language['ui.main-menu']} <BorderedHeader>
{settings.language['ui.main-menu']}
</BorderedHeader>
</div> </div>
<div className='settings-content'> <div className='settings-content'>
{renderOption('uiSound', settings.uiSound)} {renderOption('uiLanguage')}
{renderOption('uiLanguage', settings.uiLanguage)} {renderOption('musicVolume')}
{renderOption('uiSound')}
</div> </div>
<button <button
className='settings-button' className='settings-button'
@@ -46,6 +46,11 @@
cursor: $cursorPointer, auto; cursor: $cursorPointer, auto;
color: inherit; color: inherit;
text-decoration: none; text-decoration: none;
&:focus {
outline: none;
box-shadow: $hoverBox;
}
} }
} }
} }
@@ -27,7 +27,7 @@ export const ViewComponent = ({src}: Props) => {
return () => { return () => {
clearTimeout(timer) clearTimeout(timer)
} }
}, [src, imageSrc]) }, [src])
return ( return (
<div <div
+1
View File
@@ -9,6 +9,7 @@ import en from './locales/en.json'
const defaultSettings: Settings = { const defaultSettings: Settings = {
language: ru, language: ru,
musicVolume: 1.0,
currentLanguage: ru['ui.language'], currentLanguage: ru['ui.language'],
uiLanguage: [ru['ui.language'], en['ui.language']], uiLanguage: [ru['ui.language'], en['ui.language']],
uiSound: true uiSound: true
+4 -2
View File
@@ -1,10 +1,12 @@
{ {
"ui.button.places": "Places", "ui.button.places": "Places",
"ui.button.views": "View", "ui.button.views": "Views",
"ui.button.close": "Close", "ui.button.close": "Close",
"ui.main-menu": "Main Menu", "ui.main-menu": "Main Menu",
"ui.uiSound": "UI Sound", "ui.uiSound": "UI Sound",
"ui.uiLanguage": "Language", "ui.uiLanguage": "Language",
"ui.language": "English", "ui.language": "English",
"place.stormwind-park": "Stormwind Park" "ui.musicVolume": "Music",
"place.stormwind-park": "Stormwind Park",
"place.halls-of-valor": "Halls Of Valor"
} }
+3 -1
View File
@@ -6,5 +6,7 @@
"ui.uiSound": "Звуки интерфейса", "ui.uiSound": "Звуки интерфейса",
"ui.uiLanguage": "Язык", "ui.uiLanguage": "Язык",
"ui.language": "Русский", "ui.language": "Русский",
"place.stormwind-park": "Парк Штормграда" "ui.musicVolume": "Музыка",
"place.stormwind-park": "Парк Штормграда",
"place.halls-of-valor": "Чертоги Доблести"
} }
+3 -43
View File
@@ -1,50 +1,10 @@
interface Config {
volume?: number
}
const name = 'UIAudio'
export default class Sound { export default class Sound {
public audio: HTMLAudioElement public audio: HTMLAudioElement
private volume: number private volume: number
constructor(file: string, config?: Config) { constructor(file: string, volumeValue?: number) {
const validateURI = (fileURI: string) => { const volume = this.validateVolume(volumeValue)
if (fileURI) { this.audio = new Audio(file)
return fileURI
} else {
throw Error('Requires valid URI path for "file"')
}
}
const volume = this.validateVolume(config && config.volume)
const appendAudioElement = (fileValue: string) => {
const hashFn = (str: string) => {
let hash = 0
if (str.length === 0) {
return hash
}
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i)
hash = (hash << 5) - hash + char
hash = hash & hash
}
return Math.abs(hash)
}
const id = `${name}-${hashFn(fileValue)}`
const audioElement = document.createElement('audio')
audioElement.id = id
audioElement.src = file
audioElement.preload = 'auto'
document.body.appendChild(audioElement)
return file
}
const audioNode = appendAudioElement(validateURI(file))
this.audio = new Audio(audioNode)
this.audio.load() this.audio.load()
this.volume = volume this.volume = volume
} }
+2
View File
@@ -6,6 +6,7 @@ import en from './locales/en.json'
export interface Settings { export interface Settings {
language: typeof ru | typeof en language: typeof ru | typeof en
musicVolume: number
currentLanguage: string currentLanguage: string
uiLanguage: string[] uiLanguage: string[]
uiSound: boolean uiSound: boolean
@@ -24,6 +25,7 @@ interface Props {
const defaultSettings: SettingsContextType = { const defaultSettings: SettingsContextType = {
settings: { settings: {
language: ru, language: ru,
musicVolume: 1.0,
currentLanguage: ru['ui.language'], currentLanguage: ru['ui.language'],
uiLanguage: [ru['ui.language'], en['ui.language']], uiLanguage: [ru['ui.language'], en['ui.language']],
uiSound: true uiSound: true
+23
View File
@@ -1,4 +1,5 @@
import { LOADING_DURATION } from './constants' import { LOADING_DURATION } from './constants'
import Sound from '../modules/sound'
export { export {
PREVIEW_WIDTH, PREVIEW_WIDTH,
@@ -8,4 +9,26 @@ export {
LOADING_DURATION, LOADING_DURATION,
ANIMATION_DURATION ANIMATION_DURATION
} from './constants' } from './constants'
export const delay = () => new Promise(resolve => setTimeout(resolve, LOADING_DURATION)) export const delay = () => new Promise(resolve => setTimeout(resolve, LOADING_DURATION))
export const soundLoad = (soundFile: string, soundVolume: number) => (
new Sound(soundFile, soundVolume)
)
export const randomNumber = (min: number, max: number) => (
Math.floor(Math.random() * (max - min)) + min
)
export const debounce = (fn: () => any, ms: number) => {
let timer: NodeJS.Timeout | null
return () => {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
timer = null
fn()
}, ms)
}
}