add custom background for skin viewer
1
.gitignore
vendored
@ -27,3 +27,4 @@ storage/options.php
|
||||
.phpunit.result.cache
|
||||
.php_cs.cache
|
||||
resources/views/overrides
|
||||
public/bg
|
||||
|
@ -1,9 +1,11 @@
|
||||
import React, { useState, useEffect, useRef } from 'react'
|
||||
import * as skinview3d from 'skinview3d'
|
||||
import { trans } from '../scripts/i18n'
|
||||
import { t } from '../scripts/i18n'
|
||||
import styles from './Viewer.scss'
|
||||
import SkinSteve from '../../../misc/textures/steve.png'
|
||||
|
||||
export const PICTURES_COUNT = 7
|
||||
|
||||
interface Props {
|
||||
skin?: string
|
||||
cape?: string
|
||||
@ -40,15 +42,19 @@ const Viewer: React.FC<Props> = props => {
|
||||
const stuffRef = useRef(emptyStuff)
|
||||
|
||||
const [paused, setPaused] = useState(false)
|
||||
const [running, setRunning] = useState(false)
|
||||
const [reset, setReset] = useState(0)
|
||||
const [background, setBackground] = useState('#fff')
|
||||
const [bgPicture, setBgPicture] = useState(0)
|
||||
|
||||
const indicator = (() => {
|
||||
const { skin, cape } = props
|
||||
if (skin && cape) {
|
||||
return `${trans('general.skin')} & ${trans('general.cape')}`
|
||||
return `${t('general.skin')} & ${t('general.cape')}`
|
||||
} else if (skin) {
|
||||
return trans('general.skin')
|
||||
return t('general.skin')
|
||||
} else if (cape) {
|
||||
return trans('general.cape')
|
||||
return t('general.cape')
|
||||
}
|
||||
return ''
|
||||
})()
|
||||
@ -117,12 +123,19 @@ const Viewer: React.FC<Props> = props => {
|
||||
viewer.playerObject.skin.slim = !!props.isAlex
|
||||
}, [props.isAlex])
|
||||
|
||||
useEffect(() => {
|
||||
if (bgPicture !== 0) {
|
||||
setBackground(`url("${blessing.base_url}/bg/${bgPicture}.png")`)
|
||||
}
|
||||
}, [bgPicture])
|
||||
|
||||
const togglePause = () => {
|
||||
setPaused(paused => !paused)
|
||||
viewRef.current.animationPaused = !viewRef.current.animationPaused
|
||||
}
|
||||
|
||||
const toggleRun = () => {
|
||||
setRunning(running => !running)
|
||||
const { handles } = stuffRef.current
|
||||
handles.run.paused = !handles.run.paused
|
||||
handles.walk.paused = false
|
||||
@ -137,52 +150,103 @@ const Viewer: React.FC<Props> = props => {
|
||||
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 (
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="d-flex justify-content-between">
|
||||
<h3 className="card-title">
|
||||
<span>{trans('general.texturePreview')}</span>
|
||||
<span>{t('general.texturePreview')}</span>
|
||||
{props.showIndicator && (
|
||||
<span className="badge bg-olive ml-1">{indicator}</span>
|
||||
)}
|
||||
</h3>
|
||||
<div className={styles.actions}>
|
||||
<i
|
||||
className="fas fa-forward"
|
||||
className={`fas fa-${running ? 'walking' : 'running'}`}
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title={`${trans('general.walk')} / ${trans('general.run')}`}
|
||||
title={`${t('general.walk')} / ${t('general.run')}`}
|
||||
onClick={toggleRun}
|
||||
></i>
|
||||
<i
|
||||
className="fas fa-redo-alt"
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title={trans('general.rotation')}
|
||||
title={t('general.rotation')}
|
||||
onClick={toggleRotate}
|
||||
></i>
|
||||
<i
|
||||
className={`fas fa-${paused ? 'play' : 'pause'}`}
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title={trans('general.pause')}
|
||||
title={t('general.pause')}
|
||||
onClick={togglePause}
|
||||
></i>
|
||||
<i
|
||||
className="fas fa-stop"
|
||||
data-toggle="tooltip"
|
||||
data-placement="bottom"
|
||||
title={trans('general.reset')}
|
||||
title={t('general.reset')}
|
||||
onClick={handleReset}
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="card-body" style={{ background }}>
|
||||
<div ref={containerRef} className={styles.viewer}></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>
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React from 'react'
|
||||
import { render, fireEvent } from '@testing-library/react'
|
||||
import { trans } from '@/scripts/i18n'
|
||||
import Viewer from '@/components/Viewer'
|
||||
import { t } from '@/scripts/i18n'
|
||||
import Viewer, { PICTURES_COUNT } from '@/components/Viewer'
|
||||
|
||||
test('custom footer', () => {
|
||||
const { queryByText } = render(<Viewer>footer</Viewer>)
|
||||
@ -11,25 +11,25 @@ test('custom footer', () => {
|
||||
describe('indicator', () => {
|
||||
it('hidden by default', () => {
|
||||
const { queryByText } = render(<Viewer skin="skin" />)
|
||||
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('nothing', () => {
|
||||
const { queryByText } = render(<Viewer showIndicator />)
|
||||
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument()
|
||||
expect(queryByText(trans('general.cape'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('general.cape'))).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('skin only', () => {
|
||||
const { queryByText } = render(<Viewer skin="skin" showIndicator />)
|
||||
expect(queryByText(trans('general.skin'))).toBeInTheDocument()
|
||||
expect(queryByText(trans('general.cape'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('general.skin'))).toBeInTheDocument()
|
||||
expect(queryByText(t('general.cape'))).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('cape only', () => {
|
||||
const { queryByText } = render(<Viewer cape="cape" showIndicator />)
|
||||
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument()
|
||||
expect(queryByText(trans('general.cape'))).toBeInTheDocument()
|
||||
expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('general.cape'))).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('skin and cape', () => {
|
||||
@ -37,35 +37,94 @@ describe('indicator', () => {
|
||||
<Viewer skin="skin" cape="cape" showIndicator />,
|
||||
)
|
||||
expect(
|
||||
queryByText(`${trans('general.skin')} & ${trans('general.cape')}`),
|
||||
queryByText(`${t('general.skin')} & ${t('general.cape')}`),
|
||||
).toBeInTheDocument()
|
||||
expect(queryByText(trans('general.skin'))).not.toBeInTheDocument()
|
||||
expect(queryByText(trans('general.cape'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('general.skin'))).not.toBeInTheDocument()
|
||||
expect(queryByText(t('general.cape'))).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe('actions', () => {
|
||||
it('toggle run', () => {
|
||||
const { getByTitle } = render(<Viewer />)
|
||||
fireEvent.click(
|
||||
getByTitle(`${trans('general.walk')} / ${trans('general.run')}`),
|
||||
)
|
||||
fireEvent.click(getByTitle(`${t('general.walk')} / ${t('general.run')}`))
|
||||
})
|
||||
|
||||
it('toggle rotation', () => {
|
||||
const { getByTitle } = render(<Viewer />)
|
||||
fireEvent.click(getByTitle(trans('general.rotation')))
|
||||
fireEvent.click(getByTitle(t('general.rotation')))
|
||||
})
|
||||
|
||||
it('toggle pause', () => {
|
||||
const { getByTitle } = render(<Viewer />)
|
||||
const icon = getByTitle(trans('general.pause'))
|
||||
const icon = getByTitle(t('general.pause'))
|
||||
fireEvent.click(icon)
|
||||
expect(icon).toHaveClass('fa-play')
|
||||
})
|
||||
|
||||
it('reset', () => {
|
||||
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)')
|
||||
})
|
||||
})
|
||||
|
@ -305,6 +305,13 @@ general:
|
||||
previews: Texture Previews
|
||||
last-modified: Last Modified
|
||||
|
||||
colors:
|
||||
black: Black
|
||||
white: White
|
||||
gray: Gray
|
||||
prev: Previous Background
|
||||
next: Next Background
|
||||
|
||||
vendor:
|
||||
datatable:
|
||||
search: Search
|
||||
|
BIN
resources/misc/backgrounds/1.png
Executable file
After Width: | Height: | Size: 408 KiB |
BIN
resources/misc/backgrounds/2.png
Executable file
After Width: | Height: | Size: 326 KiB |
BIN
resources/misc/backgrounds/3.png
Executable file
After Width: | Height: | Size: 306 KiB |
BIN
resources/misc/backgrounds/4.png
Executable file
After Width: | Height: | Size: 296 KiB |
BIN
resources/misc/backgrounds/5.png
Executable file
After Width: | Height: | Size: 276 KiB |
BIN
resources/misc/backgrounds/6.png
Executable file
After Width: | Height: | Size: 219 KiB |
BIN
resources/misc/backgrounds/7.png
Executable file
After Width: | Height: | Size: 303 KiB |
@ -23,6 +23,7 @@
|
||||
- Support searching players at players page.
|
||||
- Added Blessing Skin Shell.
|
||||
- Support specifying "from" email address and name when sending email.
|
||||
- 3D skin viewer can be with background now.
|
||||
|
||||
## Tweaked
|
||||
|
||||
|
@ -23,6 +23,7 @@
|
||||
- 角色页面可进行搜索
|
||||
- 新增 Blessing Skin Shell
|
||||
- 支持单独指定邮件发件人的地址和名称
|
||||
- 3D 皮肤预览现在是带背景的
|
||||
|
||||
## 调整
|
||||
|
||||
|
@ -20,6 +20,7 @@ if ($Simple) {
|
||||
# Copy static files
|
||||
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/misc/backgrounds/ ./public/bg -Recurse
|
||||
Write-Host 'Static files copied.' -ForegroundColor Green
|
||||
|
||||
# Write commit ID
|
||||
|