Add display frames and improve create-page page management.

Store phone/tablet/photo-frame shells with catalogs, append pages from extra images or PDFs, and reorder drafts with drag-and-drop.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-20 14:20:16 +03:30
co-authored by Cursor
parent 16cd3197ef
commit c236b0f621
23 changed files with 1069 additions and 134 deletions
+10 -3
View File
@@ -1,6 +1,6 @@
import { eq } from "drizzle-orm";
import type { EffectId } from "../shared/types";
import { DEFAULT_FLIP_MS } from "../shared/types";
import type { EffectId, FrameId } from "../shared/types";
import { DEFAULT_FLIP_MS, DEFAULT_FRAME, isFrameId } from "../shared/types";
import type { DbBundle } from "./db";
import { catalogsTable } from "./schema";
@@ -9,22 +9,26 @@ export type CatalogRow = {
deleteToken: string;
title: string;
effect: EffectId;
frame: FrameId;
pageCount: number;
flipMs: number;
createdAt: string;
};
export type CatalogInsert = Omit<CatalogRow, "createdAt" | "flipMs"> & {
export type CatalogInsert = Omit<CatalogRow, "createdAt" | "flipMs" | "frame"> & {
createdAt?: string;
flipMs?: number;
frame?: FrameId;
};
function toRow(record: typeof catalogsTable.$inferSelect): CatalogRow {
const frameRaw = record.frame ?? DEFAULT_FRAME;
return {
id: record.id,
deleteToken: record.deleteToken,
title: record.title,
effect: record.effect as EffectId,
frame: isFrameId(frameRaw) ? frameRaw : DEFAULT_FRAME,
pageCount: record.pageCount,
flipMs: record.flipMs ?? DEFAULT_FLIP_MS,
createdAt: record.createdAt,
@@ -36,6 +40,7 @@ export function createCatalogRepo(db: DbBundle) {
create(input: CatalogInsert): CatalogRow {
const createdAt = input.createdAt ?? new Date().toISOString();
const flipMs = input.flipMs ?? DEFAULT_FLIP_MS;
const frame = input.frame ?? DEFAULT_FRAME;
db.drizzle.insert(catalogsTable).values({
id: input.id,
deleteToken: input.deleteToken,
@@ -43,6 +48,7 @@ export function createCatalogRepo(db: DbBundle) {
effect: input.effect,
pageCount: input.pageCount,
flipMs,
frame,
createdAt,
}).run();
db.persist();
@@ -51,6 +57,7 @@ export function createCatalogRepo(db: DbBundle) {
deleteToken: input.deleteToken,
title: input.title,
effect: input.effect,
frame,
pageCount: input.pageCount,
flipMs,
createdAt,
+5
View File
@@ -33,6 +33,7 @@ export async function openDatabase(databasePath: string): Promise<DbBundle> {
effect TEXT NOT NULL,
page_count INTEGER NOT NULL,
flip_ms INTEGER NOT NULL DEFAULT 1000,
frame TEXT NOT NULL DEFAULT 'none',
created_at TEXT NOT NULL
)
`);
@@ -42,6 +43,10 @@ export async function openDatabase(databasePath: string): Promise<DbBundle> {
if (!colNames.has("flip_ms")) {
sqlite.run("ALTER TABLE catalogs ADD COLUMN flip_ms INTEGER NOT NULL DEFAULT 1000");
}
if (!colNames.has("frame")) {
sqlite.run("ALTER TABLE catalogs ADD COLUMN frame TEXT NOT NULL DEFAULT 'none'");
console.log("[db] added catalogs.frame column");
}
const persist = () => {
fs.mkdirSync(path.dirname(databasePath), { recursive: true });
+1
View File
@@ -12,6 +12,7 @@ export const messages = {
tooLarge: "حجم یا تعداد صفحه بیش از حد مجاز است",
badType: "نوع فایل پشتیبانی نمی‌شود",
badEffect: "افکت نامعتبر است",
badFrame: "قالب نامعتبر است",
notFound: "کاتالوگ پیدا نشد",
forbiddenDelete: "اجازه حذف ندارید",
rateLimited: "تعداد ساخت از این آدرس بیش از حد مجاز است",
+9
View File
@@ -5,6 +5,8 @@ import {
clampFlipMs,
fileTooLargeMessage,
isEffectId,
isFrameId,
DEFAULT_FRAME,
LIMITS,
tooManyPagesMessage,
type CreateCatalogResponse,
@@ -37,6 +39,7 @@ function toPublic(row: {
id: string;
title: string;
effect: CatalogPublic["effect"];
frame: CatalogPublic["frame"];
pageCount: number;
flipMs: number;
createdAt: string;
@@ -45,6 +48,7 @@ function toPublic(row: {
id: row.id,
title: row.title,
effect: row.effect,
frame: row.frame,
pageCount: row.pageCount,
flipMs: row.flipMs,
createdAt: row.createdAt,
@@ -102,6 +106,10 @@ export function catalogRoutes(opts: {
throw new HttpError(400, messages.badEffect);
}
const flipMs = clampFlipMs(body.flipMs);
const frameRaw = typeof body.frame === "string" && body.frame ? body.frame : DEFAULT_FRAME;
if (!isFrameId(frameRaw)) {
throw new HttpError(400, messages.badFrame);
}
const pages = asFileArray(body.pages);
if (pages.length < 1) {
@@ -127,6 +135,7 @@ export function catalogRoutes(opts: {
effect: effectRaw,
pageCount: buffers.length,
flipMs,
frame: frameRaw,
});
const payload: CreateCatalogResponse = {
+1
View File
@@ -7,5 +7,6 @@ export const catalogsTable = sqliteTable("catalogs", {
effect: text("effect").notNull(),
pageCount: integer("page_count").notNull(),
flipMs: integer("flip_ms").notNull(),
frame: text("frame").notNull(),
createdAt: text("created_at").notNull(),
});