add custom background for skin viewer
1
.gitignore
vendored
@ -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
|
||||||
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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)')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -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
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.
|
- 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
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
- 角色页面可进行搜索
|
- 角色页面可进行搜索
|
||||||
- 新增 Blessing Skin Shell
|
- 新增 Blessing Skin Shell
|
||||||
- 支持单独指定邮件发件人的地址和名称
|
- 支持单独指定邮件发件人的地址和名称
|
||||||
|
- 3D 皮肤预览现在是带背景的
|
||||||
|
|
||||||
## 调整
|
## 调整
|
||||||
|
|
||||||
|
@ -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
|
||||||
|