rewrite skin library with React
This commit is contained in:
parent
1d6da0eab9
commit
c219a0f03f
|
|
@ -59,6 +59,11 @@ class ClosetController extends Controller
|
||||||
->paginate(6);
|
->paginate(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function allIds()
|
||||||
|
{
|
||||||
|
return auth()->user()->closet()->pluck('texture_tid');
|
||||||
|
}
|
||||||
|
|
||||||
public function add(Request $request)
|
public function add(Request $request)
|
||||||
{
|
{
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,12 @@ use App\Models\Texture;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Auth;
|
use Auth;
|
||||||
use Blessing\Filter;
|
use Blessing\Filter;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Option;
|
use Option;
|
||||||
use Parsedown;
|
use Parsedown;
|
||||||
use Session;
|
|
||||||
use Storage;
|
use Storage;
|
||||||
use View;
|
|
||||||
|
|
||||||
class SkinlibController extends Controller
|
class SkinlibController extends Controller
|
||||||
{
|
{
|
||||||
|
|
@ -35,80 +34,42 @@ class SkinlibController extends Controller
|
||||||
8 => 'A PHP extension stopped the file upload.',
|
8 => 'A PHP extension stopped the file upload.',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
public function library(Request $request)
|
||||||
* Get skin library data filtered.
|
|
||||||
* Available Query String: filter, uploader, page, sort, keyword, items_per_page.
|
|
||||||
*/
|
|
||||||
public function getSkinlibFiltered(Request $request)
|
|
||||||
{
|
{
|
||||||
$user = Auth::user();
|
$user = Auth::user();
|
||||||
|
|
||||||
// Available filters: skin, steve, alex, cape
|
// Available filters: skin, steve, alex, cape
|
||||||
$filter = $request->input('filter', 'skin');
|
$type = $request->input('filter', 'skin');
|
||||||
|
$uploader = $request->input('uploader');
|
||||||
// Filter result by uploader's uid
|
$keyword = $request->input('keyword');
|
||||||
$uploader = intval($request->input('uploader', 0));
|
|
||||||
|
|
||||||
// Current page
|
|
||||||
$page = $request->input('page', 1);
|
|
||||||
$currentPage = ($page <= 0) ? 1 : $page;
|
|
||||||
|
|
||||||
// How many items to show in one page
|
|
||||||
$itemsPerPage = $request->input('items_per_page', 20);
|
|
||||||
$itemsPerPage = $itemsPerPage <= 0 ? 20 : $itemsPerPage;
|
|
||||||
|
|
||||||
// Keyword to search
|
|
||||||
$keyword = $request->input('keyword', '');
|
|
||||||
|
|
||||||
if ($filter == 'skin') {
|
|
||||||
$query = Texture::where(function ($innerQuery) {
|
|
||||||
// Nested condition, DO NOT MODIFY
|
|
||||||
$innerQuery->where('type', 'steve')->orWhere('type', 'alex');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
$query = Texture::where('type', $filter);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($keyword !== '') {
|
|
||||||
$query = $query->like('name', $keyword);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($uploader !== 0) {
|
|
||||||
$query = $query->where('uploader', $uploader);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$user) {
|
|
||||||
// Show public textures only to anonymous visitors
|
|
||||||
$query = $query->where('public', true);
|
|
||||||
} else {
|
|
||||||
// Show private textures when show uploaded textures of current user
|
|
||||||
if ($uploader != $user->uid && !$user->isAdmin()) {
|
|
||||||
$query = $query->where(function ($innerQuery) use ($user) {
|
|
||||||
$innerQuery->where('public', true)->orWhere('uploader', '=', $user->uid);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$totalPages = ceil($query->count() / $itemsPerPage);
|
|
||||||
|
|
||||||
$sort = $request->input('sort', 'time');
|
$sort = $request->input('sort', 'time');
|
||||||
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
|
$sortBy = $sort == 'time' ? 'upload_at' : $sort;
|
||||||
$query = $query->orderBy($sortBy, 'desc');
|
|
||||||
|
|
||||||
$textures = $query->skip(($currentPage - 1) * $itemsPerPage)->take($itemsPerPage)->get();
|
return Texture::orderBy($sortBy, 'desc')
|
||||||
|
->when($type === 'skin', function (Builder $query) {
|
||||||
if ($user) {
|
return $query->whereIn('type', ['steve', 'alex']);
|
||||||
$closet = $user->closet()->get();
|
}, function (Builder $query) use ($type) {
|
||||||
foreach ($textures as $item) {
|
return $query->where('type', $type);
|
||||||
$item->liked = $closet->contains('tid', $item->tid);
|
})
|
||||||
}
|
->when($keyword, function (Builder $query, $keyword) {
|
||||||
}
|
return $query->like('name', $keyword);
|
||||||
|
})
|
||||||
return json('', 0, [
|
->when($uploader, function (Builder $query, $uploader) {
|
||||||
'items' => $textures,
|
return $query->where('uploader', $uploader);
|
||||||
'current_uid' => $user ? $user->uid : 0,
|
})
|
||||||
'total_pages' => $totalPages,
|
->when($user, function (Builder $query, User $user) {
|
||||||
]);
|
if (!$user->isAdmin()) {
|
||||||
|
return $query
|
||||||
|
->where('public', true)
|
||||||
|
->orWhere('uploader', $user->uid);
|
||||||
|
}
|
||||||
|
}, function (Builder $query) {
|
||||||
|
// show public textures only to anonymous visitors
|
||||||
|
return $query->where('public', true);
|
||||||
|
})
|
||||||
|
->join('users', 'uid', 'uploader')
|
||||||
|
->select(['tid', 'name', 'type', 'uploader', 'public', 'likes', 'nickname'])
|
||||||
|
->paginate(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function show(Filter $filter, $tid)
|
public function show(Filter $filter, $tid)
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@ class Texture extends Model
|
||||||
return $query->where($field, 'LIKE', "%$value%");
|
return $query->where($field, 'LIKE', "%$value%");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function owner()
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class, 'uploader');
|
||||||
|
}
|
||||||
|
|
||||||
public function likers()
|
public function likers()
|
||||||
{
|
{
|
||||||
return $this->belongsToMany(User::class, 'user_closet')->withPivot('item_name');
|
return $this->belongsToMany(User::class, 'user_closet')->withPivot('item_name');
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@
|
||||||
"vue": "^2.6.11",
|
"vue": "^2.6.11",
|
||||||
"vue-good-table": "^2.18.1",
|
"vue-good-table": "^2.18.1",
|
||||||
"vue-recaptcha": "^1.2.0",
|
"vue-recaptcha": "^1.2.0",
|
||||||
"vuejs-paginate": "^2.1.0",
|
|
||||||
"xterm": "^4.4.0",
|
"xterm": "^4.4.0",
|
||||||
"xterm-addon-fit": "^0.3.0"
|
"xterm-addon-fit": "^0.3.0"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,112 +0,0 @@
|
||||||
<template>
|
|
||||||
<a class="ml-3 mr-2 mb-2" :href="urlToDetail">
|
|
||||||
<div class="card skinlib-item">
|
|
||||||
<div class="card-body texture-img">
|
|
||||||
<div v-if="!isPublic" class="ribbon-wrapper">
|
|
||||||
<div class="ribbon bg-pink">{{ $t('skinlib.private') }}</div>
|
|
||||||
</div>
|
|
||||||
<img class="card-img-top" :src="urlToPreview">
|
|
||||||
</div>
|
|
||||||
<div class="card-footer pb-0 pt-2 pl-1 pr-1">
|
|
||||||
<div class="container d-flex justify-content-between">
|
|
||||||
<p>
|
|
||||||
<span :title="name">{{ name|truncate }}
|
|
||||||
<small>{{ $t('skinlib.filter.' + type) }}</small>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a
|
|
||||||
:title="likeActionText"
|
|
||||||
class="btn-like"
|
|
||||||
:class="{ liked }"
|
|
||||||
href="#"
|
|
||||||
@click.stop="toggleLiked"
|
|
||||||
>
|
|
||||||
<i class="fas fa-heart" />
|
|
||||||
<span>{{ likes }}</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import addClosetItem from './mixins/addClosetItem'
|
|
||||||
import removeClosetItem from './mixins/removeClosetItem'
|
|
||||||
import truncateText from './mixins/truncateText'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SkinLibItem',
|
|
||||||
mixins: [
|
|
||||||
addClosetItem,
|
|
||||||
removeClosetItem,
|
|
||||||
truncateText,
|
|
||||||
],
|
|
||||||
props: {
|
|
||||||
tid: Number,
|
|
||||||
name: String,
|
|
||||||
type: {
|
|
||||||
validator: value => ['steve', 'alex', 'cape'].includes(value),
|
|
||||||
},
|
|
||||||
liked: Boolean,
|
|
||||||
likes: Number,
|
|
||||||
anonymous: Boolean,
|
|
||||||
isPublic: Boolean, // `public` is a reserved keyword
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
urlToDetail() {
|
|
||||||
return `${blessing.base_url}/skinlib/show/${this.tid}`
|
|
||||||
},
|
|
||||||
urlToPreview() {
|
|
||||||
return `${blessing.base_url}/preview/${this.tid}?height=150`
|
|
||||||
},
|
|
||||||
likeActionText() {
|
|
||||||
if (this.anonymous) {
|
|
||||||
return this.$t('skinlib.anonymous')
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.liked
|
|
||||||
? this.$t('skinlib.removeFromCloset')
|
|
||||||
: this.$t('skinlib.addToCloset')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
toggleLiked() {
|
|
||||||
if (this.anonymous) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.liked) {
|
|
||||||
this.removeFromCloset()
|
|
||||||
} else {
|
|
||||||
this.addClosetItem()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async removeFromCloset() {
|
|
||||||
this.$once('item-removed', () => this.$emit('like-toggled', false))
|
|
||||||
await this.removeClosetItem()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.skinlib-item
|
|
||||||
width 245px
|
|
||||||
transition-property box-shadow
|
|
||||||
transition-duration 0.3s
|
|
||||||
&:hover
|
|
||||||
box-shadow 0 .5rem 1rem rgba(0, 0, 0, 0.15)
|
|
||||||
|
|
||||||
.texture-img
|
|
||||||
background #eff1f0
|
|
||||||
img
|
|
||||||
height 210px
|
|
||||||
|
|
||||||
.btn-like
|
|
||||||
color #6c757d
|
|
||||||
&.liked, &:hover
|
|
||||||
color #e0353b
|
|
||||||
</style>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import { showModal, toast } from '../../scripts/notify'
|
|
||||||
import { truthy } from '../../scripts/validators'
|
|
||||||
|
|
||||||
export default Vue.extend<{
|
|
||||||
name: string
|
|
||||||
tid: number
|
|
||||||
}, { addClosetItem(): Promise<void> }, {}>({
|
|
||||||
methods: {
|
|
||||||
async addClosetItem() {
|
|
||||||
let value: string
|
|
||||||
try {
|
|
||||||
({ value } = await showModal({
|
|
||||||
mode: 'prompt',
|
|
||||||
title: this.$t('skinlib.setItemName'),
|
|
||||||
text: this.$t('skinlib.applyNotice'),
|
|
||||||
input: this.name,
|
|
||||||
validator: truthy(this.$t('skinlib.emptyItemName')),
|
|
||||||
}))
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, message } = await this.$http.post(
|
|
||||||
'/user/closet/add',
|
|
||||||
{ tid: this.tid, name: value },
|
|
||||||
)
|
|
||||||
if (code === 0) {
|
|
||||||
toast.success(message!)
|
|
||||||
this.$emit('like-toggled', true)
|
|
||||||
} else {
|
|
||||||
toast.error(message!)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import { showModal, toast } from '../../scripts/notify'
|
|
||||||
|
|
||||||
export default Vue.extend<{
|
|
||||||
name: string
|
|
||||||
tid: number
|
|
||||||
}, { removeClosetItem(): Promise<void> }, {}>({
|
|
||||||
methods: {
|
|
||||||
async removeClosetItem() {
|
|
||||||
try {
|
|
||||||
await showModal({
|
|
||||||
text: this.$t('user.removeFromClosetNotice'),
|
|
||||||
okButtonType: 'danger',
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { code, message } = await this.$http.post(`/user/closet/remove/${this.tid}`)
|
|
||||||
if (code === 0) {
|
|
||||||
this.$emit('item-removed')
|
|
||||||
toast.success(message!)
|
|
||||||
} else {
|
|
||||||
toast.error(message!)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
|
|
||||||
export default Vue.extend({
|
|
||||||
filters: {
|
|
||||||
truncate(text: string = ''): string {
|
|
||||||
return text.length > 15 ? `${text.slice(0, 15)}...` : text
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
@ -111,7 +111,7 @@ export default [
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'skinlib',
|
path: 'skinlib',
|
||||||
component: () => import('../views/skinlib/List.vue'),
|
react: () => import('../views/skinlib/SkinLibrary'),
|
||||||
el: '.content-wrapper',
|
el: '.content-wrapper',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
export function queryString(key: string, defaultValue: string = ''): string {
|
|
||||||
const result = new RegExp(`[?&]${key}=([^&]+)`, 'i').exec(location.search)
|
|
||||||
|
|
||||||
if (result === null || result.length < 1) {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return result[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
export function queryStringify(params: { [key: string]: string }): string {
|
|
||||||
return Object.keys(params)
|
|
||||||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
|
|
||||||
.join('&')
|
|
||||||
}
|
|
||||||
|
|
@ -1,305 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="content-wrapper">
|
|
||||||
<div class="container">
|
|
||||||
<!-- Content Header (Page header) -->
|
|
||||||
<div class="content-header">
|
|
||||||
<div class="container-fluid d-flex justify-content-between flex-wrap">
|
|
||||||
<h1>{{ $t('general.skinlib') }}</h1>
|
|
||||||
<ol class="breadcrumb float-sm-right">
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<i class="fas fa-tags" /> {{ $t('skinlib.nowShowing') }}
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<span v-if="filter === 'cape'" v-t="'general.cape'" />
|
|
||||||
<span v-else>
|
|
||||||
{{ $t('general.skin') }}
|
|
||||||
{{ $t('skinlib.filter.' + filter) }}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item">{{ uploaderIndicator }}</li>
|
|
||||||
<li class="breadcrumb-item active">{{ sortIndicator }}</li>
|
|
||||||
</ol>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content -->
|
|
||||||
<section class="content">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="form-group pt-0 mb-3 d-flex justify-content-between">
|
|
||||||
<form @submit.prevent="submitSearch">
|
|
||||||
<div class="input-group">
|
|
||||||
<div class="input-group-prepend">
|
|
||||||
<button
|
|
||||||
class="btn btn-default dropdown-toggle"
|
|
||||||
type="button"
|
|
||||||
data-toggle="dropdown"
|
|
||||||
>
|
|
||||||
{{ filterText }}
|
|
||||||
</button>
|
|
||||||
<div class="dropdown-menu">
|
|
||||||
<button
|
|
||||||
class="dropdown-item"
|
|
||||||
:class="{ active: filter === 'skin' }"
|
|
||||||
@click="filter = 'skin'"
|
|
||||||
>
|
|
||||||
{{ $t('general.skin') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="dropdown-item"
|
|
||||||
:class="{ active: filter === 'steve' }"
|
|
||||||
@click="filter = 'steve'"
|
|
||||||
>
|
|
||||||
Steve
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="dropdown-item"
|
|
||||||
:class="{ active: filter === 'alex' }"
|
|
||||||
@click="filter = 'alex'"
|
|
||||||
>
|
|
||||||
Alex
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="dropdown-item"
|
|
||||||
:class="{ active: filter === 'cape' }"
|
|
||||||
@click="filter = 'cape'"
|
|
||||||
>
|
|
||||||
{{ $t('general.cape') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
v-model="keyword"
|
|
||||||
class="form-control"
|
|
||||||
type="text"
|
|
||||||
data-test="keyword"
|
|
||||||
:placeholder="$t('vendor.datatable.search')"
|
|
||||||
>
|
|
||||||
<div class="input-group-append">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary pl-3 pr-3"
|
|
||||||
data-test="btn-search"
|
|
||||||
@click="submitSearch"
|
|
||||||
>
|
|
||||||
{{ $t('general.submit') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="d-none d-sm-block">
|
|
||||||
<div class="btn-group">
|
|
||||||
<button
|
|
||||||
class="btn bg-olive"
|
|
||||||
:class="{ active: sort === 'likes' }"
|
|
||||||
@click="sort = 'likes'"
|
|
||||||
>
|
|
||||||
{{ $t('skinlib.sort.likes') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn bg-olive"
|
|
||||||
:class="{ active: sort === 'time' }"
|
|
||||||
@click="sort = 'time'"
|
|
||||||
>
|
|
||||||
{{ $t('skinlib.sort.time') }}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn bg-olive"
|
|
||||||
:class="{ active: uploader === currentUid }"
|
|
||||||
@click="uploader = currentUid"
|
|
||||||
>
|
|
||||||
{{ $t('skinlib.seeMyUpload') }}
|
|
||||||
</button>
|
|
||||||
<button class="btn bg-olive" @click="reset">
|
|
||||||
{{ $t('skinlib.reset') }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="items.length" class="d-flex flex-wrap">
|
|
||||||
<skin-lib-item
|
|
||||||
v-for="(item, index) in items"
|
|
||||||
:key="item.tid"
|
|
||||||
:tid="item.tid"
|
|
||||||
:name="item.name"
|
|
||||||
:type="item.type"
|
|
||||||
:liked="item.liked"
|
|
||||||
:likes="item.likes"
|
|
||||||
:is-public="item.public"
|
|
||||||
:anonymous="anonymous"
|
|
||||||
@like-toggled="onLikeToggled(index, $event)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p v-else class="text-center m-5">
|
|
||||||
{{ $t('general.noResult') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="box-footer">
|
|
||||||
<paginate
|
|
||||||
v-model="page"
|
|
||||||
:page-count="totalPages"
|
|
||||||
class="float-right mr-3"
|
|
||||||
container-class="pagination pagination-sm no-margin"
|
|
||||||
page-class="page-item"
|
|
||||||
page-link-class="page-link"
|
|
||||||
prev-class="page-item"
|
|
||||||
prev-link-class="page-link"
|
|
||||||
next-class="page-item"
|
|
||||||
next-link-class="page-link"
|
|
||||||
first-button-text="«"
|
|
||||||
prev-text="‹"
|
|
||||||
next-text="›"
|
|
||||||
last-button-text="»"
|
|
||||||
:click-handler="pageChanged"
|
|
||||||
:first-last-button="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-show="pending" class="overlay">
|
|
||||||
<span>
|
|
||||||
<i class="fas fa-sync-alt fa-spin" />
|
|
||||||
{{ $t('general.loading') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import Paginate from 'vuejs-paginate'
|
|
||||||
import { queryString, queryStringify } from '../../scripts/utils'
|
|
||||||
import SkinLibItem from '../../components/SkinLibItem.vue'
|
|
||||||
import emitMounted from '../../components/mixins/emitMounted'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
name: 'SkinLibrary',
|
|
||||||
components: {
|
|
||||||
Paginate,
|
|
||||||
SkinLibItem,
|
|
||||||
},
|
|
||||||
mixins: [
|
|
||||||
emitMounted,
|
|
||||||
],
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
filter: queryString('filter', 'skin'),
|
|
||||||
uploader: +queryString('uploader', 0),
|
|
||||||
sort: queryString('sort', 'time'),
|
|
||||||
keyword: decodeURIComponent(queryString('keyword', '')),
|
|
||||||
page: +queryString('page', 1),
|
|
||||||
items: [],
|
|
||||||
totalPages: 0,
|
|
||||||
currentUid: 0,
|
|
||||||
pending: false,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
anonymous() {
|
|
||||||
return !this.currentUid
|
|
||||||
},
|
|
||||||
uploaderIndicator() {
|
|
||||||
return this.uploader
|
|
||||||
? this.$t('skinlib.filter.uploader', { uid: this.uploader })
|
|
||||||
: this.$t('skinlib.filter.allUsers')
|
|
||||||
},
|
|
||||||
filterText() {
|
|
||||||
switch (this.filter) {
|
|
||||||
case 'steve':
|
|
||||||
return 'Steve'
|
|
||||||
case 'alex':
|
|
||||||
return 'Alex'
|
|
||||||
default:
|
|
||||||
return this.$t(`general.${this.filter}`)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sortIndicator() {
|
|
||||||
return this.$t(`skinlib.sort.${this.sort}`)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
filter() {
|
|
||||||
this.fetchData()
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
uploader() {
|
|
||||||
this.fetchData()
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
sort() {
|
|
||||||
this.fetchData()
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
},
|
|
||||||
beforeMount() {
|
|
||||||
this.fetchData()
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
async fetchData() {
|
|
||||||
this.pending = true
|
|
||||||
const {
|
|
||||||
data: {
|
|
||||||
items, total_pages: totalPages, current_uid: currentUid,
|
|
||||||
},
|
|
||||||
} = await this.$http.get(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: this.filter,
|
|
||||||
uploader: this.uploader,
|
|
||||||
sort: this.sort,
|
|
||||||
keyword: this.keyword,
|
|
||||||
page: this.page,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
this.items = items
|
|
||||||
this.totalPages = totalPages
|
|
||||||
this.currentUid = currentUid
|
|
||||||
this.pending = false
|
|
||||||
},
|
|
||||||
updateQueryString() {
|
|
||||||
const qs = queryStringify({
|
|
||||||
filter: this.filter,
|
|
||||||
uploader: this.uploader,
|
|
||||||
sort: this.sort,
|
|
||||||
keyword: this.keyword,
|
|
||||||
page: this.page,
|
|
||||||
})
|
|
||||||
window.history.pushState(null, '', `skinlib?${qs}`)
|
|
||||||
},
|
|
||||||
submitSearch() {
|
|
||||||
this.fetchData()
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
pageChanged(page) {
|
|
||||||
this.page = page
|
|
||||||
this.fetchData()
|
|
||||||
this.updateQueryString()
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
this.filter = 'skin'
|
|
||||||
this.uploader = 0
|
|
||||||
this.sort = 'time'
|
|
||||||
this.keyword = ''
|
|
||||||
this.page = 1
|
|
||||||
},
|
|
||||||
onLikeToggled(index, action) {
|
|
||||||
this.items[index].liked = action
|
|
||||||
this.items[index].likes += action ? 1 : -1
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="stylus">
|
|
||||||
.overlay span
|
|
||||||
position absolute
|
|
||||||
top 50%
|
|
||||||
left 50%
|
|
||||||
margin-left -40px
|
|
||||||
margin-top 25px
|
|
||||||
color #000
|
|
||||||
font-size 20px
|
|
||||||
</style>
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { showModal, toast } from '@/scripts/notify'
|
||||||
import { Texture } from '@/scripts/types'
|
import { Texture } from '@/scripts/types'
|
||||||
|
|
||||||
export default async function addClosetItem(
|
export default async function addClosetItem(
|
||||||
texture: Texture,
|
texture: Pick<Texture, 'tid' | 'name'>,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
let name: string
|
let name: string
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
31
resources/assets/src/views/skinlib/SkinLibrary/Button.tsx
Normal file
31
resources/assets/src/views/skinlib/SkinLibrary/Button.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
active?: boolean
|
||||||
|
bg?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attributes = React.DetailedHTMLProps<
|
||||||
|
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
HTMLButtonElement
|
||||||
|
>
|
||||||
|
|
||||||
|
const Button: React.FC<Props & Attributes> = (props) => {
|
||||||
|
const classes = [props.className ?? '']
|
||||||
|
if (props.bg) {
|
||||||
|
classes.push('btn', `bg-${props.bg}`)
|
||||||
|
}
|
||||||
|
if (props.active) {
|
||||||
|
classes.push('active')
|
||||||
|
}
|
||||||
|
|
||||||
|
const rest = { ...props, active: undefined, bg: undefined }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button {...rest} className={classes.join(' ')}>
|
||||||
|
{props.children}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Button
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import Button from './Button'
|
||||||
|
import { Filter } from './types'
|
||||||
|
import { humanizeType } from './utils'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
filter: Filter
|
||||||
|
onChange(filter: Filter): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const FilterSelector: React.FC<Props> = (props) => {
|
||||||
|
const { filter, onChange } = props
|
||||||
|
|
||||||
|
const handleSkinClick = () => onChange('skin')
|
||||||
|
const handleSteveClick = () => onChange('steve')
|
||||||
|
const handleAlexClick = () => onChange('alex')
|
||||||
|
const handleCapeClick = () => onChange('cape')
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="btn btn-default dropdown-toggle"
|
||||||
|
type="button"
|
||||||
|
data-toggle="dropdown"
|
||||||
|
>
|
||||||
|
{humanizeType(filter)}
|
||||||
|
</button>
|
||||||
|
<div className="dropdown-menu">
|
||||||
|
<Button
|
||||||
|
className="dropdown-item"
|
||||||
|
active={filter === 'skin'}
|
||||||
|
onClick={handleSkinClick}
|
||||||
|
>
|
||||||
|
{t('general.skin')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="dropdown-item"
|
||||||
|
active={filter === 'steve'}
|
||||||
|
onClick={handleSteveClick}
|
||||||
|
>
|
||||||
|
Steve
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="dropdown-item"
|
||||||
|
active={filter === 'alex'}
|
||||||
|
onClick={handleAlexClick}
|
||||||
|
>
|
||||||
|
Alex
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="dropdown-item"
|
||||||
|
active={filter === 'cape'}
|
||||||
|
onClick={handleCapeClick}
|
||||||
|
>
|
||||||
|
{t('general.cape')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterSelector
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
@use '../../../styles/utils';
|
||||||
|
|
||||||
|
.card {
|
||||||
|
width: 245px;
|
||||||
|
transition-property: box-shadow;
|
||||||
|
transition-duration: 0.3s;
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
background-color: #eff1f0;
|
||||||
|
img {
|
||||||
|
height: 210px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.truncate {
|
||||||
|
@include utils.truncate-text;
|
||||||
|
}
|
||||||
79
resources/assets/src/views/skinlib/SkinLibrary/Item.tsx
Normal file
79
resources/assets/src/views/skinlib/SkinLibrary/Item.tsx
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import { LibraryItem } from './types'
|
||||||
|
import { humanizeType } from './utils'
|
||||||
|
import styles from './Item.module.scss'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
item: LibraryItem
|
||||||
|
liked: boolean
|
||||||
|
onAdd(texture: LibraryItem): Promise<void>
|
||||||
|
onRemove(texture: LibraryItem): Promise<void>
|
||||||
|
onUploaderClick(uploader: number): void
|
||||||
|
}
|
||||||
|
|
||||||
|
const Item: React.FC<Props> = (props) => {
|
||||||
|
const { item } = props
|
||||||
|
|
||||||
|
const link = `${blessing.base_url}/skinlib/show/${item.tid}`
|
||||||
|
const preview = `${blessing.base_url}/preview/${item.tid}?height=150`
|
||||||
|
|
||||||
|
const heartColor = props.liked ? 'text-red' : 'text-gray'
|
||||||
|
|
||||||
|
const handleUploaderClick = (event: React.MouseEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
props.onUploaderClick(item.uploader)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleHeartClick = () => {
|
||||||
|
props.liked ? props.onRemove(item) : props.onAdd(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="ml-3 mr-2 mb-2">
|
||||||
|
<div className={`card ${styles.card}`}>
|
||||||
|
<div className={`card-body ${styles.image}`}>
|
||||||
|
{item.public || (
|
||||||
|
<div className="ribbon-wrapper">
|
||||||
|
<div className="ribbon bg-pink">{t('skinlib.private')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<a href={link} target="_blank">
|
||||||
|
<img src={preview} alt={item.name} className="card-img-top" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="card-footer">
|
||||||
|
<a
|
||||||
|
className={`d-block mb-1 ${styles.truncate}`}
|
||||||
|
title={item.name}
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
<div className="d-flex justify-content-between">
|
||||||
|
<div>
|
||||||
|
<span className="badge bg-teal py-1 mr-1">
|
||||||
|
{humanizeType(item.type)}
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
className="badge bg-indigo py-1"
|
||||||
|
href="#"
|
||||||
|
title={t('skinlib.show.uploader')}
|
||||||
|
onClick={handleUploaderClick}
|
||||||
|
>
|
||||||
|
{item.nickname}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<a href="#" className={heartColor} onClick={handleHeartClick}>
|
||||||
|
<i className="fas fa-heart mr-1"></i>
|
||||||
|
{item.likes}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Item
|
||||||
269
resources/assets/src/views/skinlib/SkinLibrary/index.tsx
Normal file
269
resources/assets/src/views/skinlib/SkinLibrary/index.tsx
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
import React, { useState, useEffect } from 'react'
|
||||||
|
import { hot } from 'react-hot-loader/root'
|
||||||
|
import useBlessingExtra from '@/scripts/hooks/useBlessingExtra'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { toast } from '@/scripts/notify'
|
||||||
|
import { Paginator } from '@/scripts/types'
|
||||||
|
import Loading from '@/components/Loading'
|
||||||
|
import Pagination from '@/components/Pagination'
|
||||||
|
import addClosetItem from '../Show/addClosetItem'
|
||||||
|
import removeClosetItem from '@/views/user/Closet/removeClosetItem'
|
||||||
|
import FilterSelector from './FilterSelector'
|
||||||
|
import Button from './Button'
|
||||||
|
import Item from './Item'
|
||||||
|
import { Filter, LibraryItem } from './types'
|
||||||
|
|
||||||
|
const SkinLibrary: React.FC = () => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [items, setItems] = useState<LibraryItem[]>([])
|
||||||
|
const [closet, setCloset] = useState<number[]>([])
|
||||||
|
const [filter, setFilter] = useState<Filter>('skin')
|
||||||
|
const [name, setName] = useState('')
|
||||||
|
const [keyword, setKeyword] = useState('')
|
||||||
|
const [uploader, setUploader] = useState<number | null>(0)
|
||||||
|
const [sort, setSort] = useState('time')
|
||||||
|
const [page, setPage] = useState(1)
|
||||||
|
const [totalPages, setTotalPages] = useState(1)
|
||||||
|
const currentUid = useBlessingExtra<number | null>('currentUid', null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const parseSearch = (query: string | URLSearchParams) => {
|
||||||
|
const search =
|
||||||
|
typeof query === 'string' ? new URLSearchParams(query) : query
|
||||||
|
|
||||||
|
const filter = search.get('filter') ?? ''
|
||||||
|
setFilter(
|
||||||
|
['skin', 'steve', 'alex', 'cape'].includes(filter)
|
||||||
|
? (filter as Filter)
|
||||||
|
: 'skin',
|
||||||
|
)
|
||||||
|
|
||||||
|
const keyword = decodeURIComponent(search.get('keyword') ?? '')
|
||||||
|
setName(keyword)
|
||||||
|
setKeyword(keyword)
|
||||||
|
|
||||||
|
const uploader = search.get('uploader') ?? '0'
|
||||||
|
setUploader(Number.parseInt(uploader))
|
||||||
|
|
||||||
|
setSort(search.get('sort') ?? 'time')
|
||||||
|
}
|
||||||
|
|
||||||
|
parseSearch(location.search)
|
||||||
|
|
||||||
|
const handler = (event: PopStateEvent) => {
|
||||||
|
parseSearch(event.state as URLSearchParams)
|
||||||
|
}
|
||||||
|
window.addEventListener('popstate', handler)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('popstate', handler)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getItems = async () => {
|
||||||
|
setIsLoading(true)
|
||||||
|
|
||||||
|
const search = new URLSearchParams()
|
||||||
|
search.append('filter', filter)
|
||||||
|
if (keyword) {
|
||||||
|
search.append('keyword', keyword)
|
||||||
|
}
|
||||||
|
if (uploader) {
|
||||||
|
search.append('uploader', uploader.toString())
|
||||||
|
}
|
||||||
|
search.append('sort', sort)
|
||||||
|
search.append('page', page.toString())
|
||||||
|
window.history.pushState(search, '', `?${search}`)
|
||||||
|
|
||||||
|
const result = await fetch.get<Paginator<LibraryItem>>(
|
||||||
|
'/skinlib/list',
|
||||||
|
search,
|
||||||
|
)
|
||||||
|
setItems(result.data)
|
||||||
|
setPage(result.current_page)
|
||||||
|
setTotalPages(result.last_page)
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
getItems()
|
||||||
|
}, [filter, keyword, uploader, sort, page])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getCloset = async () => {
|
||||||
|
const closet = await fetch.get<number[]>('/user/closet/ids')
|
||||||
|
setCloset(closet)
|
||||||
|
}
|
||||||
|
if (currentUid) {
|
||||||
|
getCloset()
|
||||||
|
}
|
||||||
|
}, [currentUid])
|
||||||
|
|
||||||
|
const handleFilterChange = (filter: Filter) => setFilter(filter)
|
||||||
|
|
||||||
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
setName(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFormSubmit = (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
setKeyword(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleLikesSortClick = () => setSort('likes')
|
||||||
|
const handleTimeSortClick = () => setSort('time')
|
||||||
|
const handleSelfUploadClick = () => setUploader(currentUid)
|
||||||
|
const handleResetClick = () => {
|
||||||
|
setFilter('skin')
|
||||||
|
setName('')
|
||||||
|
setKeyword('')
|
||||||
|
setSort('time')
|
||||||
|
setUploader(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleUploaderClick = (uploader: number) => setUploader(uploader)
|
||||||
|
|
||||||
|
const handleAddToCloset = async (item: LibraryItem, index: number) => {
|
||||||
|
if (!currentUid) {
|
||||||
|
toast.warning(t('skinlib.anonymous'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const ok = await addClosetItem(item)
|
||||||
|
if (ok) {
|
||||||
|
setCloset((closet) => [...closet, item.tid])
|
||||||
|
setItems((items) => {
|
||||||
|
items[index] = { ...item, likes: item.likes + 1 }
|
||||||
|
return items.slice()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveFromCloset = async (item: LibraryItem, index: number) => {
|
||||||
|
const ok = await removeClosetItem(item.tid)
|
||||||
|
if (ok) {
|
||||||
|
setCloset((closet) => closet.filter((id) => id !== item.tid))
|
||||||
|
setItems((items) => {
|
||||||
|
items[index] = { ...item, likes: item.likes - 1 }
|
||||||
|
return items.slice()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePageChange = (page: number) => setPage(page)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container">
|
||||||
|
<div className="content-header">
|
||||||
|
<div className="container-fluid d-flex justify-content-between">
|
||||||
|
<h1>{t('general.skinlib')}</h1>
|
||||||
|
<span>
|
||||||
|
{uploader ? (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-user mr-1"></i>
|
||||||
|
{t('skinlib.filter.uploader', { uid: uploader })}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<i className="fas fa-user-friends mr-1"></i>
|
||||||
|
{t('skinlib.filter.allUsers')}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<section className="content">
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="form-group pt-0 mb-3 d-flex justify-content-between">
|
||||||
|
<form onSubmit={handleFormSubmit}>
|
||||||
|
<div className="input-group">
|
||||||
|
<div className="input-group-prepend">
|
||||||
|
<FilterSelector
|
||||||
|
filter={filter}
|
||||||
|
onChange={handleFilterChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="form-control"
|
||||||
|
value={name}
|
||||||
|
placeholder={t('vendor.datatable.search')}
|
||||||
|
onChange={handleNameChange}
|
||||||
|
/>
|
||||||
|
<div className="input-group-append">
|
||||||
|
<button className="btn btn-primary px-3" type="submit">
|
||||||
|
{t('general.submit')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div className="d-none d-sm-block">
|
||||||
|
<div className="btn-group">
|
||||||
|
<Button
|
||||||
|
bg="olive"
|
||||||
|
active={sort === 'likes'}
|
||||||
|
onClick={handleLikesSortClick}
|
||||||
|
>
|
||||||
|
{t('skinlib.sort.likes')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
bg="olive"
|
||||||
|
active={sort === 'time'}
|
||||||
|
onClick={handleTimeSortClick}
|
||||||
|
>
|
||||||
|
{t('skinlib.sort.time')}
|
||||||
|
</Button>
|
||||||
|
{currentUid !== null && (
|
||||||
|
<Button
|
||||||
|
bg="olive"
|
||||||
|
active={uploader === currentUid}
|
||||||
|
onClick={handleSelfUploadClick}
|
||||||
|
>
|
||||||
|
{t('skinlib.seeMyUpload')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button bg="olive" onClick={handleResetClick}>
|
||||||
|
{t('skinlib.reset')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{items.length > 0 ? (
|
||||||
|
<div className="d-flex flex-wrap">
|
||||||
|
{items.map((item, i) => (
|
||||||
|
<Item
|
||||||
|
key={item.tid}
|
||||||
|
item={item}
|
||||||
|
liked={closet.includes(item.tid)}
|
||||||
|
onAdd={(item) => handleAddToCloset(item, i)}
|
||||||
|
onRemove={(item) => handleRemoveFromCloset(item, i)}
|
||||||
|
onUploaderClick={handleUploaderClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-center m-5">{t('general.noResult')}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="card-footer">
|
||||||
|
<div className="float-right">
|
||||||
|
<Pagination
|
||||||
|
page={page}
|
||||||
|
totalPages={totalPages}
|
||||||
|
onChange={handlePageChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="overlay">
|
||||||
|
<Loading />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default hot(SkinLibrary)
|
||||||
13
resources/assets/src/views/skinlib/SkinLibrary/types.ts
Normal file
13
resources/assets/src/views/skinlib/SkinLibrary/types.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { TextureType } from '@/scripts/types'
|
||||||
|
|
||||||
|
export type Filter = 'skin' | TextureType
|
||||||
|
|
||||||
|
export type LibraryItem = {
|
||||||
|
tid: number
|
||||||
|
name: string
|
||||||
|
type: TextureType
|
||||||
|
uploader: number
|
||||||
|
public: boolean
|
||||||
|
likes: number
|
||||||
|
nickname: string
|
||||||
|
}
|
||||||
13
resources/assets/src/views/skinlib/SkinLibrary/utils.ts
Normal file
13
resources/assets/src/views/skinlib/SkinLibrary/utils.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import { Filter } from './types'
|
||||||
|
|
||||||
|
export function humanizeType(type: Filter): string {
|
||||||
|
switch (type) {
|
||||||
|
case 'steve':
|
||||||
|
return 'Steve'
|
||||||
|
case 'alex':
|
||||||
|
return 'Alex'
|
||||||
|
default:
|
||||||
|
return t(`general.${type}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,113 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { flushPromises } from '../utils'
|
|
||||||
import { showModal, toast } from '@/scripts/notify'
|
|
||||||
import SkinLibItem from '@/components/SkinLibItem.vue'
|
|
||||||
|
|
||||||
jest.mock('@/scripts/notify')
|
|
||||||
|
|
||||||
test('urls', () => {
|
|
||||||
const wrapper = mount(SkinLibItem, {
|
|
||||||
propsData: { tid: 1 },
|
|
||||||
})
|
|
||||||
expect(wrapper.find('a').attributes('href')).toBe('/skinlib/show/1')
|
|
||||||
expect(wrapper.find('img').attributes('src')).toBe('/preview/1?height=150')
|
|
||||||
})
|
|
||||||
|
|
||||||
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('.btn-like')
|
|
||||||
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('.btn-like')
|
|
||||||
|
|
||||||
expect(button.attributes('title')).toBe('skinlib.removeFromCloset')
|
|
||||||
expect(button.classes('liked')).toBeTrue()
|
|
||||||
|
|
||||||
wrapper.setProps({ liked: false })
|
|
||||||
expect(button.attributes('title')).toBe('skinlib.addToCloset')
|
|
||||||
expect(button.classes('liked')).toBeFalse()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('remove from closet', async () => {
|
|
||||||
Vue.prototype.$http.post.mockResolvedValue({ code: 0 })
|
|
||||||
showModal.mockResolvedValue({ value: '' })
|
|
||||||
const wrapper = mount(SkinLibItem, {
|
|
||||||
propsData: {
|
|
||||||
tid: 1, liked: true, anonymous: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
wrapper.find('.btn-like').trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.emitted('like-toggled')[0]).toEqual([false])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('add to closet', async () => {
|
|
||||||
Vue.prototype.$http.post
|
|
||||||
.mockResolvedValueOnce({ code: 1, message: '1' })
|
|
||||||
.mockResolvedValue({ code: 0 })
|
|
||||||
showModal
|
|
||||||
.mockRejectedValueOnce(null)
|
|
||||||
.mockResolvedValue({ value: 'name' })
|
|
||||||
const wrapper = mount(SkinLibItem, {
|
|
||||||
propsData: {
|
|
||||||
tid: 1, liked: false, anonymous: false,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const button = wrapper.find('.btn-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(toast.error).toBeCalledWith('1')
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
await flushPromises()
|
|
||||||
expect(wrapper.emitted('like-toggled')[0]).toEqual([true])
|
|
||||||
})
|
|
||||||
|
|
||||||
test('truncate too long texture name', () => {
|
|
||||||
const wrapper = mount(SkinLibItem, {
|
|
||||||
propsData: {
|
|
||||||
name: 'very-very-long-texture-name',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
expect(wrapper.text()).toContain('very-very-long-...')
|
|
||||||
})
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import * as utils from '@/scripts/utils'
|
|
||||||
|
|
||||||
test('queryString', () => {
|
|
||||||
history.pushState({}, 'page', `${location.href}?key=value`)
|
|
||||||
expect(utils.queryString('key')).toBe('value')
|
|
||||||
expect(utils.queryString('a')).toBe('')
|
|
||||||
expect(utils.queryString('a', 'b')).toBe('b')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('queryStringify', () => {
|
|
||||||
expect(utils.queryStringify({ a: 'b', c: 'd' })).toBe('a=b&c=d')
|
|
||||||
})
|
|
||||||
|
|
@ -1,262 +0,0 @@
|
||||||
import Vue from 'vue'
|
|
||||||
import { mount } from '@vue/test-utils'
|
|
||||||
import { flushPromises } from '../../utils'
|
|
||||||
import { trans } from '@/scripts/i18n'
|
|
||||||
import { queryString } from '@/scripts/utils'
|
|
||||||
import List from '@/views/skinlib/List.vue'
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
window.history.pushState(null, '', 'skinlib')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('fetch data before mounting', () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
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({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount(List)
|
|
||||||
expect(wrapper.text()).toContain(trans('general.noResult'))
|
|
||||||
})
|
|
||||||
|
|
||||||
test('toggle texture type', () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount(List)
|
|
||||||
const options = wrapper.findAll('.dropdown-item')
|
|
||||||
const btnSkin = options.at(0)
|
|
||||||
const btnSteve = options.at(1)
|
|
||||||
const btnAlex = options.at(2)
|
|
||||||
const btnCape = options.at(3)
|
|
||||||
const dropdownToggle = wrapper.find('.dropdown-toggle')
|
|
||||||
const breadcrumb = wrapper.find('.breadcrumb')
|
|
||||||
|
|
||||||
expect(btnSkin.classes()).toContain('active')
|
|
||||||
expect(btnSteve.classes()).not.toContain('active')
|
|
||||||
expect(btnAlex.classes()).not.toContain('active')
|
|
||||||
expect(btnCape.classes()).not.toContain('active')
|
|
||||||
expect(dropdownToggle.text()).toContain(trans('general.skin'))
|
|
||||||
expect(breadcrumb.text()).toContain(trans('skinlib.filter.skin'))
|
|
||||||
|
|
||||||
btnSteve.trigger('click')
|
|
||||||
expect(btnSkin.classes()).not.toContain('active')
|
|
||||||
expect(btnSteve.classes()).toContain('active')
|
|
||||||
expect(btnAlex.classes()).not.toContain('active')
|
|
||||||
expect(btnCape.classes()).not.toContain('active')
|
|
||||||
expect(dropdownToggle.text()).toContain('Steve')
|
|
||||||
expect(breadcrumb.text()).toContain(trans('skinlib.filter.steve'))
|
|
||||||
expect(queryString('filter')).toBe('steve')
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'steve', uploader: 0, sort: 'time', keyword: '', page: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
btnAlex.trigger('click')
|
|
||||||
expect(btnSkin.classes()).not.toContain('active')
|
|
||||||
expect(btnSteve.classes()).not.toContain('active')
|
|
||||||
expect(btnAlex.classes()).toContain('active')
|
|
||||||
expect(btnCape.classes()).not.toContain('active')
|
|
||||||
expect(dropdownToggle.text()).toContain('Alex')
|
|
||||||
expect(breadcrumb.text()).toContain(trans('skinlib.filter.alex'))
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'alex', uploader: 0, sort: 'time', keyword: '', page: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(queryString('filter')).toBe('alex')
|
|
||||||
|
|
||||||
btnCape.trigger('click')
|
|
||||||
expect(btnSkin.classes()).not.toContain('active')
|
|
||||||
expect(btnSteve.classes()).not.toContain('active')
|
|
||||||
expect(btnAlex.classes()).not.toContain('active')
|
|
||||||
expect(btnCape.classes()).toContain('active')
|
|
||||||
expect(dropdownToggle.text()).toContain(trans('general.cape'))
|
|
||||||
expect(breadcrumb.text()).toContain(trans('general.cape'))
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'cape', uploader: 0, sort: 'time', keyword: '', page: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(queryString('filter')).toBe('cape')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('check specified uploader', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 1,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount(List)
|
|
||||||
await flushPromises()
|
|
||||||
const breadcrumb = wrapper.find('.breadcrumb')
|
|
||||||
const button = wrapper.findAll('.bg-olive').at(2)
|
|
||||||
expect(breadcrumb.text()).toContain(trans('skinlib.filter.allUsers'))
|
|
||||||
|
|
||||||
button.trigger('click')
|
|
||||||
expect(button.classes()).toContain('active')
|
|
||||||
expect(breadcrumb.text()).toContain(trans('skinlib.filter.uploader', { uid: 1 }))
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'skin', uploader: 1, sort: 'time', keyword: '', page: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(queryString('uploader')).toBe('1')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('sort items', () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount(List)
|
|
||||||
const buttons = wrapper.findAll('.bg-olive')
|
|
||||||
const sortByLikes = buttons.at(0)
|
|
||||||
const sortByTime = buttons.at(1)
|
|
||||||
|
|
||||||
sortByLikes.trigger('click')
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'skin', uploader: 0, sort: 'likes', keyword: '', page: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(wrapper.text()).toContain(trans('skinlib.sort.likes'))
|
|
||||||
expect(sortByLikes.classes()).toContain('active')
|
|
||||||
expect(queryString('sort')).toBe('likes')
|
|
||||||
|
|
||||||
sortByTime.trigger('click')
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(wrapper.text()).toContain(trans('skinlib.sort.time'))
|
|
||||||
expect(sortByTime.classes()).toContain('active')
|
|
||||||
expect(queryString('sort')).toBe('time')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('search by keyword', () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount(List)
|
|
||||||
const input = wrapper.find('[data-test="keyword"]')
|
|
||||||
|
|
||||||
input.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,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(queryString('keyword')).toBe('a')
|
|
||||||
|
|
||||||
input.setValue('b')
|
|
||||||
wrapper.find('[data-test="btn-search"]').trigger('click')
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'skin', uploader: 0, sort: 'time', keyword: 'b', page: 1,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(queryString('keyword')).toBe('b')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('reset all filters', () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount(List)
|
|
||||||
wrapper
|
|
||||||
.findAll('.dropdown-item')
|
|
||||||
.at(3)
|
|
||||||
.trigger('click')
|
|
||||||
wrapper.setData({ keyword: 'abc' })
|
|
||||||
const buttons = wrapper.findAll('.bg-olive')
|
|
||||||
buttons.at(1).trigger('click')
|
|
||||||
|
|
||||||
Vue.prototype.$http.get.mockClear()
|
|
||||||
buttons.at(3).trigger('click')
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledTimes(1)
|
|
||||||
})
|
|
||||||
|
|
||||||
test('is anonymous', () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount<Vue & { anonymous: boolean }>(List)
|
|
||||||
expect(wrapper.vm.anonymous).toBeTrue()
|
|
||||||
})
|
|
||||||
|
|
||||||
test('on page changed', () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [], total_pages: 0, current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount<Vue & { pageChanged(page: number): void }>(List)
|
|
||||||
wrapper.vm.pageChanged(2)
|
|
||||||
expect(Vue.prototype.$http.get).toBeCalledWith(
|
|
||||||
'/skinlib/data',
|
|
||||||
{
|
|
||||||
filter: 'skin', uploader: 0, sort: 'time', keyword: '', page: 2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
expect(queryString('page')).toBe('2')
|
|
||||||
})
|
|
||||||
|
|
||||||
test('on like toggled', async () => {
|
|
||||||
Vue.prototype.$http.get.mockResolvedValue({
|
|
||||||
data: {
|
|
||||||
items: [{
|
|
||||||
tid: 1, liked: false, likes: 0,
|
|
||||||
}],
|
|
||||||
total_pages: 1,
|
|
||||||
current_uid: 0,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
const wrapper = mount<Vue & {
|
|
||||||
onLikeToggled(tid: number, like: boolean): void
|
|
||||||
items: Array<{ liked: boolean, likes: number }>
|
|
||||||
}>(List)
|
|
||||||
await flushPromises()
|
|
||||||
wrapper.vm.onLikeToggled(0, true)
|
|
||||||
expect(wrapper.vm.items[0].liked).toBeTrue()
|
|
||||||
expect(wrapper.vm.items[0].likes).toBe(1)
|
|
||||||
|
|
||||||
wrapper.vm.onLikeToggled(0, false)
|
|
||||||
expect(wrapper.vm.items[0].liked).toBeFalse()
|
|
||||||
expect(wrapper.vm.items[0].likes).toBe(0)
|
|
||||||
})
|
|
||||||
433
resources/assets/tests/views/skinlib/SkinLibrary.test.tsx
Normal file
433
resources/assets/tests/views/skinlib/SkinLibrary.test.tsx
Normal file
|
|
@ -0,0 +1,433 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { render, fireEvent, wait } from '@testing-library/react'
|
||||||
|
import { t } from '@/scripts/i18n'
|
||||||
|
import * as fetch from '@/scripts/net'
|
||||||
|
import { Paginator } from '@/scripts/types'
|
||||||
|
import SkinLibrary from '@/views/skinlib/SkinLibrary'
|
||||||
|
import { LibraryItem } from '@/views/skinlib/SkinLibrary/types'
|
||||||
|
|
||||||
|
jest.mock('@/scripts/net')
|
||||||
|
|
||||||
|
const fixtureItem: Readonly<LibraryItem> = Object.freeze<LibraryItem>({
|
||||||
|
tid: 1,
|
||||||
|
name: 'my skin',
|
||||||
|
type: 'steve',
|
||||||
|
uploader: 1,
|
||||||
|
nickname: 'me',
|
||||||
|
public: true,
|
||||||
|
likes: 70,
|
||||||
|
})
|
||||||
|
|
||||||
|
function createPaginator(data: LibraryItem[]): Paginator<LibraryItem> {
|
||||||
|
return {
|
||||||
|
data,
|
||||||
|
total: data.length,
|
||||||
|
from: 1,
|
||||||
|
to: data.length,
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
window.blessing.extra = { currentUid: null }
|
||||||
|
})
|
||||||
|
|
||||||
|
test('without authenticated', async () => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
|
||||||
|
const { queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toBeCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('filter')).toBe('skin')
|
||||||
|
expect(search.get('sort')).toBe('time')
|
||||||
|
expect(search.get('page')).toBe('1')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(fetch.get).not.toBeCalledWith('/user/closet/ids')
|
||||||
|
expect(queryByText(t('skinlib.seeMyUpload'))).not.toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('search by keyword', async () => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
|
||||||
|
const { getByText, getByPlaceholderText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.input(getByPlaceholderText(t('vendor.datatable.search')), {
|
||||||
|
target: { value: 'k' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.submit')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('keyword')).toBe('k')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('select uploaded by self', async () => {
|
||||||
|
window.blessing.extra.currentUid = 1
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
|
||||||
|
const { getByText, queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('skinlib.seeMyUpload')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('uploader')).toBe('1')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryByText(t('skinlib.filter.uploader', { uid: 1 })))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reset query', async () => {
|
||||||
|
window.blessing.extra.currentUid = 1
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
|
||||||
|
const { getByText, getByPlaceholderText, queryByText } = render(
|
||||||
|
<SkinLibrary />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Steve'))
|
||||||
|
await wait()
|
||||||
|
fireEvent.input(getByPlaceholderText(t('vendor.datatable.search')), {
|
||||||
|
target: { value: 'k' },
|
||||||
|
})
|
||||||
|
fireEvent.click(getByText(t('general.submit')))
|
||||||
|
await wait()
|
||||||
|
fireEvent.click(getByText(t('skinlib.seeMyUpload')))
|
||||||
|
await wait()
|
||||||
|
fireEvent.click(getByText(t('skinlib.sort.likes')))
|
||||||
|
await wait()
|
||||||
|
fireEvent.click(getByText(t('skinlib.reset')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('filter')).toBe('skin')
|
||||||
|
expect(search.get('keyword')).toBeNull()
|
||||||
|
expect(search.get('uploader')).toBeNull()
|
||||||
|
expect(search.get('sort')).toBe('time')
|
||||||
|
expect(search.get('page')).toBe('1')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryByText(t('skinlib.filter.uploader', { uid: 1 })))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('browser goes back', async () => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
|
||||||
|
const { getByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Steve'))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
const state: URLSearchParams = window.history.state
|
||||||
|
state.set('filter', 'skin')
|
||||||
|
const event = new PopStateEvent('popstate', { state })
|
||||||
|
window.dispatchEvent(event)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('filter')).toBe('skin')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('pagination', async () => {
|
||||||
|
const response = { ...createPaginator([]), last_page: 2 }
|
||||||
|
fetch.get.mockResolvedValue(response)
|
||||||
|
|
||||||
|
const { getByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText('2'))
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('page')).toBe('2')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('library item', async () => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([fixtureItem]))
|
||||||
|
|
||||||
|
const { getByText, queryByText, queryAllByText, queryByAltText } = render(
|
||||||
|
<SkinLibrary />,
|
||||||
|
)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(queryAllByText('Steve')).toHaveLength(2)
|
||||||
|
expect(queryByText(fixtureItem.name)).toBeInTheDocument()
|
||||||
|
expect(queryByAltText(fixtureItem.name)).toHaveAttribute(
|
||||||
|
'src',
|
||||||
|
`/preview/${fixtureItem.tid}?height=150`,
|
||||||
|
)
|
||||||
|
expect(queryByText(fixtureItem.nickname)).toBeInTheDocument()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(fixtureItem.nickname))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('uploader')).toBe(fixtureItem.uploader.toString())
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const search = new URLSearchParams(location.search)
|
||||||
|
expect(search.get('uploader')).toBe(fixtureItem.uploader.toString())
|
||||||
|
})
|
||||||
|
|
||||||
|
test('private texture', async () => {
|
||||||
|
const item = { ...fixtureItem, public: false }
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([item]))
|
||||||
|
|
||||||
|
const { queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(queryByText(t('skinlib.private'))).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('by filter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('skin', async () => {
|
||||||
|
const { getByText, queryAllByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Steve'))
|
||||||
|
await wait()
|
||||||
|
fireEvent.click(getByText(t('general.skin')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('filter')).toBe('skin')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryAllByText(t('general.skin'))).toHaveLength(2)
|
||||||
|
const search = new URLSearchParams(location.search)
|
||||||
|
expect(search.get('filter')).toBe('skin')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('steve', async () => {
|
||||||
|
const { getByText, queryAllByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Steve'))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('filter')).toBe('steve')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryAllByText('Steve')).toHaveLength(2)
|
||||||
|
const search = new URLSearchParams(location.search)
|
||||||
|
expect(search.get('filter')).toBe('steve')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('alex', async () => {
|
||||||
|
const { getByText, queryAllByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText('Alex'))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('filter')).toBe('alex')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryAllByText('Alex')).toHaveLength(2)
|
||||||
|
const search = new URLSearchParams(location.search)
|
||||||
|
expect(search.get('filter')).toBe('alex')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('cape', async () => {
|
||||||
|
const { getByText, queryAllByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('general.cape')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('filter')).toBe('cape')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
expect(queryAllByText(t('general.cape'))).toHaveLength(2)
|
||||||
|
const search = new URLSearchParams(location.search)
|
||||||
|
expect(search.get('filter')).toBe('cape')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('sorting', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockResolvedValue(createPaginator([]))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('by time', async () => {
|
||||||
|
const { getByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('skinlib.sort.likes')))
|
||||||
|
await wait()
|
||||||
|
fireEvent.click(getByText(t('skinlib.sort.time')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('sort')).toBe('time')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const search = new URLSearchParams(location.search)
|
||||||
|
expect(search.get('sort')).toBe('time')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('by likes', async () => {
|
||||||
|
const { getByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(t('skinlib.sort.likes')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.get).toHaveBeenLastCalledWith(
|
||||||
|
'/skinlib/list',
|
||||||
|
expect.toSatisfy((search: URLSearchParams) => {
|
||||||
|
expect(search.get('sort')).toBe('likes')
|
||||||
|
return true
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
const search = new URLSearchParams(location.search)
|
||||||
|
expect(search.get('sort')).toBe('likes')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('add to closet', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
fetch.get.mockImplementation((url: string) => {
|
||||||
|
if (url === '/skinlib/list') {
|
||||||
|
return Promise.resolve(createPaginator([fixtureItem]))
|
||||||
|
} else {
|
||||||
|
return Promise.resolve([])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('without authenticated', async () => {
|
||||||
|
const { getByText, queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(fixtureItem.likes.toString()))
|
||||||
|
expect(queryByText(t('skinlib.anonymous'))).toBeInTheDocument()
|
||||||
|
expect(fetch.post).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
window.blessing.extra.currentUid = 1
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||||
|
|
||||||
|
const { getByText, queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(fixtureItem.likes.toString()))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalled()
|
||||||
|
expect(queryByText((fixtureItem.likes + 1).toString())).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
window.blessing.extra.currentUid = 1
|
||||||
|
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||||
|
|
||||||
|
const { getByText, queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(fixtureItem.likes.toString()))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalled()
|
||||||
|
expect(queryByText(fixtureItem.likes.toString())).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('remove from closet', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
window.blessing.extra.currentUid = 1
|
||||||
|
fetch.get.mockImplementation((url: string) => {
|
||||||
|
if (url === '/skinlib/list') {
|
||||||
|
return Promise.resolve(createPaginator([fixtureItem]))
|
||||||
|
} else {
|
||||||
|
return Promise.resolve([fixtureItem.tid])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('succeeded', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 0, message: 'ok' })
|
||||||
|
|
||||||
|
const { getByText, queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(fixtureItem.likes.toString()))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalled()
|
||||||
|
expect(queryByText((fixtureItem.likes - 1).toString())).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('failed', async () => {
|
||||||
|
fetch.post.mockResolvedValue({ code: 1, message: 'failed' })
|
||||||
|
|
||||||
|
const { getByText, queryByText } = render(<SkinLibrary />)
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
fireEvent.click(getByText(fixtureItem.likes.toString()))
|
||||||
|
fireEvent.click(getByText(t('general.confirm')))
|
||||||
|
await wait()
|
||||||
|
|
||||||
|
expect(fetch.post).toBeCalled()
|
||||||
|
expect(queryByText(fixtureItem.likes.toString())).toBeInTheDocument()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
- 3D skin viewer can be with background now.
|
- 3D skin viewer can be with background now.
|
||||||
- Added support of installing plugin by uploading archive.
|
- Added support of installing plugin by uploading archive.
|
||||||
- Added support of installing plugin by submitting remote URL.
|
- Added support of installing plugin by submitting remote URL.
|
||||||
|
- Added support of clicking on the uploader's nickname in skin library to view other uploads of that user.
|
||||||
|
|
||||||
## Tweaked
|
## Tweaked
|
||||||
|
|
||||||
|
|
@ -66,6 +67,7 @@
|
||||||
- Fixed when uploading duplicated texture, alert is missing.
|
- Fixed when uploading duplicated texture, alert is missing.
|
||||||
- Fixed that "score cost per closet item" isn't calculated at "texture upload" page.
|
- Fixed that "score cost per closet item" isn't calculated at "texture upload" page.
|
||||||
- Fixed that administrator can't add private texture to his/her closet.
|
- Fixed that administrator can't add private texture to his/her closet.
|
||||||
|
- Fixed that button "See My Upload" existed when user isn't authenticated.
|
||||||
|
|
||||||
## Removed
|
## Removed
|
||||||
|
|
||||||
|
|
@ -85,6 +87,7 @@
|
||||||
- Removed cache for Profile JSON.
|
- Removed cache for Profile JSON.
|
||||||
- Removed cache for existence of player.
|
- Removed cache for existence of player.
|
||||||
- Removed settings of "Respond 204 for unexisted players". (Install plugin if you need it.)
|
- Removed settings of "Respond 204 for unexisted players". (Install plugin if you need it.)
|
||||||
|
- Removed breadcrumb of skin library.
|
||||||
|
|
||||||
## Internal Changes
|
## Internal Changes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@
|
||||||
- 3D 皮肤预览现在是带背景的
|
- 3D 皮肤预览现在是带背景的
|
||||||
- 可通过上传压缩包来安装插件
|
- 可通过上传压缩包来安装插件
|
||||||
- 可通过提交 URL 来安装插件
|
- 可通过提交 URL 来安装插件
|
||||||
|
- 皮肤库中可通过点击上传者昵称来查看该用户的其它上传
|
||||||
|
|
||||||
## 调整
|
## 调整
|
||||||
|
|
||||||
|
|
@ -66,6 +67,7 @@
|
||||||
- 修复上传重复材质时没有提示用户的问题
|
- 修复上传重复材质时没有提示用户的问题
|
||||||
- 「材质上传」页面的积分消耗没有计算衣柜收藏所需的积分
|
- 「材质上传」页面的积分消耗没有计算衣柜收藏所需的积分
|
||||||
- 修复管理员不能添加私有材质到衣柜的问题
|
- 修复管理员不能添加私有材质到衣柜的问题
|
||||||
|
- 修复未登录的用户在浏览皮肤库时出现「我的上传」按钮问题
|
||||||
|
|
||||||
## 移除
|
## 移除
|
||||||
|
|
||||||
|
|
@ -85,6 +87,7 @@
|
||||||
- 移除对 Profile JSON 的缓存
|
- 移除对 Profile JSON 的缓存
|
||||||
- 移除对角色存在与否的缓存
|
- 移除对角色存在与否的缓存
|
||||||
- 移除「对不存在的角色返回 204」的选项(如有需要,请安装插件)
|
- 移除「对不存在的角色返回 204」的选项(如有需要,请安装插件)
|
||||||
|
- 移除皮肤库右上角的 breadcrumb
|
||||||
|
|
||||||
## 内部更改
|
## 内部更改
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,3 +5,9 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="content-wrapper"></div>
|
<div class="content-wrapper"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block before_foot %}
|
||||||
|
<script>
|
||||||
|
blessing.extra = {{ {currentUid: auth_user().uid}|json_encode|raw }}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,7 @@ Route::prefix('user')
|
||||||
Route::prefix('closet')->name('closet.')->group(function () {
|
Route::prefix('closet')->name('closet.')->group(function () {
|
||||||
Route::get('', 'ClosetController@index')->name('page');
|
Route::get('', 'ClosetController@index')->name('page');
|
||||||
Route::get('list', 'ClosetController@getClosetData')->name('list');
|
Route::get('list', 'ClosetController@getClosetData')->name('list');
|
||||||
|
Route::get('ids', 'ClosetController@allIds')->name('ids');
|
||||||
Route::post('add', 'ClosetController@add')->name('add');
|
Route::post('add', 'ClosetController@add')->name('add');
|
||||||
Route::post('remove/{tid}', 'ClosetController@remove')->name('remove');
|
Route::post('remove/{tid}', 'ClosetController@remove')->name('remove');
|
||||||
Route::post('rename/{tid}', 'ClosetController@rename')->name('rename');
|
Route::post('rename/{tid}', 'ClosetController@rename')->name('rename');
|
||||||
|
|
@ -96,9 +97,9 @@ Route::prefix('user')
|
||||||
|
|
||||||
Route::prefix('skinlib')->name('skinlib.')->group(function () {
|
Route::prefix('skinlib')->name('skinlib.')->group(function () {
|
||||||
Route::view('', 'skinlib.index')->name('home');
|
Route::view('', 'skinlib.index')->name('home');
|
||||||
Route::any('info/{tid}', 'SkinlibController@info')->name('info');
|
Route::get('info/{tid}', 'SkinlibController@info')->name('info');
|
||||||
Route::any('show/{tid}', 'SkinlibController@show')->name('show');
|
Route::get('show/{tid}', 'SkinlibController@show')->name('show');
|
||||||
Route::any('data', 'SkinlibController@getSkinlibFiltered')->name('list');
|
Route::get('list', 'SkinlibController@library')->name('list');
|
||||||
|
|
||||||
Route::middleware(['authorize', 'verified'])->group(function () {
|
Route::middleware(['authorize', 'verified'])->group(function () {
|
||||||
Route::prefix('upload')->name('upload')->group(function () {
|
Route::prefix('upload')->name('upload')->group(function () {
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,17 @@ class ClosetControllerTest extends TestCase
|
||||||
]]);
|
]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testAllIds()
|
||||||
|
{
|
||||||
|
$texture = factory(Texture::class)->create();
|
||||||
|
$user = factory(User::class)->create();
|
||||||
|
$user->closet()->attach($texture->tid, ['item_name' => '']);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->getJson(route('user.closet.ids'))
|
||||||
|
->assertJson([$texture->tid]);
|
||||||
|
}
|
||||||
|
|
||||||
public function testAdd()
|
public function testAdd()
|
||||||
{
|
{
|
||||||
$uploader = factory(User::class)->create(['score' => 0]);
|
$uploader = factory(User::class)->create(['score' => 0]);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use App\Models\Player;
|
||||||
use App\Models\Texture;
|
use App\Models\Texture;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use Blessing\Filter;
|
use Blessing\Filter;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||||
use Illuminate\Http\UploadedFile;
|
use Illuminate\Http\UploadedFile;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
|
@ -15,250 +16,95 @@ class SkinlibControllerTest extends TestCase
|
||||||
{
|
{
|
||||||
use DatabaseTransactions;
|
use DatabaseTransactions;
|
||||||
|
|
||||||
public function testGetSkinlibFiltered()
|
public function testLibrary()
|
||||||
{
|
{
|
||||||
$this->getJson('/skinlib/data')
|
$steve = factory(Texture::class)->create([
|
||||||
->assertJson(['data' => [
|
'name' => 'ab',
|
||||||
'items' => [],
|
'upload_at' => Carbon::now()->subDays(2),
|
||||||
'current_uid' => 0,
|
'likes' => 80,
|
||||||
'total_pages' => 0,
|
]);
|
||||||
]]);
|
$alex = factory(Texture::class)->states('alex')->create([
|
||||||
|
'name' => 'cd',
|
||||||
|
'upload_at' => Carbon::now()->subDays(1),
|
||||||
|
'likes' => 60,
|
||||||
|
]);
|
||||||
|
$private = factory(Texture::class)->states('private')->create([
|
||||||
|
'upload_at' => Carbon::now(),
|
||||||
|
]);
|
||||||
|
|
||||||
$steves = factory(Texture::class, 5)->create();
|
// default
|
||||||
$alexs = factory(Texture::class, 5)->states('alex')->create();
|
$this->getJson('/skinlib/list')
|
||||||
$skins = $steves->merge($alexs);
|
->assertJson([
|
||||||
$capes = factory(Texture::class, 5)->states('cape')->create();
|
'data' => [
|
||||||
|
['tid' => $alex->tid, 'nickname' => $alex->owner->nickname],
|
||||||
// Default arguments
|
['tid' => $steve->tid, 'nickname' => $steve->owner->nickname],
|
||||||
$items = $this->getJson('/skinlib/data')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount(10, $items);
|
|
||||||
$this->assertTrue(collect($items)->every(function ($item) {
|
|
||||||
return $item['type'] == 'steve' || $item['type'] == 'alex';
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Only steve
|
|
||||||
$items = $this->getJson('/skinlib/data?filter=steve')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount(5, $items);
|
|
||||||
$this->assertTrue(collect($items)->every(function ($item) {
|
|
||||||
return $item['type'] == 'steve';
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Invalid type
|
|
||||||
$this->getJson('/skinlib/data?filter=what')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'items' => [],
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 0,
|
|
||||||
]]);
|
|
||||||
|
|
||||||
// Only capes
|
|
||||||
$items = $this->getJson('/skinlib/data?filter=cape')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount(5, $items);
|
|
||||||
$this->assertTrue(collect($items)->every(function ($item) {
|
|
||||||
return $item['type'] == 'cape';
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Only specified uploader
|
|
||||||
$uid = $skins->random()->uploader;
|
|
||||||
$owned = $skins
|
|
||||||
->filter(function ($texture) use ($uid) {
|
|
||||||
return $texture->uploader == $uid;
|
|
||||||
});
|
|
||||||
$items = $this->getJson('/skinlib/data?uploader='.$uid)
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount($owned->count(), $items);
|
|
||||||
$this->assertTrue(collect($items)->every(function ($item) use ($uid) {
|
|
||||||
return $item['uploader'] == $uid;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Sort by `tid`
|
|
||||||
$ordered = $skins->sortByDesc('tid')->map(function ($skin) {
|
|
||||||
return $skin->tid;
|
|
||||||
})->values()->all();
|
|
||||||
$items = $this->getJson('/skinlib/data?sort=tid')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$items = array_map(function ($item) {
|
|
||||||
return $item['tid'];
|
|
||||||
}, $items);
|
|
||||||
$this->assertEquals($ordered, $items);
|
|
||||||
|
|
||||||
// Search
|
|
||||||
$keyword = Str::limit($skins->random()->name, 1, '');
|
|
||||||
$keyworded = $skins
|
|
||||||
->filter(function ($texture) use ($keyword) {
|
|
||||||
return Str::contains($texture->name, [$keyword, strtolower($keyword)]);
|
|
||||||
});
|
|
||||||
$items = $this->getJson('/skinlib/data?keyword='.$keyword)
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount($keyworded->count(), $items);
|
|
||||||
|
|
||||||
// More than one argument
|
|
||||||
$keyword = Str::limit($skins->random()->name, 1, '');
|
|
||||||
$filtered = $skins
|
|
||||||
->filter(function ($texture) use ($keyword) {
|
|
||||||
return Str::contains($texture->name, [$keyword, strtolower($keyword)]);
|
|
||||||
})
|
|
||||||
->sortByDesc('size')
|
|
||||||
->map(function ($skin) {
|
|
||||||
return $skin->tid;
|
|
||||||
})
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
$items = $this->getJson('/skinlib/data?sort=size&keyword='.$keyword)
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 1,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$items = array_map(function ($item) {
|
|
||||||
return $item['tid'];
|
|
||||||
}, $items);
|
|
||||||
$this->assertCount(count($filtered), $items);
|
|
||||||
$this->assertEquals($filtered, $items);
|
|
||||||
|
|
||||||
// Pagination
|
|
||||||
$steves = factory(Texture::class)
|
|
||||||
->times(15)
|
|
||||||
->create()
|
|
||||||
->merge($steves);
|
|
||||||
$skins = $steves->merge($alexs);
|
|
||||||
$items = $this->getJson('/skinlib/data')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 2,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount(20, $items);
|
|
||||||
$items = $this->getJson('/skinlib/data?page=-5')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 2,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount(20, $items);
|
|
||||||
$page2Count = $skins->forPage(2, 20)->count();
|
|
||||||
$items = $this->getJson('/skinlib/data?page=2')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 2,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount(5, $items);
|
|
||||||
$this->getJson('/skinlib/data?page=8')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'items' => [],
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 2,
|
|
||||||
]]);
|
|
||||||
$items = $this->getJson('/skinlib/data?items_per_page=-6&page=2')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 2,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount($page2Count, $items);
|
|
||||||
$page3Count = $skins->forPage(3, 8)->count();
|
|
||||||
$items = $this->getJson('/skinlib/data?page=3&items_per_page=8')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 4,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertCount($page3Count, $items);
|
|
||||||
|
|
||||||
// Add some private textures
|
|
||||||
$uploader = factory(User::class)->create();
|
|
||||||
$otherUser = factory(User::class)->create();
|
|
||||||
$private = factory(Texture::class)
|
|
||||||
->times(5)
|
|
||||||
->create(['public' => false, 'uploader' => $uploader->uid]);
|
|
||||||
|
|
||||||
// If not logged in, private textures should not be shown
|
|
||||||
$items = $this->getJson('/skinlib/data')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => 0,
|
|
||||||
'total_pages' => 2,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertTrue(collect($items)->every(function ($item) {
|
|
||||||
return $item['public'] == true;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Other users should not see someone's private textures
|
|
||||||
$items = $this->actingAs($otherUser)
|
|
||||||
->getJson('/skinlib/data')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'current_uid' => $otherUser->uid,
|
|
||||||
'total_pages' => 2,
|
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertTrue(collect($items)->every(function ($item) {
|
|
||||||
return !$item['liked'];
|
|
||||||
}));
|
|
||||||
|
|
||||||
// A user has added a texture from skin library to his closet
|
|
||||||
$texture = $skins->sortByDesc('upload_at')->values()->first();
|
|
||||||
$otherUser->closet()->attach($texture->tid, ['item_name' => $texture->name]);
|
|
||||||
$this->getJson('/skinlib/data')
|
|
||||||
->assertJson(['data' => [
|
|
||||||
'items' => [
|
|
||||||
['tid' => $texture->tid, 'liked' => true],
|
|
||||||
],
|
],
|
||||||
'current_uid' => $otherUser->uid,
|
]);
|
||||||
'total_pages' => 2,
|
|
||||||
]]);
|
|
||||||
|
|
||||||
// Uploader can see his private textures
|
// with filter
|
||||||
$items = $this->actingAs($uploader)
|
$this->getJson('/skinlib/list?filter=steve')
|
||||||
->getJson('/skinlib/data')
|
->assertJson([
|
||||||
->assertJson(['data' => [
|
'data' => [
|
||||||
'current_uid' => $uploader->uid,
|
['tid' => $steve->tid, 'nickname' => $steve->owner->nickname],
|
||||||
'total_pages' => 2,
|
],
|
||||||
]])
|
]);
|
||||||
->decodeResponseJson('data')['items'];
|
|
||||||
$this->assertTrue(collect($items)->contains(function ($item) {
|
|
||||||
return $item['public'] == false;
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Administrators can see private textures
|
// with keyword
|
||||||
$admin = factory(User::class)->states('admin')->create();
|
$this->getJson('/skinlib/list?keyword=a')
|
||||||
$items = $this->actingAs($admin)
|
->assertJson([
|
||||||
->getJson('/skinlib/data')
|
'data' => [
|
||||||
->assertJson(['data' => [
|
['tid' => $steve->tid, 'nickname' => $steve->owner->nickname],
|
||||||
'current_uid' => $admin->uid,
|
],
|
||||||
'total_pages' => 2,
|
]);
|
||||||
]])
|
|
||||||
->decodeResponseJson('data')['items'];
|
// with uploader
|
||||||
$this->assertTrue(collect($items)->contains(function ($item) {
|
$this->getJson('/skinlib/list?uploader='.$steve->uploader)
|
||||||
return $item['public'] == false;
|
->assertJson([
|
||||||
}));
|
'data' => [
|
||||||
|
['tid' => $steve->tid, 'nickname' => $steve->owner->nickname],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// sort by likes
|
||||||
|
$this->getJson('/skinlib/list?sort=likes')
|
||||||
|
->assertJson([
|
||||||
|
'data' => [
|
||||||
|
['tid' => $steve->tid, 'nickname' => $steve->owner->nickname],
|
||||||
|
['tid' => $alex->tid, 'nickname' => $alex->owner->nickname],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// private textures are not available for other user
|
||||||
|
$this->actingAs(factory(User::class)->create())
|
||||||
|
->getJson('/skinlib/list')
|
||||||
|
->assertJson([
|
||||||
|
'data' => [
|
||||||
|
['tid' => $alex->tid, 'nickname' => $alex->owner->nickname],
|
||||||
|
['tid' => $steve->tid, 'nickname' => $steve->owner->nickname],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// private textures are available for uploader
|
||||||
|
$this->actingAs($private->owner)
|
||||||
|
->getJson('/skinlib/list')
|
||||||
|
->assertJson([
|
||||||
|
'data' => [
|
||||||
|
['tid' => $private->tid],
|
||||||
|
['tid' => $alex->tid],
|
||||||
|
['tid' => $steve->tid],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
// private textures are available for administrators
|
||||||
|
$this->actingAs(factory(User::class)->states('admin')->create())
|
||||||
|
->getJson('/skinlib/list')
|
||||||
|
->assertJson([
|
||||||
|
'data' => [
|
||||||
|
['tid' => $private->tid],
|
||||||
|
['tid' => $alex->tid],
|
||||||
|
['tid' => $steve->tid],
|
||||||
|
],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testShow()
|
public function testShow()
|
||||||
|
|
|
||||||
|
|
@ -10438,10 +10438,6 @@ vue@^2.6.11:
|
||||||
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5"
|
||||||
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
|
integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ==
|
||||||
|
|
||||||
vuejs-paginate@^2.1.0:
|
|
||||||
version "2.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/vuejs-paginate/-/vuejs-paginate-2.1.0.tgz#93e1ad1539b713a688c7a2d3080bda60fcc6c77d"
|
|
||||||
|
|
||||||
w3c-hr-time@^1.0.1:
|
w3c-hr-time@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
|
resolved "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz#82ac2bff63d950ea9e3189a58a65625fedf19045"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user