feat: enable Android Share Sheet for graphics & files; handle direct PDF/image clipping
This commit is contained in:
1 parent
338d388c2e
commit
3ed7fb8036
3 files changed
+217
-11
No files matched your search
+30
-4
@@ -47,13 +47,39 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"share_target": {
|
"share_target": {
|
||||||
"action": "/",
|
"action": "/share",
|
||||||
"method": "GET",
|
"method": "POST",
|
||||||
"enctype": "application/x-www-form-urlencoded",
|
"enctype": "multipart/form-data",
|
||||||
"params": {
|
"params": {
|
||||||
"title": "title",
|
"title": "title",
|
||||||
"text": "text",
|
"text": "text",
|
||||||
"url": "url"
|
"url": "url",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"name": "files",
|
||||||
|
"accept": [
|
||||||
|
"image/*",
|
||||||
|
"application/pdf",
|
||||||
|
"text/*",
|
||||||
|
"audio/*",
|
||||||
|
"video/*",
|
||||||
|
".pdf",
|
||||||
|
".jpg",
|
||||||
|
".jpeg",
|
||||||
|
".png",
|
||||||
|
".webp",
|
||||||
|
".gif",
|
||||||
|
".svg",
|
||||||
|
".enex",
|
||||||
|
".md",
|
||||||
|
".txt",
|
||||||
|
".mp3",
|
||||||
|
".m4a",
|
||||||
|
".wav",
|
||||||
|
".mp4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"shortcuts": [
|
"shortcuts": [
|
||||||
|
|||||||
@@ -321,11 +321,18 @@ export async function POST(req: NextRequest) {
|
|||||||
appendedMediaHtml += `<p><audio controls src="/api/attachments/${att.id}"></audio></p>\n`;
|
appendedMediaHtml += `<p><audio controls src="/api/attachments/${att.id}"></audio></p>\n`;
|
||||||
} else if (mimeType.startsWith('video/')) {
|
} else if (mimeType.startsWith('video/')) {
|
||||||
appendedMediaHtml += `<p><video controls src="/api/attachments/${att.id}" style="max-width: 100%; border-radius: 8px;"></video></p>\n`;
|
appendedMediaHtml += `<p><video controls src="/api/attachments/${att.id}" style="max-width: 100%; border-radius: 8px;"></video></p>\n`;
|
||||||
|
} else if (mimeType === 'application/pdf' || filename.toLowerCase().endsWith('.pdf')) {
|
||||||
|
appendedMediaHtml += `<p><a href="/api/attachments/${att.id}" target="_blank" rel="noopener noreferrer">📄 View PDF: ${filename}</a></p>\n`;
|
||||||
} else {
|
} else {
|
||||||
appendedMediaHtml += `<p><a href="/api/attachments/${att.id}" target="_blank" rel="noopener noreferrer">📎 ${filename}</a></p>\n`;
|
appendedMediaHtml += `<p><a href="/api/attachments/${att.id}" target="_blank" rel="noopener noreferrer">📎 ${filename}</a></p>\n`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const firstImgAtt = await prisma.attachment.findFirst({
|
||||||
|
where: { noteId: note.id, mimeType: { startsWith: 'image/' } },
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
|
||||||
if (appendedMediaHtml) {
|
if (appendedMediaHtml) {
|
||||||
const combinedHtml = `${finalHtml ? `${finalHtml}\n` : ''}${appendedMediaHtml}`;
|
const combinedHtml = `${finalHtml ? `${finalHtml}\n` : ''}${appendedMediaHtml}`;
|
||||||
await prisma.note.update({
|
await prisma.note.update({
|
||||||
@@ -334,6 +341,7 @@ export async function POST(req: NextRequest) {
|
|||||||
contentHtml: combinedHtml,
|
contentHtml: combinedHtml,
|
||||||
contentText: stripHtmlToText(combinedHtml),
|
contentText: stripHtmlToText(combinedHtml),
|
||||||
excerpt: makeExcerpt(stripHtmlToText(combinedHtml)),
|
excerpt: makeExcerpt(stripHtmlToText(combinedHtml)),
|
||||||
|
...(firstImgAtt ? { thumbnailUrl: `/api/attachments/${firstImgAtt.id}` } : {}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+179
-7
@@ -926,13 +926,14 @@ export async function clipUrlToNote(
|
|||||||
}
|
}
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const notebookId = options.notebookId || (await ensureDefaultNotebook()).id;
|
const notebookId = options.notebookId || (await ensureDefaultNotebook()).id;
|
||||||
let html = '';
|
const userId = await getCurrentUserId();
|
||||||
|
let res: Response | null = null;
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url, {
|
res = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent':
|
'User-Agent':
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,application/pdf,*/*;q=0.8',
|
||||||
'Accept-Language': 'en-US,en;q=0.9',
|
'Accept-Language': 'en-US,en;q=0.9',
|
||||||
'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
'Sec-Ch-Ua': '"Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
||||||
'Sec-Ch-Ua-Mobile': '?0',
|
'Sec-Ch-Ua-Mobile': '?0',
|
||||||
@@ -943,10 +944,182 @@ export async function clipUrlToNote(
|
|||||||
'Sec-Fetch-User': '?1',
|
'Sec-Fetch-User': '?1',
|
||||||
'Upgrade-Insecure-Requests': '1',
|
'Upgrade-Insecure-Requests': '1',
|
||||||
},
|
},
|
||||||
signal: AbortSignal.timeout(15000),
|
signal: AbortSignal.timeout(20000),
|
||||||
});
|
});
|
||||||
html = await res.text();
|
} catch (fetchErr) {
|
||||||
} catch {
|
console.warn('Failed to fetch URL for clipping:', fetchErr);
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentType = res ? (res.headers.get('content-type')?.toLowerCase() || '') : '';
|
||||||
|
const cleanUrlPath = url.split('?')[0].toLowerCase();
|
||||||
|
const isPdf = contentType.includes('application/pdf') || cleanUrlPath.endsWith('.pdf');
|
||||||
|
const isImage = contentType.startsWith('image/') || /\.(jpe?g|png|webp|gif|avif|svg)$/i.test(cleanUrlPath);
|
||||||
|
|
||||||
|
// === CASE A: Direct PDF clipping ===
|
||||||
|
if (res && res.ok && isPdf) {
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
|
|
||||||
|
let filename = '';
|
||||||
|
const cd = res.headers.get('content-disposition') || '';
|
||||||
|
const fnMatch = /filename\*?=(?:UTF-8'')?"?([^";]+)"?/i.exec(cd);
|
||||||
|
if (fnMatch) {
|
||||||
|
try {
|
||||||
|
filename = decodeURIComponent(fnMatch[1].trim());
|
||||||
|
} catch {
|
||||||
|
filename = fnMatch[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!filename) {
|
||||||
|
try {
|
||||||
|
const rawPart = url.split('?')[0].split('/').pop() || 'document.pdf';
|
||||||
|
filename = decodeURIComponent(rawPart);
|
||||||
|
} catch {
|
||||||
|
filename = 'document.pdf';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!filename.toLowerCase().endsWith('.pdf')) {
|
||||||
|
filename += '.pdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvedTitle = '';
|
||||||
|
if (options.title && options.title !== url && !options.title.startsWith('http')) {
|
||||||
|
resolvedTitle = options.title.trim();
|
||||||
|
} else {
|
||||||
|
const headerSnippet = buf.toString('latin1', 0, 8192);
|
||||||
|
const titleMatch = /\/Title\s*\(([^)]+)\)/i.exec(headerSnippet);
|
||||||
|
if (titleMatch && titleMatch[1].trim()) {
|
||||||
|
resolvedTitle = titleMatch[1].replace(/\\([()\\])/g, '$1').trim();
|
||||||
|
}
|
||||||
|
if (!resolvedTitle) {
|
||||||
|
resolvedTitle = filename.replace(/\.pdf$/i, '').replace(/[_-]+/g, ' ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedBlob = await saveAttachmentBuffer(buf, filename, 'application/pdf');
|
||||||
|
const tagsToApply = [...(options.tags || ['inbox', 'web-clip'])];
|
||||||
|
if (!tagsToApply.includes('pdf')) {
|
||||||
|
tagsToApply.push('pdf');
|
||||||
|
}
|
||||||
|
const tagIds = await getOrCreateTagIds(tagsToApply, userId);
|
||||||
|
|
||||||
|
const note = await prisma.note.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
title: resolvedTitle || filename,
|
||||||
|
contentHtml: `<p>Saved PDF document: ${escapeHtml(filename)}</p>\n<p>Source: <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a></p>`,
|
||||||
|
contentText: savedBlob.extractedText || resolvedTitle || filename,
|
||||||
|
excerpt: makeExcerpt(savedBlob.extractedText || resolvedTitle || filename),
|
||||||
|
notebookId,
|
||||||
|
sourceUrl: url,
|
||||||
|
isShortcut: options.isShortcut ?? false,
|
||||||
|
tags: {
|
||||||
|
create: tagIds.map((tagId) => ({ tagId })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const att = await prisma.attachment.create({
|
||||||
|
data: {
|
||||||
|
noteId: note.id,
|
||||||
|
filename,
|
||||||
|
mimeType: 'application/pdf',
|
||||||
|
sizeBytes: savedBlob.sizeBytes,
|
||||||
|
md5Hash: savedBlob.md5Hash,
|
||||||
|
storagePath: savedBlob.storagePath,
|
||||||
|
extractedText: savedBlob.extractedText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.note.update({
|
||||||
|
where: { id: note.id },
|
||||||
|
data: {
|
||||||
|
contentHtml: `<p>Saved PDF document: <a href="/api/attachments/${att.id}" target="_blank" rel="noopener noreferrer">${escapeHtml(filename)}</a></p>\n<p>Source: <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a></p>`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
enqueueAttachmentOcr(att.id);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { applyAutoSortToNote } = await import('./autoSort');
|
||||||
|
await applyAutoSortToNote(note.id, userId);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return note.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === CASE B: Direct Image clipping ===
|
||||||
|
if (res && res.ok && isImage) {
|
||||||
|
const buf = Buffer.from(await res.arrayBuffer());
|
||||||
|
const mimeType = contentType.startsWith('image/') ? contentType.split(';')[0].trim() : 'image/jpeg';
|
||||||
|
let filename = '';
|
||||||
|
try {
|
||||||
|
filename = decodeURIComponent(url.split('?')[0].split('/').pop() || 'image.jpg');
|
||||||
|
} catch {
|
||||||
|
filename = 'image.jpg';
|
||||||
|
}
|
||||||
|
const savedBlob = await saveAttachmentBuffer(buf, filename, mimeType);
|
||||||
|
const tagsToApply = [...(options.tags || ['inbox', 'web-clip'])];
|
||||||
|
const tagIds = await getOrCreateTagIds(tagsToApply, userId);
|
||||||
|
|
||||||
|
const resolvedTitle =
|
||||||
|
options.title && !options.title.startsWith('http')
|
||||||
|
? options.title.trim()
|
||||||
|
: filename.replace(/\.[^.]+$/, '').replace(/[_-]+/g, ' ').trim();
|
||||||
|
|
||||||
|
const note = await prisma.note.create({
|
||||||
|
data: {
|
||||||
|
userId,
|
||||||
|
title: resolvedTitle || 'Image Clip',
|
||||||
|
contentHtml: `<p>Loading image...</p>`,
|
||||||
|
contentText: resolvedTitle || 'Image Clip',
|
||||||
|
excerpt: makeExcerpt(resolvedTitle || 'Image Clip'),
|
||||||
|
notebookId,
|
||||||
|
sourceUrl: url,
|
||||||
|
isShortcut: options.isShortcut ?? false,
|
||||||
|
tags: {
|
||||||
|
create: tagIds.map((tagId) => ({ tagId })),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const att = await prisma.attachment.create({
|
||||||
|
data: {
|
||||||
|
noteId: note.id,
|
||||||
|
filename,
|
||||||
|
mimeType,
|
||||||
|
sizeBytes: savedBlob.sizeBytes,
|
||||||
|
md5Hash: savedBlob.md5Hash,
|
||||||
|
storagePath: savedBlob.storagePath,
|
||||||
|
extractedText: savedBlob.extractedText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await prisma.note.update({
|
||||||
|
where: { id: note.id },
|
||||||
|
data: {
|
||||||
|
contentHtml: `<p><img src="/api/attachments/${att.id}" alt="${escapeHtml(filename)}" /></p>\n<p>Source: <a href="${escapeHtml(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(url)}</a></p>`,
|
||||||
|
thumbnailUrl: `/api/attachments/${att.id}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { applyAutoSortToNote } = await import('./autoSort');
|
||||||
|
await applyAutoSortToNote(note.id, userId);
|
||||||
|
} catch {}
|
||||||
|
|
||||||
|
return note.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
let html = '';
|
||||||
|
if (res) {
|
||||||
|
try {
|
||||||
|
html = await res.text();
|
||||||
|
} catch {
|
||||||
|
html = `<p>Saved link: <a href="${escapeHtml(url)}">${escapeHtml(url)}</a></p>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
html = `<p>Saved link: <a href="${escapeHtml(url)}">${escapeHtml(url)}</a></p>`;
|
html = `<p>Saved link: <a href="${escapeHtml(url)}">${escapeHtml(url)}</a></p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -994,7 +1167,6 @@ export async function clipUrlToNote(
|
|||||||
contentHtml = `<p><img src="${escapeHtml(heroImgSrc!)}" alt="${escapeHtml(title)}" /></p>\n${contentHtml}`;
|
contentHtml = `<p><img src="${escapeHtml(heroImgSrc!)}" alt="${escapeHtml(title)}" /></p>\n${contentHtml}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const userId = await getCurrentUserId();
|
|
||||||
const contentText = stripHtmlToText(contentHtml);
|
const contentText = stripHtmlToText(contentHtml);
|
||||||
const excerpt = makeExcerpt(article?.excerpt || contentText);
|
const excerpt = makeExcerpt(article?.excerpt || contentText);
|
||||||
const tagIds = await getOrCreateTagIds(tagsToApply, userId);
|
const tagIds = await getOrCreateTagIds(tagsToApply, userId);
|
||||||
|
|||||||
Reference in new issue
Block a user