Add new places; add music component; add audio module; add touch events; fixes
@@ -1,28 +1,26 @@
|
||||
import * as React 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 { delay, UI_MUSIC_VOLUME, UI_SOUND_VOLUME } from '../utils'
|
||||
import { delay, soundLoad, UI_SOUND_VOLUME } from '../utils'
|
||||
import { useSettings } from '../hooks'
|
||||
|
||||
import PanelOpenAudio from '../assets/audio/sound/panel-open.ogg'
|
||||
import PanelCloseAudio from '../assets/audio/sound/panel-close.ogg'
|
||||
import PanelOpenAudio from '../assets/audio/panel-open.ogg'
|
||||
import PanelCloseAudio from '../assets/audio/panel-close.ogg'
|
||||
|
||||
import SettingsOpenAudio from '../assets/audio/sound/menu-open.ogg'
|
||||
import SettingsCloseAudio from '../assets/audio/sound/menu-close.ogg'
|
||||
import SettingsOpenAudio from '../assets/audio/menu-open.ogg'
|
||||
import SettingsCloseAudio from '../assets/audio/menu-close.ogg'
|
||||
|
||||
import CheckBoxOnAudio from '../assets/audio/sound/check-box-on.ogg'
|
||||
import CheckBoxOffAudio from '../assets/audio/sound/check-box-off.ogg'
|
||||
import CheckBoxOnAudio from '../assets/audio/check-box-on.ogg'
|
||||
import CheckBoxOffAudio from '../assets/audio/check-box-off.ogg'
|
||||
|
||||
import StormwindParkMusic1 from '../assets/audio/music/stormwind-park-music-1.mp3'
|
||||
import StormwindParkMusic2 from '../assets/audio/music/stormwind-park-music-2.mp3'
|
||||
import Sound from '../modules/sound'
|
||||
|
||||
import './style.scss'
|
||||
|
||||
export default function App() {
|
||||
const {settings} = useSettings()
|
||||
const {settings: {uiSound, musicVolume, language}} = useSettings()
|
||||
const [isSettingsShown, setSettingsShown] = useState(false)
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [isPlaying, setPlaying] = useState(false)
|
||||
@@ -31,12 +29,6 @@ export default function App() {
|
||||
const [activePlace, setActivePlace] = 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 panelCloseSound = useMemo(() => soundLoad(PanelCloseAudio, 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 checkboxOffSound = useMemo(() => soundLoad(CheckBoxOffAudio, UI_SOUND_VOLUME), [])
|
||||
|
||||
const StormwindMusic1 = useMemo(() => soundLoad(StormwindParkMusic1, UI_MUSIC_VOLUME), [])
|
||||
const StormwindMusic2 = useMemo(() => soundLoad(StormwindParkMusic2, UI_MUSIC_VOLUME), [])
|
||||
const [currentPlaying, setCurrentPlaying] = useState(StormwindMusic1)
|
||||
const [currentPlaying, setCurrentPlaying] = useState<Sound>()
|
||||
|
||||
const app = useRef<HTMLDivElement>(null)
|
||||
|
||||
@@ -60,20 +50,20 @@ export default function App() {
|
||||
setBottomPanelShown(!isBottomPanelShown)
|
||||
}, [isBottomPanelShown])
|
||||
|
||||
const handleLeftPreviewClick = useCallback((value: number) => {
|
||||
setActivePlace(value)
|
||||
}, [])
|
||||
|
||||
const handleBottomPreviewClick = (value: number) => {
|
||||
const delayedChange = useCallback((fn: (value: number) => void, value: number) => {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
setActiveView(value)
|
||||
fn(value)
|
||||
setLoading(true)
|
||||
delay().then(() => {
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
}, [isLoading])
|
||||
|
||||
const handleLeftPreviewClick = (value: number) => delayedChange(setActivePlace, value)
|
||||
|
||||
const handleBottomPreviewClick = (value: number) => delayedChange(setActiveView, value)
|
||||
|
||||
useEffect(() => {
|
||||
if (app && app.current) {
|
||||
@@ -82,40 +72,24 @@ export default function App() {
|
||||
}, [app])
|
||||
|
||||
useLayoutEffect(() => {
|
||||
document.title = settings.language['place.stormwind-park']
|
||||
}, [settings.language])
|
||||
document.title = language[`place.${places[activePlace].name}` as keyof typeof language]
|
||||
}, [activePlace, language])
|
||||
|
||||
useEffect(() => {
|
||||
StormwindMusic1.audio.onplay = () => {
|
||||
setCurrentPlaying(StormwindMusic1)
|
||||
setPlaying(true)
|
||||
if (!currentPlaying) {
|
||||
return
|
||||
}
|
||||
StormwindMusic2.audio.onplay = () => {
|
||||
setCurrentPlaying(StormwindMusic2)
|
||||
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])
|
||||
currentPlaying.setVolume(musicVolume)
|
||||
}, [currentPlaying, musicVolume])
|
||||
|
||||
const appClick = () => currentPlaying.playMusic()
|
||||
const appClick = () => currentPlaying && currentPlaying.playMusic()
|
||||
|
||||
const openCloseSettings = () => {
|
||||
setSettingsShown(!isSettingsShown)
|
||||
if (app && app.current) {
|
||||
app.current.focus()
|
||||
}
|
||||
if (!settings.uiSound) {
|
||||
if (!uiSound) {
|
||||
return
|
||||
}
|
||||
if (isSettingsShown) {
|
||||
@@ -136,6 +110,9 @@ export default function App() {
|
||||
openCloseSettings()
|
||||
break
|
||||
case 32:
|
||||
if (!currentPlaying) {
|
||||
return
|
||||
}
|
||||
if (isPlaying) {
|
||||
currentPlaying.pause()
|
||||
setPlaying(false)
|
||||
@@ -164,6 +141,7 @@ export default function App() {
|
||||
>
|
||||
{places.map((place, index) => (
|
||||
<PreviewComponent
|
||||
name={`place.${place.name}`}
|
||||
isLoading={isLoading}
|
||||
key={index}
|
||||
value={index}
|
||||
@@ -198,6 +176,11 @@ export default function App() {
|
||||
/>
|
||||
)
|
||||
}
|
||||
<MusicComponent
|
||||
music={places[activePlace].music}
|
||||
setPlaying={setPlaying}
|
||||
setCurrentPlaying={setCurrentPlaying}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@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;
|
||||
$previewWidth: 320px;
|
||||
$font: 'Arial Narrow';
|
||||
@@ -59,7 +60,7 @@ button {
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: $hoverColor;
|
||||
box-shadow: $hoverBox;
|
||||
}
|
||||
|
||||
&: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');
|
||||
$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');
|
||||
$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 view from './view'
|
||||
import music from './music'
|
||||
|
||||
export default {
|
||||
preview,
|
||||
view
|
||||
name: 'stormwind-park',
|
||||
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 {
|
||||
outline: none;
|
||||
box-shadow: inset $hoverColor;
|
||||
box-shadow: inset $hoverBox;
|
||||
}
|
||||
|
||||
&--checked {
|
||||
|
||||
@@ -2,5 +2,8 @@ export { ViewComponent } from './view-component/view-component'
|
||||
export { SelectComponent } from './select-component/select-component'
|
||||
export { PanelComponent } from './panel-component/panel-component'
|
||||
export { PreviewComponent } from './preview-component/preview-component'
|
||||
export { RangeComponent } from './range-component/range-component'
|
||||
export { SettingsComponent } from './settings-component/settings-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;
|
||||
$panelHeight: $previewHeight + 40px;
|
||||
|
||||
$panelBorderSize: 8px;
|
||||
|
||||
.panel {
|
||||
@@ -33,7 +32,7 @@ $panelBorderSize: 8px;
|
||||
border-top: $panelBorderSize double $fontColor;
|
||||
border-image: $borderTop 16 32 16 32;
|
||||
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;
|
||||
|
||||
.panel-content {
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
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 './panel-component.scss'
|
||||
@@ -28,19 +38,12 @@ export const PanelComponent = ({
|
||||
const {settings: {language, uiSound}} = useSettings()
|
||||
const [isDrag, setDrag] = useState(false)
|
||||
const [trackMouse, setTrackMouse] = useState(0)
|
||||
const [position, setPosition] = useState(0)
|
||||
const [lastPosition, setLastPosition] = useState(0)
|
||||
|
||||
const panel = useRef<HTMLInputElement>(null)
|
||||
let position = 0
|
||||
const isBottom = useMemo(() => orientation === 'bottom', [orientation])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isShown && panel.current) {
|
||||
panel.current.style.transform = 'unset'
|
||||
setLastPosition(0)
|
||||
}
|
||||
}, [isShown])
|
||||
|
||||
const handleClick = (event: MouseEvent) => {
|
||||
event.preventDefault()
|
||||
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) => {
|
||||
if (!isDrag) {
|
||||
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 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()
|
||||
}
|
||||
|
||||
@@ -69,39 +121,71 @@ export const PanelComponent = ({
|
||||
if (!panel.current) {
|
||||
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)`
|
||||
}
|
||||
|
||||
const handlePress = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
e.nativeEvent.stopImmediatePropagation()
|
||||
setTrackMouse(isBottom ? e.clientX : e.clientY)
|
||||
setDrag(true)
|
||||
}
|
||||
|
||||
const handleFree = (e: MouseEvent | FocusEvent) => {
|
||||
e.preventDefault()
|
||||
const handleTouchstart = (e: TouchEvent) => {
|
||||
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)
|
||||
setLastPosition(position)
|
||||
}
|
||||
|
||||
const handleScroll = (e: WheelEvent) => {
|
||||
const {deltaY} = e
|
||||
const value = deltaY > 0 ? 50 : -50
|
||||
position = value + lastPosition
|
||||
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 diff = (deltaY > 0 ? 80 : -80) + lastPosition
|
||||
if (Math.abs(diff) > overflow + 40 || diff < 0) {
|
||||
return
|
||||
}
|
||||
setPosition(diff)
|
||||
changePosition()
|
||||
setLastPosition(position)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseDown={handlePress}
|
||||
onMouseDown={handleMouseDown}
|
||||
onTouchStart={handleTouchstart}
|
||||
onTouchMove={handleTouchMove}
|
||||
onTouchEnd={handleFree}
|
||||
onMouseUp={handleFree}
|
||||
onMouseMove={handleDragScroll}
|
||||
onMouseLeave={handleFree}
|
||||
|
||||
@@ -3,8 +3,12 @@
|
||||
$previewBorderWidth: 10px;
|
||||
$plugSize: 90px;
|
||||
|
||||
$previewNameWidth: 250px;
|
||||
$previewNameHeight: 25px;
|
||||
|
||||
.preview {
|
||||
z-index: 4;
|
||||
position: relative;
|
||||
cursor: $cursorInteract, auto;
|
||||
min-width: $previewWidth;
|
||||
min-height: $previewHeight;
|
||||
@@ -19,6 +23,15 @@ $plugSize: 90px;
|
||||
opacity: 1;
|
||||
transition: opacity $transitionDuration $transitionType;
|
||||
|
||||
&-name {
|
||||
position: absolute;
|
||||
width: $previewNameWidth;
|
||||
height: $previewNameHeight;
|
||||
bottom: -$previewNameHeight + 5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
&--not-loaded {
|
||||
background-size: $plugSize;
|
||||
}
|
||||
@@ -28,6 +41,12 @@ $plugSize: 90px;
|
||||
}
|
||||
|
||||
&: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 { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { useSettings } from '../../hooks'
|
||||
import { Plug } from '../../assets'
|
||||
import './preview-component.scss'
|
||||
import { BorderedHeader } from '..'
|
||||
|
||||
interface Props {
|
||||
name?: string
|
||||
isLoading: boolean
|
||||
src: string
|
||||
value: number
|
||||
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 image = useMemo(() => {
|
||||
setLoaded(false)
|
||||
@@ -39,6 +43,7 @@ export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) =
|
||||
onContextMenu={handleClick}
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
margin: `${name ? '10px 5px' : '5px'}`,
|
||||
backgroundImage: `url(${isLoaded ? image.src : Plug})`
|
||||
}}
|
||||
className={
|
||||
@@ -48,7 +53,15 @@ export const PreviewComponent = ({src, value, handleChange, isLoading}: Props) =
|
||||
? '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;
|
||||
}
|
||||
|
||||
&:active {
|
||||
&:active:not(.select--opened) {
|
||||
.select-arrow {
|
||||
background-image: $selectArrowClick;
|
||||
}
|
||||
@@ -44,7 +44,7 @@ $dropDownOutset: $DropDownBorder / 2;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: inset $hoverColor;
|
||||
box-shadow: inset $hoverBox;
|
||||
}
|
||||
|
||||
&-arrow {
|
||||
@@ -54,6 +54,7 @@ $dropDownOutset: $DropDownBorder / 2;
|
||||
}
|
||||
|
||||
&-drop-down {
|
||||
z-index: 7;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -81,7 +82,7 @@ $dropDownOutset: $DropDownBorder / 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 17px;
|
||||
height: 20px;
|
||||
|
||||
&:hover {
|
||||
background: linear-gradient(90deg,
|
||||
@@ -91,6 +92,10 @@ $dropDownOutset: $DropDownBorder / 2;
|
||||
rgba(173, 154, 32, 0.1) 100%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
background: linear-gradient(90deg,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as React from 'react'
|
||||
import { FocusEvent, KeyboardEvent, MouseEvent, useRef, useState } from 'react'
|
||||
import { FocusEvent, KeyboardEvent, useRef, useState } from 'react'
|
||||
|
||||
import './select-component.scss'
|
||||
|
||||
@@ -14,12 +14,7 @@ export const SelectComponent = ({children, options, current, handleChange}: Prop
|
||||
const [isSelectShown, setSelectShown] = useState(false)
|
||||
const dropDownRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const handleSelectClick = (e: MouseEvent) => {
|
||||
if (dropDownRef && dropDownRef.current && dropDownRef.current.contains(e.target as Node)) {
|
||||
return
|
||||
}
|
||||
setSelectShown(!isSelectShown)
|
||||
}
|
||||
const handleSelectClick = () => setSelectShown(!isSelectShown)
|
||||
|
||||
const handleBlur = (e: FocusEvent) => {
|
||||
if (!dropDownRef || !dropDownRef.current) {
|
||||
@@ -36,9 +31,6 @@ export const SelectComponent = ({children, options, current, handleChange}: Prop
|
||||
if (e.keyCode !== 13 && e.keyCode !== 32) {
|
||||
return
|
||||
}
|
||||
if (dropDownRef && dropDownRef.current && dropDownRef.current.contains(e.target as Node)) {
|
||||
return
|
||||
}
|
||||
setSelectShown(!isSelectShown)
|
||||
}
|
||||
|
||||
@@ -57,7 +49,7 @@ export const SelectComponent = ({children, options, current, handleChange}: Prop
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleBlur}
|
||||
tabIndex={0}
|
||||
className='select'
|
||||
className={`select ${isSelectShown ? 'select--opened' : ''}`}
|
||||
>
|
||||
{children}
|
||||
<div className='select-arrow'/>
|
||||
|
||||
@@ -29,23 +29,11 @@ $settingsHeaderHeight: 25px;
|
||||
&-header {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-bottom: 2px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
color: $fontColor;
|
||||
top: -$settingsHeaderHeight;
|
||||
width: $settingsHeaderWidth;
|
||||
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 {
|
||||
|
||||
@@ -4,12 +4,10 @@ import ru from '../../locales/ru.json'
|
||||
import en from '../../locales/en.json'
|
||||
import { useSettings } from '../../hooks'
|
||||
import { Settings } from '../../settings-context'
|
||||
import { CheckBoxComponent, SelectComponent } from '..'
|
||||
import { BorderedHeader, CheckBoxComponent, RangeComponent, SelectComponent } from '..'
|
||||
|
||||
import './settings-component.scss'
|
||||
|
||||
type languageValue = keyof typeof ru;
|
||||
|
||||
interface Props {
|
||||
closeSettings: () => void
|
||||
checkboxOnSoundPlay: (volume?: number) => void
|
||||
@@ -46,29 +44,51 @@ export const SettingsComponent = ({closeSettings, checkboxOnSoundPlay, checkboxO
|
||||
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 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 (
|
||||
<div className='settings-option'>
|
||||
<div className='settings-option-name'>
|
||||
{language[valueName]}
|
||||
</div>
|
||||
{typeof value !== 'boolean' ? (
|
||||
<SelectComponent
|
||||
handleChange={handleChangeLanguage}
|
||||
current={language['ui.language']}
|
||||
options={settings[option] as []}
|
||||
>
|
||||
{language['ui.language']}
|
||||
</SelectComponent>
|
||||
) : (
|
||||
<CheckBoxComponent
|
||||
handleClick={handleCheckboxClick}
|
||||
optionName={option}
|
||||
value={value}
|
||||
/>
|
||||
)}
|
||||
{chooseOption(option)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -76,11 +96,14 @@ export const SettingsComponent = ({closeSettings, checkboxOnSoundPlay, checkboxO
|
||||
return (
|
||||
<div className='settings'>
|
||||
<div className='settings-header'>
|
||||
{settings.language['ui.main-menu']}
|
||||
<BorderedHeader>
|
||||
{settings.language['ui.main-menu']}
|
||||
</BorderedHeader>
|
||||
</div>
|
||||
<div className='settings-content'>
|
||||
{renderOption('uiSound', settings.uiSound)}
|
||||
{renderOption('uiLanguage', settings.uiLanguage)}
|
||||
{renderOption('uiLanguage')}
|
||||
{renderOption('musicVolume')}
|
||||
{renderOption('uiSound')}
|
||||
</div>
|
||||
<button
|
||||
className='settings-button'
|
||||
|
||||
@@ -46,6 +46,11 @@
|
||||
cursor: $cursorPointer, auto;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
box-shadow: $hoverBox;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export const ViewComponent = ({src}: Props) => {
|
||||
return () => {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}, [src, imageSrc])
|
||||
}, [src])
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -9,6 +9,7 @@ import en from './locales/en.json'
|
||||
|
||||
const defaultSettings: Settings = {
|
||||
language: ru,
|
||||
musicVolume: 1.0,
|
||||
currentLanguage: ru['ui.language'],
|
||||
uiLanguage: [ru['ui.language'], en['ui.language']],
|
||||
uiSound: true
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
{
|
||||
"ui.button.places": "Places",
|
||||
"ui.button.views": "View",
|
||||
"ui.button.views": "Views",
|
||||
"ui.button.close": "Close",
|
||||
"ui.main-menu": "Main Menu",
|
||||
"ui.uiSound": "UI Sound",
|
||||
"ui.uiLanguage": "Language",
|
||||
"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.uiLanguage": "Язык",
|
||||
"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 {
|
||||
public audio: HTMLAudioElement
|
||||
private volume: number
|
||||
|
||||
constructor(file: string, config?: Config) {
|
||||
const validateURI = (fileURI: string) => {
|
||||
if (fileURI) {
|
||||
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)
|
||||
constructor(file: string, volumeValue?: number) {
|
||||
const volume = this.validateVolume(volumeValue)
|
||||
this.audio = new Audio(file)
|
||||
this.audio.load()
|
||||
this.volume = volume
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import en from './locales/en.json'
|
||||
|
||||
export interface Settings {
|
||||
language: typeof ru | typeof en
|
||||
musicVolume: number
|
||||
currentLanguage: string
|
||||
uiLanguage: string[]
|
||||
uiSound: boolean
|
||||
@@ -24,6 +25,7 @@ interface Props {
|
||||
const defaultSettings: SettingsContextType = {
|
||||
settings: {
|
||||
language: ru,
|
||||
musicVolume: 1.0,
|
||||
currentLanguage: ru['ui.language'],
|
||||
uiLanguage: [ru['ui.language'], en['ui.language']],
|
||||
uiSound: true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LOADING_DURATION } from './constants'
|
||||
import Sound from '../modules/sound'
|
||||
|
||||
export {
|
||||
PREVIEW_WIDTH,
|
||||
@@ -8,4 +9,26 @@ export {
|
||||
LOADING_DURATION,
|
||||
ANIMATION_DURATION
|
||||
} from './constants'
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||