Optimize closet

1. Use jqPaginator
2. Use CSR for closet
3. Use AJAX for closet
4. Just type to search instead of pressing ENTER key
5. Link to skin library is according to current category when closet is empty
6. Texture indicator shows category of texture
This commit is contained in:
gplane 2017-04-23 23:58:22 +08:00
parent 7607beebaf
commit f53bb7acb6
11 changed files with 180 additions and 104 deletions

View File

@ -25,43 +25,40 @@ class ClosetController extends Controller
$this->closet = new Closet(session('uid')); $this->closet = new Closet(session('uid'));
} }
public function index(Request $request) public function index()
{
return view('user.closet')->with('user', app('user.current'));
}
public function getClosetData(Request $request)
{ {
$category = $request->input('category', 'skin'); $category = $request->input('category', 'skin');
$page = $request->input('page', 1); $page = abs($request->input('page', 1));
$page = $page <= 0 ? 1 : $page;
$q = $request->input('q', null); $q = $request->input('q', null);
$items = []; $items = [];
if ($q) { if ($q) {
foreach (['skin', 'cape'] as $category) { // do search
// do search foreach ($this->closet->getItems($category) as $item) {
foreach ($this->closet->getItems($category) as $item) { if (stristr($item->name, $q)) {
if (strstr($item->name, $q)) { $items[] = $item;
$items[$category][] = $item;
}
} }
} }
} else { } else {
$items['skin'] = $this->closet->getItems('skin'); $items = $this->closet->getItems($category);
$items['cape'] = $this->closet->getItems('cape');
} }
// pagination // pagination
$total_pages = []; $total_pages = ceil(count($items) / 6);
foreach ($items as $key => $value) { $items = array_slice($items, ($page - 1) * 6, 6);
$total_pages[] = ceil(count($items[$key]) / 6);
$items[$key] = array_slice($value, ($page-1)*6, 6);
}
return view('user.closet')->with('items', $items) return response()->json([
->with('page', $page) 'category' => $category,
->with('q', $q) 'items' => $items,
->with('category', $category) 'total_pages' => $total_pages
->with('total_pages', $total_pages ? max($total_pages) : 0) ]);
->with('user', app('user.current'));
} }
public function info() public function info()

View File

@ -65,6 +65,7 @@ Route::group(['middleware' => 'auth', 'prefix' => 'user'], function ()
// Closet // Closet
Route::get ('/closet', 'ClosetController@index'); Route::get ('/closet', 'ClosetController@index');
Route::get ('/closet-data', 'ClosetController@getClosetData');
Route::post('/closet/add', 'ClosetController@add'); Route::post('/closet/add', 'ClosetController@add');
Route::post('/closet/remove', 'ClosetController@remove'); Route::post('/closet/remove', 'ClosetController@remove');
Route::post('/closet/rename', 'ClosetController@rename'); Route::post('/closet/rename', 'ClosetController@rename');

View File

@ -24,6 +24,11 @@
"font-awesome": "Font-Awesome#^4.6.3", "font-awesome": "Font-Awesome#^4.6.3",
"bootstrap-fileinput": "^4.3.3", "bootstrap-fileinput": "^4.3.3",
"bootstrap": "^3.3.6", "bootstrap": "^3.3.6",
"sweetalert2": "^4.1.6" "sweetalert2": "^4.1.6",
"jqPaginator": "^1.2.0",
"lodash": "^4.17.4"
},
"resolutions": {
"jquery": "1.9.1 - 3"
} }
} }

View File

