Switch backend to NeDB storage, fix image/json creation

This commit is contained in:
Adam Goldsmith 2019-01-04 17:04:30 -05:00
parent 828ddf39e2
commit 8afda19685
9 changed files with 271 additions and 162 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/decks /decks
/dist/ /dist/
/.cache/ /.cache/
/decks.db

61
package-lock.json generated
View File

@ -1297,6 +1297,14 @@
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.12.0.tgz",
"integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg==" "integrity": "sha512-DYWGk01lDcxeS/K9IHPGWfT8PsJmbXRtRd2Sx72Tnb8pcYZQFF1oSDb8hJtS1vhp212q1Rzi5dUf9+nq0o9UIg=="
}, },
"binary-search-tree": {
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/binary-search-tree/-/binary-search-tree-0.2.5.tgz",
"integrity": "sha1-fbs7IQ/coIJFDa0jNMMErzm9x4Q=",
"requires": {
"underscore": "~1.4.4"
}
},
"bindings": { "bindings": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "http://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz", "resolved": "http://registry.npmjs.org/bindings/-/bindings-1.2.1.tgz",
@ -4000,6 +4008,11 @@
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
"integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA==" "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA=="
}, },
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha1-nbHb0Pr43m++D13V5Wu2BigN5ps="
},
"import-fresh": { "import-fresh": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@ -4422,6 +4435,22 @@
"type-check": "~0.3.2" "type-check": "~0.3.2"
} }
}, },
"lie": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz",
"integrity": "sha1-mkNrLMd0bKWd56QfpGmz77dr2H4=",
"requires": {
"immediate": "~3.0.5"
}
},
"localforage": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/localforage/-/localforage-1.7.3.tgz",
"integrity": "sha512-1TulyYfc4udS7ECSBT2vwJksWbkwwTX8BzeUIiq8Y07Riy7bDAAnxDaPU/tWyOVmQAcWJIEIFP9lPfBGqVoPgQ==",
"requires": {
"lie": "3.1.1"
}
},
"lodash": { "lodash": {
"version": "4.17.11", "version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
@ -4679,6 +4708,33 @@
"to-regex": "^3.0.1" "to-regex": "^3.0.1"
} }
}, },
"nedb": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/nedb/-/nedb-1.8.0.tgz",
"integrity": "sha1-DjUCzYLABNU1WkPJ5VV3vXvZHYg=",
"requires": {
"async": "0.2.10",
"binary-search-tree": "0.2.5",
"localforage": "^1.3.0",
"mkdirp": "~0.5.1",
"underscore": "~1.4.4"
},
"dependencies": {
"async": {
"version": "0.2.10",
"resolved": "http://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
}
}
},
"nedb-promises": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/nedb-promises/-/nedb-promises-3.0.2.tgz",
"integrity": "sha512-9GyvhZF7LRHMbOsk+6peJc9IdmWifZAGG/er0sE8VZSOsNJII+Aywcrm5OcesSbu9GZ7E1PxnL63kYqypeZw9Q==",
"requires": {
"nedb": "^1.8.0"
}
},
"negotiator": { "negotiator": {
"version": "0.6.1", "version": "0.6.1",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
@ -7026,6 +7082,11 @@
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
}, },
"underscore": {
"version": "1.4.4",
"resolved": "http://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz",
"integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ="
},
"unicode-canonical-property-names-ecmascript": { "unicode-canonical-property-names-ecmascript": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",

View File

@ -14,6 +14,7 @@
"dependencies": { "dependencies": {
"express": "^4.16.4", "express": "^4.16.4",
"interactjs": "^1.3.4", "interactjs": "^1.3.4",
"nedb-promises": "^3.0.2",
"parcel-bundler": "^1.11.0", "parcel-bundler": "^1.11.0",
"phantom": "^4.0.12", "phantom": "^4.0.12",
"v-runtime-template": "^1.5.2", "v-runtime-template": "^1.5.2",

19
src/404.vue Normal file
View File

@ -0,0 +1,19 @@
<template>
<div>
<vue-headful title="404 Not Found"> </vue-headful>
<h1>Error 404: Path {{ path }} not found</h1>
You seem to have gone to the wrong place, would you like to go
back to the <a href="/">Deck Index</a>?
</div>
</template>
<script>
export default {
name: 'Err404',
data() {
return {
path: window.location.pathname,
}
},
}
</script>

47
src/Deck.vue Normal file
View File

@ -0,0 +1,47 @@
<template>
<!-- style is inline for phantomjs -->
<div style="white-space: nowrap;">
<div v-for="cardRow in chunkedCards">
<span v-for="card in cardRow" @click="$emit('input', card)">
<Hero v-bind="card.card"> </Hero>
</span>
</div>
</div>
</template>
<script>
import Hero from './template/hero/Hero.vue'
export default {
name: 'Deck',
props: ['deckInfo'],
components: {Hero},
computed: {
cards() {
console.log
return Object
.keys(this.deckInfo)
.filter(cardType => cardType !== 'meta')
.flatMap(cardType => this.deckInfo[cardType].flatMap((card, index) => {
let cardWrapper = {
type: cardType,
card: this.deckInfo[cardType][index],
props: Hero.props,
};
return Array(card.count || 1).fill(cardWrapper);
}));
},
chunkedCards() {
// find minimum box to fit cards
let columns = Math.ceil(Math.sqrt(this.cards.length));
let rows = Math.ceil(this.cards.length / columns) || 0;
return Array(rows)
.fill()
.map((_, index) => index * columns)
.map(begin => this.cards.slice(begin, begin + columns));
},
},
}
</script>

View File

@ -1,14 +1,11 @@
<template> <template>
<body> <body>
<label>Create New Deck: <router-link to="/edit/new">Create New Deck</router-link>
<input type="text"
@change="window.location='/deck/' + event.target.value + '/editor'">
</label>
<ul> <ul>
<li v-for="deck in decks"> <li v-for="deck in decks">
{{ deck }}: {{ deck.deck.meta.name }}:
<router-link :to="'/play/' + deck"> Play </router-link> <router-link :to="'/play/' + deck._id"> Play </router-link>
<router-link :to="'/edit/' + deck"> Edit </router-link> <router-link :to="'/edit/' + deck._id"> Edit </router-link>
</li> </li>
</ul> </ul>
</body> </body>
@ -26,7 +23,8 @@
created() { created() {
fetch('/decks.json') fetch('/decks.json')
.then(r => r.json()) .then(r => r.json())
.then(d => this.decks = d); .then(d => this.decks = d)
.catch(err => console.log("Couldn't get deck list"));
}, },
} }
</script> </script>

View File

@ -8,7 +8,7 @@
</button> </button>
<button type="button" <button type="button"
@click="downloadFile('deck.tts.json', deckName + '.json')"> @click="downloadFile('/decks/' + deckID + '.tts.json', deckInfo.meta.name + '.tts.json')">
Download Tabletop Output JSON Download Tabletop Output JSON
</button> </button>
</div> </div>
@ -21,11 +21,11 @@
<form> <form>
<div> <div>
<label> Deck Name: <input type="text" v-model="deckName"> </label> <label> Deck Name: <input type="text" v-model="deckInfo.meta.name"> </label>
</div> </div>
<div> <div>
<label> Deck Type: <label> Deck Type:
<select v-model="deckInfo.type"> <select v-model="deckInfo.meta.type">
<option value="hero">hero</option> <option value="hero">hero</option>
<option value="villain">villain</option> <option value="villain">villain</option>
<option value="environment">environment</option> <option value="environment">environment</option>
@ -37,84 +37,53 @@
<div id="cardEditor" v-if="selected"> <div id="cardEditor" v-if="selected">
<button class="close-editor" @click="selected = null">X</button> <button class="close-editor" @click="selected = null">X</button>
<div v-for="(type, prop) in selectedCardProps"> <div v-for="(type, prop) in selected.props">
<label> {{ prop }} <label> {{ prop }}
<input v-if="type === Number" v-model="selected.card[prop]"/> <input v-if="type === Number" v-model="selected.card[prop]"/>
<input v-if="type === 'file'" v-model="selected.card[prop]"/> <input v-if="type === 'file'" type="file" accept="image/*"
@change="fileUploaded(prop, $event)" />
<textarea v-else rows="1" v-model="selected.card[prop]"> </textarea> <textarea v-else rows="1" v-model="selected.card[prop]"> </textarea>
</label> </label>
</div> </div>
</div> </div>
<div ref="deck" :style="deckStyle"> <Deck ref="deck" :deckInfo="deckInfo" v-model="selected"> </Deck>
<div v-for="card in cards" @click="selected = card">
<Hero v-bind="card.card"> </Hero>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import Hero from './template/hero/Hero.vue' import Deck from './Deck.vue';
export default { export default {
name: 'Editor', name: 'Editor',
components: {Deck},
components: {Hero}, props: ['deckID'],
props: ['deckName'],
data() { data() {
return { return {
selected: null, selected: null,
deckInfo: {}, deckInfo: {meta: {name: "", type: ""}},
}; };
}, },
watch: { watch: {
deckName() { deckInfo() {
document.title = "Editor|" + deckName; document.title = "Editor|" + this.deckInfo.meta.name;
}, },
}, },
created() { created() {
fetch('/decks/' + this.deckName + '.input.json') if (this.deckID !== 'new') {
.then(r => r.json()) fetch('/decks/' + this.deckID + '.json')
.then(j => this.deckInfo = j); .then(r => r.json())
.then(j => this.deckInfo = j.input)
.catch((err) => console.log('did not get old JSON, starting new deck'));
}
/* window.addEventListener( /* window.addEventListener(
* 'beforeunload', e => e.returnValue = "Unsaved changes blah blah"); */ * 'beforeunload', e => e.returnValue = "Unsaved changes blah blah"); */
}, },
computed: {
cards() {
return Object
.keys(this.deckInfo)
.filter(cardType => typeof this.deckInfo[cardType] !== 'string')
.flatMap(cardType => this.deckInfo[cardType].flatMap((card, index) => {
let cardWrapper = {
type: cardType,
card: this.deckInfo[cardType][index],
};
return Array(card.count || 1).fill(cardWrapper);
}));
},
deckStyle() {
// find minimum box to fit cards
let rows = Math.ceil(Math.sqrt(this.cards.length));
let columns = this.cards.length > rows * (rows - 1) ? rows : rows - 1;
return {
display: 'grid',
'grid-template-columns': `repeat(${rows}, 1fr)`,
'grid-template-rows': `repeat(${columns}, 1fr)`,
};
},
selectedCardProps() {
// todo: make better
return Hero.props;
},
},
methods: { methods: {
// deck JSON uploader // deck JSON uploader
jsonUpload(event) { jsonUpload(event) {
@ -131,22 +100,15 @@
console.log(JSON.stringify(this.deckInfo)); console.log(JSON.stringify(this.deckInfo));
this.downloadFile('data:application/json;charset=utf-8,' + this.downloadFile('data:application/json;charset=utf-8,' +
encodeURIComponent(JSON.stringify(this.deckInfo)), encodeURIComponent(JSON.stringify(this.deckInfo)),
this.deckName + '.input.json') this.deckID + '.input.json')
}, },
// chrome doesn't seem to send input event on file select fileUploaded(event, prop) {
fileUploaded(event) { let reader = new FileReader();
let prop = event.target.id.substring(5); reader.onload = e => {
if (prop === "image") { this.selected.card[prop] = e.target.result;
let files = event.target.files; };
let reader = new FileReader(); reader.readAsDataURL(event.target.files[0]);
reader.onload = e => {
selected.svg.querySelector('#' + prop)
.setAttributeNS("http://www.w3.org/1999/xlink", "href", e.target.result);
selected.json[prop] = e.target.result;
};
reader.readAsDataURL(files[0]);
}
}, },
downloadFile(file, name) { downloadFile(file, name) {
@ -159,13 +121,19 @@
}, },
upload() { upload() {
// POST the generated SVGs to the server // POST the inputed json to the server
let data = (new XMLSerializer()).serializeToString(this.$refs.deck); fetch('/upload', {
fetch('upload', {
method: 'post', method: 'post',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: JSON.stringify({body: data, json: this.deckInfo}) body: JSON.stringify({
}); id: this.deckID === 'new' ? undefined : this.deckID,
deck: this.deckInfo,
body: (new XMLSerializer()).serializeToString(this.$refs.deck.$el),
})
})
.then(r => r.json())
.then(j => this.$router.replace('/edit/' + j.id))
.catch(err => console.log('Failed to upload' + err));
}, },
}, },
} }

