add custom background for skin viewer

This commit is contained in:
Pig Fang 2020-03-04 19:37:17 +08:00
parent a804bb8d7b
commit af24dcf9f9
14 changed files with 164 additions and 30 deletions

1
.gitignore vendored
View File

@ -27,3 +27,4 @@ storage/options.php
.phpunit.result.cache .phpunit.result.cache
.php_cs.cache .php_cs.cache
resources/views/overrides resources/views/overrides
public/bg

View File

@ -1,9 +1,11 @@
import React, { useState, useEffect, useRef } from 'react' import React, { useState, useEffect, useRef } from 'react'
import * as skinview3d from 'skinview3d' import * as skinview3d from 'skinview3d'
import { trans } from '../scripts/i18n' import { t } from '../scripts/i18n'
import styles from './Viewer.scss' import styles from './Viewer.scss'
import SkinSteve from '../../../misc/textures/steve.png' import SkinSteve from '../../../misc/textures/steve.png'
export const PICTURES_COUNT = 7
interface Props { interface Props {
skin?: string skin?: string
cape?: string cape?: string
@ -40,15 +42,19 @@ const Viewer: React.FC<Props> = props => {
const stuffRef = useRef(emptyStuff) const stuffRef = useRef(emptyStuff)
const [paused, setPaused] = useState(false) const [paused, setPaused] = useState(false)
const [running, setRunning] = useState(false)
const [reset, setReset] = useState(0) const [reset, setReset] = useState(0)
const [background, setBackground] = useState('#fff')
const [bgPicture, setBgPicture] = useState(0)
const indicator = (() => { const indicator = (() => {
const { skin, cape } = props const { skin, cape } = props
if (skin && cape) { if (skin && cape) {
return `${trans('general.skin')} & ${trans('general.cape')}` return `${t('general.skin')} & ${t('general.cape')}`
} else if (skin) { } else if (skin) {
return trans('general.skin') return t('general.skin')
} else if (cape) { } else if (cape) {
return trans('general.cape') return t('general.cape')
} }
return '' return ''
})() })()
@ -117,12 +123,19 @@ const Viewer: React.FC<Props> = props => {
viewer.playerObject.skin.slim = !!props.isAlex viewer.playerObject.skin.slim = !!props.isAlex
}, [props.isAlex]) }, [props.isAlex])
useEffect(() => {
if (bgPicture !== 0) {
setBackground(`url("${blessing.base_url}/bg/${bgPicture}.png")`)
}
}, [bgPicture])
const togglePause = () => { const togglePause = () => {
setPaused(paused => !paused) setPaused(paused => !paused)
viewRef.current.animationPaused = !viewRef.current.animationPaused viewRef.current.animationPaused = !viewRef.current.animationPaused
} }
const toggleRun = () => { const toggleRun = () => {
setRunning(running => !running)
const { handles } = stuffRef.current const { handles } = stuffRef.current
handles.run.paused = !handles.run.paused handles.run.paused = !handles.run.paused
handles.walk.paused = false handles.walk.paused = false
@ -137,52 +150,103 @@ const Viewer: React.FC<Props> = props => {
setReset(c => c + 1) setReset(c => c + 1)
} }
const setWhite = () => setBackground('#fff')
const setGray = () => setBackground('#6c757d')
const setBlack = () => setBackground('#000')
const setPrevPicture = () => {
if (bgPicture <= 1) {
setBgPicture(PICTURES_COUNT)
} else {
setBgPicture(bg => bg - 1)
}
}
const setNextPicture = () => {
if (bgPicture >= PICTURES_COUNT) {
setBgPicture(1)
} else {
setBgPicture(bg => bg + 1)
}
}
return ( return (
<div className="card"> <div className="card">
<div className="card-header"> <div className="card-header">
<div className="d-flex justify-content-between"> <div className="d-flex justify-content-between">
<h3 className="card-title"> <h3 className="card-title">
<span>{trans('general.texturePreview')}</span> <span>{t('general.texturePreview')}</span>
{props.showIndicator && ( {props.showIndicator && (
<span className="badge bg-olive ml-1">{indicator}</span> <span className="badge bg-olive ml-1">{indicator}</span>
)} )}
</h3> </h3>
<div className={styles.actions}> <div className={styles.actions}>
<i <i
className="fas fa-forward" className={`fas fa-${running ? 'walking' : 'running'}`}
data-toggle="tooltip" data-toggle="tooltip"
data-placement="bottom" data-placement="bottom"
title={`${trans('general.walk')} / ${trans('general.run')}`} title={`${t('general.walk')} / ${t('general.run')}`}
onClick={toggleRun} onClick={toggleRun}
></i> ></i>
<i <i
className="fas fa-redo-alt" className="fas fa-redo-alt"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="bottom" data-placement="bottom"
title={trans('general.rotation')} title={t('general.rotation')}
onClick={toggleRotate} onClick={toggleRotate}
></i> ></i>
<i <i
className={`fas fa-${paused ? 'play' : 'pause'}`} className={`fas fa-${paused ? 'play' : 'pause'}`}
data-toggle="tooltip" data-toggle="tooltip"
data-placement="bottom" data-placement="bottom"
title={trans('general.pause')} title={t('general.pause')}
onClick={togglePause} onClick={togglePause}
></i> ></i>
<i <i
className="fas fa-stop" className="fas fa-stop"
data-toggle="tooltip" data-toggle="tooltip"
data-placement="bottom" data-placement="bottom"
title={trans('general.reset')} title={t('general.reset')}
onClick={handleReset} onClick={handleReset}
></i> ></i>
</div> </div>
</div> </div>
</div> </div>
<div className="card-body"> <div className="card-body" style={{ background }}>
<div ref={containerRef} className={styles.viewer}></div> <div ref={containerRef} className={styles.viewer}></div>
</div> </div>
{props.children && <div className="card-footer">{props.children}</div>} <div className="card-footer">
<div className="mt-2 mb-3 d-flex">
<div
className="btn-color bg-white display-inline-block rounded-pill mr-2 mb-1 elevation-2"
title={t('colors.white')}
onClick={setWhite}
/>
<div
className="btn-color bg-black display-inline-block rounded-pill mr-2 mb-1 elevation-2"
title={t('colors.black')}
onClick={setBlack}
/>
<div
className="btn-color bg-gray display-inline-block rounded-pill mr-2 mb-1 elevation-2"
title={t('colors.gray')}
onClick={setGray}
/>
<div
className="btn-color bg-green display-inline-block rounded-pill mr-2 mb-1 elevation-2 text-center"
title={t('colors.prev')}
onClick={setPrevPicture}
>
<i className="fas fa-arrow-left"></i>
</div>
<div
className="btn-color bg-green display-inline-block rounded-pill mr-2 mb-1 elevation-2 text-center"
title={t('colors.next')}
onClick={setNextPicture}
>
<i className="fas fa-arrow-right"></i>
</div>
</div>
{props.children}
</div>
</div> </div>
) )
} }

