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
/dist/
/.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",
"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": {
"version": "1.2.1",
"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",
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz",
@ -4422,6 +4435,22 @@
"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": {
"version": "4.17.11",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
@ -4679,6 +4708,33 @@
"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": {
"version": "0.6.1",
"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",
"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": {
"version": "1.0.4",
"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": {
"express": "^4.16.4",
"interactjs": "^1.3.4",
"nedb-promises": "^3.0.2",
"parcel-bundler": "^1.11.0",
"phantom": "^4.0.12",
"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>
<body>
<label>Create New Deck:
<input type="text"
@change="window.location='/deck/' + event.target.value + '/editor'">
</label>
<router-link to="/edit/new">Create New Deck</router-link>
<ul>
<li v-for="deck in decks">
{{ deck }}:
<router-link :to="'/play/' + deck"> Play </router-link>
<router-link :to="'/edit/' + deck"> Edit </router-link>
{{ deck.deck.meta.name }}:
<router-link :to="'/play/' + deck._id"> Play </router-link>
<router-link :to="'/edit/' + deck._id"> Edit </router-link>
</li>
</ul>
</body>
@ -26,7 +23,8 @@
created() {
fetch('/decks.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>

View File

@ -8,7 +8,7 @@
</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
</button>
</div>
@ -21,11 +21,11 @@
<form>
<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>
<label> Deck Type:
<select v-model="deckInfo.type">
<select v-model="deckInfo.meta.type">
<option value="hero">hero</option>
<option value="villain">villain</option>
<option value="environment">environment</option>
@ -37,84 +37,53 @@
<div id="cardEditor" v-if="selected">
<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 }}
<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>
</label>
</div>
</div>
<div ref="deck" :style="deckStyle">
<div v-for="card in cards" @click="selected = card">
<Hero v-bind="card.card"> </Hero>
</div>
</div>
<Deck ref="deck" :deckInfo="deckInfo" v-model="selected"> </Deck>
</div>
</template>
<script>
import Hero from './template/hero/Hero.vue'
import Deck from './Deck.vue';
export default {
name: 'Editor',
components: {Deck},
components: {Hero},
props: ['deckName'],
props: ['deckID'],
data() {
return {
selected: null,
deckInfo: {},
deckInfo: {meta: {name: "", type: ""}},
};
},
watch: {
deckName() {
document.title = "Editor|" + deckName;
deckInfo() {
document.title = "Editor|" + this.deckInfo.meta.name;
},
},
created() {
fetch('/decks/' + this.deckName + '.input.json')
.then(r => r.json())
.then(j => this.deckInfo = j);
if (this.deckID !== 'new') {
fetch('/decks/' + this.deckID + '.json')
.then(r => r.json())
.then(j => this.deckInfo = j.input)
.catch((err) => console.log('did not get old JSON, starting new deck'));
}
/* window.addEventListener(
* '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: {
// deck JSON uploader
jsonUpload(event) {
@ -131,22 +100,15 @@
console.log(JSON.stringify(this.deckInfo));
this.downloadFile('data:application/json;charset=utf-8,' +
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) {
let prop = event.target.id.substring(5);
if (prop === "image") {
let files = event.target.files;
let reader = new FileReader();
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]);
}
fileUploaded(event, prop) {
let reader = new FileReader();
reader.onload = e => {
this.selected.card[prop] = e.target.result;
};
reader.readAsDataURL(event.target.files[0]);
},
downloadFile(file, name) {
@ -159,13 +121,19 @@
},
upload() {
// POST the generated SVGs to the server
let data = (new XMLSerializer()).serializeToString(this.$refs.deck);
fetch('upload', {
// POST the inputed json to the server
fetch('/upload', {
method: 'post',
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 DeckIndex from './DeckIndex.vue';
import Editor from './Editor.vue';
import Err404 from './404.vue';
Vue.use(VueRouter);
const router = new VueRouter({
mode: 'history',
routes: [
{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
"use strict";
const express = require('express'),
Bundler = require('parcel-bundler'),
fs = require('fs'),
path = require('path'),
phantom = require('phantom'),
port = process.env.PORT || 1234;
const express = require('express'),
Bundler = require('parcel-bundler'),
fs = require('fs'),
path = require('path'),
phantom = require('phantom'),
Datastore = require('nedb-promises'),
port = process.env.PORT || 1234;
const decks = ["the_Unholy_Priest_update_2",
"NZoths_Invasion_1.2",
"Puffer_Fish_input_1.3"];
const db = Datastore.create('decks.db');
const app = express();
app.use(express.json());
app.use(express.json({limit: '50mb'}));
app.use('/template', express.static('template'));
app.use('/decks', express.static('decks'));
app.get('/decks.json', (req, res) => res.json(decks));
app.get('/decks/:deckID.tts.json', getTTSJSON);
app.get('/decks/:deckID.json', getInputJSON);
app.get('/decks/:deckID.png', getDeckImage);
app.get('/decks.json', getDecksList);
app.post('/upload', handleUpload);
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}!`));
function handleUpload(req, res) {
const json = request.body;
const deckJSON = json.json;
const cardTemplate = fs.readFileSync('template/card.json');
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);
function getDecksList(req, res) {
db.find({}, {'deck.meta': 1})
.then(docs => res.json(docs))
.catch(err => res.status(404).end());
}
let deckOut = JSON.parse(fs.readFileSync('template/deck.json'));
deckOut.ObjectStates[0].Nickname = deckJSON.name;
function getInputJSON(req, res) {
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'],
{NumWidth: Math.ceil(Math.sqrt(cardCount)),
NumHeight: Math.ceil(Math.sqrt(cardCount)),
FaceURL: `http://${req.headers.host}/deck/${deckJSON.name}/deck.png`,
BackURL: "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"});
function getDeckImage(req, res) {
db.findOne({_id: req.params.deckID})
.then(doc => res.send(new Buffer.from(doc.image, 'base64')))
.catch(err => res.status(404).end());
}
let index = 100;
deckOut.ObjectStates[0].ContainedObjects =
Object.entries(template.cardTypes).map(
cardType => deckJSON[cardType[0]].map(cardIn => {
let cardOut = JSON.parse(cardTemplate);
Object.assign(cardOut, {Nickname: cardIn.name,
Description: cardIn.keywords,
CardID: index});
function getTTSJSON(req, res) {
db.findOne({_id: req.params.deckID})
.then(doc => {
let deckIn = doc.deck;
const cardTemplate = fs.readFileSync(__dirname + '/template/card.json');
const template = JSON.parse(fs.readFileSync(__dirname + `/template/${deckIn.type}/input.json`));
const cardCount = Object.entries(template.cardTypes)
.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++) {
deckOut.ObjectStates[0].DeckIDs.push(index);
}
index++;
let deckOut = JSON.parse(fs.readFileSync(__dirname + '/template/deck.json'));
deckOut.ObjectStates[0].Nickname = deckIn.meta.name;
if(cardType[1].back) {
let cardBack = JSON.parse(cardTemplate);
Object.assign(cardBack, {Nickname: cardIn.back.name,
Description: cardIn.back.keywords,
CardID: index});
cardOut.States = {"2": cardBack};
Object.assign(deckOut.ObjectStates[0].CustomDeck['1'],
{NumWidth: Math.ceil(Math.sqrt(cardCount)),
NumHeight: Math.ceil(cardCount/Math.ceil(Math.sqrt(cardCount))),
FaceURL: `http://${req.headers.host}/decks/${doc.meta.name}.png`,
BackURL: "http://cloud-3.steamusercontent.com/ugc/156906385556221451/CE2C3AFE1759790CB0B532FFD636D05A99EC91F4/"});
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++;
}
return cardOut;
}))
.reduce((sum, cur) => sum.concat(cur), []);
fs.writeFileSync(`decks/${deckJSON.name}.json`, JSON.stringify(deckOut));
fs.writeFileSync(`decks/${deckJSON.name}.input.json`, JSON.stringify(deckJSON));
console.log("making page");
phantom.create().then(
ph => ph.createPage().then(
page => {
page.on('onLoadFinished', status => {
if (status !== 'success') {
console.log('Failed to load page');
ph.exit(1);
if(card.back) {
cardOut.States = {"2": {...JSON.parse(cardTemplate),
Nickname: card.back.name,
Description: card.back.keywords,
CardID: index}};
index++;
}
else {
page.render(`decks/${deckJSON.name}.png`);
return cardOut;
}))
.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.property('zoomFactor', 2); // pretty arbitrary
page.property('content', '<body style="margin:0;">' + json.body + '</body>');
}));
decks.push(deckJSON.name);
}
function send404(res, uri) {
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');
});
}
else {
console.log('Failed to load page');
ph.exit(1);
}
});
page.property('zoomFactor', 2); // pretty arbitrary
page.property('content', '<body style="margin:0;">' + json.body + '</body>');
}));
}