categorys and titles for images
This commit is contained in:
parent
8abbb13fcc
commit
239c879649
22 changed files with 964 additions and 86 deletions
1
build/public/assets/index.1c0291a2.js
Normal file
1
build/public/assets/index.1c0291a2.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -4,9 +4,9 @@
|
|||
<meta charset="UTF-8">
|
||||
|
||||
<title>🧩 jigsaw.hyottoko.club</title>
|
||||
<script type="module" crossorigin src="/assets/index.b49dadca.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index.1c0291a2.js"></script>
|
||||
<link rel="modulepreload" href="/assets/vendor.8616a479.js">
|
||||
<link rel="stylesheet" href="/assets/index.6c4f6859.css">
|
||||
<link rel="stylesheet" href="/assets/index.c5b0553c.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import exif from 'exif';
|
|||
import sharp from 'sharp';
|
||||
import bodyParser from 'body-parser';
|
||||
import v8 from 'v8';
|
||||
import bsqlite from 'better-sqlite3';
|
||||
|
||||
class Rng {
|
||||
constructor(seed) {
|
||||
|
|
@ -50,6 +51,12 @@ class Rng {
|
|||
}
|
||||
}
|
||||
|
||||
const slug = (str) => {
|
||||
let tmp = str.toLowerCase();
|
||||
tmp = tmp.replace(/[^a-z0-9]+/g, '-');
|
||||
tmp = tmp.replace(/^-|-$/, '');
|
||||
return tmp;
|
||||
};
|
||||
const pad = (x, pad) => {
|
||||
const str = `${x}`;
|
||||
if (str.length >= pad.length) {
|
||||
|
|
@ -194,6 +201,7 @@ function asQueryArgs(data) {
|
|||
}
|
||||
var Util = {
|
||||
hash,
|
||||
slug,
|
||||
uniqId,
|
||||
encodeShape,
|
||||
decodeShape,
|
||||
|
|
@ -207,7 +215,7 @@ var Util = {
|
|||
asQueryArgs,
|
||||
};
|
||||
|
||||
const log$4 = logger('WebSocketServer.js');
|
||||
const log$5 = logger('WebSocketServer.js');
|
||||
/*
|
||||
Example config
|
||||
|
||||
|
|
@ -245,12 +253,12 @@ class WebSocketServer {
|
|||
this._websocketserver.on('connection', (socket, request) => {
|
||||
const pathname = new URL(this.config.connectstring).pathname;
|
||||
if (request.url.indexOf(pathname) !== 0) {
|
||||
log$4.log('bad request url: ', request.url);
|
||||
log$5.log('bad request url: ', request.url);
|
||||
socket.close();
|
||||
return;
|
||||
}
|
||||
socket.on('message', (data) => {
|
||||
log$4.log(`ws`, socket.protocol, data);
|
||||
log$5.log(`ws`, socket.protocol, data);
|
||||
this.evt.dispatch('message', { socket, data });
|
||||
});
|
||||
socket.on('close', () => {
|
||||
|
|
@ -1163,9 +1171,11 @@ const BASE_DIR = `${__dirname}/../..`;
|
|||
const DATA_DIR = `${BASE_DIR}/data`;
|
||||
const UPLOAD_DIR = `${BASE_DIR}/data/uploads`;
|
||||
const UPLOAD_URL = `/uploads`;
|
||||
const PUBLIC_DIR = `${BASE_DIR}/build/public/`;
|
||||
const PUBLIC_DIR = `${BASE_DIR}/build/public/`;
|
||||
const DB_PATCHES_DIR = `${BASE_DIR}/src/dbpatches`;
|
||||
const DB_FILE = `${BASE_DIR}/data/db.sqlite`;
|
||||
|
||||
const log$3 = logger('GameLog.js');
|
||||
const log$4 = logger('GameLog.js');
|
||||
const filename = (gameId) => `${DATA_DIR}/log_${gameId}.log`;
|
||||
const create = (gameId) => {
|
||||
const file = filename(gameId);
|
||||
|
|
@ -1196,8 +1206,8 @@ const get = (gameId) => {
|
|||
return JSON.parse(line);
|
||||
}
|
||||
catch (e) {
|
||||
log$3.log(line);
|
||||
log$3.log(e);
|
||||
log$4.log(line);
|
||||
log$4.log(e);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
@ -1248,16 +1258,71 @@ async function getExifOrientation(imagePath) {
|
|||
});
|
||||
});
|
||||
}
|
||||
const allImages = (sort) => {
|
||||
const getCategories = (db, imageId) => {
|
||||
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, imageId) => {
|
||||
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),
|
||||
created: i.created * 1000,
|
||||
};
|
||||
};
|
||||
const allImagesFromDb = (db, categorySlug, sort) => {
|
||||
const sortMap = {
|
||||
alpha_asc: [{ filename: 1 }],
|
||||
alpha_desc: [{ filename: -1 }],
|
||||
date_asc: [{ created: 1 }],
|
||||
date_desc: [{ created: -1 }],
|
||||
};
|
||||
// TODO: .... clean up
|
||||
const wheresRaw = {};
|
||||
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,
|
||||
filename: i.filename,
|
||||
file: `${UPLOAD_DIR}/${i.filename}`,
|
||||
url: `${UPLOAD_URL}/${encodeURIComponent(i.filename)}`,
|
||||
title: i.title,
|
||||
categories: getCategories(db, i.id),
|
||||
created: i.created * 1000,
|
||||
}));
|
||||
};
|
||||
const allImagesFromDisk = (category, sort) => {
|
||||
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: [],
|
||||
created: fs.statSync(`${UPLOAD_DIR}/${f}`).mtime.getTime(),
|
||||
}));
|
||||
switch (sort) {
|
||||
case 'alpha_asc':
|
||||
|
|
@ -1272,13 +1337,13 @@ const allImages = (sort) => {
|
|||
break;
|
||||
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;
|
||||
}
|
||||
|
|
@ -1298,7 +1363,9 @@ async function getDimensions(imagePath) {
|
|||
return dimensions;
|
||||
}
|
||||
var Images = {
|
||||
allImages,
|
||||
allImagesFromDisk,
|
||||
imageFromDb,
|
||||
allImagesFromDb,
|
||||
resizeImage,
|
||||
getDimensions,
|
||||
};
|
||||
|
|
@ -1477,7 +1544,7 @@ const determinePuzzleInfo = (w, h, targetTiles) => {
|
|||
};
|
||||
};
|
||||
|
||||
const log$2 = logger('GameStorage.js');
|
||||
const log$3 = logger('GameStorage.js');
|
||||
const DIRTY_GAMES = {};
|
||||
function setDirty(gameId) {
|
||||
DIRTY_GAMES[gameId] = true;
|
||||
|
|
@ -1504,7 +1571,7 @@ function loadGame(gameId) {
|
|||
game = JSON.parse(contents);
|
||||
}
|
||||
catch {
|
||||
log$2.log(`[ERR] unable to load game from file ${file}`);
|
||||
log$3.log(`[ERR] unable to load game from file ${file}`);
|
||||
}
|
||||
if (typeof game.puzzle.data.started === 'undefined') {
|
||||
game.puzzle.data.started = Math.round(fs.statSync(file).ctimeMs);
|
||||
|
|
@ -1549,7 +1616,7 @@ function persistGame(gameId) {
|
|||
players: game.players,
|
||||
scoreMode: game.scoreMode,
|
||||
}));
|
||||
log$2.info(`[INFO] persisted game ${game.id}`);
|
||||
log$3.info(`[INFO] persisted game ${game.id}`);
|
||||
}
|
||||
var GameStorage = {
|
||||
loadGames,
|
||||
|
|
@ -1615,7 +1682,7 @@ var Game = {
|
|||
getFinishTs: GameCommon.getFinishTs,
|
||||
};
|
||||
|
||||
const log$1 = logger('GameSocket.js');
|
||||
const log$2 = logger('GameSocket.js');
|
||||
// Map<gameId, Socket[]>
|
||||
const SOCKETS = {};
|
||||
function socketExists(gameId, socket) {
|
||||
|
|
@ -1629,8 +1696,8 @@ function removeSocket(gameId, socket) {
|
|||
return;
|
||||
}
|
||||
SOCKETS[gameId] = SOCKETS[gameId].filter((s) => s !== socket);
|
||||
log$1.log('removed socket: ', gameId, socket.protocol);
|
||||
log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
log$2.log('removed socket: ', gameId, socket.protocol);
|
||||
log$2.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
}
|
||||
function addSocket(gameId, socket) {
|
||||
if (!(gameId in SOCKETS)) {
|
||||
|
|
@ -1638,8 +1705,8 @@ function addSocket(gameId, socket) {
|
|||
}
|
||||
if (!SOCKETS[gameId].includes(socket)) {
|
||||
SOCKETS[gameId].push(socket);
|
||||
log$1.log('added socket: ', gameId, socket.protocol);
|
||||
log$1.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
log$2.log('added socket: ', gameId, socket.protocol);
|
||||
log$2.log('socket count: ', Object.keys(SOCKETS[gameId]).length);
|
||||
}
|
||||
}
|
||||
function getSockets(gameId) {
|
||||
|
|
@ -1655,6 +1722,155 @@ var GameSockets = {
|
|||
getSockets,
|
||||
};
|
||||
|
||||
const log$1 = logger('Db.ts');
|
||||
class Db {
|
||||
constructor(file, patchesDir) {
|
||||
this.file = file;
|
||||
this.patchesDir = patchesDir;
|
||||
this.dbh = bsqlite(this.file);
|
||||
}
|
||||
close() {
|
||||
this.dbh.close();
|
||||
}
|
||||
patch(verbose = true) {
|
||||
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$1.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$1.info(`Running: ${q}`);
|
||||
}
|
||||
this.run(q);
|
||||
}
|
||||
this.insert('db_patches', { id: f });
|
||||
})(all);
|
||||
log$1.info(`✓ applied db patch: ${f}`);
|
||||
}
|
||||
catch (e) {
|
||||
log$1.error(`✖ unable to apply patch: ${f} ${e}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
_buildWhere(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((_) => '?') + ')');
|
||||
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((_) => '?') + ')');
|
||||
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) {
|
||||
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, params = []) {
|
||||
return this.dbh.prepare(query).get(...params);
|
||||
}
|
||||
run(query, params = []) {
|
||||
return this.dbh.prepare(query).run(...params);
|
||||
}
|
||||
_getMany(query, params = []) {
|
||||
return this.dbh.prepare(query).all(...params);
|
||||
}
|
||||
get(table, whereRaw = {}, orderBy = []) {
|
||||
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, whereRaw = {}, orderBy = []) {
|
||||
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, whereRaw = {}) {
|
||||
const where = this._buildWhere(whereRaw);
|
||||
const sql = 'DELETE FROM ' + table + where.sql;
|
||||
return this.run(sql, where.values);
|
||||
}
|
||||
exists(table, whereRaw) {
|
||||
return !!this.get(table, whereRaw);
|
||||
}
|
||||
upsert(table, data, check, idcol = null) {
|
||||
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, data) {
|
||||
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, data, whereRaw = {}) {
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
const db = new Db(DB_FILE, DB_PATCHES_DIR);
|
||||
db.patch();
|
||||
let configFile = '';
|
||||
let last = '';
|
||||
for (const val of process.argv) {
|
||||
|
|
@ -1686,8 +1902,8 @@ app.get('/api/conf', (req, res) => {
|
|||
app.get('/api/newgame-data', (req, res) => {
|
||||
const q = req.query;
|
||||
res.send({
|
||||
images: Images.allImages(q.sort),
|
||||
categories: [],
|
||||
images: Images.allImagesFromDb(db, q.category, q.sort),
|
||||
categories: db.getMany('categories', {}, [{ title: 1 }]),
|
||||
});
|
||||
});
|
||||
app.get('/api/index-data', (req, res) => {
|
||||
|
|
@ -1722,12 +1938,24 @@ app.post('/upload', (req, res) => {
|
|||
log.log(err);
|
||||
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));
|
||||
});
|
||||
});
|
||||
app.post('/newgame', bodyParser.json(), async (req, res) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue