Added A way to get Game-icons.net icons into the database (it's not secure yet), plus some icon ui
This commit is contained in:
@@ -51,4 +51,3 @@ static-html
|
||||
aldeed:collection2@3.0.0
|
||||
aldeed:schema-index
|
||||
akryum:vue-component
|
||||
autopublish@1.0.7
|
||||
|
||||
@@ -12,7 +12,6 @@ akryum:vue-router2@0.2.2
|
||||
aldeed:collection2@3.0.1
|
||||
aldeed:schema-index@3.0.0
|
||||
allow-deny@1.1.0
|
||||
autopublish@1.0.7
|
||||
autoupdate@1.5.0
|
||||
babel-compiler@7.2.4
|
||||
babel-runtime@1.3.0
|
||||
|
||||
50
app/imports/api/icons/Icons.js
Normal file
50
app/imports/api/icons/Icons.js
Normal file
@@ -0,0 +1,50 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
|
||||
let Icons = new Mongo.Collection('icons');
|
||||
|
||||
iconsSchema = new SimpleSchema({
|
||||
name: {
|
||||
type: String,
|
||||
unique: true,
|
||||
index: 1,
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
optional: true,
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
optional: true,
|
||||
index: 1,
|
||||
},
|
||||
'tags.$': {
|
||||
type: String,
|
||||
},
|
||||
shape: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
if (Meteor.isServer) {
|
||||
Icons._ensureIndex({
|
||||
'name': 'text',
|
||||
'description': 'text',
|
||||
'tags': 'text',
|
||||
});
|
||||
}
|
||||
|
||||
Icons.attachSchema(iconsSchema);
|
||||
|
||||
const writeIcons = new ValidatedMethod({
|
||||
name: 'writeIcons',
|
||||
validate: null,
|
||||
run(icons){
|
||||
if (Meteor.isServer){
|
||||
this.unblock();
|
||||
Icons.rawCollection().insert(icons, {ordered: false});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export { writeIcons };
|
||||
export default Icons;
|
||||
26
app/imports/server/publications/icons.js
Normal file
26
app/imports/server/publications/icons.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import Icons from '/imports/api/icons/Icons.js';
|
||||
|
||||
Meteor.publish("sampleIcons", function(){
|
||||
return Icons.find({}, {limit: 50});
|
||||
});
|
||||
|
||||
|
||||
Meteor.publish("searchIcons", function(searchValue) {
|
||||
// Don't publish anything if there's no search value
|
||||
if (!searchValue) {
|
||||
return [];
|
||||
}
|
||||
return Icons.find(
|
||||
{ $text: {$search: searchValue} },
|
||||
{
|
||||
// relevant documents have a higher score.
|
||||
fields: {
|
||||
score: { $meta: "textScore" }
|
||||
},
|
||||
// `score` property specified in the projection fields above.
|
||||
sort: {
|
||||
score: { $meta: "textScore" }
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import "./characterList.js";
|
||||
import "./library.js";
|
||||
import "./singleCharacter.js";
|
||||
import "./user.js";
|
||||
import "./users.js";
|
||||
import './characterList.js';
|
||||
import './library.js';
|
||||
import './singleCharacter.js';
|
||||
import './user.js';
|
||||
import './users.js';
|
||||
import './icons.js';
|
||||
|
||||
@@ -44,8 +44,10 @@
|
||||
import DialogStack from '/imports/ui/dialogStack/DialogStack.Story.vue';
|
||||
import HealthBar from '/imports/ui/components/HealthBar.Story.vue';
|
||||
import HitDiceListTile from '/imports/ui/components/HitDiceListTile.Story.vue';
|
||||
import IconSearch from '/imports/ui/components/IconSearch.Story.vue';
|
||||
import SkillListTile from '/imports/ui/components/SkillListTile.Story.vue';
|
||||
import ToolbarLayout from '/imports/ui/layouts/ToolbarLayout.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AbilityListTile,
|
||||
@@ -54,6 +56,7 @@
|
||||
DialogStack,
|
||||
HealthBar,
|
||||
HitDiceListTile,
|
||||
IconSearch,
|
||||
SkillListTile,
|
||||
ToolbarLayout,
|
||||
},
|
||||
|
||||
16
app/imports/ui/components/IconSearch.Story.vue
Normal file
16
app/imports/ui/components/IconSearch.Story.vue
Normal file
@@ -0,0 +1,16 @@
|
||||
<template lang="html">
|
||||
<icon-search/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconSearch from '/imports/ui/components/IconSearch.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IconSearch,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
76
app/imports/ui/components/IconSearch.vue
Normal file
76
app/imports/ui/components/IconSearch.vue
Normal file
@@ -0,0 +1,76 @@
|
||||
<template lang="html">
|
||||
<v-autocomplete
|
||||
v-model="model"
|
||||
:search-input.sync="searchString"
|
||||
:items="items"
|
||||
:loading="!$subReady.searchIcons || isLoading"
|
||||
item-text="name"
|
||||
item-value="_id"
|
||||
label="Search icons"
|
||||
hide-no-data
|
||||
@input="input"
|
||||
>
|
||||
<template
|
||||
slot="item"
|
||||
slot-scope="{ item, tile }"
|
||||
>
|
||||
<v-list-tile-avatar>
|
||||
<svg class="avatar" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#000" :d="item.shape"/></svg>
|
||||
</v-list-tile-avatar>
|
||||
<v-list-tile-content>
|
||||
<v-list-tile-title v-text="item.name"></v-list-tile-title>
|
||||
</v-list-tile-content>
|
||||
</template>
|
||||
</v-autocomplete>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Icons from '/imports/api/icons/Icons.js';
|
||||
|
||||
export default {
|
||||
data(){ return {
|
||||
model: this.value,
|
||||
searchString: null,
|
||||
serverSearchString: null,
|
||||
isLoading: false,
|
||||
}},
|
||||
props: {
|
||||
value: String,
|
||||
},
|
||||
watch: {
|
||||
searchString(string){
|
||||
this.isLoading = true;
|
||||
this.searchServer(string)
|
||||
},
|
||||
value(newValue){
|
||||
this.model = newValue;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
searchServer: _.debounce(function(string){
|
||||
this.serverSearchString = string;
|
||||
}, 200),
|
||||
input(e){
|
||||
this.$emit('input', e);
|
||||
}
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
searchIcons() {
|
||||
this.isLoading = false;
|
||||
return [this.serverSearchString];
|
||||
},
|
||||
},
|
||||
items(){
|
||||
return Icons.find({}, { sort: [['score', 'desc']] }).fetch();
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
83
app/imports/ui/icons/IconAdmin.vue
Normal file
83
app/imports/ui/icons/IconAdmin.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<template lang="html">
|
||||
<toolbar-layout>
|
||||
<div slot="toolbar">
|
||||
DiceCloud
|
||||
</div>
|
||||
<div class="content">
|
||||
<v-card class="ma-4">
|
||||
<v-card-text>
|
||||
<v-layout column align-center>
|
||||
<upload-btn
|
||||
:fileChangedCallback="fileChanged"
|
||||
/>
|
||||
<v-text-field
|
||||
label="Search"
|
||||
@click:append="updateSearchString"
|
||||
@keydown.enter="updateSearchString"
|
||||
ref="iconSearchField"
|
||||
append-icon="search"
|
||||
/>
|
||||
<v-container grid-list-md fill-height>
|
||||
<v-layout row wrap>
|
||||
<v-flex xs3 md2 xl1 v-for="icon in icons" :key="icon._id._str || icon._id">
|
||||
<v-card>
|
||||
<v-card-title class="title">{{icon.name}}</v-card-title>
|
||||
<v-card-text>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="#000" :d="icon.shape"/></svg>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
</v-layout>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</toolbar-layout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import UploadButton from 'vuetify-upload-button';
|
||||
import ToolbarLayout from "/imports/ui/layouts/ToolbarLayout.vue";
|
||||
import importIcons from '/imports/ui/icons/importIcons.js';
|
||||
import Icons from '/imports/api/icons/Icons.js';
|
||||
import { _ } from 'meteor/underscore';
|
||||
|
||||
export default {
|
||||
data(){ return {
|
||||
searchString: '',
|
||||
}},
|
||||
methods: {
|
||||
fileChanged (file) {
|
||||
importIcons(file);
|
||||
},
|
||||
updateSearchString(){
|
||||
this.searchString = this.$refs.iconSearchField.internalValue;
|
||||
},
|
||||
},
|
||||
components: {
|
||||
ToolbarLayout,
|
||||
'upload-btn': UploadButton
|
||||
},
|
||||
meteor: {
|
||||
$subscribe: {
|
||||
searchIcons() {
|
||||
return [this.searchString];
|
||||
},
|
||||
},
|
||||
icons(){
|
||||
return Icons.find({}, { sort: [["score", "desc"]] });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
svg {
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
}
|
||||
.v-card {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
31
app/imports/ui/icons/importIcons.js
Normal file
31
app/imports/ui/icons/importIcons.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { writeIcons } from '/imports/api/icons/Icons.js';
|
||||
|
||||
/*
|
||||
* Import a SVG sprite file. All the icons must contain one id and one path with a
|
||||
* single 'd' attribute.
|
||||
*
|
||||
* A svg sprite file can be created by downloading the entire archive of
|
||||
* https://game-icons.net/ then using the search function with *.svg to copy
|
||||
* all the individual files into a single directory, and then using the npm
|
||||
* sprite-generator to run `svg-sprite-generate -d icons -o sprite.svg` to save
|
||||
* the sprite file.
|
||||
*/
|
||||
|
||||
export default function importIcons(file){
|
||||
let id, d, icons = [];
|
||||
let reader = new FileReader();
|
||||
|
||||
reader.onload = function(){
|
||||
reader.result.match(/i?d="([^"])+"/gi).forEach(s => {
|
||||
if (s[0] === 'i'){
|
||||
id = s.slice(4, -1);
|
||||
} else if (s[0] === 'd'){
|
||||
d = s.slice(3, -1);
|
||||
icons.push ({_id: Random.id(), name: id, shape: d});
|
||||
}
|
||||
});
|
||||
writeIcons.call(icons);
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
};
|
||||
@@ -49,6 +49,7 @@ RouterFactory.configure(factory => {
|
||||
//Development routes
|
||||
if (Meteor.isDevelopment){
|
||||
let StoryBook = require('/imports/ui/StoryBook.vue').default;
|
||||
let IconAdmin = require('/imports/ui/icons/IconAdmin.vue').default;
|
||||
factory.addRoutes([
|
||||
{
|
||||
path: '/storybook/:component',
|
||||
@@ -58,6 +59,10 @@ RouterFactory.configure(factory => {
|
||||
path: '/storybook',
|
||||
name: 'storybook',
|
||||
component: StoryBook,
|
||||
}, {
|
||||
path: '/icon-admin',
|
||||
name: "iconAdmin",
|
||||
component: IconAdmin,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
5
app/package-lock.json
generated
5
app/package-lock.json
generated
@@ -2149,6 +2149,11 @@
|
||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-1.4.2.tgz",
|
||||
"integrity": "sha512-HnMBBtO5FqTPzr6FXVlxnYulGMrHVlcmBL7SSLE/AQnNbdUpT6Jw9D/Kdw9fbK5mdn7JSWMcfWpPJUrqYTtK1w=="
|
||||
},
|
||||
"vuetify-upload-button": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/vuetify-upload-button/-/vuetify-upload-button-1.2.2.tgz",
|
||||
"integrity": "sha512-WQmAevQl4F2musSLpPzyT3fCBCksFiL7V6Jh8qD1fxGxHjAvjGywJKWjPLMzXDHkJfo6q2OPJtynPU0QXN5hGA=="
|
||||
},
|
||||
"vuex": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/vuex/-/vuex-3.0.1.tgz",
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"vue-meteor-tracker": "^2.0.0-beta.5",
|
||||
"vue-router": "^3.0.2",
|
||||
"vuetify": "^1.4.2",
|
||||
"vuetify-upload-button": "^1.2.2",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {}
|
||||
|
||||
Reference in New Issue
Block a user