previews on home
This commit is contained in:
@@ -11,6 +11,9 @@ from backend.database import get_user_id
|
||||
|
||||
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]:
|
||||
seen: list[str] = []
|
||||
@@ -35,6 +38,16 @@ def extract_title(text: str) -> str:
|
||||
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:
|
||||
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:
|
||||
db.row_factory = aiosqlite.Row
|
||||
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"
|
||||
" WHERE n.deleted_at IS NULL AND u.name = ?"
|
||||
" ORDER BY n.updated_at DESC LIMIT ?",
|
||||
(username, limit),
|
||||
)
|
||||
notes = [dict(r) for r in await rows.fetchall()]
|
||||
for note in notes:
|
||||
note["tags"] = await get_note_tags(note["id"])
|
||||
await _attach_meta(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:
|
||||
db.row_factory = aiosqlite.Row
|
||||
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"
|
||||
" WHERE n.deleted_at IS NULL AND u.name IN (?, ?)"
|
||||
" ORDER BY n.updated_at DESC LIMIT ?",
|
||||
(username, settings.common_space, limit),
|
||||
)
|
||||
notes = [dict(r) for r in await rows.fetchall()]
|
||||
for note in notes:
|
||||
note["tags"] = await get_note_tags(note["id"])
|
||||
await _attach_meta(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()]
|
||||
|
||||
|
||||
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 with aiosqlite.connect(settings.database_path) as db:
|
||||
db.row_factory = aiosqlite.Row
|
||||
|
||||
@@ -273,8 +273,20 @@ body {
|
||||
.note-meta { color: var(--muted); font-size: 12px; margin-top: 2px; }
|
||||
.note-tags { color: var(--primary); font-size: 12px; margin-top: 4px; }
|
||||
|
||||
.note-common { border-left: 3px solid var(--secondary); }
|
||||
.common-badge {
|
||||
.note-item { display: flex; align-items: center; gap: 12px; }
|
||||
.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;
|
||||
background: color-mix(in srgb, var(--secondary) 18%, transparent);
|
||||
color: var(--secondary);
|
||||
|
||||
@@ -410,10 +410,15 @@ function noteList(notes) {
|
||||
const meta = el("div", { class: "note-meta" });
|
||||
if (isCommon) meta.appendChild(el("span", { class: "common-badge", text: "common" }));
|
||||
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" }),
|
||||
meta,
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user