@ -25,6 +25,7 @@ var srcPath = 'resources/assets/src/';
var distPath = 'resources/assets/dist/'; var distPath = 'resources/assets/dist/';
var vendorScripts = [ var vendorScripts = [
'lodash/dist/lodash.min.js',
'jquery/dist/jquery.min.js', 'jquery/dist/jquery.min.js',
'bootstrap/dist/js/bootstrap.min.js', 'bootstrap/dist/js/bootstrap.min.js',
'AdminLTE/dist/js/app.min.js', 'AdminLTE/dist/js/app.min.js',
@ -35,6 +36,7 @@ var vendorScripts = [
'toastr/toastr.min.js', 'toastr/toastr.min.js',
'es6-promise/es6-promise.auto.min.js', 'es6-promise/es6-promise.auto.min.js',
'sweetalert2/dist/sweetalert2.min.js', 'sweetalert2/dist/sweetalert2.min.js',
'jqPaginator/dist/jqPaginator.min.js',
'resources/assets/dist/scripts/general.js', 'resources/assets/dist/scripts/general.js',
]; ];

View File

@ -18,13 +18,13 @@ $('body').on('click', '#preview-switch', () => {
TexturePreview.previewType == '3D' ? TexturePreview.show2dPreview() : TexturePreview.show3dPreview(); TexturePreview.previewType == '3D' ? TexturePreview.show2dPreview() : TexturePreview.show3dPreview();
}); });
var selected = []; let skinSelected = false, capeSelected = false;
$('body').on('click', '.item-body', function () { $('body').on('click', '.item-body', function () {
$('.item-selected').parent().removeClass('item-selected'); $('.item-selected').parent().removeClass('item-selected');
$(this).parent().addClass('item-selected'); $(this).parent().addClass('item-selected');
let tid = $(this).parent().attr('tid'); const tid = $(this).parent().attr('tid');
$.ajax({ $.ajax({
type: "POST", type: "POST",
@ -33,24 +33,95 @@ $('body').on('click', '.item-body', function () {
success: (json) => { success: (json) => {
if (json.type == "cape") { if (json.type == "cape") {
MSP.changeCape('../textures/' + json.hash); MSP.changeCape('../textures/' + json.hash);
selected['cape'] = tid; capeSelected = true;
} else { } else {
MSP.changeSkin('../textures/' + json.hash); MSP.changeSkin('../textures/' + json.hash);
selected['skin'] = tid; skinSelected = true;
} }
selected.length = 0; if (skinSelected && capeSelected)
$('#textures-indicator').text(`${trans('general.skin')} & ${trans('general.cape')}`);
['skin', 'cape'].forEach((key) => { else if (skinSelected)
if (selected[key] !== undefined) selected.length++; $('#textures-indicator').text(trans('general.skin'));
else if (capeSelected)
$('#textures-indicator').html(selected.length); $('#textures-indicator').text(trans('general.cape'));
});
}, },
error: showAjaxError error: showAjaxError
}); });
}); });
$('body').on('click', '.category-switch', () => {
const category = $('a[href="#skin-category"]').parent().hasClass('active') ? 'cape' : 'skin';
const page = parseInt($('#closet-paginator').attr(`last-${category}-page`));
const search = $('input[name=q]').val();
reloadCloset(category, page, search);
});
function renderClosetItemComponent(item, rootPath) {
return `
<div class="item" tid="${item.tid}">
<div class="item-body">
<img src="${rootPath}/preview/${item.tid}.png">
</div>
<div class="item-footer">
<p class="texture-name">
<span title="${item.name}">${item.name} <small>(${item.type})</small></span>
</p>
<a href="${rootPath}/skinlib/show/${item.tid}" title="${trans('user.viewInSkinlib')}" class="more" data-toggle="tooltip" data-placement="bottom"><i class="fa fa-share"></i></a>
<span title="${trans('general.more')}" class="more" data-toggle="dropdown" aria-haspopup="true" id="more-button"><i class="fa fa-cog"></i></span>
<ul class="dropup dropdown-menu" aria-labelledby="more-button">
<li><a href="javascript:renameClosetItem(${item.tid}, '${item.name}');">${trans('user.renameItem')}</a></li>
<li><a href="javascript:removeFromCloset(${item.tid});">${trans('user.removeItem')}</a></li>
<li><a href="javascript:setAsAvatar(${item.tid});">${trans('user.setAsAvatar')}</a></li>
</ul>
</div>
</div>`;
}
function renderCloset(items, category) {
const rootPath = /(^https?.*)\/user\/closet/.exec(window.location.href)[1];
const search = $('input[name=q]').val();
let container = $(`#${category}-category`);
container.html('');
if (items.length === 0) {
$('#closet-paginator').hide();
if (search === '') {
container.html(`<div class="empty-msg">
${trans('user.emptyClosetMsg', { url: rootPath + '/skinlib?filter=' + category })}</div>`);
} else {
container.html(`<div class="empty-msg">${trans('general.noResult')}</div>`);
}
} else {
$('#closet-paginator').show();
for (const item of items) {
container.append(renderClosetItemComponent(item, rootPath));
}
}
}
function reloadCloset(category, page, search) {
Promise.resolve($.ajax({
type: 'GET',
url: /(^https?.*)\/user\/closet/.exec(window.location.href)[1] + '/user/closet-data',
dataType: 'json',
data: {
category: category,
page: page,
q: search
}
})).then(result => {
renderCloset(result.items, result.category);
let paginator = $('#closet-paginator');
paginator.attr(`last-${result.category}-page`, page);
paginator.jqPaginator('option', {
currentPage: page,
totalPages: result.total_pages
});
}).catch(error => showAjaxError);
}
function showPlayerTexturePreview(pid) { function showPlayerTexturePreview(pid) {
$.ajax({ $.ajax({
type: "POST", type: "POST",
@ -179,6 +250,43 @@ $(document).ready(function() {
confirmButtonText: trans('general.confirm'), confirmButtonText: trans('general.confirm'),
cancelButtonText: trans('general.cancel') cancelButtonText: trans('general.cancel')
}); });
if (window.location.pathname.includes('/user/closet')) {
Promise.resolve($.ajax({
type: 'GET',
url: /(^https?.*)\/user\/closet/.exec(window.location.href)[1] + '/user/closet-data',
dataType: 'json'
})).then(result => {
renderCloset(result.items, result.category);
$('#closet-paginator').jqPaginator({
totalPages: result.total_pages,
visiblePages: 5,
currentPage: 1,
first: '<li><a style="cursor: pointer;">«</a></li>',
prev: '<li><a style="cursor: pointer;"></a></li>',
next: '<li><a style="cursor: pointer;"></a></li>',
last: '<li><a style="cursor: pointer;">»</a></li>',
page: '<li><a style="cursor: pointer;">{{page}}</a></li>',
wrapper: '<ul class="pagination pagination-sm no-margin"></ul>',
onPageChange: page => {
reloadCloset(
$('#skin-category').hasClass('active') ? 'skin' : 'cape',
page,
$('input[name=q]').val()
);
}
});
}).catch(error => showAjaxError);
$('input[name=q]').on('input', _.debounce(() => {
const category = $('#skin-category').hasClass('active') ? 'skin' : 'cape';
reloadCloset(
category,
1,
$('input[name=q]').val()
);
}, 350));
}
}); });
function setTexture() { function setTexture() {

View File

@ -68,6 +68,11 @@
signInRemainingTime: 'Available after :time hours', signInRemainingTime: 'Available after :time hours',
// Closet // Closet
emptyClosetMsg: '<p>Nothing in your closet...</p><p>Why not explore the <a href=":url">Skin Library</a> for a while?</p>',
renameItem: 'Rename item',
removeItem: 'Remove from closet',
setAsAvatar: 'Set as avatar',
viewInSkinlib: 'View in skin library',
switch2dPreview: 'Switch to 2D Preview', switch2dPreview: 'Switch to 2D Preview',
switch3dPreview: 'Switch to 3D Preview', switch3dPreview: 'Switch to 3D Preview',
removeFromClosetNotice: 'Sure to remove this texture from your closet?', removeFromClosetNotice: 'Sure to remove this texture from your closet?',
@ -153,10 +158,14 @@
extracting: 'Extracting update package..' extracting: 'Extracting update package..'
}, },
general: { general: {
skin: 'Skin',
cape: 'Cape',
fatalError: 'Fatal Error (Please contact the author)', fatalError: 'Fatal Error (Please contact the author)',
confirmLogout: 'Sure to log out?', confirmLogout: 'Sure to log out?',
confirm: 'OK', confirm: 'OK',
cancel: 'Cancel' cancel: 'Cancel',
more: 'More',
noResult: 'No result.'
} }
}; };
})(window.jQuery); })(window.jQuery);

