Compare commits
6 Commits
Author | SHA1 | Date |
---|---|---|
koehr | f4e603b98e | 4 years ago |
koehr | c7022133a0 | 4 years ago |
koehr | 749ab36ac1 | 4 years ago |
koehr | 59de19b63c | 4 years ago |
koehr | 0c6212faad | 4 years ago |
koehr | f235266108 | 4 years ago |
@ -0,0 +1,160 @@
|
||||
.card-front, .card-back {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
.card-front {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.card-front > header {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
height: 3.6rem;
|
||||
color: white;
|
||||
font-size: 1.2rem;
|
||||
font-weight: normal;
|
||||
font-variant: small-caps;
|
||||
padding: 0 1em;
|
||||
text-align: left;
|
||||
}
|
||||
.card-front > header > h1 {
|
||||
margin: 0;
|
||||
line-height: .9em;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.card-front > header > img {
|
||||
height: 3rem;
|
||||
}
|
||||
.card-front > main {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
margin: .1rem .4rem .5rem;
|
||||
padding: .2rem 1rem;
|
||||
background: white;
|
||||
border-radius: 1rem;
|
||||
font-size: 1.2rem;
|
||||
color: black;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-back {
|
||||
justify-content: center;
|
||||
}
|
||||
.card-back > .icon-wrapper {
|
||||
margin: 3em;
|
||||
}
|
||||
|
||||
.card-content .cdx-block {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-content .ce-paragraph, .card-content p {
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.card-content ul {
|
||||
list-style-position: inside;
|
||||
margin: 0;
|
||||
padding-left: .5em;
|
||||
}
|
||||
.card-content li > p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.card-content h2 {
|
||||
font-size: 1.4rem;
|
||||
color: var(--highlight-color);
|
||||
margin: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.card-content h3 {
|
||||
font-size: 1.4rem;
|
||||
color: var(--highlight-color);
|
||||
margin: 0 0 .2em 0;
|
||||
font-weight: normal;
|
||||
font-variant: small-caps;
|
||||
line-height: .9em;
|
||||
border-bottom: 1px solid var(--highlight-color);
|
||||
}
|
||||
|
||||
.card-content .card-delimiter {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
padding: 0;
|
||||
border: 2px solid var(--highlight-color);
|
||||
}
|
||||
.card-content .card-delimiter.pointing-right {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
border-style: solid;
|
||||
border-width: 2px 0 2px 220px;
|
||||
border-color: transparent transparent transparent var(--highlight-color);
|
||||
}
|
||||
.card-content .card-delimiter.pointing-left {
|
||||
height: 0;
|
||||
margin: .2em 0;
|
||||
border-style: solid;
|
||||
border-width: 2px 220px 2px 0;
|
||||
border-color: transparent var(--highlight-color) transparent transparent;
|
||||
}
|
||||
.card-content .cdx-list__item {
|
||||
padding: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.card-content .card-charges-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
min-height: 1em;
|
||||
}
|
||||
.card-content .card-charges-wrapper.card-charges-stretch { justify-content: space-around; }
|
||||
.card-content .card-charges-wrapper > .card-charge {
|
||||
width: 1.0em;
|
||||
height: 1.0em;
|
||||
border: 2px solid var(--highlight-color);
|
||||
margin: .5em .2em;
|
||||
}
|
||||
.card-content .card-charges-wrapper > .card-charge-circle { border-radius: 100%; }
|
||||
.card-content .card-charges-wrapper > .card-charge-size-1 { width: 1.0em; height: 1.0em; }
|
||||
.card-content .card-charges-wrapper > .card-charge-size-2 { width: 1.2em; height: 1.2em; }
|
||||
.card-content .card-charges-wrapper > .card-charge-size-3 { width: 1.4em; height: 1.4em; }
|
||||
.card-content .card-charges-wrapper > .card-charge-size-4 { width: 1.6em; height: 1.6em; }
|
||||
.card-content .card-charges-wrapper > .card-charge-size-5 { width: 1.8em; height: 1.8em; }
|
||||
|
||||
.card-content .card-dnd-stats {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
color: var(--highlight-color);
|
||||
}
|
||||
.card-content .dnd-stat-block {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
font-size: .8em;
|
||||
}
|
||||
.card-content .dnd-stat-block > .dnd-stat-title {
|
||||
width: 100%;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
.card-content .dnd-stat-block > input {
|
||||
width: 50%;
|
||||
background: white;
|
||||
color: var(--highlight-color);
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.card-content .dnd-stat-block {
|
||||
}
|
||||
[contenteditable="true"] { outline: none; }
|
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="switch">
|
||||
<input :id="id" class="checkbox" type="checkbox" :checked="value" @change="$emit('input', !value)" />
|
||||
<label :for="id">
|
||||
<div class="switch-label-text">{{ label }}</div>
|
||||
<div class="switch-elements-wrapper">
|
||||
<div class="switch-elements">
|
||||
<div class="switch-element off"><slot name="off">NO</slot></div>
|
||||
<div class="switch-element btn"></div>
|
||||
<div class="switch-element on"><slot name="on">YES</slot></div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
|
||||
@Component
|
||||
export default class FlipSwitch extends Vue {
|
||||
@Prop() public readonly id!: string
|
||||
@Prop() public readonly value!: boolean
|
||||
@Prop() public readonly label!: string
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.switch > input {
|
||||
display: none;
|
||||
}
|
||||
.switch .switch-elements-wrapper {
|
||||
height: 2em;
|
||||
width: 4em;
|
||||
border: 4px solid black;
|
||||
border-radius: 2em;
|
||||
background-color: black;
|
||||
overflow: hidden;
|
||||
}
|
||||
.switch .switch-elements {
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
transition: transform .2s ease-in;
|
||||
}
|
||||
.switch .switch-element {
|
||||
height: 1.8em;
|
||||
width: 1.8em;
|
||||
margin: .1em;
|
||||
line-height: 1.8em;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.switch .btn {
|
||||
background-color: gray;
|
||||
border-radius: 5em;
|
||||
}
|
||||
|
||||
input.checkbox:checked + label .switch-elements-wrapper > .switch-elements {
|
||||
transform: translate(-2em, 0);
|
||||
}
|
||||
input.checkbox:checked + label .switch-elements-wrapper {
|
||||
box-shadow: 0 0 15px 2px green;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<div id="print-options-form">
|
||||
<header>Print Deck</header>
|
||||
|
||||
<form @submit.prevent="printDeck">
|
||||
<div class="deck-form-fields">
|
||||
<label for="print-option-page-size">
|
||||
Page Size
|
||||
<select class="print-option-select" id="print-option-page-size" v-model="pageSize">
|
||||
<option :key="size.value" :value="size.value" v-for="size in pageSizes">{{ size.title }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label for="print-option-card-size">
|
||||
Card Size
|
||||
<select class="print-option-select" id="print-option-card-size" v-model="cardSize">
|
||||
<option :key="size.value" :value="size.value" v-for="size in cardSizes">{{ size.title }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label for="print-option-arrangement">
|
||||
Arrangement
|
||||
<select class="print-option-select" id="print-option-arrangement" v-model="arrangement">
|
||||
<option :key="arrangement.value" :value="arrangement.value" v-for="arrangement in arrangements">{{ arrangement.title }}</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<FlipSwitch id="print-option-rounded-corners" label="Rounded Corners" v-model="roundedCorners">
|
||||
</FlipSwitch>
|
||||
|
||||
<button type="submit">Print deck</button>
|
||||
<button class="cancel" @click.prevent="$emit('close')">cancel</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import { Deck } from '@/types'
|
||||
import {
|
||||
cardSizeOptions,
|
||||
pageSizeOptions,
|
||||
arrangementOptions,
|
||||
defaultCardSize,
|
||||
defaultPageSize,
|
||||
defaultArrangement
|
||||
} from '@/consts'
|
||||
import FlipSwitch from '@/components/flip-switch.vue'
|
||||
|
||||
@Component({
|
||||
components: { FlipSwitch }
|
||||
})
|
||||
export default class EditDeckForm extends Vue {
|
||||
@Prop() public readonly deck!: Deck
|
||||
|
||||
private pageSizes = pageSizeOptions
|
||||
private cardSizes = cardSizeOptions
|
||||
private arrangements = arrangementOptions
|
||||
|
||||
private pageSize = defaultPageSize
|
||||
private cardSize = defaultCardSize
|
||||
private arrangement = defaultArrangement
|
||||
private roundedCorners = true
|
||||
|
||||
private mounted () {
|
||||
this.cardSize = this.deck.cardSize
|
||||
this.pageSize = this.deck.pageSize
|
||||
this.arrangement = this.deck.arrangement
|
||||
this.roundedCorners = this.deck.roundedCorners
|
||||
}
|
||||
|
||||
private printDeck () {
|
||||
this.$storage.saveDeck({
|
||||
...this.deck,
|
||||
arrangement: this.arrangement,
|
||||
pageSize: this.pageSize,
|
||||
cardSize: this.cardSize,
|
||||
roundedCorners: this.roundedCorners
|
||||
})
|
||||
console.log('would print on', this.pageSize, `(${this.arrangement})`, this.deck.cards.length, 'cards of size', this.cardSize, this.roundedCorners ? 'with rounded corners' : '')
|
||||
window.open(`/print/${this.deck.id}`, '_blank')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.print-option-select {
|
||||
width: 55%;
|
||||
}
|
||||
.deck-form-fields {
|
||||
width: 100%;
|
||||
max-width: 20em;
|
||||
margin: auto;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="card" :style="containerStyle">
|
||||
<div class="card-front" v-if="showFront">
|
||||
<header>
|
||||
<h1>{{ card.name }}</h1>
|
||||
<img :src="icon" />
|
||||
</header>
|
||||
<main ref="cardEl" class="card-content">
|
||||
</main>
|
||||
</div>
|
||||
<div class="card-back" v-if="showBack">BACK</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator'
|
||||
import { Deck, Card } from '@/types'
|
||||
import { iconPath } from '@/lib'
|
||||
|
||||
import Editor from '@editorjs/editorjs'
|
||||
import List from '@editorjs/list'
|
||||
import { Heading, Delimiter, Charges, DnDStats } from '@/editor'
|
||||
|
||||
@Component
|
||||
export default class StaticCard extends Vue {
|
||||
@Prop() public readonly card!: Card
|
||||
@Prop() public readonly deck!: Deck
|
||||
@Prop({ default: false }) public readonly showFront!: boolean
|
||||
@Prop({ default: false }) public readonly showBack!: boolean
|
||||
|
||||
private editor!: Editor
|
||||
|
||||
private mounted () {
|
||||
this.editor = new Editor({
|
||||
holder: this.$refs.cardEl as HTMLElement,
|
||||
autofocus: false,
|
||||
hideToolbar: true,
|
||||
tools: {
|
||||
list: { class: List, inlineToolbar: false },
|
||||
heading: { class: Heading, inlineToolbar: false },
|
||||
delimiter: { class: Delimiter, inlineToolbar: false },
|
||||
charges: { class: Charges, inlineToolbar: false },
|
||||
dndstats: { class: DnDStats, inlineToolbar: false }
|
||||
},
|
||||
data: this.card.content,
|
||||
onReady: () => {
|
||||
console.log('editor is ready, what to do?')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private get icon () {
|
||||
const icon = this.card.icon || this.deck.icon
|
||||
return iconPath(icon)
|
||||
}
|
||||
|
||||
private get backIcon () {
|
||||
const icon = this.card.backIcon || this.deck.icon
|
||||
return iconPath(icon)
|
||||
}
|
||||
|
||||
private get containerStyle () {
|
||||
const color = (this.deck && this.deck.color) || this.card.color
|
||||
|
||||
return {
|
||||
'--highlight-color': color
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="@/assets/card.css" />
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
height: auto;
|
||||
width: auto;
|
||||
background-color: var(--highlight-color);
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
margin: 0;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
}
|
||||
.card-front, .card-back {
|
||||
width: var(--card-width);
|
||||
height: var(--card-height);
|
||||
}
|
||||
</style>
|
@ -1,46 +0,0 @@
|
||||
interface KV<V> {
|
||||
[key: string]: V;
|
||||
}
|
||||
|
||||
interface ContentBlock {
|
||||
type: string;
|
||||
data: object;
|
||||
}
|
||||
|
||||
interface CardContent {
|
||||
time: number;
|
||||
blocks: ContentBlock[];
|
||||
version: string;
|
||||
}
|
||||
|
||||
interface Card {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number;
|
||||
tags: string[];
|
||||
icon: string;
|
||||
content: CardContent;
|
||||
backIcon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface Deck {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
cards: Card[];
|
||||
cardWidth: number;
|
||||
cardHeight: number;
|
||||
titleFontSize?: number;
|
||||
}
|
||||
|
||||
interface Settings {
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface StoredStuff {
|
||||
decks: Deck[];
|
||||
defaults: Settings;
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
// page width x page height
|
||||
export const enum PageSize {
|
||||
A4 = '210mm 297mm',
|
||||
USLetter = '8.5in 11in',
|
||||
JISB4 = '182mm 257mm',
|
||||
A3 = '297mm 420mm',
|
||||
A5 = '148mm 210mm',
|
||||
USLegal = '8.5in 14in',
|
||||
USLedger = '11in 17in',
|
||||
JISB5 = '257mm 364mm'
|
||||
}
|
||||
|
||||
// card width x card height
|
||||
export const enum CardSize {
|
||||
Poker = '64x89',
|
||||
Bridge = '57x89'
|
||||
}
|
||||
|
||||
export const enum Arrangement {
|
||||
DoubleSided = 'doublesided',
|
||||
FrontOnly = 'frontonly',
|
||||
SideBySide = 'sidebyside'
|
||||
}
|
||||
|
||||
export interface KV<V> {
|
||||
[key: string]: V;
|
||||
}
|
||||
|
||||
export interface ContentBlock {
|
||||
type: string;
|
||||
data: object;
|
||||
}
|
||||
|
||||
export interface CardContent {
|
||||
time: number;
|
||||
blocks: ContentBlock[];
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface Card {
|
||||
id: string;
|
||||
name: string;
|
||||
count: number;
|
||||
tags: string[];
|
||||
icon: string;
|
||||
content: CardContent;
|
||||
backIcon?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
export interface Deck {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
color: string;
|
||||
icon: string;
|
||||
cards: Card[];
|
||||
cardSize: CardSize;
|
||||
arrangement: Arrangement;
|
||||
pageSize: PageSize;
|
||||
roundedCorners: boolean;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface StoredStuff {
|
||||
decks: Deck[];
|
||||
defaults: Settings;
|
||||
}
|
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<main id="print-view" name="print-view" :class="{ loading, 'not-found': notFound }" :style="pageSizeCSS">
|
||||
<div class="loading" v-if="loading">— loading —</div>
|
||||
<div class="not-found" v-else-if="notFound">Deck not found :(</div>
|
||||
<template v-else>
|
||||
<div class="page">
|
||||
<Card :key="card.id" v-for="card in deck.cards"
|
||||
:card="card"
|
||||
:deck="deck"
|
||||
:show-front="true"
|
||||
/>
|
||||
</div>
|
||||
<div class="page">
|
||||
<header>Page 2</header>
|
||||
<p>foo bar baz</p>
|
||||
</div>
|
||||
</template>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator'
|
||||
import { Deck } from '../types'
|
||||
import { defaultCardSize, defaultPageSize } from '../consts'
|
||||
import Card from '../components/static-card.vue'
|
||||
import { iconPath } from '../lib'
|
||||
|
||||
interface Dimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
components: { Card }
|
||||
})
|
||||
export default class PrintDeck extends Vue {
|
||||
private loading = true
|
||||
private notFound = false
|
||||
private deck: Deck | null = null
|
||||
|
||||
private landscape = false // TODO: not yet implemented
|
||||
|
||||
private mounted () {
|
||||
const currentDeckId = this.$route.params.id
|
||||
this.deck = this.$storage.findDeck(currentDeckId)
|
||||
if (this.deck === null) this.notFound = true
|
||||
this.loading = false
|
||||
}
|
||||
|
||||
private get pageSize (): Dimensions {
|
||||
const pageSize = this.deck === null ? defaultPageSize : this.deck.pageSize
|
||||
const [width, height] = pageSize.split(' ').map(x => parseFloat(x))
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
private get cardSize (): Dimensions {
|
||||
const cardSize = this.deck === null ? defaultCardSize : this.deck.cardSize
|
||||
const [height, width] = cardSize.split('x').map(x => parseFloat(x))
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
private get cardsPerPage (): number {
|
||||
if (this.deck === null || this.deck.cards.length === 0) return 0
|
||||
}
|
||||
|
||||
private get deckIcon () {
|
||||
if (this.deck === null) return ''
|
||||
return iconPath(this.deck.icon)
|
||||
}
|
||||
|
||||
private get pageSizeCSS () {
|
||||
const cardHeight = `${this.cardSize.height}mm`
|
||||
const cardWidth = `${this.cardSize.width}mm`
|
||||
|
||||
const pageHeight = `${this.pageSize.height}mm`
|
||||
const pageWidth = `${this.pageSize.width}mm`
|
||||
|
||||
console.log(cardHeight, cardWidth, pageHeight, pageWidth)
|
||||
|
||||
return {
|
||||
'--width': pageWidth,
|
||||
'--height': pageHeight,
|
||||
'--card-width': cardWidth,
|
||||
'--card-height': cardHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@page {
|
||||
margin: 0;
|
||||
size: var(--size);
|
||||
}
|
||||
#print-view > .page {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
justify-content: flex-start;
|
||||
align-content: flex-start;
|
||||
page-break-after: always;
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
margin: 5mm auto;
|
||||
padding: 1cm 9mm;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
@media print {
|
||||
html,body {
|
||||
background-color: gray;
|
||||
}
|
||||
#app > .home-link {
|
||||
display: none;
|
||||
}
|
||||
#print-view > .page {
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue