update UI and QOL
This commit is contained in:
61
server/db.js
61
server/db.js
@@ -128,4 +128,63 @@ function deleteApp(id) {
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = { listAds, getAd, insertAd, updateAd, updateAdFile, updateAdThumbnail, deleteAd, listApps, getApp, insertApp, updateApp, deleteApp };
|
||||
// ── developers ───────────────────────────────────────────────────────────────
|
||||
|
||||
const DEVS_DB_PATH = process.env.DEVS_DB_PATH || path.join(__dirname, '..', 'data', 'developers.json');
|
||||
|
||||
const DEFAULT_DEVELOPERS = [
|
||||
'Belgica, Sean',
|
||||
'Caamino, Adrian',
|
||||
'Castro, Jesus',
|
||||
'Coronel, Will',
|
||||
'Esteron, Jericson',
|
||||
'Fajardo, Jules',
|
||||
'Florentino, Marcus',
|
||||
'Lenomta, MJ',
|
||||
'Malana, Marco',
|
||||
'Mocorro, Jet',
|
||||
'Mondejar, Kevin',
|
||||
'Obzunar, Nic',
|
||||
'Tecson, Nat',
|
||||
'Tesalona, Allen',
|
||||
];
|
||||
|
||||
function readDevsDb() {
|
||||
try { return JSON.parse(fs.readFileSync(DEVS_DB_PATH, 'utf8')); }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
function writeDevsDb(devs) {
|
||||
fs.mkdirSync(path.dirname(DEVS_DB_PATH), { recursive: true });
|
||||
fs.writeFileSync(DEVS_DB_PATH, JSON.stringify(devs, null, 2), 'utf8');
|
||||
}
|
||||
|
||||
function listDevelopers() {
|
||||
const stored = readDevsDb();
|
||||
if (stored) return stored;
|
||||
// Seed from defaults on first access
|
||||
const seeded = DEFAULT_DEVELOPERS.map(name => ({ id: crypto.randomUUID(), name }));
|
||||
writeDevsDb(seeded);
|
||||
return seeded;
|
||||
}
|
||||
|
||||
function addDeveloper(name) {
|
||||
const devs = listDevelopers();
|
||||
if (devs.some(d => d.name.toLowerCase() === name.toLowerCase())) return null;
|
||||
const dev = { id: crypto.randomUUID(), name };
|
||||
devs.push(dev);
|
||||
devs.sort((a, b) => a.name.localeCompare(b.name));
|
||||
writeDevsDb(devs);
|
||||
return dev;
|
||||
}
|
||||
|
||||
function removeDeveloper(id) {
|
||||
const devs = listDevelopers();
|
||||
const idx = devs.findIndex(d => d.id === id);
|
||||
if (idx === -1) return null;
|
||||
const [dev] = devs.splice(idx, 1);
|
||||
writeDevsDb(devs);
|
||||
return dev;
|
||||
}
|
||||
|
||||
module.exports = { listAds, getAd, insertAd, updateAd, updateAdFile, updateAdThumbnail, deleteAd, listApps, getApp, insertApp, updateApp, deleteApp, listDevelopers, addDeveloper, removeDeveloper };
|
||||
|
||||
@@ -31,6 +31,7 @@ app.use('/api/auth', require('./routes/auth').router);
|
||||
|
||||
// Protected API routes
|
||||
app.use('/api/apps', requireAuth, require('./routes/apps'));
|
||||
app.use('/api/developers', requireAuth, require('./routes/developers'));
|
||||
app.use('/api/ads', requireAuth, require('./routes/upload'));
|
||||
app.use('/api/ads', requireAuth, require('./routes/ads'));
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const multer = require('multer');
|
||||
const { listAds, getAd, updateAd, updateAdFile, updateAdThumbnail, deleteAd } = require('../db');
|
||||
const { captureScreenshot } = require('../utils/screenshot');
|
||||
const { captureScreenshot, captureScreenshotCandidates } = require('../utils/screenshot');
|
||||
|
||||
const UPLOADS_DIR = process.env.UPLOADS_DIR || path.join(__dirname, '..', '..', 'uploads');
|
||||
|
||||
@@ -91,6 +91,22 @@ router.post('/:id/screenshot', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/screenshot-candidates', async (req, res) => {
|
||||
const ad = getAd(req.params.id);
|
||||
if (!ad) return res.status(404).json({ error: 'Not found' });
|
||||
|
||||
try {
|
||||
const port = process.env.PORT || 3000;
|
||||
const url = `http://127.0.0.1:${port}/uploads/${ad.filename}`;
|
||||
const buffers = await captureScreenshotCandidates(url);
|
||||
const candidates = buffers.map(buf => 'data:image/png;base64,' + buf.toString('base64'));
|
||||
res.json({ candidates });
|
||||
} catch (err) {
|
||||
console.error('Screenshot candidates failed:', err.message);
|
||||
res.status(500).json({ error: 'Screenshot capture failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/:id/file', uploadFile.single('file'), (req, res) => {
|
||||
const ad = getAd(req.params.id);
|
||||
if (!ad) return res.status(404).json({ error: 'Not found' });
|
||||
|
||||
24
server/routes/developers.js
Normal file
24
server/routes/developers.js
Normal file
@@ -0,0 +1,24 @@
|
||||
const express = require('express');
|
||||
const { listDevelopers, addDeveloper, removeDeveloper } = require('../db');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', (_req, res) => {
|
||||
res.json(listDevelopers());
|
||||
});
|
||||
|
||||
router.post('/', (req, res) => {
|
||||
const { name } = req.body;
|
||||
if (!name?.trim()) return res.status(400).json({ error: 'name is required' });
|
||||
const dev = addDeveloper(name.trim());
|
||||
if (!dev) return res.status(409).json({ error: 'Developer already exists' });
|
||||
res.status(201).json(dev);
|
||||
});
|
||||
|
||||
router.delete('/:id', (req, res) => {
|
||||
const dev = removeDeveloper(req.params.id);
|
||||
if (!dev) return res.status(404).json({ error: 'Not found' });
|
||||
res.status(204).end();
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -2,7 +2,8 @@ const puppeteer = require('puppeteer');
|
||||
const fs = require('fs');
|
||||
|
||||
const FINAL_SETTLE_MS = 800;
|
||||
const CANDIDATE_DELAYS_MS = [0, 500, 500];
|
||||
const BURST_DELAYS_MS = [0, 500, 500];
|
||||
const CANDIDATE_GAPS_MS = [2000, 2000]; // delays between candidates: T+0, T+2s, T+4s
|
||||
const EDGE_PATHS = [
|
||||
'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
|
||||
@@ -17,11 +18,10 @@ function getBrowserExecutablePath() {
|
||||
return EDGE_PATHS.find(p => fs.existsSync(p));
|
||||
}
|
||||
|
||||
async function captureScreenshot(url, width = 390, height = 844) {
|
||||
const executablePath = getBrowserExecutablePath();
|
||||
const browser = await puppeteer.launch({
|
||||
function getLaunchArgs() {
|
||||
return {
|
||||
headless: true,
|
||||
executablePath,
|
||||
executablePath: getBrowserExecutablePath(),
|
||||
args: [
|
||||
'--no-sandbox',
|
||||
'--disable-setuid-sandbox',
|
||||
@@ -29,11 +29,17 @@ async function captureScreenshot(url, width = 390, height = 844) {
|
||||
'--enable-webgl',
|
||||
'--ignore-gpu-blocklist',
|
||||
],
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
async function withPage(url, width, height, fn) {
|
||||
const browser = await puppeteer.launch(getLaunchArgs());
|
||||
try {
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width, height, deviceScaleFactor: 1 });
|
||||
// isMobile ensures the CSS viewport meta tag is respected the same way a
|
||||
// mobile browser would, so playables that size themselves via vw/vh or
|
||||
// window.innerWidth match what the device frame shows.
|
||||
await page.setViewport({ width, height, deviceScaleFactor: 1, isMobile: true, hasTouch: true });
|
||||
page.on('console', msg => console.log(`[screenshot:${msg.type()}] ${msg.text()}`));
|
||||
|
||||
// Unity clears WebGL buffers by default, so force readable buffers before
|
||||
@@ -51,6 +57,14 @@ async function captureScreenshot(url, width = 390, height = 844) {
|
||||
console.log(`[screenshot] loading ${url}`);
|
||||
await page.goto(url, { waitUntil: 'load', timeout: 60000 });
|
||||
|
||||
// Hide scrollbars and fire resize so JS-based layout callbacks re-run
|
||||
// with the correct viewport dimensions.
|
||||
await page.evaluate(() => {
|
||||
document.documentElement.style.overflow = 'hidden';
|
||||
if (document.body) document.body.style.overflow = 'hidden';
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
});
|
||||
|
||||
try {
|
||||
await page.waitForFunction(() => {
|
||||
const canvases = Array.from(document.querySelectorAll('canvas'))
|
||||
@@ -69,21 +83,37 @@ async function captureScreenshot(url, width = 390, height = 844) {
|
||||
console.log(`[screenshot] canvas readiness timed out: ${err.message}`);
|
||||
}
|
||||
|
||||
// HTML overlays and Unity's first fully composited frame can lag behind
|
||||
// canvas readiness, so capture a short burst and keep the richest PNG.
|
||||
await wait(FINAL_SETTLE_MS);
|
||||
let bestBuffer = null;
|
||||
for (const delay of CANDIDATE_DELAYS_MS) {
|
||||
if (delay) await wait(delay);
|
||||
const buffer = await page.screenshot({ type: 'png' });
|
||||
if (!bestBuffer || buffer.length > bestBuffer.length) bestBuffer = buffer;
|
||||
}
|
||||
|
||||
console.log(`[screenshot] selected ${bestBuffer.length} bytes`);
|
||||
return bestBuffer;
|
||||
return await fn(page);
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { captureScreenshot };
|
||||
async function captureScreenshot(url, width = 390, height = 844) {
|
||||
return withPage(url, width, height, async (page) => {
|
||||
let bestBuffer = null;
|
||||
for (const delay of BURST_DELAYS_MS) {
|
||||
if (delay) await wait(delay);
|
||||
const buffer = await page.screenshot({ type: 'png' });
|
||||
if (!bestBuffer || buffer.length > bestBuffer.length) bestBuffer = buffer;
|
||||
}
|
||||
console.log(`[screenshot] selected ${bestBuffer.length} bytes`);
|
||||
return bestBuffer;
|
||||
});
|
||||
}
|
||||
|
||||
async function captureScreenshotCandidates(url, width = 390, height = 844) {
|
||||
return withPage(url, width, height, async (page) => {
|
||||
const buffers = [];
|
||||
buffers.push(await page.screenshot({ type: 'png' }));
|
||||
for (const gap of CANDIDATE_GAPS_MS) {
|
||||
await wait(gap);
|
||||
buffers.push(await page.screenshot({ type: 'png' }));
|
||||
}
|
||||
console.log(`[screenshot] captured ${buffers.length} candidates`);
|
||||
return buffers;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = { captureScreenshot, captureScreenshotCandidates };
|
||||
|
||||
Reference in New Issue
Block a user