View File

@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import { render, fireEvent } from '@testing-library/react' import { render, fireEvent } from '@testing-library/react'
import { trans } from '@/scripts/i18n' import { t } from '@/scripts/i18n'
import Viewer from '@/components/Viewer' import Viewer, { PICTURES_COUNT } from '@/components/Viewer'
test('custom footer', () => { test('custom footer', () => {
const { queryByText } = render(<Viewer>footer</Viewer>) const { queryByText } = render(<Viewer>footer</Viewer>)
@ -11,25 +11,25 @@ test('custom footer', () => {
describe('indicator', () => { describe('indicator', () => {
it('hidden by default', () => { it('hidden by default', () => {
const { queryByText } = render(<Viewer skin="skin" />) const { queryByText } = render(<Viewer skin="skin" />)
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument() expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
}) })
it('nothing', () => { it('nothing', () => {
const { queryByText } = render(<Viewer showIndicator />) const { queryByText } = render(<Viewer showIndicator />)
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument() expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
expect(queryByText(trans('general.cape'))).not.toBeInTheDocument() expect(queryByText(t('general.cape'))).not.toBeInTheDocument()
}) })
it('skin only', () => { it('skin only', () => {
const { queryByText } = render(<Viewer skin="skin" showIndicator />) const { queryByText } = render(<Viewer skin="skin" showIndicator />)
expect(queryByText(trans('general.skin'))).toBeInTheDocument() expect(queryByText(t('general.skin'))).toBeInTheDocument()
expect(queryByText(trans('general.cape'))).not.toBeInTheDocument() expect(queryByText(t('general.cape'))).not.toBeInTheDocument()
}) })
it('cape only', () => { it('cape only', () => {
const { queryByText } = render(<Viewer cape="cape" showIndicator />) const { queryByText } = render(<Viewer cape="cape" showIndicator />)
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument() expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
expect(queryByText(trans('general.cape'))).toBeInTheDocument() expect(queryByText(t('general.cape'))).toBeInTheDocument()
}) })
it('skin and cape', () => { it('skin and cape', () => {
@ -37,35 +37,94 @@ describe('indicator', () => {
<Viewer skin="skin" cape="cape" showIndicator />, <Viewer skin="skin" cape="cape" showIndicator />,
) )
expect( expect(
queryByText(`${trans('general.skin')} & ${trans('general.cape')}`), queryByText(`${t('general.skin')} & ${t('general.cape')}`),
).toBeInTheDocument() ).toBeInTheDocument()
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument() expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
expect(queryByText(trans('general.cape'))).not.toBeInTheDocument() expect(queryByText(t('general.cape'))).not.toBeInTheDocument()
}) })
}) })
describe('actions', () => { describe('actions', () => {
it('toggle run', () => { it('toggle run', () => {
const { getByTitle } = render(<Viewer />) const { getByTitle } = render(<Viewer />)
fireEvent.click( fireEvent.click(getByTitle(`${t('general.walk')} / ${t('general.run')}`))
getByTitle(`${trans('general.walk')} / ${trans('general.run')}`),
)
}) })
it('toggle rotation', () => { it('toggle rotation', () => {
const { getByTitle } = render(<Viewer />) const { getByTitle } = render(<Viewer />)
fireEvent.click(getByTitle(trans('general.rotation'))) fireEvent.click(getByTitle(t('general.rotation')))
}) })
it('toggle pause', () => { it('toggle pause', () => {
const { getByTitle } = render(<Viewer />) const { getByTitle } = render(<Viewer />)
const icon = getByTitle(trans('general.pause')) const icon = getByTitle(t('general.pause'))
fireEvent.click(icon) fireEvent.click(icon)
expect(icon).toHaveClass('fa-play') expect(icon).toHaveClass('fa-play')
}) })
it('reset', () => { it('reset', () => {
const { getByTitle } = render(<Viewer />) const { getByTitle } = render(<Viewer />)
fireEvent.click(getByTitle(trans('general.reset'))) fireEvent.click(getByTitle(t('general.reset')))
})
})
describe('background', () => {
it('white', () => {
const { getByTitle, baseElement } = render(<Viewer />)
fireEvent.click(getByTitle(t('colors.white')))
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe('rgb(255, 255, 255)')
})
it('black', () => {
const { getByTitle, baseElement } = render(<Viewer />)
fireEvent.click(getByTitle(t('colors.black')))
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe('rgb(0, 0, 0)')
})
it('white', () => {
const { getByTitle, baseElement } = render(<Viewer />)
fireEvent.click(getByTitle(t('colors.gray')))
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe('rgb(108, 117, 125)')
})
it('previous picture', () => {
const { getByTitle, baseElement } = render(<Viewer />)
fireEvent.click(getByTitle(t('colors.prev')))
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe(`url(/bg/${PICTURES_COUNT}.png)`)
fireEvent.click(getByTitle(t('colors.prev')))
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe(`url(/bg/${PICTURES_COUNT - 1}.png)`)
})
it('next picture', () => {
const { getByTitle, baseElement } = render(<Viewer />)
fireEvent.click(getByTitle(t('colors.next')))
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe('url(/bg/1.png)')
fireEvent.click(getByTitle(t('colors.next')))
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe('url(/bg/2.png)')
Array.from({ length: PICTURES_COUNT - 1 }).forEach(() => {
fireEvent.click(getByTitle(t('colors.next')))
})
expect(
baseElement.querySelector<HTMLDivElement>('.card-body')!.style.background,
).toBe('url(/bg/1.png)')
}) })
}) })

View File

@ -305,6 +305,13 @@ general:
previews: Texture Previews previews: Texture Previews
last-modified: Last Modified last-modified: Last Modified
colors:
black: Black
white: White
gray: Gray
prev: Previous Background
next: Next Background
vendor: vendor:
datatable: datatable:
search: Search search: Search

BIN
resources/misc/backgrounds/1.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

BIN
resources/misc/backgrounds/2.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 KiB

BIN
resources/misc/backgrounds/3.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 306 KiB

BIN
resources/misc/backgrounds/4.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

BIN
resources/misc/backgrounds/5.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

BIN
resources/misc/backgrounds/6.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

BIN
resources/misc/backgrounds/7.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

View File

@ -23,6 +23,7 @@
- Support searching players at players page. - Support searching players at players page.
- Added Blessing Skin Shell. - Added Blessing Skin Shell.
- Support specifying "from" email address and name when sending email. - Support specifying "from" email address and name when sending email.
- 3D skin viewer can be with background now.
## Tweaked ## Tweaked

View File

@ -23,6 +23,7 @@
- 角色页面可进行搜索 - 角色页面可进行搜索
- 新增 Blessing Skin Shell - 新增 Blessing Skin Shell
- 支持单独指定邮件发件人的地址和名称 - 支持单独指定邮件发件人的地址和名称
- 3D 皮肤预览现在是带背景的
## 调整 ## 调整

View File

@ -20,6 +20,7 @@ if ($Simple) {
# Copy static files # Copy static files
Copy-Item -Path ./resources/assets/src/images/bg.png -Destination ./public/app Copy-Item -Path ./resources/assets/src/images/bg.png -Destination ./public/app
Copy-Item -Path ./resources/assets/src/images/favicon.ico -Destination ./public/app Copy-Item -Path ./resources/assets/src/images/favicon.ico -Destination ./public/app
Copy-Item -Path ./resources/misc/backgrounds/ ./public/bg -Recurse
Write-Host 'Static files copied.' -ForegroundColor Green Write-Host 'Static files copied.' -ForegroundColor Green
# Write commit ID # Write commit ID