update UI and QOL

This commit is contained in:
HPL-JesusCastro
2026-06-03 10:36:02 +08:00
parent 5e34832234
commit f26bf647b1
22 changed files with 965 additions and 57 deletions

View File

@@ -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 };

View File

@@ -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'));

View File

@@ -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' });

View 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;

View File

@@ -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 };