feat: board thumbnails, inline rename, UX overhaul + zoom/counter fixes

- Auto-generate board thumbnail from canvas content on save (webp, 400px)
- Collection cards show stacked/fanned board thumbnails with random offsets
- Three-dot menu on all cards with Rename, Share, Delete actions
- Inline rename for collections (header + card) and boards
- Collection cards show public/private status and member count
- Share button on collection cards for quick access
- Add updateCollection/updateBoard API functions
- Add thumbnail + object_count columns to boards table with migration
- Fix zoom drift: use element-relative coords (offsetX/Y) not getScenePoint
- Fix thumbnail generation breaking viewport: use finally block for restore
- Fix thumbnail bounds: use scene-space obj coords not screen-space getBoundingRect
- Fix object counter: live count from canvas instead of stale DB images table
- Fix three-dot menu clipping by removing overflow:hidden from card containers
- Status bar shows "objects" instead of "images", updates on add/delete
This commit is contained in:
Hiren
2026-03-09 19:10:25 +05:30
parent 00c3370634
commit 0c9ab32aef
8 changed files with 824 additions and 305 deletions
+37 -6
View File
@@ -57,6 +57,8 @@ db.exec(`
name TEXT NOT NULL,
description TEXT DEFAULT '',
canvas_state TEXT DEFAULT '{}',
thumbnail TEXT DEFAULT NULL,
object_count INTEGER NOT NULL DEFAULT 0,
created_by TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
@@ -83,6 +85,18 @@ db.exec(`
CREATE INDEX IF NOT EXISTS idx_images_board ON images(board_id);
`);
// Migrations — add columns to existing tables
try {
db.prepare("SELECT thumbnail FROM boards LIMIT 0").get();
} catch {
db.exec("ALTER TABLE boards ADD COLUMN thumbnail TEXT DEFAULT NULL");
}
try {
db.prepare("SELECT object_count FROM boards LIMIT 0").get();
} catch {
db.exec("ALTER TABLE boards ADD COLUMN object_count INTEGER NOT NULL DEFAULT 0");
}
// ---------------------
// User helpers
// ---------------------
@@ -128,7 +142,10 @@ function getUserCount() {
function getCollections(userId, search, limit = 50, offset = 0) {
let query = `
SELECT DISTINCT c.*, cm.role as member_role,
(SELECT COUNT(*) FROM boards WHERE collection_id = c.id) as board_count
(SELECT COUNT(*) FROM boards WHERE collection_id = c.id) as board_count,
(SELECT COUNT(*) FROM collection_members WHERE collection_id = c.id) as member_count,
(SELECT b.thumbnail FROM boards b WHERE b.collection_id = c.id AND b.thumbnail IS NOT NULL ORDER BY b.updated_at DESC LIMIT 1) as preview_thumbnail,
(SELECT GROUP_CONCAT(sub.thumbnail, '|||') FROM (SELECT thumbnail FROM boards WHERE collection_id = c.id AND thumbnail IS NOT NULL ORDER BY updated_at DESC LIMIT 4) sub) as preview_thumbnails
FROM collections c
LEFT JOIN collection_members cm ON c.id = cm.collection_id AND cm.user_id = ?
WHERE (cm.user_id IS NOT NULL OR c.is_public = 1)
@@ -230,8 +247,9 @@ function checkCollectionAccess(collectionId, userId) {
// ---------------------
function getCollectionBoards(collectionId, search, limit = 50, offset = 0) {
let query = `
SELECT b.*,
(SELECT COUNT(*) FROM images WHERE board_id = b.id) as image_count
SELECT b.id, b.collection_id, b.name, b.description, b.thumbnail,
b.created_by, b.created_at, b.updated_at,
b.object_count as image_count
FROM boards b
WHERE b.collection_id = ?
`;
@@ -281,9 +299,22 @@ function deleteBoard(boardId) {
db.prepare('DELETE FROM boards WHERE id = ?').run(boardId);
}
function saveBoardCanvas(boardId, canvasState) {
db.prepare("UPDATE boards SET canvas_state = ?, updated_at = datetime('now') WHERE id = ?")
.run(typeof canvasState === 'string' ? canvasState : JSON.stringify(canvasState), boardId);
function saveBoardCanvas(boardId, canvasState, thumbnail) {
const stateStr = typeof canvasState === 'string' ? canvasState : JSON.stringify(canvasState);
// Count objects from canvas state JSON
let objectCount = 0;
try {
const parsed = typeof canvasState === 'string' ? JSON.parse(canvasState) : canvasState;
if (parsed && Array.isArray(parsed.objects)) objectCount = parsed.objects.length;
} catch {}
if (thumbnail) {
db.prepare("UPDATE boards SET canvas_state = ?, thumbnail = ?, object_count = ?, updated_at = datetime('now') WHERE id = ?")
.run(stateStr, thumbnail, objectCount, boardId);
} else {
db.prepare("UPDATE boards SET canvas_state = ?, object_count = ?, updated_at = datetime('now') WHERE id = ?")
.run(stateStr, objectCount, boardId);
}
}
// ---------------------