M6 1900: any-file attachments + audio memos (broaden beyond images)
A note can now carry any file, not just images — PDFs, documents, audio memos, etc. "Dump anything" capture. Backend: - note_attachments.filename (migration 0019) records the original name for download + display. - Upload drops the image-only mime gate: accepts any type, derives the storage extension from the filename, and enforces a DB-backed per-file cap — new setting max_attachment_mb (default 25, rule 25). App body ceiling raised 12→64 MB (also lifts the import-zip / sync-push limits); the per-file cap is the effective attachment limit. - Serve sets Content-Disposition: images inline, everything else downloads with its original (header-sanitized) filename. - Import (native + Keep Takeout) now brings in ANY attachment, not just images — completing the Keep audio-memo gap; preserves filename + sha256. - Attachment metadata (delta feed + REST) carries filename. Frontend: - Editor renders attachments by kind: images inline (thumbnail), audio via an inline <audio> player, any other file as a download chip (paperclip + filename + size). File picker accepts any type; "Attach a file". - Card previews the first image; non-image files show as compact chips. Tests (DB-free): _safe_filename, _attachment_ext, _header_filename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FRgehjoz7Yv8LkUfADxACm
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, ref, watch } from "vue";
|
||||
import { computed, onBeforeUnmount, ref, watch } from "vue";
|
||||
import { useNotesStore } from "../stores/notes";
|
||||
import {
|
||||
LABEL_CHIP_CLASSES,
|
||||
@@ -24,6 +24,10 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
const notes = useNotesStore();
|
||||
|
||||
// The card previews the first image inline; non-image files show as compact chips.
|
||||
const firstImage = computed(() => props.note.attachments.find((a) => a.mime.startsWith("image/")));
|
||||
const otherAttachments = computed(() => props.note.attachments.filter((a) => !a.mime.startsWith("image/")));
|
||||
|
||||
const root = ref<HTMLElement | null>(null);
|
||||
|
||||
// --- Drag-to-reorder. Native HTML5 DnD, gated behind an explicit grip handle so
|
||||
@@ -139,14 +143,24 @@ onBeforeUnmount(() => document.removeEventListener("mousedown", onDocMousedown))
|
||||
</button>
|
||||
|
||||
<img
|
||||
v-if="note.attachments.length"
|
||||
:src="note.attachments[0].url"
|
||||
v-if="firstImage"
|
||||
:src="firstImage.url"
|
||||
alt=""
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
class="mb-2 max-h-48 w-full cursor-pointer rounded-lg object-cover"
|
||||
@click="emit('open', note)"
|
||||
/>
|
||||
<div v-if="otherAttachments.length" class="mb-2 flex flex-wrap gap-1">
|
||||
<span
|
||||
v-for="att in otherAttachments"
|
||||
:key="att.id"
|
||||
class="inline-flex max-w-full items-center gap-1 rounded-md bg-black/5 px-1.5 py-0.5 text-xs text-neutral-500 dark:bg-white/10 dark:text-neutral-400"
|
||||
>
|
||||
<Icon name="paperclip" />
|
||||
<span class="max-w-[140px] truncate">{{ att.filename || "file" }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Checklist notes can't nest interactive controls in a <button>, so use a
|
||||
focusable div; text notes keep a semantic button. -->
|
||||
|
||||
Reference in New Issue
Block a user