PDF upload, gallery → assets with type tabs, orphan detection fix

PDF upload support:
* index.php: application/pdf added to $mimeMap + $extMap in the
  /api/upload handler. PDFs are stored in uploads/ like images/audio.
* editor.js uploadFile(): detects PDF via MIME or .pdf extension.
  Inserts a markdown link [📄 filename](uploads/...) instead of an
  image embed. Paste + drop handlers updated to accept PDFs.
* index.php: new toolbar button #md-upload-pdf with ph-file-pdf icon
  next to the extract button. Triggers a hidden file input
  (accept=".pdf"). Wired in editor.js.

Gallery → Assets rename:
* index.php footer button: "Images" → "Assets", icon ph-folder-open.
* index.php gallery overlay header: "images" → "assets", icon
  ph-folder-open, upload input accept extended to include .pdf.
* Empty state text: "no images" → "no assets".

File-type tabs in the assets overlay:
* index.php: new #gallery-tabs div with 4 buttons (All / Images /
  Audio / PDF), each with data-filter attribute.
* gallery.js: fileType(url) helper returns 'image'|'audio'|'pdf'|
  'other'. Tab clicks toggle .active class and filter gallery-items
  by data-url + fileType. activeFilter persists across re-opens.
* CSS: .gallery-tab styling with hover + .active accent fill.

PDF rendering in the assets grid:
* gallery.js mediaTile(): PDFs render as ph-file-pdf icon + filename
  instead of a thumbnail. Orphaned PDFs show the same icon tile.
* Click on an orphaned PDF opens it in a new tab (window.open).
* "New card" from an orphaned PDF inserts a markdown link (not
  image embed).

Orphan detection fix (gallery.js):
* Root cause: loadCards() in app.js uses two-phase lazy loading —
  first /api/cards/meta (body=null on all cards), then batches of 25
  with full bodies. Opening the gallery before the last batch
  completes meant most cards had body=null, getReferencedUrls()
  found no URLs, and every file appeared orphaned.
* Fix: openGallery() now fetches its own card list with full bodies
  via api('api/cards') in parallel with api('api/uploads'), stored in
  a module-local galleryCards array. Both getReferencedUrls() and
  findCardForUrl() read from galleryCards instead of state.cards.
* Bonus: added a fallback regex uploads/[hex]{32}.[ext] that catches
  references in any format the specific patterns might miss (HTML
  attributes, custom blocks, inline text).

chore: bump version to 0.9.18

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.6 committed 2026-04-12 11:39:50 +02:00
1 parent 195b0e343f
commit bf81d046e6
5 files changed
+128 -29

No files matched your search

+18 -7
View File
@@ -994,10 +994,11 @@ body.addEventListener('blur', () => { setTimeout(hideLinkAc, 150); setTimeout(hi
async function uploadFile(file) {
const isAudio = file.type.startsWith('audio/');
const isImage = file.type.startsWith('image/');
if (!isAudio && !isImage) return;
const isPdf = file.type === 'application/pdf' || file.name?.toLowerCase().endsWith('.pdf');
if (!isAudio && !isImage && !isPdf) return;
const ext = file.name?.split('.').pop() || file.type.split('/')[1] || 'bin';
const placeholder = isAudio ? '[uploading audio...]' : '![uploading...]()';
const placeholder = isPdf ? '[uploading pdf...]' : isAudio ? '[uploading audio...]' : '![uploading...]()';
const pos = body.selectionStart;
body.setRangeText(placeholder, pos, body.selectionEnd, 'end');
updatePreview();
@@ -1010,9 +1011,11 @@ async function uploadFile(file) {
const data = JSON.parse(text);
if (data.error) throw new Error(data.error);
if (data.url) {
const md = isAudio
? `<audio controls preload="metadata" src="${data.url}"></audio>`
: `![${data.name}](${data.url})`;
const md = isPdf
? `[📄 ${file.name || data.name}](${data.url})`
: isAudio
? `<audio controls preload="metadata" src="${data.url}"></audio>`
: `![${data.name}](${data.url})`;
const idx = body.value.indexOf(placeholder);
if (idx !== -1) {
body.value = body.value.substring(0, idx) + md + body.value.substring(idx + placeholder.length);
@@ -1033,7 +1036,7 @@ body.addEventListener('paste', e => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/') || item.type.startsWith('audio/')) {
if (item.type.startsWith('image/') || item.type.startsWith('audio/') || item.type === 'application/pdf') {
e.preventDefault();
const file = item.getAsFile();
if (file) uploadFile(file);
@@ -1047,7 +1050,7 @@ body.addEventListener('drop', e => {
const files = e.dataTransfer?.files;
if (!files?.length) return;
for (const file of files) {
if (file.type.startsWith('image/') || file.type.startsWith('audio/')) {
if (file.type.startsWith('image/') || file.type.startsWith('audio/') || file.type === 'application/pdf') {
e.preventDefault();
uploadFile(file);
return;
@@ -1055,6 +1058,14 @@ body.addEventListener('drop', e => {
}
});
// PDF upload toolbar button
$('md-upload-pdf')?.addEventListener('click', () => $('md-upload-pdf-file')?.click());
$('md-upload-pdf-file')?.addEventListener('change', e => {
const file = e.target.files?.[0];
if (file) uploadFile(file);
e.target.value = '';
});
// ── Image resize ──
document.addEventListener('mousedown', e => {