previews on home

This commit is contained in:
schmeeve
2026-09-23 19:10:01 -07:00
parent 197da6754f
commit 7788c10fde
3 changed files with 52 additions and 9 deletions

View File

@@ -11,6 +11,9 @@ from backend.database import get_user_id
TAG_RE = re.compile(r"#[A-Za-z0-9_-]+") TAG_RE = re.compile(r"#[A-Za-z0-9_-]+")
ATTACH_RE = re.compile(r"/api/attachments/([^/\s]+)/([A-Za-z0-9._-]+)")
IMAGE_EXT_RE = re.compile(r"\.(png|jpe?g|gif|webp|svg|avif|bmp|ico|heic)$", re.IGNORECASE)
def extract_tags(text: str) -> list[str]: def extract_tags(text: str) -> list[str]:
seen: list[str] = [] seen: list[str] = []
@@ -35,6 +38,16 @@ def extract_title(text: str) -> str:
return "Untitled" return "Untitled"
def note_attachments(body: str) -> tuple[int, str | None]:
count = 0
preview = None
for m in ATTACH_RE.finditer(body):
count += 1
if preview is None and IMAGE_EXT_RE.search(m.group(2)):
preview = f"/api/attachments/{m.group(1)}/{m.group(2)}"
return count, preview
def note_path(username: str, note_id: str) -> str: def note_path(username: str, note_id: str) -> str:
return f"{username}/{note_id}.md" return f"{username}/{note_id}.md"
@@ -231,15 +244,15 @@ async def list_notes(username: str, limit: int = 100) -> list[dict]:
async with aiosqlite.connect(settings.database_path) as db: async with aiosqlite.connect(settings.database_path) as db:
db.row_factory = aiosqlite.Row db.row_factory = aiosqlite.Row
rows = await db.execute( rows = await db.execute(
"SELECT n.id, n.title, n.updated_at, n.created_at, u.name AS username" "SELECT n.id, n.title, n.updated_at, n.created_at, n.path,"
" u.name AS username"
" FROM notes n JOIN users u ON u.id = n.user_id" " FROM notes n JOIN users u ON u.id = n.user_id"
" WHERE n.deleted_at IS NULL AND u.name = ?" " WHERE n.deleted_at IS NULL AND u.name = ?"
" ORDER BY n.updated_at DESC LIMIT ?", " ORDER BY n.updated_at DESC LIMIT ?",
(username, limit), (username, limit),
) )
notes = [dict(r) for r in await rows.fetchall()] notes = [dict(r) for r in await rows.fetchall()]
for note in notes: await _attach_meta(notes)
note["tags"] = await get_note_tags(note["id"])
return notes return notes
@@ -247,15 +260,15 @@ async def list_recent(username: str, limit: int = 30) -> list[dict]:
async with aiosqlite.connect(settings.database_path) as db: async with aiosqlite.connect(settings.database_path) as db:
db.row_factory = aiosqlite.Row db.row_factory = aiosqlite.Row
rows = await db.execute( rows = await db.execute(
"SELECT n.id, n.title, n.updated_at, n.created_at, u.name AS username" "SELECT n.id, n.title, n.updated_at, n.created_at, n.path,"
" u.name AS username"
" FROM notes n JOIN users u ON u.id = n.user_id" " FROM notes n JOIN users u ON u.id = n.user_id"
" WHERE n.deleted_at IS NULL AND u.name IN (?, ?)" " WHERE n.deleted_at IS NULL AND u.name IN (?, ?)"
" ORDER BY n.updated_at DESC LIMIT ?", " ORDER BY n.updated_at DESC LIMIT ?",
(username, settings.common_space, limit), (username, settings.common_space, limit),
) )
notes = [dict(r) for r in await rows.fetchall()] notes = [dict(r) for r in await rows.fetchall()]
for note in notes: await _attach_meta(notes)
note["tags"] = await get_note_tags(note["id"])
return notes return notes
@@ -270,6 +283,19 @@ async def get_note_tags(note_id: str) -> list[str]:
return [r["name"] for r in await rows.fetchall()] return [r["name"] for r in await rows.fetchall()]
async def _attach_meta(notes: list[dict]) -> None:
for note in notes:
note["tags"] = await get_note_tags(note["id"])
try:
body = await read_note_file(note["path"])
count, preview = note_attachments(body)
except FileNotFoundError:
count, preview = 0, None
note["attachments"] = count
note["image"] = preview
note.pop("path", None)
async def tag_counts() -> list[dict]: async def tag_counts() -> list[dict]:
async with aiosqlite.connect(settings.database_path) as db: async with aiosqlite.connect(settings.database_path) as db:
db.row_factory = aiosqlite.Row db.row_factory = aiosqlite.Row

View File

@@ -273,8 +273,20 @@ body {
.note-meta { color: var(--muted); font-size: 12px; margin-top: 2px; } .note-meta { color: var(--muted); font-size: 12px; margin-top: 2px; }
.note-tags { color: var(--primary); font-size: 12px; margin-top: 4px; } .note-tags { color: var(--primary); font-size: 12px; margin-top: 4px; }
.note-common { border-left: 3px solid var(--secondary); } .note-item { display: flex; align-items: center; gap: 12px; }
.common-badge { .note-body { flex: 1; min-width: 0; }
.note-thumb {
width: 56px;
height: 56px;
object-fit: cover;
border-radius: 6px;
border: 1px solid var(--border);
background: var(--surface);
flex-shrink: 0;
}
.attach-count { color: var(--muted); margin-left: 8px; }
.note-common { border-left: 3px solid var(--secondary); }.common-badge {
display: inline-block; display: inline-block;
background: color-mix(in srgb, var(--secondary) 18%, transparent); background: color-mix(in srgb, var(--secondary) 18%, transparent);
color: var(--secondary); color: var(--secondary);

View File

@@ -410,10 +410,15 @@ function noteList(notes) {
const meta = el("div", { class: "note-meta" }); const meta = el("div", { class: "note-meta" });
if (isCommon) meta.appendChild(el("span", { class: "common-badge", text: "common" })); if (isCommon) meta.appendChild(el("span", { class: "common-badge", text: "common" }));
meta.appendChild(document.createTextNode(`${n.username} · ${relTime(n.updated_at)}`)); meta.appendChild(document.createTextNode(`${n.username} · ${relTime(n.updated_at)}`));
ul.appendChild(el("li", { class: isCommon ? "note-item note-common" : "note-item", onclick: () => openNote(n.id) }, [ if (n.attachments) meta.appendChild(el("span", { class: "attach-count", text: n.attachments + " attachment" + (n.attachments === 1 ? "" : "s") }));
const bodyEl = el("div", { class: "note-body" }, [
el("div", { class: "note-title", text: n.title || "Untitled" }), el("div", { class: "note-title", text: n.title || "Untitled" }),
meta, meta,
el("div", { class: "note-tags", text: tags }), el("div", { class: "note-tags", text: tags }),
]);
ul.appendChild(el("li", { class: isCommon ? "note-item note-common" : "note-item", onclick: () => openNote(n.id) }, [
n.image ? el("img", { class: "note-thumb", src: n.image, alt: "", loading: "lazy" }) : null,
bodyEl,
])); ]));
} }
return ul; return ul;