Compare commits
4 commits
077806411c
...
9616a2fe97
| Author | SHA1 | Date | |
|---|---|---|---|
| 9616a2fe97 | |||
| 0475e45275 | |||
| 38b92829cd | |||
| 0eeaa08239 |
10 changed files with 270 additions and 118 deletions
|
|
@ -210,10 +210,10 @@ class FFTCGDB
|
|||
logger.debug "getDecks: OK '#{userID}'"
|
||||
resolve (id: row.deck, content: JSON.parse row.json for row, i in rows)
|
||||
|
||||
delDeck: (deckID) ->
|
||||
delDeck: (userID, deckID) ->
|
||||
new Promise (resolve, reject) =>
|
||||
stmt = @db.prepare 'DELETE FROM decks WHERE deck = ?'
|
||||
stmt.run [deckID], (err) ->
|
||||
stmt = @db.prepare 'DELETE FROM decks WHERE deck = ? AND user = ?'
|
||||
stmt.run [deckID, userID], (err) ->
|
||||
stmt.finalize()
|
||||
if err
|
||||
logger.warn "delDeck: FAIL db '#{err.code}' for '#{deckID}'"
|
||||
|
|
|
|||
34
backend/routes/decks/add.coffee
Normal file
34
backend/routes/decks/add.coffee
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
logger = (require 'logging').default '/decks/add'
|
||||
|
||||
# session storage (volatile data)
|
||||
session = (require '../../session')
|
||||
# fftcg.db (persistent data)
|
||||
fftcgdb = (require '../../db')
|
||||
|
||||
module.exports =
|
||||
url: '/decks/add'
|
||||
method: 'POST'
|
||||
# schema: (require './modify.schema')
|
||||
|
||||
handler: (request, reply) ->
|
||||
session.check request.body.session ? ""
|
||||
.then (userid) ->
|
||||
# active session found, get associated user
|
||||
fftcgdb.addDeck (userid), (request.body.deckCards)
|
||||
.then (deckID) ->
|
||||
logger.info "OK user '#{userid}' added deck '#{deckID}'"
|
||||
reply.send
|
||||
success: true
|
||||
deck: deckID
|
||||
|
||||
.catch (err) ->
|
||||
# couldnt get user details
|
||||
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||
reply.send
|
||||
success: false
|
||||
|
||||
.catch ->
|
||||
# no session found
|
||||
logger.info "FAIL '#{request.body.session}' session not found"
|
||||
reply.send
|
||||
success: false
|
||||
34
backend/routes/decks/delete.coffee
Normal file
34
backend/routes/decks/delete.coffee
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
logger = (require 'logging').default '/decks/delete'
|
||||
|
||||
# session storage (volatile data)
|
||||
session = (require '../../session')
|
||||
# fftcg.db (persistent data)
|
||||
fftcgdb = (require '../../db')
|
||||
|
||||
module.exports =
|
||||
url: '/decks/delete'
|
||||
method: 'POST'
|
||||
# schema: (require './modify.schema')
|
||||
|
||||
handler: (request, reply) ->
|
||||
session.check request.body.session ? ""
|
||||
.then (userid) ->
|
||||
# active session found, get associated user
|
||||
fftcgdb.delDeck (userid), (request.body.deckID)
|
||||
.then (deckID) ->
|
||||
logger.info "OK user '#{userid}' deleted deck '#{deckID}'"
|
||||
reply.send
|
||||
success: true
|
||||
deck: deckID
|
||||
|
||||
.catch (err) ->
|
||||
# couldnt get user details
|
||||
logger.warn "FAIL '#{err}' for user id '#{userid}'"
|
||||
reply.send
|
||||
success: false
|
||||
|
||||
.catch ->
|
||||
# no session found
|
||||
logger.info "FAIL '#{request.body.session}' session not found"
|
||||
reply.send
|
||||
success: false
|
||||
|
|
@ -22,8 +22,12 @@ fastify.route (require "./routes/#{route}") for route in [
|
|||
'user/register'
|
||||
# list decks
|
||||
'decks/list'
|
||||
# add deck
|
||||
'decks/add'
|
||||
# modify deck
|
||||
'decks/modify'
|
||||
# delete deck
|
||||
'decks/delete'
|
||||
]
|
||||
|
||||
# request logging
|
||||
|
|
|
|||
|
|
@ -3,67 +3,94 @@
|
|||
import CardsDB from '@/plugins/ffdecks'
|
||||
|
||||
export default class {
|
||||
constructor() {
|
||||
this.id = null
|
||||
constructor(id) {
|
||||
this.id = id
|
||||
this.name = ''
|
||||
this.note = ''
|
||||
this.cards = []
|
||||
}
|
||||
|
||||
from_plainObject(obj) {
|
||||
this.id = obj.id
|
||||
this.name = obj.content.name
|
||||
this.note = obj.content.note
|
||||
this.cards = obj.content.cards
|
||||
populate() {
|
||||
for (let card of this.cards) {
|
||||
card.dbentry = CardsDB[card.serial]
|
||||
}
|
||||
}
|
||||
|
||||
from_object(obj) {
|
||||
if (obj) {
|
||||
this.name = obj.name
|
||||
this.note = obj.note
|
||||
this.cards = obj.cards
|
||||
}
|
||||
}
|
||||
|
||||
plainObject() {
|
||||
let plainCards = []
|
||||
for (let card of this.cards) {
|
||||
plainCards.push({
|
||||
serial: card.serial,
|
||||
count: card.count
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
name: this.name,
|
||||
note: this.note,
|
||||
cards: plainCards
|
||||
}
|
||||
}
|
||||
|
||||
from_deckList(str) {
|
||||
// select all lines containing card serial numbers
|
||||
let cardLinesRE = /^.*\b\d-\d{3}[A-Z]?\b.*$/gm
|
||||
let cardLinesRE = /^.*\b\d+-0*\d{1,3}[A-Z]?\b.*$/gm
|
||||
let cardLines = str.match(cardLinesRE)
|
||||
let cardCounts = {}
|
||||
|
||||
cardLines.forEach(cardLine => {
|
||||
// extract serial (guaranteed to be in here!)
|
||||
let serialRE = /\b(\d-\d{3})[A-Z]?\b/i
|
||||
let serial = serialRE.exec(cardLine)[1]
|
||||
if (cardLines) {
|
||||
for (let cardLine of cardLines) {
|
||||
// extract serial (guaranteed to be in here!)
|
||||
let serialRE = /\b(\d+)-0*(\d{1,3})[A-Z]?\b/i
|
||||
let serial = serialRE.exec(cardLine)
|
||||
// force format 'x-xxx'
|
||||
serial = `${serial[1]}-${serial[2].padStart(3, '0')}`
|
||||
|
||||
// strip out serial number
|
||||
cardLine = cardLine.replace(serialRE, '')
|
||||
// strip out serial number
|
||||
cardLine = cardLine.replace(serialRE, '')
|
||||
|
||||
let countREs = [
|
||||
// prioritize a count with "times" symbol *, x, ×
|
||||
/\b([0-9]+)(?:[*×]|[x]\b)/,
|
||||
/(?:[*×]|\b[x])([0-9]+)\b/,
|
||||
// next priority: count with whitespace
|
||||
/\s+([0-9]+)\s+/,
|
||||
/\s+([0-9]+)\b/,
|
||||
/\b([0-9]+)\s+/,
|
||||
// least priority: any simple number
|
||||
/\b([0-9]+)\b/
|
||||
]
|
||||
let countREs = [
|
||||
// prioritize a count with "times" symbol *, x, ×
|
||||
/\b([0-9]+)(?:[*×]|[x]\b)/,
|
||||
/(?:[*×]|\b[x])([0-9]+)\b/,
|
||||
// next priority: count with whitespace
|
||||
/\s+([0-9]+)\s+/,
|
||||
/\s+([0-9]+)\b/,
|
||||
/\b([0-9]+)\s+/,
|
||||
// least priority: any simple number
|
||||
/\b([0-9]+)\b/
|
||||
]
|
||||
|
||||
// fallback value
|
||||
let count = 1
|
||||
for (let countRE of countREs) {
|
||||
let data = countRE.exec(cardLine)
|
||||
// fallback value
|
||||
let count = 1
|
||||
for (let countRE of countREs) {
|
||||
let data = countRE.exec(cardLine)
|
||||
|
||||
if (data) {
|
||||
count = Number(data[1])
|
||||
break
|
||||
if (data) {
|
||||
count = Number(data[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// count copies
|
||||
if (!cardCounts[serial]) {
|
||||
cardCounts[serial] = 0
|
||||
// count copies
|
||||
if (!cardCounts[serial]) {
|
||||
cardCounts[serial] = 0
|
||||
}
|
||||
cardCounts[serial] += count
|
||||
}
|
||||
cardCounts[serial] += count
|
||||
})
|
||||
}
|
||||
|
||||
// push card data into deck
|
||||
this.cards = []
|
||||
for(let serial in cardCounts) {
|
||||
for (let serial in cardCounts) {
|
||||
this.cards.push({
|
||||
serial: serial,
|
||||
count: cardCounts[serial]
|
||||
|
|
@ -98,14 +125,6 @@ export default class {
|
|||
}
|
||||
}
|
||||
|
||||
plainObject() {
|
||||
return {
|
||||
name: this.name,
|
||||
note: this.note,
|
||||
cards: this.cards
|
||||
}
|
||||
}
|
||||
|
||||
parts() {
|
||||
let retval = ['Forwards', 'Backups', 'Summons, Monsters & more'].map(
|
||||
item => ({
|
||||
|
|
@ -150,10 +169,10 @@ export default class {
|
|||
lines.push('')
|
||||
|
||||
// list each deck part
|
||||
for(let part of this.parts()) {
|
||||
for (let part of this.parts()) {
|
||||
lines.push(`${part.heading} (${part.count}):`)
|
||||
|
||||
for(let card of part.cards)
|
||||
for (let card of part.cards)
|
||||
lines.push(`${card.count}x ${card.serial} "${card.dbentry.name}"`)
|
||||
|
||||
lines.push('')
|
||||
|
|
@ -165,10 +184,4 @@ export default class {
|
|||
count() {
|
||||
return this.cards.reduce((total, card) => total + card.count, 0)
|
||||
}
|
||||
|
||||
populate() {
|
||||
for (let card of this.cards) {
|
||||
card.dbentry = CardsDB[card.serial]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@
|
|||
<v-list-tile avatar>
|
||||
<v-tooltip @input="booted = true" bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-list-tile-avatar v-on="on">
|
||||
<v-list-tile-avatar v-on="on" style="cursor: zoom-in">
|
||||
<svg
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="16px"
|
||||
height="30px"
|
||||
viewBox="0 0 16 30"
|
||||
style="enable-background:new 0 0 16 30;"
|
||||
style="enable-background:new 0 0 16 30"
|
||||
xml:space="preserve"
|
||||
>
|
||||
<polygon points="0,5 8,0 16,5 16,25 8,30 0,25" :fill="color" />
|
||||
|
|
|
|||
|
|
@ -28,15 +28,49 @@
|
|||
<v-btn fab absolute bottom right @click.native="editing = true">
|
||||
<v-icon>edit</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-dialog v-model="deleting" persistent>
|
||||
<v-btn fab absolute bottom left slot="activator">
|
||||
<v-icon>delete</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-card>
|
||||
<v-card-title class="headline">
|
||||
Really delete this deck?
|
||||
</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
Are you sure you want to delete this deck? This cannot be undone.
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn color="error" @click.native="deleting = false">
|
||||
Cancel
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="success"
|
||||
@click.native="
|
||||
deleting = false
|
||||
delete_deck()
|
||||
"
|
||||
>
|
||||
Confirm
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
|
||||
<DeckEditor
|
||||
:visible="editing"
|
||||
:deck="deck"
|
||||
@close="editing = false"
|
||||
@change="change_deck"
|
||||
/>
|
||||
<DeckEditor v-if="editing" :deck="deck" @save="save_deck">
|
||||
<v-btn color="error" @click.native="editing = false">
|
||||
<v-icon>cancel</v-icon>
|
||||
cancel
|
||||
</v-btn>
|
||||
</DeckEditor>
|
||||
</v-expansion-panel-content>
|
||||
</template>
|
||||
|
||||
|
|
@ -60,7 +94,8 @@ export default {
|
|||
},
|
||||
|
||||
data: () => ({
|
||||
editing: false
|
||||
editing: false,
|
||||
deleting: false
|
||||
}),
|
||||
|
||||
computed: {
|
||||
|
|
@ -68,18 +103,30 @@ export default {
|
|||
},
|
||||
|
||||
methods: {
|
||||
change_deck(new_deck) {
|
||||
save_deck(new_deck) {
|
||||
axios
|
||||
.post('/decks/modify', {
|
||||
session: this.session,
|
||||
deckID: this.deck.id,
|
||||
deckCards: new_deck
|
||||
deckCards: new_deck.plainObject()
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
this.editing = false
|
||||
this.$emit('change')
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
delete_deck() {
|
||||
axios
|
||||
.post('/decks/delete', {
|
||||
session: this.session,
|
||||
deckID: this.deck.id
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
this.$emit('change')
|
||||
} else {
|
||||
this.editing = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,17 @@
|
|||
<template>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel v-model="open">
|
||||
<Deck
|
||||
v-for="deck in linked"
|
||||
:deck="deck"
|
||||
:key="deck.id"
|
||||
@change="refresh_decks"
|
||||
/>
|
||||
<NewDeck />
|
||||
<NewDeck
|
||||
@change="
|
||||
open = null
|
||||
refresh_decks()
|
||||
"
|
||||
/>
|
||||
</v-expansion-panel>
|
||||
</template>
|
||||
|
||||
|
|
@ -26,6 +31,10 @@ export default {
|
|||
NewDeck
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
open: null
|
||||
}),
|
||||
|
||||
asyncComputed: {
|
||||
decks: {
|
||||
get() {
|
||||
|
|
@ -50,8 +59,9 @@ export default {
|
|||
let result = []
|
||||
|
||||
for (let plainDeck of this.decks) {
|
||||
let deck = new DeckJS()
|
||||
deck.from_plainObject(plainDeck)
|
||||
let deck = new DeckJS(plainDeck.id)
|
||||
|
||||
deck.from_object(plainDeck.content)
|
||||
deck.populate()
|
||||
|
||||
result.push(deck)
|
||||
|
|
|
|||
|
|
@ -6,13 +6,16 @@
|
|||
|
||||
<v-container>
|
||||
<v-card flat>
|
||||
<!-- <DeckEditor :visible="true" value="" @close="" @change="" /> -->
|
||||
<DeckEditor ref="editor" @save="save_deck" />
|
||||
</v-card>
|
||||
</v-container>
|
||||
</v-expansion-panel-content>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import * as Cookies from 'js-cookie'
|
||||
import axios from '@/plugins/axios'
|
||||
|
||||
import DeckEditor from './forms/DeckEditor.vue'
|
||||
|
||||
export default {
|
||||
|
|
@ -20,6 +23,26 @@ export default {
|
|||
|
||||
components: {
|
||||
DeckEditor
|
||||
},
|
||||
|
||||
computed: {
|
||||
session: () => Cookies.get('session')
|
||||
},
|
||||
|
||||
methods: {
|
||||
save_deck(new_deck) {
|
||||
axios
|
||||
.post('/decks/add', {
|
||||
session: this.session,
|
||||
deckCards: new_deck.plainObject()
|
||||
})
|
||||
.then(response => {
|
||||
if (response.data.success) {
|
||||
this.$emit('change')
|
||||
this.$refs.editor.clear()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<v-container v-if="visible">
|
||||
<v-container>
|
||||
<v-card flat>
|
||||
<v-alert :value="check.count !== 50" type="warning">
|
||||
{{ check.count }} cards detected! (Decks should have exactly 50 cards)
|
||||
|
|
@ -10,33 +10,26 @@
|
|||
</v-alert>
|
||||
|
||||
<v-textarea
|
||||
ref="deckList"
|
||||
label="Edit Deck"
|
||||
v-model="new_decklist"
|
||||
rows="35"
|
||||
hint="Change card counts and/or serial numbers. Names will be updated accordingly!"
|
||||
style="font-family: monospace"
|
||||
:value="new_deck.deckList()"
|
||||
@input="check.checked = false"
|
||||
>
|
||||
</v-textarea>
|
||||
|
||||
<v-card-actions>
|
||||
<v-btn color="error" @click.native="close">
|
||||
<v-icon>cancel</v-icon>
|
||||
cancel
|
||||
</v-btn>
|
||||
|
||||
<slot></slot>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn color="info" @click.native="check_deck">
|
||||
<v-btn color="info" @click.native="validate" :disabled="check.checked">
|
||||
<v-icon>check</v-icon>
|
||||
validate
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
color="success"
|
||||
@click.native="save_deck"
|
||||
:disabled="!check.checked"
|
||||
>
|
||||
<v-btn color="success" @click.native="save" :disabled="!check.checked">
|
||||
<v-icon>save</v-icon>
|
||||
save
|
||||
</v-btn>
|
||||
|
|
@ -52,57 +45,51 @@ export default {
|
|||
name: 'DeckEditor',
|
||||
|
||||
props: {
|
||||
deck: Object,
|
||||
visible: Boolean
|
||||
deck: Object
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
check: {
|
||||
count: 50,
|
||||
maximum: 0,
|
||||
checked: false
|
||||
},
|
||||
new_decklist: null
|
||||
check: null,
|
||||
new_deck: null
|
||||
}),
|
||||
|
||||
created() {
|
||||
this.new_decklist = this.deck.deckList()
|
||||
},
|
||||
|
||||
computed: {
|
||||
new_deck() {
|
||||
let deck = new Deck()
|
||||
deck.from_deckList(this.new_decklist)
|
||||
deck.populate()
|
||||
|
||||
this.new_decklist = deck.deckList()
|
||||
return deck
|
||||
}
|
||||
this.clear()
|
||||
},
|
||||
|
||||
methods: {
|
||||
close() {
|
||||
this.check.checked = false
|
||||
this.$emit('close')
|
||||
clear() {
|
||||
this.check = {
|
||||
count: 50,
|
||||
maximum: 0,
|
||||
checked: false
|
||||
}
|
||||
|
||||
this.new_deck = new Deck(0)
|
||||
if (this.deck)
|
||||
// this.deck should already be populated!
|
||||
this.new_deck.from_object(this.deck)
|
||||
},
|
||||
|
||||
check_deck() {
|
||||
validate() {
|
||||
this.new_deck.from_deckList(this.$refs.deckList.lazyValue)
|
||||
this.new_deck.populate()
|
||||
|
||||
// count number of cards
|
||||
this.check.count = this.new_deck.count()
|
||||
|
||||
// find most frequent card
|
||||
this.check.maximum = 0
|
||||
this.new_deck.cards.forEach(card => {
|
||||
for (let card of this.new_deck.cards) {
|
||||
if (card.count > this.check.maximum) this.check.maximum = card.count
|
||||
})
|
||||
}
|
||||
|
||||
// deck has now been checked
|
||||
this.check.checked = true
|
||||
},
|
||||
|
||||
save_deck() {
|
||||
this.$emit('change', this.new_deck.plainObject())
|
||||
this.close()
|
||||
save() {
|
||||
this.$emit('save', this.new_deck)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Reference in a new issue