#19, added tags, and tags filtering
This commit is contained in:
@@ -31,6 +31,7 @@ class Db {
|
||||
await dbo.createCollection(c);
|
||||
}catch(err){}
|
||||
}
|
||||
await dbo.collection('assets').createIndex({ name: 'text', 'description': 'text', tags: 'text' }, {name:'fts', default_language: "none" });
|
||||
}finally{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ const execFile = util.promisify(npExecFile);
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import Utils from "../Utils.js";
|
||||
|
||||
const collection = 'assets';
|
||||
|
||||
/**
|
||||
@@ -144,6 +146,11 @@ class GameObjectsManager{
|
||||
project: { name:1, id:1, type:1, asset:1}
|
||||
});
|
||||
}
|
||||
|
||||
this.getTags = async function(q){
|
||||
let objects = await db.distinct(collection, 'tags', q ? {tags: {$regex: Utils.escapeRegExp(q), $options: 'i'}} : {});
|
||||
return objects;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -16,7 +16,7 @@ class GameObjectsController{
|
||||
* @param {App} app The application instance, апликация
|
||||
*/
|
||||
init(app){
|
||||
const { gameObject, am } = app;
|
||||
const { gameObject, am, db } = app;
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
@@ -26,7 +26,7 @@ class GameObjectsController{
|
||||
*/
|
||||
router.put('/', multipartMiddleware, async (req, res)=>{
|
||||
try{
|
||||
let data = req.body;
|
||||
let data = JSON.parse(req.body.object);
|
||||
let action = data.id ? 'update' : 'create';
|
||||
let object = await gameObject[action](req, data)
|
||||
res.json({status: 'OK', object});
|
||||
@@ -45,11 +45,16 @@ class GameObjectsController{
|
||||
* @memberof GameObjectsController
|
||||
*/
|
||||
router.post('/', async (req, res)=>{
|
||||
let result = await gameObject.list(req.body);
|
||||
let result = await gameObject.list(db.sanitizeQuery(req.body));
|
||||
res.json(result);
|
||||
am.audit(req, `game-object-list`, null, {q: req.body});
|
||||
})
|
||||
|
||||
router.post('/tags', async (req, res)=>{
|
||||
let tags = await gameObject.getTags(req.body.q);
|
||||
res.json(tags);
|
||||
})
|
||||
|
||||
/**
|
||||
* API: GET /api/game-object/:id Retrieve game object by ID, извличане на обект по идентификатор
|
||||
* @function read
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<v-chip-group variant="flat" v-if="!hideFilter" class="pa-4" multiple column v-model="selectedTypes">
|
||||
<v-chip v-for="(f,i) in $p.objectTypes" :key="i" :text="l[f.value]" :value="f.value" :color="f.color" filter></v-chip>
|
||||
</v-chip-group>
|
||||
<v-container fluid class="d-flex flex-wrap">
|
||||
<v-chip :text="'All'" class="mt-2 mr-2" @click="selectedTypes = selectedTypes.length ? [] : $p.objectTypes.map(f => f.value)" filter></v-chip>
|
||||
<v-chip-group variant="flat" v-if="!hideFilter" multiple column v-model="selectedTypes">
|
||||
<v-chip v-for="(f, i) in $p.objectTypes" :key="i" :text="l[f.value]" :value="f.value" :color="f.color"
|
||||
filter></v-chip>
|
||||
</v-chip-group>
|
||||
<v-text-field :loading="loading" append-inner-icon="mdi-magnify" density="compact" :label="l.search"
|
||||
hide-details @update:model-value="debounce(load, 500, true);" rounded v-model="textSearch"></v-text-field>
|
||||
</v-container>
|
||||
|
||||
<v-container class="asset-browser">
|
||||
<v-row>
|
||||
<v-col v-for="(v, i) in items" :key="i" cols="12" xs="6" sm="4" md="3" xl="2" class="position-relative img-preview">
|
||||
@@ -26,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import Utils from '#/app/Utils';
|
||||
export default {
|
||||
props:[
|
||||
'modelValue', 'query', 'hideFilter'
|
||||
@@ -39,6 +46,9 @@ export default {
|
||||
selectedTypes: this.$p.objectTypes.map(f=>f.value),
|
||||
previewObject: null,
|
||||
previewDialog: false,
|
||||
loading: false,
|
||||
textSearch: '',
|
||||
tags: []
|
||||
}
|
||||
},
|
||||
|
||||
@@ -48,20 +58,32 @@ export default {
|
||||
|
||||
watch:{
|
||||
async selectedTypes(n){
|
||||
this.q.type = { $in: n };
|
||||
this.q.type = { '*in': n };
|
||||
await this.load();
|
||||
}
|
||||
},
|
||||
|
||||
methods:{
|
||||
async load(){
|
||||
this.loading = true;
|
||||
Object.assign(this.q, this.query || {});
|
||||
if (this.textSearch) {
|
||||
this.q['*or'] = [
|
||||
{ name: { '*regex': Utils.escapeRegExp(this.textSearch), '*options': 'i' } },
|
||||
{ tags: { '*regex': Utils.escapeRegExp(this.textSearch), '*options': 'i' } },
|
||||
{ description: { '*regex': Utils.escapeRegExp(this.textSearch), '*options': 'i' } }
|
||||
];
|
||||
}else{
|
||||
delete this.q['*or'];
|
||||
}
|
||||
this.items = (await this.$api.gameObject.search(this.q)).data.data;
|
||||
this.loading = false;
|
||||
this.tags = await this.$api.gameObject.getTags(this.textSearch || null);
|
||||
},
|
||||
preview(v){
|
||||
this.previewObject = v;
|
||||
this.previewDialog = true;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -39,6 +39,7 @@ export default {
|
||||
engine.clearScene();
|
||||
engine.stop();
|
||||
engine.tm?.setGame(null);
|
||||
engine.destroy();
|
||||
},
|
||||
|
||||
computed:{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useAppStore } from '@/stores/app';
|
||||
|
||||
const debounceData = [];
|
||||
|
||||
export default {
|
||||
data(){
|
||||
return {
|
||||
@@ -56,6 +58,16 @@ export default {
|
||||
if (this.store?.prefs?.debug){
|
||||
console.log(...args);
|
||||
}
|
||||
}
|
||||
},
|
||||
debounce(fn){
|
||||
let f = debounceData.find(f=>f.fn == fn);
|
||||
if (f){
|
||||
clearTimeout(f.to);
|
||||
}else{
|
||||
f = {fn};
|
||||
debounceData.push(f);
|
||||
}
|
||||
f.to = setTimeout(...arguments);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -16,8 +16,17 @@
|
||||
<v-textarea :label="l.description" v-model="object.description"></v-textarea>
|
||||
<v-select :label="l.objectType" v-model="object.type" :items="$p.objectTypes.map(ot=>({title:l[ot.value], value:ot.value}))" tit :rules="[rules.required]">
|
||||
</v-select>
|
||||
<v-file-input :label="l.objectFile" v-model="object.file" :rules="[rules.requiredFile]"></v-file-input>
|
||||
<div v-if="object.asset">{{ object.asset.name }} | {{ object.asset.ofn }}</div>
|
||||
<v-file-input :label="l.objectFile" v-model="file" :rules="[rules.requiredFile]"></v-file-input>
|
||||
<v-combobox clearable chips multiple :label="l.tags" v-model="object.tags" :items="tagList">
|
||||
<template v-slot:chip="{ props, item }">
|
||||
<v-chip v-bind="props" label>
|
||||
<template v-slot:close>
|
||||
<v-icon icon="$close" size="14"></v-icon>
|
||||
</template>
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-combobox>
|
||||
<div v-if="object.asset" closable-chips deletable-chips :delimiters="[',']">{{ object.asset.name }} | {{ object.asset.ofn }}</div>
|
||||
</v-form>
|
||||
<v-card-actions>
|
||||
<v-btn @click="save" :loading="loading" prepend-icon="mdi-content-save" color="success"
|
||||
@@ -54,6 +63,8 @@ export default {
|
||||
return {
|
||||
panel: this.$route.query?.tab || 'edit',
|
||||
object: {},
|
||||
tagList: [],
|
||||
file: null,
|
||||
valid: false,
|
||||
rules: {
|
||||
required: v => v ? true : this.l.fieldRequired,
|
||||
@@ -66,11 +77,9 @@ export default {
|
||||
if (this.id && this.id != 'add') {
|
||||
this.object = (await this.$api.gameObject.load(this.id)).data;
|
||||
}
|
||||
this.tagList = (await this.$api.gameObject.getTags()).data;
|
||||
},
|
||||
computed: {
|
||||
fileToUpload() {
|
||||
return this.object?.file instanceof File
|
||||
},
|
||||
id() {
|
||||
return this.$route.params?.id;
|
||||
},
|
||||
@@ -83,17 +92,16 @@ export default {
|
||||
this.loading = true;
|
||||
try {
|
||||
let fd = new FormData();
|
||||
let keys = ['name', 'type'];
|
||||
if (this.fileToUpload) keys.push('file');
|
||||
if (this.id != 'add') keys.push('id');
|
||||
if (this.object.thumb) keys.push('thumb');
|
||||
|
||||
keys.forEach(e => fd.append(e, this.object[e]))
|
||||
if (this.file) {
|
||||
fd.append('file', this.file)
|
||||
}
|
||||
fd.append('object', JSON.stringify(this.object));
|
||||
let result = await this.$api.gameObject.save(fd);
|
||||
Object.assign(this.object, result.data.object);
|
||||
if (this.id == 'add') {
|
||||
this.$router.replace({ params: { id: this.object.id }, query:{ tab:'preview' } });
|
||||
}
|
||||
this.debug(this.object)
|
||||
// await this.$nextTick();
|
||||
// this.panel = 'preview';
|
||||
// if (!params?.thumbOnly) await this.$refs.assetPreview.loadAsset();
|
||||
|
||||
@@ -33,6 +33,9 @@ export default {
|
||||
},
|
||||
async remove(id){
|
||||
return await $ax.delete(`/game-object/${id}`)
|
||||
},
|
||||
async getTags(q){
|
||||
return await $ax.post('/game-object/tags', {q});
|
||||
}
|
||||
},
|
||||
scenario:{
|
||||
|
||||
@@ -10,6 +10,8 @@ const lang = {
|
||||
name: 'Name',
|
||||
id: 'Identifier',
|
||||
description: 'Description',
|
||||
tags: 'Tags',
|
||||
search: 'Search',
|
||||
fieldRequired: 'Field is required',
|
||||
objectType: 'Object type',
|
||||
objectFile: 'File',
|
||||
@@ -163,6 +165,8 @@ const lang = {
|
||||
name: 'Име',
|
||||
id: 'Идентификатор',
|
||||
description: 'Описание',
|
||||
tags: 'Етикети',
|
||||
search: 'Търсене',
|
||||
fieldRequired: 'Полето е задължително',
|
||||
objectType: 'Тип обект',
|
||||
objectFile: 'Файл',
|
||||
|
||||
@@ -24,6 +24,9 @@ export default createVuetify({
|
||||
VSelect: {
|
||||
variant: 'outlined'
|
||||
},
|
||||
VCombobox: {
|
||||
variant: 'outlined'
|
||||
},
|
||||
VTextField: {
|
||||
variant: 'outlined'
|
||||
},
|
||||
|
||||
@@ -115,7 +115,7 @@ video{
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
max-width: 100vw;
|
||||
height: calc(100vh - 244px);
|
||||
height: calc(100vh - 277px);
|
||||
&.pan {
|
||||
cursor: grab;
|
||||
}
|
||||
@@ -144,4 +144,5 @@ audio {
|
||||
left:unset !important;
|
||||
bottom: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user