diff --git a/app/Http/Controllers/SkinlibController.php b/app/Http/Controllers/SkinlibController.php
index 724bc3e9..2399c17e 100644
--- a/app/Http/Controllers/SkinlibController.php
+++ b/app/Http/Controllers/SkinlibController.php
@@ -56,9 +56,6 @@ class SkinlibController extends Controller
// Keyword to search
$keyword = $request->input('keyword', '');
- // Check if user logged in
- $anonymous = !Auth::check();
-
if ($filter == "skin") {
$query = Texture::where(function ($innerQuery) {
// Nested condition, DO NOT MODIFY
@@ -76,7 +73,7 @@ class SkinlibController extends Controller
$query = $query->where('uploader', $uploader);
}
- if ($anonymous) {
+ if (! $currentUser) {
// Show public textures only to anonymous visitors
$query = $query->where('public', true);
} else {
@@ -95,7 +92,7 @@ class SkinlibController extends Controller
->take($itemsPerPage)
->get();
- if (! $anonymous) {
+ if ($currentUser) {
$closet = new Closet($currentUser->uid);
foreach ($textures as $item) {
$item->liked = $closet->has($item->tid);
@@ -104,7 +101,7 @@ class SkinlibController extends Controller
return response()->json([
'items' => $textures,
- 'anonymous' => $anonymous,
+ 'current_uid' => $currentUser ? $currentUser->uid : 0,
'total_pages' => $totalPages
]);
}
diff --git a/resources/assets/src/components/route.js b/resources/assets/src/components/route.js
index b133ac7c..99967821 100644
--- a/resources/assets/src/components/route.js
+++ b/resources/assets/src/components/route.js
@@ -54,4 +54,9 @@ export default [
component: () => import('./auth/Reset'),
el: 'form'
},
+ {
+ path: 'skinlib',
+ component: () => import('./skinlib/List'),
+ el: '.content-wrapper'
+ },
];
diff --git a/resources/assets/src/components/skinlib/List.vue b/resources/assets/src/components/skinlib/List.vue
new file mode 100644
index 00000000..2c681044
--- /dev/null
+++ b/resources/assets/src/components/skinlib/List.vue
@@ -0,0 +1,272 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $t('general.noResult') }}
+
+
+
+
+
+
+
+
+ {{ $t('general.loading') }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/src/components/skinlib/SkinLibItem.vue b/resources/assets/src/components/skinlib/SkinLibItem.vue
new file mode 100644
index 00000000..e0aec710
--- /dev/null
+++ b/resources/assets/src/components/skinlib/SkinLibItem.vue
@@ -0,0 +1,161 @@
+
+
+
+
+
![]()
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/src/js/utils.js b/resources/assets/src/js/utils.js
index b8da1057..37b3f23f 100644
--- a/resources/assets/src/js/utils.js
+++ b/resources/assets/src/js/utils.js
@@ -9,3 +9,20 @@ export function debounce(func, delay) {
timer = setTimeout(func, delay);
};
}
+
+/**
+ * Get parameters in query string with key.
+ *
+ * @param {string} key
+ * @param {string} defaultValue
+ * @return {string}
+ */
+export function queryString(key, defaultValue) {
+ const result = location.search.match(new RegExp('[?&]'+key+'=([^&]+)', 'i'));
+
+ if (result === null || result.length < 1){
+ return defaultValue;
+ } else {
+ return result[1];
+ }
+}
diff --git a/resources/assets/tests/components/skinlib/List.test.js b/resources/assets/tests/components/skinlib/List.test.js
new file mode 100644
index 00000000..915c9686
--- /dev/null
+++ b/resources/assets/tests/components/skinlib/List.test.js
@@ -0,0 +1,181 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import List from '@/components/skinlib/List';
+
+test('fetch data before mounting', () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ mount(List);
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 1 }
+ );
+});
+
+test('empty skin library', () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ const wrapper = mount(List);
+ expect(wrapper.text()).toContain('general.noResult');
+});
+
+test('toggle texture type', () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ window.$ = jest.fn(() => ({
+ iCheck() {}
+ }));
+ const wrapper = mount(List);
+ const breadcrumb = wrapper.find('.breadcrumb');
+
+ jest.runAllTimers();
+ expect(breadcrumb.text()).toContain('skinlib.filter.skin');
+
+ wrapper.find('[value=steve]').setChecked();
+ jest.runAllTimers();
+ expect(breadcrumb.text()).toContain('skinlib.filter.steve');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'steve', uploader: 0, sort: 'time', keyword: '', page: 1 }
+ );
+
+ wrapper.find('[value=alex]').setChecked();
+ jest.runAllTimers();
+ expect(breadcrumb.text()).toContain('skinlib.filter.alex');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'alex', uploader: 0, sort: 'time', keyword: '', page: 1 }
+ );
+
+ wrapper.find('[value=cape]').setChecked();
+ jest.runAllTimers();
+ expect(breadcrumb.text()).toContain('general.cape');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'cape', uploader: 0, sort: 'time', keyword: '', page: 1 }
+ );
+});
+
+test('check specified uploader', async () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 1
+ });
+ const wrapper = mount(List);
+ await wrapper.vm.$nextTick();
+ const breadcrumb = wrapper.find('.breadcrumb');
+ const button = wrapper.find('.btn-default');
+
+ jest.runAllTimers();
+ expect(breadcrumb.text()).toContain('skinlib.filter.allUsers');
+
+ button.trigger('click');
+ expect(breadcrumb.text()).toContain('skinlib.filter.uploader');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'skin', uploader: 1, sort: 'time', keyword: '', page: 1 }
+ );
+});
+
+test('sort items', () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ const wrapper = mount(List);
+ const sortByLikes = wrapper.find('.dropdown-menu > li:nth-child(1) > a');
+ const sortByTime = wrapper.find('.dropdown-menu > li:nth-child(2) > a');
+ jest.runAllTimers();
+
+ sortByLikes.trigger('click');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'skin', uploader: 0, sort: 'likes', keyword: '', page: 1 }
+ );
+ expect(wrapper.text()).toContain('skinlib.sort.likes');
+ jest.runAllTimers();
+
+ sortByTime.trigger('click');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 1 }
+ );
+ expect(wrapper.text()).toContain('skinlib.sort.time');
+});
+
+test('search by keyword', () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ const wrapper = mount(List);
+ const searchBox = wrapper.find('input[type=text]');
+ jest.runAllTimers();
+
+ searchBox.setValue('a');
+ wrapper.find('form').trigger('submit');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'skin', uploader: 0, sort: 'time', keyword: 'a', page: 1 }
+ );
+ jest.runAllTimers();
+
+ searchBox.setValue('b');
+ wrapper.find('.input-group-btn > button').trigger('click');
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'skin', uploader: 0, sort: 'time', keyword: 'b', page: 1 }
+ );
+});
+
+test('reset all filters', async () => {
+ window.$ = jest.fn(() => ({
+ iCheck() {}
+ }));
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ const wrapper = mount(List);
+ jest.runAllTimers();
+ wrapper.find('input[value=cape]').setChecked();
+ jest.runAllTimers();
+ wrapper.find('input[type=text]').setValue('abc');
+ jest.runAllTimers();
+ wrapper.find('.dropdown-menu > li:nth-child(1) > a').trigger('click');
+ jest.runAllTimers();
+
+ Vue.prototype.$http.get.mockClear();
+ wrapper.find('.btn-warning').trigger('click');
+ expect(Vue.prototype.$http.get).toHaveBeenCalledTimes(1);
+});
+
+test('is anonymous', () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ const wrapper = mount(List);
+ expect(wrapper.vm.anonymous).toBeTrue();
+});
+
+test('on page changed', () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [], total_pages: 0, current_uid: 0
+ });
+ const wrapper = mount(List);
+ jest.runAllTimers();
+ wrapper.vm.pageChanged(2);
+ expect(Vue.prototype.$http.get).toBeCalledWith(
+ '/skinlib/data',
+ { filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 2 }
+ );
+});
+
+test('on like toggled', async () => {
+ Vue.prototype.$http.get.mockResolvedValue({
+ items: [{ tid: 1, liked: false }], total_pages: 1, current_uid: 0
+ });
+ const wrapper = mount(List);
+ await wrapper.vm.$nextTick();
+ wrapper.vm.onLikeToggled(0, true);
+ expect(wrapper.vm.items[0].liked).toBeTrue();
+});
diff --git a/resources/assets/tests/components/skinlib/SkinLibItem.test.js b/resources/assets/tests/components/skinlib/SkinLibItem.test.js
new file mode 100644
index 00000000..25d9e099
--- /dev/null
+++ b/resources/assets/tests/components/skinlib/SkinLibItem.test.js
@@ -0,0 +1,131 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import SkinLibItem from '@/components/skinlib/SkinLibItem';
+import { flushPromises } from '../../utils';
+import { swal } from '@/js/notify';
+import toastr from 'toastr';
+
+jest.mock('@/js/notify');
+jest.mock('toastr');
+
+test('urls', () => {
+ const wrapper = mount(SkinLibItem, {
+ propsData: { tid: 1 }
+ });
+ expect(wrapper.find('a').attributes()).toHaveProperty(
+ 'href',
+ '/skinlib/show/1'
+ );
+ expect(wrapper.find('img').attributes()).toHaveProperty(
+ 'src',
+ '/preview/1.png'
+ );
+});
+
+test('render basic information', () => {
+ const wrapper = mount(SkinLibItem, {
+ propsData: {
+ tid: 1,
+ name: 'test',
+ type: 'steve',
+ }
+ });
+ expect(wrapper.text()).toContain('test');
+ expect(wrapper.text()).toContain('skinlib.filter.steve');
+});
+
+test('anonymous user', () => {
+ const wrapper = mount(SkinLibItem, {
+ propsData: { anonymous: true }
+ });
+ const button = wrapper.find('.more');
+ expect(button.attributes().title).toBe('skinlib.anonymous');
+ button.trigger('click');
+ expect(Vue.prototype.$http.post).not.toBeCalled();
+});
+
+test('private texture', () => {
+ const wrapper = mount(SkinLibItem, {
+ propsData: { isPublic: false }
+ });
+ expect(wrapper.text()).toContain('skinlib.private');
+
+ wrapper.setProps({ isPublic: true });
+ expect(wrapper.text()).not.toContain('skinlib.private');
+});
+
+test('liked state', () => {
+ const wrapper = mount(SkinLibItem, {
+ propsData: { liked: true, anonymous: false }
+ });
+ const button = wrapper.find('.like');
+
+ expect(button.attributes()).toHaveProperty('title', 'skinlib.removeFromCloset');
+ expect(button.classes()).toContain('liked');
+
+ wrapper.setProps({ liked: false });
+ expect(button.attributes()).toHaveProperty('title', 'skinlib.addToCloset');
+ expect(button.classes()).not.toContain('liked');
+});
+
+test('remove from closet', async () => {
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1' })
+ .mockResolvedValue({ errno: 0 });
+ swal.mockResolvedValueOnce({ dismiss: 1 })
+ .mockResolvedValue({});
+ jest.spyOn(toastr, 'warning');
+ const wrapper = mount(SkinLibItem, {
+ propsData: { tid: 1, liked: true, anonymous: false }
+ });
+ const button = wrapper.find('.like');
+
+ button.trigger('click');
+ expect(Vue.prototype.$http.post).not.toBeCalled();
+
+ button.trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/user/closet/remove',
+ { tid: 1 }
+ );
+ expect(toastr.warning).toBeCalledWith('1');
+
+ button.trigger('click');
+ await flushPromises();
+ expect(wrapper.emitted('like-toggled')[0]).toEqual([false]);
+});
+
+test('add to closet', async () => {
+ Vue.prototype.$http.post
+ .mockResolvedValueOnce({ errno: 1, msg: '1' })
+ .mockResolvedValue({ errno: 0 });
+ swal.mockImplementationOnce(() => ({ dismiss: 1 }))
+ .mockImplementation(({ inputValidator }) => {
+ if (inputValidator) {
+ inputValidator();
+ inputValidator('name');
+ }
+ return { value: 'name' };
+ });
+ jest.spyOn(toastr, 'warning');
+ const wrapper = mount(SkinLibItem, {
+ propsData: { tid: 1, liked: false, anonymous: false }
+ });
+ const button = wrapper.find('.like');
+
+ button.trigger('click');
+ expect(Vue.prototype.$http.post).not.toBeCalled();
+
+ button.trigger('click');
+ await flushPromises();
+ expect(Vue.prototype.$http.post).toBeCalledWith(
+ '/user/closet/add',
+ { tid: 1, name: 'name' }
+ );
+ expect(toastr.warning).toBeCalledWith('1');
+
+ button.trigger('click');
+ await flushPromises();
+ expect(wrapper.emitted('like-toggled')[0]).toEqual([true]);
+});
diff --git a/resources/lang/en/front-end.yml b/resources/lang/en/front-end.yml
index 4d1ddfb3..fcfacd3e 100644
--- a/resources/lang/en/front-end.yml
+++ b/resources/lang/en/front-end.yml
@@ -35,12 +35,16 @@ auth:
reset-button: Reset
skinlib:
+ reset: Reset Filter
+ nowShowing: Now showing
addToCloset: Add to closet
removeFromCloset: Remove from closet
setItemName: Set a name for this texture
emptyItemName: Empty texture name.
setNewTextureName: 'Please enter the new texture name:'
emptyNewTextureName: Empty new texture name.
+ anyModels: Any Models
+ seeMyUpload: Check My Upload
filter:
skin: (Any Model)
steve: (Steve)
@@ -49,6 +53,7 @@ skinlib:
uploader: 'User (UID = :uid) Uploaded'
allUsers: All Users
sort:
+ title: Sort
time: Newestly Uploaded
likes: Most Likes
badSkinSize: The size of selected skin file is not valid
@@ -276,6 +281,8 @@ general:
rotation: Rotation
pause: Pause
reset: Reset
+ skinlib: Skin Library
+ loading: Loading
user:
email: Email
nickname: Nick Name
diff --git a/resources/lang/zh_CN/front-end.yml b/resources/lang/zh_CN/front-end.yml
index 178554d0..8b7c78d3 100644
--- a/resources/lang/zh_CN/front-end.yml
+++ b/resources/lang/zh_CN/front-end.yml
@@ -35,12 +35,16 @@ auth:
reset-button: 重置
skinlib:
+ reset: 清除筛选
+ nowShowing: 当前正显示
addToCloset: 添加至衣柜
removeFromCloset: 从衣柜中移除
setItemName: 给你的皮肤起个名字吧~
emptyItemName: 你还没有填写要收藏的材质名称啊
anonymous: 请先登录
private: 私密
+ anyModels: 任意模型
+ seeMyUpload: 查看我上传的
filter:
skin: (任意模型)
steve: (Steve)
@@ -49,6 +53,7 @@ skinlib:
uploader: '用户(UID = :uid)上传'
allUsers: 所有用户
sort:
+ title: 排序
time: 最新上传
likes: 最多收藏
badSkinSize: 所选皮肤文件的尺寸不对哦
@@ -270,6 +275,8 @@ general:
rotation: 旋转
pause: 暂停
reset: 重置
+ skinlib: 皮肤库
+ loading: 正在加载
user:
email: 邮箱
nickname: 昵称
diff --git a/resources/views/skinlib/index.tpl b/resources/views/skinlib/index.tpl
index d83073cd..5463afa9 100644
--- a/resources/views/skinlib/index.tpl
+++ b/resources/views/skinlib/index.tpl
@@ -3,41 +3,5 @@
@section('title', trans('general.skinlib'))
@section('content')
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- @lang('general.loading')
-
-
-
-
-
+
@endsection
diff --git a/resources/views/skinlib/master.tpl b/resources/views/skinlib/master.tpl
index 635173f9..2563b9d9 100644
--- a/resources/views/skinlib/master.tpl
+++ b/resources/views/skinlib/master.tpl
@@ -37,60 +37,12 @@
@lang('general.my-closet')
-
- @unless (isset($with_out_filter))
-
-
-
- @lang('skinlib.general.filter')
-
-
-
-
-
-
-
- @lang('skinlib.general.sort')
-
-
-
- @endunless
-
- @unless (isset($with_out_filter))
-
- @endunless