View File

@ -28,13 +28,11 @@ score-intro:
closet: closet:
upload: Upload Texture upload: Upload Texture
search: Search Texture search: Search Texture
type-to-search: Type to search
switch-category: Switch Category switch-category: Switch Category
view: View in skin library view: View in skin library
more: More more: More
set-avatar: Set as avatar set-avatar: Set as avatar
empty-msg: |
<p>Nothing in your closet...</p>
<p>Why not explore the <a href=":url">Skin Library</a> for a while?</p>
use-as: use-as:
button: Apply... button: Apply...

View File

@ -68,6 +68,11 @@
signInRemainingTime: ':time 小时后可签到', signInRemainingTime: ':time 小时后可签到',
// Closet // Closet
emptyClosetMsg: '<p>衣柜里啥都没有哦~</p><p>去<a href=":url">皮肤库</a>看看吧~</p>',
renameItem: '重命名物品',
removeItem: '从衣柜中移除',
setAsAvatar: '设为头像',
viewInSkinlib: '在皮肤库中查看',
switch2dPreview: '切换 2D 预览', switch2dPreview: '切换 2D 预览',
switch3dPreview: '切换 3D 预览', switch3dPreview: '切换 3D 预览',
removeFromClosetNotice: '确定要从衣柜中移除此材质吗?', removeFromClosetNotice: '确定要从衣柜中移除此材质吗?',
@ -153,10 +158,14 @@
extracting: '正在解压更新包' extracting: '正在解压更新包'
}, },
general: { general: {
skin: '皮肤',
cape: '披风',
fatalError: '严重错误(请联系作者)', fatalError: '严重错误(请联系作者)',
confirmLogout: '确定要登出吗?', confirmLogout: '确定要登出吗?',
confirm: '确定', confirm: '确定',
cancel: '取消' cancel: '取消',
more: '更多',
noResult: '无结果'
}, },
vendor: { vendor: {
datatables: { datatables: {

View File

@ -35,13 +35,11 @@ score-intro:
closet: closet:
upload: 上传材质 upload: 上传材质
search: 搜索材质 search: 搜索材质
type-to-search: 输入即搜索
switch-category: 切换分类 switch-category: 切换分类
view: 在皮肤库中查看 view: 在皮肤库中查看
more: 更多 more: 更多
set-avatar: 设为头像 set-avatar: 设为头像
empty-msg: |
<p>衣柜里啥都没有哦~</p>
<p>去<a href=":url">皮肤库</a>看看吧~</p>
use-as: use-as:
button: 使用... button: 使用...

View File

@ -26,51 +26,30 @@
<div class="nav-tabs-custom"> <div class="nav-tabs-custom">
<!-- Tabs within a box --> <!-- Tabs within a box -->
<ul class="nav nav-tabs"> <ul class="nav nav-tabs">
<li class="active"><a href="#skin-category" data-toggle="tab">{{ trans('general.skin') }}</a></li> <li class="active"><a href="#skin-category" class="category-switch" data-toggle="tab">{{ trans('general.skin') }}</a></li>
<li><a href="#cape-category" data-toggle="tab">{{ trans('general.cape') }}</a></li> <li><a href="#cape-category" class="category-switch" data-toggle="tab">{{ trans('general.cape') }}</a></li>
<div style="padding: 7px;"> <li class="pull-right" style="padding: 7px;">
<div class="has-feedback pull-right"> <div class="has-feedback pull-right">
<form method="get" action="" class="user-search-form"> <div class="user-search-form">
<input type="text" name="q" class="form-control input-sm" placeholder="输入,回车搜索" value="{{ $q }}"> <input type="text" name="q" class="form-control input-sm" placeholder="{{ trans('user.closet.type-to-search') }}">
<span class="glyphicon glyphicon-search form-control-feedback"></span> <span class="glyphicon glyphicon-search form-control-feedback"></span>
</form> </div>
</div> </div>
</div> </li>
</ul> </ul>
<div class="tab-content no-padding"> <div class="tab-content no-padding">
<div class="tab-pane active box-body" id="skin-category"> <div class="tab-pane active box-body" id="skin-category"></div>
@include('vendor.closet-items', ['items' => (array)array_get($items, 'skin')]) <div class="tab-pane box-body" id="cape-category"></div>
</div>
<div class="tab-pane box-body" id="cape-category">
@include('vendor.closet-items', ['items' => (array)array_get($items, 'cape')])
</div>
</div> </div>
<div class="box-footer"> <div class="box-footer">
<ul class="pagination pagination-sm no-margin pull-right"> <div class="pull-right" id="closet-paginator" last-skin-page="1" last-cape-page="1"></div>
<?php $base_url = $q ? "?q=$q&" : "?"; ?>
<li><a href="{{ $base_url }}page=1">«</a></li>
@if ($page != 1)
<li><a href="{{ $base_url }}page={{ $page-1 }}">{{ $page - 1 }}</a></li>
@endif
<li><a href="{{ $base_url }}page={{ $page }}" class="active">{{ $page }}</a></li>
@if ($total_pages > $page)
<li><a href="{{ $base_url }}page={{ $page+1 }}">{{ $page+1 }}</a></li>
@endif
<li><a href="{{ $base_url }}page={{ $total_pages }}">»</a></li>
</ul>
<p class="pull-right">{{ trans('general.pagination', ['page' => $page, 'total' => $total_pages]) }}</p>
</div> </div>
</div><!-- /.nav-tabs-custom --> </div><!-- /.nav-tabs-custom -->
</div> </div>
<!-- Left col --> <!-- Right col -->
<div class="col-md-4"> <div class="col-md-4">
<div class="box box-default"> <div class="box box-default">

View File

@ -1,30 +0,0 @@
@forelse ($items as $item)
<div class="item" tid="{{ $item->tid }}">
<div class="item-body">
<img src="{{ url('preview/'.$item->tid) }}.png">
</div>
<div class="item-footer">
<p class="texture-name">
<span title="{{ $item->name }}">{{ $item->name }} <small>({{ $item->type }})</small></span>
</p>
<a href="{{ url('skinlib/show/'.$item->tid) }}" title="{{ trans('user.closet.view') }}" class="more" data-toggle="tooltip" data-placement="bottom"><i class="fa fa-share"></i></a>
<span title="{{ trans('user.closet.more') }}" class="more" data-toggle="dropdown" aria-haspopup="true" id="more-button"><i class="fa fa-cog"></i></span>
<ul class="dropup dropdown-menu" aria-labelledby="more-button">
<li><a href="javascript:renameClosetItem({{ $item->tid }}, '{{ $item->name }}');">{{ trans('user.closet.rename.title') }}</a></li>
<li><a href="javascript:removeFromCloset({{ $item->tid }});">{{ trans('user.closet.remove.title') }}</a></li>
<li><a href="javascript:setAsAvatar({{ $item->tid }});">{{ trans('user.closet.set-avatar') }}</a></li>
</ul>
</div>
</div>
@empty
<div class="empty-msg">
@if($q)
{{ trans('skinlib.general.no-result') }}
@else
{!! trans('user.closet.empty-msg', ['url' => url('skinlib')]) !!}
@endif
</div>
@endforelse