View File

@ -4,13 +4,16 @@ import VueRouter from 'vue-router';
import App from './App.vue'; import App from './App.vue';
import DeckIndex from './DeckIndex.vue'; import DeckIndex from './DeckIndex.vue';
import Editor from './Editor.vue'; import Editor from './Editor.vue';
import Err404 from './404.vue';
Vue.use(VueRouter); Vue.use(VueRouter);
const router = new VueRouter({ const router = new VueRouter({
mode: 'history',
routes: [ routes: [
{path: '/', component: DeckIndex}, {path: '/', component: DeckIndex},
{path: '/edit/:deckName', component: Editor, props: true}, {path: '/edit/:deckID', component: Editor, props: true},
{path: '*', component: Err404},
], ],
}); });

View File

@ -2,23 +2,25 @@
// jshint esversion:6 // jshint esversion:6
"use strict"; "use strict";
const express = require('express'), const express = require('express'),
Bundler = require('parcel-bundler'), Bundler = require('parcel-bundler'),
fs = require('fs'), fs = require('fs'),
path = require('path'), path = require('path'),
phantom = require('phantom'), phantom = require('phantom'),
port = process.env.PORT || 1234; Datastore = require('nedb-promises'),
port = process.env.PORT || 1234;
const decks = ["the_Unholy_Priest_update_2",
"NZoths_Invasion_1.2", const db = Datastore.create('decks.db');
"Puffer_Fish_input_1.3"];
const app = express(); const app = express();
app.use(express.json()); app.use(express.json({limit: '50mb'}));
app.use('/template', express.static('template')); app.use('/template', express.static('template'));
app.use('/decks', express.static('decks')); app.get('/decks/:deckID.tts.json', getTTSJSON);
app.get('/decks.json', (req, res) => res.json(decks)); app.get('/decks/:deckID.json', getInputJSON);
app.get('/decks/:deckID.png', getDeckImage);
app.get('/decks.json', getDecksList);
app.post('/upload', handleUpload); app.post('/upload', handleUpload);
let bundler = new Bundler(path.join(__dirname, 'index.html')); let bundler = new Bundler(path.join(__dirname, 'index.html'));
@ -26,84 +28,93 @@ app.use(bundler.middleware());
app.listen(port, () => console.log(`App listening on port ${port}!`)); app.listen(port, () => console.log(`App listening on port ${port}!`));
function handleUpload(req, res) { function getDecksList(req, res) {
const json = request.body; db.find({}, {'deck.meta': 1})
const deckJSON = json.json; .then(docs => res.json(docs))
const cardTemplate = fs.readFileSync('template/card.json'); .catch(err => res.status(404).end());
const template = JSON.parse(fs.readFileSync(`template/${deckJSON.type}/input.json`)); }
const cardCount = Object.entries(template.cardTypes)
.map(ct => deckJSON[ct[0]].length * (ct[1].back ? 2 : 1))
.reduce((sum, current) => sum + current, 0);
let deckOut = JSON.parse(fs.readFileSync('template/deck.json')); function getInputJSON(req, res) {
deckOut.ObjectStates[0].Nickname = deckJSON.name; db.findOne({_id: req.params.deckID})
.then(doc => res.json(doc.input))
.catch(err => res.status(404).end());
}
Object.assign(deckOut.ObjectStates[0].CustomDeck['1'], function getDeckImage(req, res) {
{NumWidth: Math.ceil(Math.sqrt(cardCount)), db.findOne({_id: req.params.deckID})
NumHeight: Math.ceil(Math.sqrt(cardCount)), .then(doc => res.send(new Buffer.from(doc.image, 'base64')))
FaceURL: `http://${req.headers.host}/deck/${deckJSON.name}/deck.png`, .catch(err => res.status(404).end());
BackURL: "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"}); }
let index = 100; function getTTSJSON(req, res) {
deckOut.ObjectStates[0].ContainedObjects = db.findOne({_id: req.params.deckID})
Object.entries(template.cardTypes).map( .then(doc => {
cardType => deckJSON[cardType[0]].map(cardIn => { let deckIn = doc.deck;
let cardOut = JSON.parse(cardTemplate); const cardTemplate = fs.readFileSync(__dirname + '/template/card.json');
Object.assign(cardOut, {Nickname: cardIn.name, const template = JSON.parse(fs.readFileSync(__dirname + `/template/${deckIn.type}/input.json`));
Description: cardIn.keywords, const cardCount = Object.entries(template.cardTypes)
CardID: index}); .map(ct => deckIn[ct[0]].length * (ct[1].back ? 2 : 1))
.reduce((sum, current) => sum + current, 0);
for (let ii=0; ii<(cardIn.count || 1); ii++) { let deckOut = JSON.parse(fs.readFileSync(__dirname + '/template/deck.json'));
deckOut.ObjectStates[0].DeckIDs.push(index); deckOut.ObjectStates[0].Nickname = deckIn.meta.name;
}
index++;
if(cardType[1].back) { Object.assign(deckOut.ObjectStates[0].CustomDeck['1'],
let cardBack = JSON.parse(cardTemplate); {NumWidth: Math.ceil(Math.sqrt(cardCount)),
Object.assign(cardBack, {Nickname: cardIn.back.name, NumHeight: Math.ceil(cardCount/Math.ceil(Math.sqrt(cardCount))),
Description: cardIn.back.keywords, FaceURL: `http://${req.headers.host}/decks/${doc.meta.name}.png`,
CardID: index}); BackURL: "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"});
cardOut.States = {"2": cardBack};
let index = 100;
deckOut.ObjectStates[0].ContainedObjects = Object
.keys(deckIn)
.filter(cardType => cardType !== 'meta')
.map(cardType => deckIn[cardType].map((card, index) => {
let cardOut = {...JSON.parse(cardTemplate),
Nickname: card.name,
Description: card.keywords,
CardID: index};
deckOut.ObjectStates[0].DeckIDs.push(...Array(card.count || 1).fill(index));
index++; index++;
}
return cardOut;
}))
.reduce((sum, cur) => sum.concat(cur), []);
fs.writeFileSync(`decks/${deckJSON.name}.json`, JSON.stringify(deckOut)); if(card.back) {
fs.writeFileSync(`decks/${deckJSON.name}.input.json`, JSON.stringify(deckJSON)); cardOut.States = {"2": {...JSON.parse(cardTemplate),
Nickname: card.back.name,
console.log("making page"); Description: card.back.keywords,
phantom.create().then( CardID: index}};
ph => ph.createPage().then( index++;
page => {
page.on('onLoadFinished', status => {
if (status !== 'success') {
console.log('Failed to load page');
ph.exit(1);
} }
else { return cardOut;
page.render(`decks/${deckJSON.name}.png`); }))
.reduce((sum, cur) => sum.concat(cur), []); // flatten
res.json(deckOut);
});
}
function handleUpload(req, res) {
const json = req.body;
console.log("Making deck image");
phantom.create().then(ph => ph.createPage().then(
page => {
page.on('onLoadFinished', status => {
if (status === 'success') {
page.renderBase64(`PNG`).then(image => {
db.update({_id: json.id},
{deck: json.deck, image: image},
{upsert: true, returnUpdatedDocs: true})
.then(doc => res.status(201).json({id: doc._id}));
page.close().then(() => ph.exit()); page.close().then(() => ph.exit());
} });
}); }
page.property('zoomFactor', 2); // pretty arbitrary else {
page.property('content', '<body style="margin:0;">' + json.body + '</body>'); console.log('Failed to load page');
})); ph.exit(1);
decks.push(deckJSON.name); }
} });
page.property('zoomFactor', 2); // pretty arbitrary
function send404(res, uri) { page.property('content', '<body style="margin:0;">' + json.body + '</body>');
res.writeHead(404, {'Content-type': "text/html; charset=utf-8"}); }));
const html = `
<head>
<title>404 Not Found</title>
<link rel="stylesheet" href="/css/common.css">
</head>
<body>
<h1>Error 404: Path ${uri.pathname} not found</h1>
You seem to have gone to the wrong place, would you like to go
back to the <a href="/">main page</a>?
</body>`;
res.end(html, 'utf-8');
} }