categorys and titles for images
This commit is contained in:
parent
8abbb13fcc
commit
239c879649
22 changed files with 964 additions and 86 deletions
|
|
@ -8,8 +8,11 @@ export type EncodedPlayer = Array<any>
|
|||
export type EncodedPiece = Array<any>
|
||||
export type EncodedPieceShape = number
|
||||
|
||||
// TODO: maybe something other than string in the future
|
||||
export type Category = string
|
||||
export interface Category {
|
||||
id: number
|
||||
slug: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface GameRng {
|
||||
obj: Rng
|
||||
|
|
@ -26,10 +29,13 @@ interface Game {
|
|||
}
|
||||
|
||||
export interface Image {
|
||||
id: number
|
||||
filename: string
|
||||
file: string
|
||||
url: string
|
||||
category: Category
|
||||
title: string
|
||||
categories: Array<Category>
|
||||
created: number
|
||||
}
|
||||
|
||||
export interface GameSettings {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,12 @@ import { EncodedPiece, EncodedPieceShape, EncodedPlayer, Piece, PieceShape, Play
|
|||
import { Point } from './Geometry'
|
||||
import { Rng } from './Rng'
|
||||
|
||||
const slug = (str: string) => {
|
||||
let tmp = str.toLowerCase()
|
||||
tmp = tmp.replace(/[^a-z0-9]+/g, '-')
|
||||
tmp = tmp.replace(/^-|-$/, '')
|
||||
return tmp
|
||||
}
|
||||
|
||||
const pad = (x: any, pad: string) => {
|
||||
const str = `${x}`
|
||||
|
|
@ -162,6 +168,7 @@ function asQueryArgs(data: any) {
|
|||
|
||||
export default {
|
||||
hash,
|
||||
slug,
|
||||
uniqId,
|
||||
|
||||
encodeShape,
|
||||
|
|
|
|||
24
src/dbpatches/01_initial.sqlite
Normal file
24
src/dbpatches/01_initial.sqlite
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
CREATE TABLE categories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
slug TEXT UNIQUE,
|
||||
title TEXT UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE images (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
|
||||
created TIMESTAMP NOT NULL,
|
||||
|
||||
filename TEXT NOT NULL UNIQUE,
|
||||
filename_original TEXT NOT NULL,
|
||||
title TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE image_x_category (
|
||||
image_id INTEGER NOT NULL,
|
||||
category_id INTEGER NOT NULL,
|
||||
|
||||
FOREIGN KEY(image_id) REFERENCES images(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(category_id) REFERENCES categories(id) ON DELETE CASCADE
|
||||
);
|
||||
|
|
@ -106,6 +106,7 @@ export default defineComponent({
|
|||
}
|
||||
.new-game-dialog .area-image {
|
||||
grid-area: image;
|
||||
margin: 20px;
|
||||
}
|
||||
.new-game-dialog .area-settings {
|
||||
grid-area: settings;
|
||||
|
|
|
|||
|
|
@ -7,15 +7,21 @@ gallery", if possible!
|
|||
<div class="overlay new-image-dialog" @click="$emit('bgclick')">
|
||||
<div class="overlay-content" @click.stop="">
|
||||
|
||||
<div class="area-image" :class="{'has-image': !!image.url, 'no-image': !image.url}">
|
||||
<div class="area-image" :class="{'has-image': !!previewUrl, 'no-image': !previewUrl}">
|
||||
<!-- TODO: ... -->
|
||||
<div v-if="image.url" class="has-image">
|
||||
<span class="remove btn" @click="image.url=''">X</span>
|
||||
<responsive-image :src="image.url" />
|
||||
<div v-if="previewUrl" class="has-image">
|
||||
<span class="remove btn" @click="previewUrl=''">X</span>
|
||||
<responsive-image :src="previewUrl" />
|
||||
</div>
|
||||
<div v-else>
|
||||
<label class="upload">
|
||||
<input type="file" style="display: none" @change="preview" accept="image/*" />
|
||||
<span class="btn">{{label || 'Upload File'}}</span>
|
||||
</label>
|
||||
|
||||
|
||||
<!-- TODO: drop area for image -->
|
||||
<upload class="upload" @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" />
|
||||
<!-- <upload class="upload" @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" /> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -23,7 +29,7 @@ gallery", if possible!
|
|||
<table>
|
||||
<tr>
|
||||
<td><label>Title</label></td>
|
||||
<td><input type="text" v-model="image.title" placeholder="Flower by @artist" /></td>
|
||||
<td><input type="text" v-model="title" placeholder="Flower by @artist" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
|
|
@ -33,13 +39,13 @@ gallery", if possible!
|
|||
<tr>
|
||||
<!-- TODO: autocomplete category -->
|
||||
<td><label>Category</label></td>
|
||||
<td><input type="text" v-model="image.category" placeholder="Plants" /></td>
|
||||
<td><input type="text" v-model="category" placeholder="Plants" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="area-buttons">
|
||||
<!-- <button class="btn" :disabled="!canPostToGallery" @click="postToGallery">🖼️ Post to gallery</button> -->
|
||||
<button class="btn" :disabled="!canPostToGallery" @click="postToGallery">🖼️ Post to gallery</button>
|
||||
<button class="btn" :disabled="!canSetupGameClick" @click="setupGameClick">🧩 Post to gallery <br /> + set up game</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -51,7 +57,6 @@ import { defineComponent } from 'vue'
|
|||
|
||||
import Upload from './Upload.vue'
|
||||
import ResponsiveImage from './ResponsiveImage.vue'
|
||||
import { Image } from '../../common/GameCommon'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'new-image-dialog',
|
||||
|
|
@ -62,35 +67,51 @@ export default defineComponent({
|
|||
emits: {
|
||||
bgclick: null,
|
||||
setupGameClick: null,
|
||||
postToGalleryClick: null,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
image: {
|
||||
file: '',
|
||||
url: '',
|
||||
title: '',
|
||||
category: '',
|
||||
} as Image,
|
||||
previewUrl: '',
|
||||
file: null as File|null,
|
||||
title: '',
|
||||
category: '',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
canPostToGallery () {
|
||||
return !!this.image.url
|
||||
canPostToGallery (): boolean {
|
||||
return !!(this.previewUrl && this.file)
|
||||
},
|
||||
canSetupGameClick () {
|
||||
return !!this.image.url
|
||||
canSetupGameClick (): boolean {
|
||||
return !!(this.previewUrl && this.file)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
mediaImgUploaded(data: any) {
|
||||
this.image.file = data.image.file
|
||||
this.image.url = data.image.url
|
||||
preview (evt: Event) {
|
||||
const target = (evt.target as HTMLInputElement)
|
||||
if (!target.files) return;
|
||||
const file = target.files[0]
|
||||
if (!file) return;
|
||||
|
||||
const r = new FileReader()
|
||||
r.readAsDataURL(file)
|
||||
r.onload = (ev: any) => {
|
||||
this.previewUrl = ev.target.result
|
||||
this.file = file
|
||||
}
|
||||
},
|
||||
postToGallery () {
|
||||
this.$emit('postToGallery', this.image)
|
||||
this.$emit('postToGalleryClick', {
|
||||
file: this.file,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
})
|
||||
},
|
||||
setupGameClick () {
|
||||
this.$emit('setupGameClick', this.image)
|
||||
this.$emit('setupGameClick', {
|
||||
file: this.file,
|
||||
title: this.title,
|
||||
category: this.category,
|
||||
})
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -111,12 +132,12 @@ export default defineComponent({
|
|||
|
||||
.new-image-dialog .area-image {
|
||||
grid-area: image;
|
||||
margin: 20px;
|
||||
}
|
||||
.new-image-dialog .area-image.no-image {
|
||||
align-content: center;
|
||||
display: grid;
|
||||
text-align: center;
|
||||
margin: 20px;
|
||||
border: dashed 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
|
@ -146,6 +167,7 @@ export default defineComponent({
|
|||
}
|
||||
.new-image-dialog .area-buttons button {
|
||||
width: 100%;
|
||||
margin-top: .5em;
|
||||
}
|
||||
.new-image-dialog .upload {
|
||||
position: absolute;
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ in jigsawpuzzles.io
|
|||
Category:
|
||||
<select v-model="filters.category" @change="filtersChanged">
|
||||
<option value="">All</option>
|
||||
<option v-for="(c, idx) in categories" :key="idx" :value="c">{{c}}</option>
|
||||
<option v-for="(c, idx) in categories" :key="idx" :value="c.slug">{{c.title}}</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
|
|
@ -30,8 +30,8 @@ in jigsawpuzzles.io
|
|||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<image-library :images="images" :categories="categories" @imageClicked="imageClicked" />
|
||||
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @setupGameClick="setupGameClick" />
|
||||
<image-library :images="images" @imageClicked="imageClicked" />
|
||||
<new-image-dialog v-if="dialog==='new-image'" @bgclick="dialog=''" @postToGalleryClick="postToGalleryClick" @setupGameClick="setupGameClick" />
|
||||
<new-game-dialog v-if="image && dialog==='new-game'" @bgclick="dialog=''" @newGame="onNewGame" :image="image" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -61,10 +61,13 @@ export default defineComponent({
|
|||
categories: [],
|
||||
|
||||
image: {
|
||||
url: '',
|
||||
id: 0,
|
||||
filename: '',
|
||||
file: '',
|
||||
url: '',
|
||||
title: '',
|
||||
category: '',
|
||||
categories: [],
|
||||
created: 0,
|
||||
} as Image,
|
||||
|
||||
dialog: '',
|
||||
|
|
@ -87,7 +90,26 @@ export default defineComponent({
|
|||
this.image = image
|
||||
this.dialog = 'new-game'
|
||||
},
|
||||
setupGameClick (image: Image) {
|
||||
async uploadImage (data: any) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', data.file, data.file.name);
|
||||
formData.append('title', data.title)
|
||||
formData.append('category', data.category)
|
||||
|
||||
const res = await fetch('/upload', {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
})
|
||||
return await res.json()
|
||||
},
|
||||
async postToGalleryClick(data: any) {
|
||||
await this.uploadImage(data)
|
||||
this.dialog = ''
|
||||
await this.loadImages()
|
||||
},
|
||||
async setupGameClick (data: any) {
|
||||
const image = await this.uploadImage(data)
|
||||
this.loadImages() // load images in background
|
||||
this.image = image
|
||||
this.dialog = 'new-game'
|
||||
},
|
||||
|
|
|
|||
212
src/server/Db.ts
Normal file
212
src/server/Db.ts
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
import fs from 'fs'
|
||||
import bsqlite from 'better-sqlite3'
|
||||
import Integer from 'integer'
|
||||
import { logger } from '../common/Util'
|
||||
|
||||
const log = logger('Db.ts')
|
||||
|
||||
/**
|
||||
* TODO: create a more specific type for OrderBy.
|
||||
* It looks like this (example):
|
||||
* [
|
||||
* {id: -1}, // by id descending
|
||||
* {name: 1}, // then by name ascending
|
||||
* ]
|
||||
*/
|
||||
type OrderBy = Array<any>
|
||||
type Data = Record<string, any>
|
||||
type WhereRaw = Record<string, any>
|
||||
type Params = Array<any>
|
||||
|
||||
interface Where {
|
||||
sql: string
|
||||
values: Array<any>
|
||||
}
|
||||
|
||||
class Db {
|
||||
file: string
|
||||
patchesDir: string
|
||||
dbh: bsqlite.Database
|
||||
|
||||
constructor(file: string, patchesDir: string) {
|
||||
this.file = file
|
||||
this.patchesDir = patchesDir
|
||||
this.dbh = bsqlite(this.file)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dbh.close()
|
||||
}
|
||||
|
||||
patch (verbose: boolean =true): void {
|
||||
if (!this.get('sqlite_master', {type: 'table', name: 'db_patches'})) {
|
||||
this.run('CREATE TABLE db_patches ( id TEXT PRIMARY KEY);', [])
|
||||
}
|
||||
|
||||
const files = fs.readdirSync(this.patchesDir)
|
||||
const patches = (this.getMany('db_patches')).map(row => row.id)
|
||||
|
||||
for (const f of files) {
|
||||
if (patches.includes(f)) {
|
||||
if (verbose) {
|
||||
log.info(`➡ skipping already applied db patch: ${f}`)
|
||||
}
|
||||
continue
|
||||
}
|
||||
const contents = fs.readFileSync(`${this.patchesDir}/${f}`, 'utf-8')
|
||||
|
||||
const all = contents.split(';').map(s => s.trim()).filter(s => !!s)
|
||||
try {
|
||||
this.dbh.transaction((all) => {
|
||||
for (const q of all) {
|
||||
if (verbose) {
|
||||
log.info(`Running: ${q}`)
|
||||
}
|
||||
this.run(q)
|
||||
}
|
||||
this.insert('db_patches', {id: f})
|
||||
})(all)
|
||||
|
||||
log.info(`✓ applied db patch: ${f}`)
|
||||
} catch (e) {
|
||||
log.error(`✖ unable to apply patch: ${f} ${e}`)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_buildWhere (where: WhereRaw): Where {
|
||||
const wheres = []
|
||||
const values = []
|
||||
for (const k of Object.keys(where)) {
|
||||
if (where[k] === null) {
|
||||
wheres.push(k + ' IS NULL')
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof where[k] === 'object') {
|
||||
let prop = '$nin'
|
||||
if (where[k][prop]) {
|
||||
if (where[k][prop].length > 0) {
|
||||
wheres.push(k + ' NOT IN (' + where[k][prop].map((_: any) => '?') + ')')
|
||||
values.push(...where[k][prop])
|
||||
}
|
||||
continue
|
||||
}
|
||||
prop = '$in'
|
||||
if (where[k][prop]) {
|
||||
if (where[k][prop].length > 0) {
|
||||
wheres.push(k + ' IN (' + where[k][prop].map((_: any) => '?') + ')')
|
||||
values.push(...where[k][prop])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// TODO: implement rest of mongo like query args ($eq, $lte, $in...)
|
||||
throw new Error('not implemented: ' + JSON.stringify(where[k]))
|
||||
}
|
||||
|
||||
wheres.push(k + ' = ?')
|
||||
values.push(where[k])
|
||||
}
|
||||
|
||||
return {
|
||||
sql: wheres.length > 0 ? ' WHERE ' + wheres.join(' AND ') : '',
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
_buildOrderBy (orderBy: OrderBy): string {
|
||||
const sorts = []
|
||||
for (const s of orderBy) {
|
||||
const k = Object.keys(s)[0]
|
||||
sorts.push(k + ' COLLATE NOCASE ' + (s[k] > 0 ? 'ASC' : 'DESC'))
|
||||
}
|
||||
return sorts.length > 0 ? ' ORDER BY ' + sorts.join(', ') : ''
|
||||
}
|
||||
|
||||
_get (query: string, params: Params = []): any {
|
||||
return this.dbh.prepare(query).get(...params)
|
||||
}
|
||||
|
||||
run (query: string, params: Params = []): bsqlite.RunResult {
|
||||
return this.dbh.prepare(query).run(...params)
|
||||
}
|
||||
|
||||
_getMany (query: string, params: Params = []): Array<any> {
|
||||
return this.dbh.prepare(query).all(...params)
|
||||
}
|
||||
|
||||
get (
|
||||
table: string,
|
||||
whereRaw: WhereRaw = {},
|
||||
orderBy: OrderBy = []
|
||||
): any {
|
||||
const where = this._buildWhere(whereRaw)
|
||||
const orderBySql = this._buildOrderBy(orderBy)
|
||||
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql
|
||||
return this._get(sql, where.values)
|
||||
}
|
||||
|
||||
getMany (
|
||||
table: string,
|
||||
whereRaw: WhereRaw = {},
|
||||
orderBy: OrderBy = []
|
||||
): Array<any> {
|
||||
const where = this._buildWhere(whereRaw)
|
||||
const orderBySql = this._buildOrderBy(orderBy)
|
||||
const sql = 'SELECT * FROM ' + table + where.sql + orderBySql
|
||||
return this._getMany(sql, where.values)
|
||||
}
|
||||
|
||||
delete (table: string, whereRaw: WhereRaw = {}): bsqlite.RunResult {
|
||||
const where = this._buildWhere(whereRaw)
|
||||
const sql = 'DELETE FROM ' + table + where.sql
|
||||
return this.run(sql, where.values)
|
||||
}
|
||||
|
||||
exists (table: string, whereRaw: WhereRaw): boolean {
|
||||
return !!this.get(table, whereRaw)
|
||||
}
|
||||
|
||||
upsert (
|
||||
table: string,
|
||||
data: Data,
|
||||
check: WhereRaw,
|
||||
idcol: string|null = null
|
||||
): any {
|
||||
if (!this.exists(table, check)) {
|
||||
return this.insert(table, data)
|
||||
}
|
||||
this.update(table, data, check)
|
||||
if (idcol === null) {
|
||||
return 0 // dont care about id
|
||||
}
|
||||
|
||||
return this.get(table, check)[idcol] // get id manually
|
||||
}
|
||||
|
||||
insert (table: string, data: Data): Integer.IntLike {
|
||||
const keys = Object.keys(data)
|
||||
const values = keys.map(k => data[k])
|
||||
const sql = 'INSERT INTO '+ table
|
||||
+ ' (' + keys.join(',') + ')'
|
||||
+ ' VALUES (' + keys.map(k => '?').join(',') + ')'
|
||||
return this.run(sql, values).lastInsertRowid
|
||||
}
|
||||
|
||||
update (table: string, data: Data, whereRaw: WhereRaw = {}): void {
|
||||
const keys = Object.keys(data)
|
||||
if (keys.length === 0) {
|
||||
return
|
||||
}
|
||||
const values = keys.map(k => data[k])
|
||||
const setSql = ' SET ' + keys.join(' = ?,') + ' = ?'
|
||||
const where = this._buildWhere(whereRaw)
|
||||
|
||||
const sql = 'UPDATE ' + table + setSql + where.sql
|
||||
this.run(sql, [...values, ...where.values])
|
||||
}
|
||||
}
|
||||
|
||||
export default Db
|
||||
|
|
@ -10,3 +10,6 @@ export const DATA_DIR = `${BASE_DIR}/data`
|
|||
export const UPLOAD_DIR = `${BASE_DIR}/data/uploads`
|
||||
export const UPLOAD_URL = `/uploads`
|
||||
export const PUBLIC_DIR = `${BASE_DIR}/build/public/`
|
||||
|
||||
export const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`
|
||||
export const DB_FILE = `${BASE_DIR}/data/db.sqlite`
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import exif from 'exif'
|
|||
import sharp from 'sharp'
|
||||
|
||||
import {UPLOAD_DIR, UPLOAD_URL} from './Dirs'
|
||||
import Db from './Db'
|
||||
|
||||
const resizeImage = async (filename: string) => {
|
||||
if (!filename.toLowerCase().match(/\.(jpe?g|webp|png)$/)) {
|
||||
|
|
@ -46,16 +47,76 @@ async function getExifOrientation(imagePath: string) {
|
|||
})
|
||||
}
|
||||
|
||||
const allImages = (sort: string) => {
|
||||
const getCategories = (db: Db, imageId: number) => {
|
||||
const query = `
|
||||
select * from categories c
|
||||
inner join image_x_category ixc on c.id = ixc.category_id
|
||||
where ixc.image_id = ?`
|
||||
return db._getMany(query, [imageId])
|
||||
}
|
||||
|
||||
const imageFromDb = (db: Db, imageId: number) => {
|
||||
const i = db.get('images', { id: imageId })
|
||||
return {
|
||||
id: i.id,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id) as any[],
|
||||
created: i.created * 1000,
|
||||
}
|
||||
}
|
||||
|
||||
const allImagesFromDb = (db: Db, categorySlug: string, sort: string) => {
|
||||
const sortMap = {
|
||||
alpha_asc: [{filename: 1}],
|
||||
alpha_desc: [{filename: -1}],
|
||||
date_asc: [{created: 1}],
|
||||
date_desc: [{created: -1}],
|
||||
} as Record<string, any>
|
||||
|
||||
// TODO: .... clean up
|
||||
const wheresRaw: Record<string, any> = {}
|
||||
if (categorySlug !== '') {
|
||||
const c = db.get('categories', {slug: categorySlug})
|
||||
if (!c) {
|
||||
return []
|
||||
}
|
||||
const ids = db._getMany(`
|
||||
select i.id from image_x_category ixc
|
||||
inner join images i on i.id = ixc.image_id
|
||||
where ixc.category_id = ?;
|
||||
`, [c.id]).map(img => img.id)
|
||||
if (ids.length === 0) {
|
||||
return []
|
||||
}
|
||||
wheresRaw['id'] = {'$in': ids}
|
||||
}
|
||||
const images = db.getMany('images', wheresRaw, sortMap[sort])
|
||||
|
||||
return images.map(i => ({
|
||||
id: i.id as number,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id) as any[],
|
||||
created: i.created * 1000,
|
||||
}))
|
||||
}
|
||||
|
||||
const allImagesFromDisk = (category: string, sort: string) => {
|
||||
let images = fs.readdirSync(UPLOAD_DIR)
|
||||
.filter(f => f.toLowerCase().match(/\.(jpe?g|webp|png)$/))
|
||||
.map(f => ({
|
||||
id: 0,
|
||||
filename: f,
|
||||
file: `${UPLOAD_DIR}/${f}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(f)}`,
|
||||
title: '',
|
||||
category: '',
|
||||
ts: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
title: f.replace(/\.[a-z]+$/, ''),
|
||||
categories: [] as any[],
|
||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
}))
|
||||
|
||||
switch (sort) {
|
||||
|
|
@ -73,14 +134,14 @@ const allImages = (sort: string) => {
|
|||
|
||||
case 'date_asc':
|
||||
images = images.sort((a, b) => {
|
||||
return a.ts > b.ts ? 1 : -1
|
||||
return a.created > b.created ? 1 : -1
|
||||
})
|
||||
break;
|
||||
|
||||
case 'date_desc':
|
||||
default:
|
||||
images = images.sort((a, b) => {
|
||||
return a.ts < b.ts ? 1 : -1
|
||||
return a.created < b.created ? 1 : -1
|
||||
})
|
||||
break;
|
||||
}
|
||||
|
|
@ -102,7 +163,9 @@ async function getDimensions(imagePath: string) {
|
|||
}
|
||||
|
||||
export default {
|
||||
allImages,
|
||||
allImagesFromDisk,
|
||||
imageFromDb,
|
||||
allImagesFromDb,
|
||||
resizeImage,
|
||||
getDimensions,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,19 @@ import GameLog from './GameLog'
|
|||
import GameSockets from './GameSockets'
|
||||
import Time from './../common/Time'
|
||||
import Images from './Images'
|
||||
import { UPLOAD_DIR, UPLOAD_URL, PUBLIC_DIR } from './Dirs'
|
||||
import {
|
||||
DB_FILE,
|
||||
DB_PATCHES_DIR,
|
||||
PUBLIC_DIR,
|
||||
UPLOAD_DIR,
|
||||
UPLOAD_URL
|
||||
} from './Dirs'
|
||||
import { GameSettings, ScoreMode } from '../common/GameCommon'
|
||||
import GameStorage from './GameStorage'
|
||||
import Db from './Db'
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR)
|
||||
db.patch()
|
||||
|
||||
let configFile = ''
|
||||
let last = ''
|
||||
|
|
@ -54,8 +64,8 @@ app.get('/api/conf', (req, res) => {
|
|||
app.get('/api/newgame-data', (req, res) => {
|
||||
const q = req.query as any
|
||||
res.send({
|
||||
images: Images.allImages(q.sort),
|
||||
categories: [],
|
||||
images: Images.allImagesFromDb(db, q.category, q.sort),
|
||||
categories: db.getMany('categories', {}, [{ title: 1 }]),
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -94,12 +104,26 @@ app.post('/upload', (req, res) => {
|
|||
res.status(400).send("Something went wrong!");
|
||||
}
|
||||
|
||||
res.send({
|
||||
image: {
|
||||
file: `${UPLOAD_DIR}/${req.file.filename}`,
|
||||
url: `${UPLOAD_URL}/${req.file.filename}`,
|
||||
},
|
||||
const imageId = db.insert('images', {
|
||||
filename: req.file.filename,
|
||||
filename_original: req.file.originalname,
|
||||
title: req.body.title || '',
|
||||
created: Time.timestamp(),
|
||||
})
|
||||
|
||||
if (req.body.category) {
|
||||
const title = req.body.category
|
||||
const slug = Util.slug(title)
|
||||
const id = db.upsert('categories', { slug, title }, { slug }, 'id')
|
||||
if (id) {
|
||||
db.insert('image_x_category', {
|
||||
image_id: imageId,
|
||||
category_id: id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
res.send(Images.imageFromDb(db, imageId as number))
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue