Add new places; add music component; add audio module; add touch events; fixes
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import HallsOfValorMusic1 from './halls-of-valor-music-1.mp3'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
HallsOfValorMusic1
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
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
|
||||||
|
]
|
||||||
|
After Width: | Height: | Size: 322 KiB |
|
After Width: | Height: | Size: 279 KiB |
|
After Width: | Height: | Size: 258 KiB |
|
After Width: | Height: | Size: 272 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
|
||||||
|
]
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import StormwindMusic1 from './stormwind-park-music-1.mp3'
|
||||||
|
import StormwindMusic2 from './stormwind-park-music-2.mp3'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
StormwindMusic1,
|
||||||
|
StormwindMusic2
|
||||||
|
]
|
||||||
|
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 {
|
||||||
|
|||||||
@@ -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,15 +44,24 @@ 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 (
|
return (
|
||||||
<div className='settings-option'>
|
|
||||||
<div className='settings-option-name'>
|
|
||||||
{language[valueName]}
|
|
||||||
</div>
|
|
||||||
{typeof value !== 'boolean' ? (
|
|
||||||
<SelectComponent
|
<SelectComponent
|
||||||
handleChange={handleChangeLanguage}
|
handleChange={handleChangeLanguage}
|
||||||
current={language['ui.language']}
|
current={language['ui.language']}
|
||||||
@@ -62,13 +69,26 @@ export const SettingsComponent = ({closeSettings, checkboxOnSoundPlay, checkboxO
|
|||||||
>
|
>
|
||||||
{language['ui.language']}
|
{language['ui.language']}
|
||||||
</SelectComponent>
|
</SelectComponent>
|
||||||
) : (
|
)
|
||||||
<CheckBoxComponent
|
case 'number':
|
||||||
handleClick={handleCheckboxClick}
|
return (
|
||||||
optionName={option}
|
<RangeComponent
|
||||||
value={value}
|
defaultValue={settings[option] as number}
|
||||||
|
handleChange={handleChangeRange}
|
||||||
/>
|
/>
|
||||||
)}
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderOption = (option: keyof Settings) => {
|
||||||
|
const {language} = settings
|
||||||
|
const valueName = `ui.${option}` as keyof typeof language
|
||||||
|
return (
|
||||||
|
<div className='settings-option'>
|
||||||
|
<div className='settings-option-name'>
|
||||||
|
{language[valueName]}
|
||||||
|
</div>
|
||||||
|
{chooseOption(option)}
|
||||||
</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'>
|
||||||
|
<BorderedHeader>
|
||||||
{settings.language['ui.main-menu']}
|
{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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": "Чертоги Доблести"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||