Add closet page
This commit is contained in:
parent
5bccfd3d26
commit
b7f0ddd7f0
309
resources/assets/src/components/user/Closet.vue
Normal file
309
resources/assets/src/components/user/Closet.vue
Normal file
|
|
@ -0,0 +1,309 @@
|
|||
<template>
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<!-- Left col -->
|
||||
<div class="col-md-8">
|
||||
<!-- Custom tabs -->
|
||||
<div class="nav-tabs-custom">
|
||||
<!-- Tabs within a box -->
|
||||
<ul class="nav nav-tabs">
|
||||
<li :class="{ active: category === 'skin' }">
|
||||
<a href="#" @click="switchCategory" v-t="'general.skin'" class="category-switch" data-toggle="tab" />
|
||||
</li>
|
||||
<li :class="{ active: category === 'cape' }">
|
||||
<a href="#" @click="switchCategory" v-t="'general.cape'" class="category-switch" data-toggle="tab" />
|
||||
</li>
|
||||
|
||||
<li class="pull-right" style="padding: 7px;">
|
||||
<div class="has-feedback pull-right">
|
||||
<div class="user-search-form">
|
||||
<input type="text" v-model="query" @input="search" class="form-control input-sm" :placeholder="$t('user.typeToSearch')">
|
||||
<span class="glyphicon glyphicon-search form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content no-padding">
|
||||
<div v-if="category === 'skin'" class="tab-pane box-body" :class="{ active: category === 'skin' }" id="skin-category">
|
||||
<div v-if="skinItems.length === 0" class="empty-msg">
|
||||
<div v-if="query !== ''" v-t="'general.noResult'"></div>
|
||||
<div v-else v-t="{ path: 'user.emptyClosetMsg', args: { url: linkToSkin } }" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<closet-item
|
||||
v-for="item in skinItems"
|
||||
:key="item.tid"
|
||||
:tid="item.tid"
|
||||
:name="item.name"
|
||||
:type="item.type"
|
||||
:selected="selectedSkin === item.tid"
|
||||
@select="selectTexture(item.tid)"
|
||||
@item-removed="removeSkinItem"
|
||||
></closet-item>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="tab-pane box-body" :class="{ active: category === 'cape' }" id="cape-category">
|
||||
<div v-if="capeItems.length === 0" class="empty-msg">
|
||||
<div v-if="query !== ''" v-t="'general.noResult'"></div>
|
||||
<div v-else v-t="{ path: 'user.emptyClosetMsg', args: { url: linkToCape } }" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<closet-item
|
||||
v-for="item in capeItems"
|
||||
:key="item.tid"
|
||||
:tid="item.tid"
|
||||
:name="item.name"
|
||||
:type="item.type"
|
||||
:selected="selectedCape === item.tid"
|
||||
@select="selectTexture(item.tid)"
|
||||
@item-removed="removeCapeItem"
|
||||
></closet-item>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<paginate
|
||||
v-if="category === 'skin'"
|
||||
v-model="skinCurrentPage"
|
||||
:page-count="skinTotalPages"
|
||||
class="pull-right"
|
||||
container-class="pagination pagination-sm no-margin"
|
||||
first-button-text="«"
|
||||
prev-text="‹"
|
||||
next-text="›"
|
||||
last-button-text="»"
|
||||
:click-handler="pageChanged"
|
||||
:first-last-button="true"
|
||||
></paginate>
|
||||
<paginate
|
||||
v-else
|
||||
v-model="capeCurrentPages"
|
||||
:page-count="capeTotalPages"
|
||||
class="pull-right"
|
||||
container-class="pagination pagination-sm no-margin"
|
||||
first-button-text="«"
|
||||
prev-text="‹"
|
||||
next-text="›"
|
||||
last-button-text="»"
|
||||
:click-handler="pageChanged"
|
||||
:first-last-button="true"
|
||||
></paginate>
|
||||
</div>
|
||||
</div><!-- /.nav-tabs-custom -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right col -->
|
||||
<div class="col-md-4">
|
||||
<previewer closet-mode :skin="skinUrl" :cape="capeUrl">
|
||||
<template slot="footer">
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
data-toggle="modal"
|
||||
data-target="#modal-use-as"
|
||||
v-t="'user.useAs'"
|
||||
@click="applyTexture"
|
||||
></button>
|
||||
<button
|
||||
class="btn btn-default pull-right"
|
||||
v-t="'user.resetSelected'"
|
||||
@click="resetSelected"
|
||||
></button>
|
||||
</template>
|
||||
</previewer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="modal-use-as" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title" v-t="'user.closet.use-as.title'"></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<template v-if="players.length !== 0">
|
||||
<div v-for="player in players" :key="player.pid" class="player-item">
|
||||
<label class="model-label" :for="player.pid">
|
||||
<input type="radio" name="player" :value="player.pid" v-model="selectedPlayer" />
|
||||
<img :src="avatarUrl(player)" width="35" height="35" />
|
||||
<span>{{ player.player_name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else v-t="'user.closet.use-as.empty'"></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="./player" class="btn btn-default pull-left" v-t="'user.closet.use-as.add'"></a>
|
||||
<a class="btn btn-primary" v-t="'general.submit'" @click="submitApplyTexture"></a>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
</section><!-- /.content -->
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios';
|
||||
import toastr from 'toastr';
|
||||
import Paginate from 'vuejs-paginate';
|
||||
import ClosetItem from './ClosetItem';
|
||||
import { debounce } from '../../js/utils';
|
||||
import { swal } from '../../js/notify';
|
||||
|
||||
export default {
|
||||
name: 'Closet',
|
||||
components: {
|
||||
Paginate,
|
||||
ClosetItem,
|
||||
Previewer: () => import('../common/Previewer'),
|
||||
},
|
||||
data: () => ({
|
||||
category: 'skin',
|
||||
query: '',
|
||||
skinItems: [],
|
||||
skinCurrentPage: 1,
|
||||
skinTotalPages: 1,
|
||||
capeItems: [],
|
||||
capeCurrentPages: 1,
|
||||
capeTotalPages: 1,
|
||||
selectedSkin: 0,
|
||||
skinUrl: '',
|
||||
selectedCape: 0,
|
||||
capeUrl: '',
|
||||
players: [],
|
||||
selectedPlayer: 0,
|
||||
}),
|
||||
computed: {
|
||||
linkToSkin: () => `${blessing.base_url}/skinlib?filter=skin`,
|
||||
linkToCape: () => `${blessing.base_url}/skinlib?filter=cape`,
|
||||
},
|
||||
methods: {
|
||||
search() {},
|
||||
async loadCloset(page = 1) {
|
||||
const { data: { items, category, total_pages } } = await axios({
|
||||
method: 'GET',
|
||||
url: '/user/closet-data',
|
||||
params: {
|
||||
category: this.category,
|
||||
q: this.query,
|
||||
page,
|
||||
}
|
||||
});
|
||||
this[`${category}TotalPages`] = total_pages;
|
||||
this[`${category}Items`] = items;
|
||||
},
|
||||
removeSkinItem(tid) {
|
||||
this.skinItems = this.skinItems.filter(item => item.tid !== tid);
|
||||
},
|
||||
removeCapeItem(tid) {
|
||||
this.capeItems = this.capeItems.filter(item => item.tid !== tid);
|
||||
},
|
||||
switchCategory() {
|
||||
this.category = this.category === 'skin' ? 'cape' : 'skin';
|
||||
this.loadCloset();
|
||||
},
|
||||
pageChanged(page) {
|
||||
this.loadCloset(page);
|
||||
},
|
||||
avatarUrl(player) {
|
||||
const tid = player.preference === 'default' ? player.tid_steve : player.tid_alex;
|
||||
return `${blessing.base_url}/avatar/35/${tid}`;
|
||||
},
|
||||
async selectTexture(tid) {
|
||||
const { data: { type, hash } } = await axios.post(`/skinlib/info/${tid}`);
|
||||
if (type === 'cape') {
|
||||
this.capeUrl = `/textures/${hash}`;
|
||||
this.selectedCape = tid;
|
||||
} else {
|
||||
this.skinUrl = `/textures/${hash}`;
|
||||
this.selectedSkin = tid;
|
||||
}
|
||||
},
|
||||
async applyTexture() {
|
||||
const { data: players } = await axios.get('/user/player/list');
|
||||
this.players = players;
|
||||
setTimeout(() => {
|
||||
$(this.$el).iCheck({
|
||||
radioClass: 'iradio_square-blue',
|
||||
checkboxClass: 'icheckbox_square-blue'
|
||||
}).on('ifChanged', function () {
|
||||
$(this)[0].dispatchEvent(new Event('change'));
|
||||
});
|
||||
}, 0);
|
||||
},
|
||||
async submitApplyTexture() {
|
||||
if (!this.selectedPlayer) {
|
||||
return toastr.info(this.$t('user.emptySelectedPlayer'));
|
||||
}
|
||||
|
||||
if (!this.selectedSkin && !this.selectedCape) {
|
||||
return toastr.info(this.$t('user.emptySelectedTexture'));
|
||||
}
|
||||
|
||||
const { data: { errno, msg } } = await axios.post(
|
||||
'/user/player/set',
|
||||
{
|
||||
pid: this.selectedPlayer,
|
||||
tid: {
|
||||
skin: this.selectedSkin || undefined,
|
||||
cape: this.selectedCape || undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
if (errno === 0) {
|
||||
swal({ type: 'success', html: msg });
|
||||
$('#modal-use-as').modal('hide');
|
||||
} else {
|
||||
toastr.warning(msg);
|
||||
}
|
||||
},
|
||||
resetSelected() {
|
||||
this.selectedSkin = this.selectedCape = 0;
|
||||
this.skinUrl = this.capeUrl = '';
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.search = debounce(this.loadCloset, 350);
|
||||
},
|
||||
beforeMount() {
|
||||
this.loadCloset();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="stylus">
|
||||
.empty-msg {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.texture-name {
|
||||
width: 65%;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-footer > .dropdown-menu {
|
||||
margin-left: 180px;
|
||||
}
|
||||
|
||||
.box-title {
|
||||
a {
|
||||
color: #6d6d6d;
|
||||
}
|
||||
|
||||
a.selected {
|
||||
color: #3c8dbc;
|
||||
}
|
||||
}
|
||||
|
||||
.player-item:not(:nth-child(1)) {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
||||
11
resources/assets/src/js/utils.js
Normal file
11
resources/assets/src/js/utils.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
/**
|
||||
* @param {Function} func
|
||||
* @param {number} delay
|
||||
*/
|
||||
export function debounce(func, delay) {
|
||||
let timer;
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(func, delay);
|
||||
};
|
||||
}
|
||||
284
resources/assets/tests/components/user/Closet.test.js
Normal file
284
resources/assets/tests/components/user/Closet.test.js
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import Closet from '@/components/user/Closet';
|
||||
import ClosetItem from '@/components/user/ClosetItem';
|
||||
import Previewer from '@/components/common/Previewer';
|
||||
import axios from 'axios';
|
||||
import toastr from 'toastr';
|
||||
import { swal } from '@/js/notify';
|
||||
|
||||
jest.mock('axios');
|
||||
jest.mock('@/js/notify');
|
||||
|
||||
test('fetch closet data before mount', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
mount(Closet);
|
||||
jest.runAllTicks();
|
||||
expect(axios).toBeCalledWith({
|
||||
method: 'GET',
|
||||
url: '/user/closet-data',
|
||||
params: {
|
||||
category: 'skin',
|
||||
q: '',
|
||||
page: 1,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('switch tabs', () => {
|
||||
axios.mockResolvedValue({
|
||||
data: {
|
||||
items: [],
|
||||
category: 'skin',
|
||||
total_pages: 1
|
||||
}
|
||||
}).mockResolvedValueOnce({
|
||||
data: {
|
||||
items: [],
|
||||
category: 'cape',
|
||||
total_pages: 1
|
||||
}
|
||||
});
|
||||
|
||||
const wrapper = mount(Closet);
|
||||
|
||||
const tabSkin = wrapper.findAll('.nav-tabs > li').at(0);
|
||||
tabSkin.find('a').trigger('click');
|
||||
jest.runAllTicks();
|
||||
expect(axios).toBeCalledWith({
|
||||
method: 'GET',
|
||||
url: '/user/closet-data',
|
||||
params: {
|
||||
category: 'skin',
|
||||
q: '',
|
||||
page: 1,
|
||||
}
|
||||
});
|
||||
|
||||
const tabCape = wrapper.findAll('.nav-tabs > li').at(1);
|
||||
tabCape.find('a').trigger('click');
|
||||
jest.runAllTicks();
|
||||
expect(axios).toBeCalledWith({
|
||||
method: 'GET',
|
||||
url: '/user/closet-data',
|
||||
params: {
|
||||
category: 'cape',
|
||||
q: '',
|
||||
page: 1,
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('different categories', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
|
||||
const wrapper = mount(Closet);
|
||||
expect(wrapper.findAll('.nav-tabs > li').at(0).classes()).toContain('active');
|
||||
expect(wrapper.find('#skin-category').classes()).toContain('active');
|
||||
|
||||
wrapper.setData({ category: 'cape' });
|
||||
expect(wrapper.findAll('.nav-tabs > li').at(1).classes()).toContain('active');
|
||||
expect(wrapper.find('#cape-category').classes()).toContain('active');
|
||||
});
|
||||
|
||||
test('search textures', () => {
|
||||
jest.useFakeTimers();
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
|
||||
const wrapper = mount(Closet);
|
||||
const input = wrapper.find('input');
|
||||
input.element.value = 'q';
|
||||
input.trigger('input');
|
||||
jest.runAllTimers();
|
||||
jest.runAllTicks();
|
||||
expect(axios).toBeCalledWith({
|
||||
method: 'GET',
|
||||
url: '/user/closet-data',
|
||||
params: {
|
||||
category: 'skin',
|
||||
q: 'q',
|
||||
page: 1,
|
||||
}
|
||||
});
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('empty closet', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
const wrapper = mount(Closet);
|
||||
expect(wrapper.find('#skin-category').text()).toContain('user.emptyClosetMsg');
|
||||
wrapper.setData({ category: 'cape' });
|
||||
expect(wrapper.find('#cape-category').text()).toContain('user.emptyClosetMsg');
|
||||
});
|
||||
|
||||
test('no matched search result', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
const wrapper = mount(Closet);
|
||||
wrapper.setData({ query: 'q' });
|
||||
expect(wrapper.find('#skin-category').text()).toContain('general.noResult');
|
||||
wrapper.setData({ category: 'cape' });
|
||||
expect(wrapper.find('#cape-category').text()).toContain('general.noResult');
|
||||
});
|
||||
|
||||
test('render items', async () => {
|
||||
axios.mockResolvedValue({ data: {
|
||||
items: [
|
||||
{ tid: 1 },
|
||||
{ tid: 2 }
|
||||
],
|
||||
category: 'skin',
|
||||
total_pages: 1
|
||||
} });
|
||||
const wrapper = mount(Closet);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.findAll(ClosetItem)).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('reload closet when page changed', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
const wrapper = mount(Closet);
|
||||
wrapper.vm.pageChanged();
|
||||
jest.runAllTicks();
|
||||
expect(axios).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
test('remove skin item', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
const wrapper = mount(Closet);
|
||||
wrapper.setData({ skinItems: [{ tid: 1 }, { tid: 2 }] });
|
||||
wrapper.vm.removeSkinItem(1);
|
||||
expect(wrapper.findAll(ClosetItem)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('remove cape item', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
const wrapper = mount(Closet);
|
||||
wrapper.setData({ capeItems: [{ tid: 1 }, { tid: 2 }], category: 'cape' });
|
||||
wrapper.vm.removeCapeItem(1);
|
||||
expect(wrapper.findAll(ClosetItem)).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('compute avatar URL', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
const wrapper = mount(Closet);
|
||||
const { avatarUrl } = wrapper.vm;
|
||||
expect(avatarUrl({ preference: 'default', tid_steve: 1 })).toBe('/avatar/35/1');
|
||||
expect(avatarUrl({ preference: 'slim', tid_alex: 1 })).toBe('/avatar/35/1');
|
||||
});
|
||||
|
||||
test('select texture', async () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
axios.post.mockResolvedValueOnce({ data: { type: 'steve', hash: 'a' } })
|
||||
.mockResolvedValueOnce({ data: { type: 'cape', hash: 'b' } });
|
||||
|
||||
const wrapper = mount(Closet);
|
||||
wrapper.setData({ skinItems: [{ tid: 1 }] });
|
||||
wrapper.find(ClosetItem).vm.$emit('select');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(axios.post).toBeCalledWith('/skinlib/info/1');
|
||||
expect(wrapper.vm.skinUrl).toBe('/textures/a');
|
||||
|
||||
wrapper.setData({ skinItems: [], capeItems: [{ tid: 2 }], category: 'cape' });
|
||||
wrapper.find(ClosetItem).vm.$emit('select');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(axios.post).toBeCalledWith('/skinlib/info/2');
|
||||
expect(wrapper.vm.capeUrl).toBe('/textures/b');
|
||||
});
|
||||
|
||||
test('apply texture', async () => {
|
||||
jest.useFakeTimers();
|
||||
window.$ = jest.fn(() => ({
|
||||
iCheck: () => ({
|
||||
on(evt, cb) {
|
||||
cb();
|
||||
},
|
||||
}),
|
||||
0: {
|
||||
dispatchEvent: () => {}
|
||||
}
|
||||
}));
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
axios.get.mockResolvedValueOnce({ data: [] })
|
||||
.mockResolvedValueOnce({ data: [
|
||||
{ pid: 1, player_name: 'name', preference: 'default', tid_steve: 10 }
|
||||
] });
|
||||
|
||||
const wrapper = mount(Closet);
|
||||
const button = wrapper.find(Previewer).findAll('button').at(0);
|
||||
button.trigger('click');
|
||||
jest.runAllTicks();
|
||||
expect(wrapper.find('.modal-body').text()).toContain('user.closet.use-as.empty');
|
||||
|
||||
button.trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(wrapper.find('input[type="radio"]').attributes()).toHaveProperty('value', '1');
|
||||
expect(wrapper.find('.model-label > img').attributes())
|
||||
.toHaveProperty('src', '/avatar/35/10');
|
||||
expect(wrapper.find('.modal-body').text()).toContain('name');
|
||||
jest.runAllTimers();
|
||||
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
test('submit applying texture', async () => {
|
||||
window.$ = jest.fn(() => ({ modal() {} }));
|
||||
jest.spyOn(toastr, 'info');
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
axios.post.mockResolvedValueOnce({ data: { errno: 1 } })
|
||||
.mockResolvedValue({ data: { errno: 0, msg: 'ok' } });
|
||||
const wrapper = mount(Closet);
|
||||
const button = wrapper.find('.modal-footer > a:nth-child(2)');
|
||||
|
||||
button.trigger('click');
|
||||
expect(toastr.info).toBeCalledWith('user.emptySelectedPlayer');
|
||||
|
||||
wrapper.setData({ selectedPlayer: 1 });
|
||||
button.trigger('click');
|
||||
expect(toastr.info).toBeCalledWith('user.emptySelectedTexture');
|
||||
|
||||
wrapper.setData({ selectedSkin: 1 });
|
||||
button.trigger('click');
|
||||
expect(axios.post).toBeCalledWith(
|
||||
'/user/player/set',
|
||||
{
|
||||
pid: 1,
|
||||
tid: {
|
||||
skin: 1,
|
||||
cape: undefined
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
wrapper.setData({ selectedSkin: 0, selectedCape: 1 });
|
||||
button.trigger('click');
|
||||
expect(axios.post).toBeCalledWith(
|
||||
'/user/player/set',
|
||||
{
|
||||
pid: 1,
|
||||
tid: {
|
||||
skin: undefined,
|
||||
cape: 1
|
||||
}
|
||||
}
|
||||
);
|
||||
await wrapper.vm.$nextTick();
|
||||
expect(swal).toBeCalledWith({ type: 'success', html: 'ok' });
|
||||
});
|
||||
|
||||
test('reset selected texture', () => {
|
||||
axios.mockResolvedValue({ data: {} });
|
||||
const wrapper = mount(Closet);
|
||||
wrapper.setData({
|
||||
selectedSkin: 1,
|
||||
selectedCape: 2,
|
||||
skinUrl: 'a',
|
||||
capeUrl: 'b'
|
||||
});
|
||||
wrapper.find(Previewer).findAll('button').at(1).trigger('click');
|
||||
expect(wrapper.vm).toEqual(expect.objectContaining({
|
||||
selectedSkin: 0,
|
||||
selectedCape: 0,
|
||||
skinUrl: '',
|
||||
capeUrl: ''
|
||||
}));
|
||||
});
|
||||
|
|
@ -83,6 +83,16 @@ user:
|
|||
emptyNewEmail: Empty new email address.
|
||||
changeEmail: 'Sure to change your email address to :new_email?'
|
||||
emptyDeletePassword: 'Please enter the current password:'
|
||||
typeToSearch: Type to search
|
||||
useAs: Apply...
|
||||
resetSelected: Clear selected
|
||||
closet:
|
||||
use-as:
|
||||
button: Apply...
|
||||
reset: Clear selected
|
||||
title: Which player should be applied to?
|
||||
empty: It seems that you own no player...
|
||||
add: Add new player
|
||||
|
||||
admin:
|
||||
operationsTitle: Operations
|
||||
|
|
@ -151,6 +161,7 @@ general:
|
|||
confirmLogout: Sure to log out?
|
||||
confirm: OK
|
||||
cancel: Cancel
|
||||
submit: Submit
|
||||
more: More
|
||||
pagination: 'Page :page, total :total'
|
||||
searchResult: '(Search result of keyword ":keyword")'
|
||||
|
|
|
|||
|
|
@ -83,6 +83,16 @@ user:
|
|||
emptyNewEmail: 你还没有填写新邮箱啊
|
||||
changeEmail: '确定要将用户邮箱更改为 :new_email 吗?'
|
||||
emptyDeletePassword: 请先输入当前用户密码
|
||||
typeToSearch: 输入即搜索
|
||||
useAs: 使用...
|
||||
resetSelected: 重置已选材质
|
||||
closet:
|
||||
use-as:
|
||||
button: 使用...
|
||||
reset: 重置已选材质
|
||||
title: 要给哪个角色使用呢?
|
||||
empty: 你好像还没有添加任何角色哦
|
||||
add: 添加角色
|
||||
|
||||
admin:
|
||||
operationsTitle: 更多操作
|
||||
|
|
@ -147,6 +157,7 @@ general:
|
|||
confirmLogout: 确定要登出吗?
|
||||
confirm: 确定
|
||||
cancel: 取消
|
||||
submit: 提交
|
||||
more: 更多
|
||||
pagination: '第 :page 页,共 :total 页'
|
||||
searchResult: '(关键词 “:keyword” 的搜索结果)'
|
||||
|
|
|
|||
|
|
@ -18,87 +18,7 @@
|
|||
</section>
|
||||
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<!-- Left col -->
|
||||
<div class="col-md-8">
|
||||
<!-- Custom tabs -->
|
||||
<div class="nav-tabs-custom" id="closet-container">
|
||||
<!-- Tabs within a box -->
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="active"><a href="#skin-category" class="category-switch" data-toggle="tab">@lang('general.skin')</a></li>
|
||||
<li><a href="#cape-category" class="category-switch" data-toggle="tab">@lang('general.cape')</a></li>
|
||||
|
||||
<li class="pull-right" style="padding: 7px;">
|
||||
<div class="has-feedback pull-right">
|
||||
<div class="user-search-form">
|
||||
<input type="text" name="q" class="form-control input-sm" placeholder="@lang('user.closet.type-to-search')">
|
||||
<span class="glyphicon glyphicon-search form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content no-padding">
|
||||
<div class="tab-pane active box-body" id="skin-category"></div>
|
||||
<div class="tab-pane box-body" id="cape-category"></div>
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<div class="pull-right" id="closet-paginator" last-skin-page="1" last-cape-page="1"></div>
|
||||
</div>
|
||||
</div><!-- /.nav-tabs-custom -->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Right col -->
|
||||
<div class="col-md-4">
|
||||
|
||||
<div class="box box-default">
|
||||
@include('common.texture-preview')
|
||||
|
||||
<div class="box-footer">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#modal-use-as">@lang('user.closet.use-as.button')</button>
|
||||
<button class="btn btn-default pull-right" id="closet-reset">@lang('user.closet.use-as.reset')</button>
|
||||
</div><!-- /.box-footer -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section><!-- /.content -->
|
||||
<section class="content"></section><!-- /.content -->
|
||||
</div><!-- /.content-wrapper -->
|
||||
|
||||
<div id="modal-use-as" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
|
||||
<h4 class="modal-title">@lang('user.closet.use-as.title')</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@forelse($user->players as $player)
|
||||
<label class="model-label" for="{{ $player->pid }}">
|
||||
<input type="radio" id="{{ $player->pid }}" name="player" /> {{ $player->player_name }}
|
||||
</label><br />
|
||||
@empty
|
||||
<p>@lang('user.closet.use-as.empty')</p>
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<a href="./player" class="btn btn-default pull-left">@lang('user.closet.use-as.add')</a>
|
||||
<a onclick="setTexture();" class="btn btn-primary">@lang('general.submit')</a>
|
||||
</div>
|
||||
</div><!-- /.modal-content -->
|
||||
</div><!-- /.modal-dialog -->
|
||||
</div><!-- /.modal -->
|
||||
|
||||
@endsection
|
||||
|
||||
@section('script')
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$.msp.config.skinUrl = defaultSteveSkin;
|
||||
initSkinViewer();
|
||||
registerAnimationController();
|
||||
registerWindowResizeHandler();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user