mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-04 17:07:00 +01:00
Terminal-themed markdown diary
Single-user diary served as one Rust binary with the Svelte frontend embedded. - axum 0.8 + SQLite (sqlx), migrations run at startup - stateless HMAC session cookie; credentials from .env, compared in constant time - FTS5 full-text search, user input rewritten into quoted prefix terms - media uploads linked to entries by scanning bodies for /api/media/<id>, which scopes share-token media access and entry deletion - unguessable share links (192-bit token) serving a public read-only page - vim keybindings via CodeMirror plus a modal browse layer and : command line; disabled on touch devices
This commit is contained in:
commit
4002a35305
41 files changed
+6195
No files matched your search
@@ -0,0 +1,99 @@
|
||||
<script lang="ts">
|
||||
import { api } from './api'
|
||||
import { hooks } from './commands'
|
||||
import { embedSnippet } from './markdown'
|
||||
import { diary } from './store.svelte'
|
||||
import { formatBytes, formatStamp } from './util'
|
||||
|
||||
async function remove(id: string, filename: string) {
|
||||
if (!confirm(`delete ${filename}? entries that embed it will break.`)) return
|
||||
await diary.guard(async () => {
|
||||
await api.deleteMedia(id)
|
||||
await diary.loadMedia()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sheet">
|
||||
<header>
|
||||
<span class="accent">:media</span>
|
||||
<span class="faint">{diary.media.length} files</span>
|
||||
<button onclick={() => (diary.overlay = 'none')}>esc</button>
|
||||
</header>
|
||||
|
||||
<ul>
|
||||
{#each diary.media as file (file.id)}
|
||||
<li>
|
||||
<div class="thumb">
|
||||
{#if file.mime.startsWith('image/')}
|
||||
<img src={file.url} alt={file.filename} loading="lazy" />
|
||||
{:else}
|
||||
<span class="faint">{file.mime.split('/')[0]}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="info">
|
||||
<span class="name">{file.filename}</span>
|
||||
<span class="faint">
|
||||
{formatBytes(file.size)} · {formatStamp(file.created_at)} ·
|
||||
{file.entry_id ? `entry:${file.entry_id}` : 'unattached'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
{#if diary.editing}
|
||||
<button
|
||||
onclick={() => {
|
||||
hooks.insertText(embedSnippet(file))
|
||||
diary.overlay = 'none'
|
||||
}}>insert</button
|
||||
>
|
||||
{/if}
|
||||
<button onclick={() => diary.copy(location.origin + file.url)}>copy url</button>
|
||||
<button onclick={() => remove(file.id, file.filename)}>rm</button>
|
||||
</div>
|
||||
</li>
|
||||
{:else}
|
||||
<li class="faint">nothing uploaded yet — attach a file from the editor.</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.sheet {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: min(100%, 90ch);
|
||||
max-height: 100%;
|
||||
background: var(--bg-alt);
|
||||
border: 1px solid var(--accent-dim);
|
||||
border-radius: var(--radius);
|
||||
padding: 14px 16px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
header { display: flex; justify-content: space-between; align-items: center; gap: 12px; }
|
||||
|
||||
ul { list-style: none; margin: 0; padding: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 2px; }
|
||||
|
||||
li { display: flex; align-items: center; gap: 12px; padding: 4px; border-bottom: 1px solid var(--line); }
|
||||
|
||||
.thumb {
|
||||
width: 48px;
|
||||
height: 36px;
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
overflow: hidden;
|
||||
font-size: 10px;
|
||||
}
|
||||
.thumb img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
.info { display: flex; flex-direction: column; min-width: 0; flex: 1; font-size: 12px; }
|
||||
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.actions { display: flex; gap: 4px; flex: none; }
|
||||
</style>
|
||||
Reference in new issue
Block a user