fix: extract URL and headline from Android share target, trigger article clipping and open editor
This commit is contained in:
1 parent
94fb793f99
commit
a99e05c53c
3 files changed
+191
-27
No files matched your search
@@ -114,20 +114,55 @@ export async function POST(req: NextRequest) {
|
|||||||
uploadedFiles.push(...files);
|
uploadedFiles.push(...files);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if Android shared a URL inside the `text` or `content` (or `title`)
|
// Check if Android shared a URL inside the `text`, `content`, or `title`
|
||||||
if (!url && content) {
|
if (!url && content) {
|
||||||
const match = /(https?:\/\/[^\s]+)/i.exec(content);
|
const match = /(https?:\/\/[^\s]+)/i.exec(content);
|
||||||
if (match) {
|
if (match) {
|
||||||
url = match[1];
|
url = match[1];
|
||||||
if (content.trim() === url) {
|
const remaining = content.replace(url, '').trim();
|
||||||
|
if (!title && remaining) {
|
||||||
|
title = remaining;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!remaining ||
|
||||||
|
remaining === title.trim() ||
|
||||||
|
title.trim().includes(remaining) ||
|
||||||
|
remaining.includes(title.trim()) ||
|
||||||
|
(!remaining.includes('\n') && remaining.length < 300)
|
||||||
|
) {
|
||||||
content = '';
|
content = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!url && title) {
|
||||||
|
const titleMatch = /(https?:\/\/[^\s]+)/i.exec(title);
|
||||||
|
if (titleMatch) {
|
||||||
|
url = titleMatch[1];
|
||||||
|
title = title.replace(url, '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!url && title && /^https?:\/\/\S+$/i.test(title.trim())) {
|
if (!url && title && /^https?:\/\/\S+$/i.test(title.trim())) {
|
||||||
url = title.trim();
|
url = title.trim();
|
||||||
title = '';
|
title = '';
|
||||||
}
|
}
|
||||||
|
if (url) {
|
||||||
|
url = url.replace(/[.,;:)\]]+$/, '');
|
||||||
|
}
|
||||||
|
if (url && content) {
|
||||||
|
const remaining = content.replace(url, '').trim();
|
||||||
|
if (
|
||||||
|
!remaining ||
|
||||||
|
remaining === title.trim() ||
|
||||||
|
title.trim().includes(remaining) ||
|
||||||
|
remaining.includes(title.trim()) ||
|
||||||
|
(!remaining.includes('\n') && remaining.length < 300)
|
||||||
|
) {
|
||||||
|
if (!title && remaining) {
|
||||||
|
title = remaining;
|
||||||
|
}
|
||||||
|
content = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const notebookId = notebookName.trim()
|
const notebookId = notebookName.trim()
|
||||||
? await getOrCreateNotebookByName(notebookName.trim(), userId)
|
? await getOrCreateNotebookByName(notebookName.trim(), userId)
|
||||||
@@ -197,21 +232,36 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
// 2. If URL is provided without body content and no files, clip the full article on server!
|
// 2. If URL is provided without body content and no files, clip the full article on server!
|
||||||
if (url && !content && !markdown && !html && uploadedFiles.length === 0) {
|
if (url && !content && !markdown && !html && uploadedFiles.length === 0) {
|
||||||
const clippedId = await clipUrlToNote(url, {
|
try {
|
||||||
title: title || undefined,
|
const clipTags = [...tags];
|
||||||
notebookId,
|
if (!clipTags.includes('web-clip')) {
|
||||||
tags,
|
clipTags.push('web-clip');
|
||||||
isShortcut,
|
}
|
||||||
});
|
const clippedId = await clipUrlToNote(url, {
|
||||||
|
title: title || undefined,
|
||||||
|
notebookId,
|
||||||
|
tags: clipTags,
|
||||||
|
isShortcut,
|
||||||
|
});
|
||||||
|
|
||||||
if (isShareTarget) {
|
await applyAutoSortToNote(clippedId, userId).catch(() => {});
|
||||||
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
|
|
||||||
|
const wantsJson =
|
||||||
|
contentType.includes('application/json') ||
|
||||||
|
Boolean(req.headers.get('accept')?.includes('application/json'));
|
||||||
|
|
||||||
|
if (isShareTarget && !wantsJson) {
|
||||||
|
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
|
||||||
|
}
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ ok: true, noteId: clippedId, mode: 'web-clip' },
|
||||||
|
{ status: 201, headers: corsHeaders }
|
||||||
|
);
|
||||||
|
} catch (clipErr) {
|
||||||
|
console.warn('URL clipping failed in /api/hooks/note, falling back to basic link note:', clipErr);
|
||||||
|
content = `<p>Saved link: <a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a></p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json(
|
|
||||||
{ ok: true, noteId: clippedId, mode: 'web-clip' },
|
|
||||||
{ status: 201, headers: corsHeaders }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let finalHtml = html;
|
let finalHtml = html;
|
||||||
@@ -309,7 +359,11 @@ export async function POST(req: NextRequest) {
|
|||||||
|
|
||||||
await applyAutoSortToNote(note.id, userId).catch(() => {});
|
await applyAutoSortToNote(note.id, userId).catch(() => {});
|
||||||
|
|
||||||
if (isShareTarget) {
|
const wantsJson =
|
||||||
|
contentType.includes('application/json') ||
|
||||||
|
Boolean(req.headers.get('accept')?.includes('application/json'));
|
||||||
|
|
||||||
|
if (isShareTarget && !wantsJson) {
|
||||||
return NextResponse.redirect(getRedirectUrl(req, note.id), 303);
|
return NextResponse.redirect(getRedirectUrl(req, note.id), 303);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,35 @@ export async function GET(req: NextRequest) {
|
|||||||
text = extracted.remainingText;
|
text = extracted.remainingText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!url && title) {
|
||||||
|
const extracted = extractUrl(title);
|
||||||
|
if (extracted.extractedUrl) {
|
||||||
|
url = extracted.extractedUrl;
|
||||||
|
title = extracted.remainingText;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!url && title && /^https?:\/\/\S+$/i.test(title)) {
|
if (!url && title && /^https?:\/\/\S+$/i.test(title)) {
|
||||||
url = title;
|
url = title;
|
||||||
title = '';
|
title = '';
|
||||||
}
|
}
|
||||||
|
if (url) {
|
||||||
|
url = url.replace(/[.,;:)\]]+$/, '');
|
||||||
|
}
|
||||||
|
if (url && text) {
|
||||||
|
const remaining = text.replace(url, '').trim();
|
||||||
|
if (
|
||||||
|
!remaining ||
|
||||||
|
remaining === title ||
|
||||||
|
title.includes(remaining) ||
|
||||||
|
remaining.includes(title) ||
|
||||||
|
(!remaining.includes('\n') && remaining.length < 300)
|
||||||
|
) {
|
||||||
|
if (!title && remaining) {
|
||||||
|
title = remaining;
|
||||||
|
}
|
||||||
|
text = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let noteId: string;
|
let noteId: string;
|
||||||
if (url && !text) {
|
if (url && !text) {
|
||||||
@@ -150,10 +175,39 @@ export async function POST(req: NextRequest) {
|
|||||||
text = extracted.remainingText;
|
text = extracted.remainingText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (!url && title) {
|
||||||
|
const extracted = extractUrl(title);
|
||||||
|
if (extracted.extractedUrl) {
|
||||||
|
url = extracted.extractedUrl;
|
||||||
|
title = extracted.remainingText;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!url && title && /^https?:\/\/\S+$/i.test(title)) {
|
if (!url && title && /^https?:\/\/\S+$/i.test(title)) {
|
||||||
url = title;
|
url = title;
|
||||||
title = '';
|
title = '';
|
||||||
}
|
}
|
||||||
|
if (url) {
|
||||||
|
url = url.replace(/[.,;:)\]]+$/, '');
|
||||||
|
}
|
||||||
|
if (url && text) {
|
||||||
|
const remaining = text.replace(url, '').trim();
|
||||||
|
if (
|
||||||
|
!remaining ||
|
||||||
|
remaining === title ||
|
||||||
|
title.includes(remaining) ||
|
||||||
|
remaining.includes(title) ||
|
||||||
|
(!remaining.includes('\n') && remaining.length < 300)
|
||||||
|
) {
|
||||||
|
if (!title && remaining) {
|
||||||
|
title = remaining;
|
||||||
|
}
|
||||||
|
text = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wantsJson =
|
||||||
|
contentType.includes('application/json') ||
|
||||||
|
Boolean(req.headers.get('accept')?.includes('application/json'));
|
||||||
|
|
||||||
// 1. If sharing a URL with no uploaded files and no extra body text, clip the webpage
|
// 1. If sharing a URL with no uploaded files and no extra body text, clip the webpage
|
||||||
if (url && !text && uploadedFiles.length === 0) {
|
if (url && !text && uploadedFiles.length === 0) {
|
||||||
@@ -164,6 +218,9 @@ export async function POST(req: NextRequest) {
|
|||||||
tags: ['inbox', 'web-clip'],
|
tags: ['inbox', 'web-clip'],
|
||||||
});
|
});
|
||||||
await applyAutoSortToNote(clippedId, userId).catch(() => {});
|
await applyAutoSortToNote(clippedId, userId).catch(() => {});
|
||||||
|
if (wantsJson) {
|
||||||
|
return NextResponse.json({ ok: true, noteId: clippedId, mode: 'web-clip' }, { status: 201 });
|
||||||
|
}
|
||||||
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
|
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('URL clipping failed in /share, falling back to basic link note:', e);
|
console.warn('URL clipping failed in /share, falling back to basic link note:', e);
|
||||||
@@ -262,6 +319,13 @@ export async function POST(req: NextRequest) {
|
|||||||
// 4. Run Smart Auto-Sorting & Auto-Tagging
|
// 4. Run Smart Auto-Sorting & Auto-Tagging
|
||||||
await applyAutoSortToNote(note.id, userId).catch(() => {});
|
await applyAutoSortToNote(note.id, userId).catch(() => {});
|
||||||
|
|
||||||
|
if (wantsJson) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ ok: true, noteId: note.id, title: note.title },
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 5. Redirect browser / Android WebAPK straight into Everclone displaying the new note
|
// 5. Redirect browser / Android WebAPK straight into Everclone displaying the new note
|
||||||
return NextResponse.redirect(getRedirectUrl(req, note.id), 303);
|
return NextResponse.redirect(getRedirectUrl(req, note.id), 303);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -471,30 +471,76 @@ export default function EvercloneWorkspace() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Android PWA Web Share Target GET Handler (/?url=... or /?text=... or /?title=...)
|
// Android PWA Web Share Target GET Handler (/?url=... or /?text=... or /?title=...)
|
||||||
const sharedUrl = sp.get('url') || '';
|
const rawSharedUrl = sp.get('url') || '';
|
||||||
const sharedText = sp.get('text') || '';
|
const rawSharedText = sp.get('text') || '';
|
||||||
const sharedTitle = sp.get('title') || '';
|
const rawSharedTitle = sp.get('title') || '';
|
||||||
if (sharedUrl || sharedText || sharedTitle) {
|
if (rawSharedUrl || rawSharedText || rawSharedTitle) {
|
||||||
window.history.replaceState({}, '', '/');
|
window.history.replaceState({}, '', '/');
|
||||||
setShareToast('Saving shared note to Everclone...');
|
// Fetch notes immediately in parallel so the notes list is never left blank
|
||||||
|
fetchNotesList();
|
||||||
|
|
||||||
|
let extractedUrl = rawSharedUrl.trim();
|
||||||
|
let cleanText = rawSharedText.trim();
|
||||||
|
let cleanTitle = rawSharedTitle.trim();
|
||||||
|
|
||||||
|
if (!extractedUrl && cleanText) {
|
||||||
|
const urlMatch = /(https?:\/\/[^\s]+)/i.exec(cleanText);
|
||||||
|
if (urlMatch) {
|
||||||
|
extractedUrl = urlMatch[1];
|
||||||
|
cleanText = cleanText.replace(extractedUrl, '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!extractedUrl && cleanTitle) {
|
||||||
|
const urlMatch = /(https?:\/\/[^\s]+)/i.exec(cleanTitle);
|
||||||
|
if (urlMatch) {
|
||||||
|
extractedUrl = urlMatch[1];
|
||||||
|
cleanTitle = cleanTitle.replace(extractedUrl, '').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (extractedUrl) {
|
||||||
|
extractedUrl = extractedUrl.replace(/[.,;:)\]]+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extractedUrl && cleanText) {
|
||||||
|
if (!cleanTitle && cleanText.length < 300 && !cleanText.includes('\n')) {
|
||||||
|
cleanTitle = cleanText;
|
||||||
|
cleanText = '';
|
||||||
|
} else if (
|
||||||
|
cleanText === cleanTitle ||
|
||||||
|
cleanTitle.includes(cleanText) ||
|
||||||
|
cleanText.includes(cleanTitle) ||
|
||||||
|
(!cleanText.includes('\n') && cleanText.length < 300)
|
||||||
|
) {
|
||||||
|
cleanText = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isWebClip = Boolean(extractedUrl && !cleanText);
|
||||||
|
setShareToast(isWebClip ? 'Clipping web article to Everclone...' : 'Saving note to Everclone...');
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/hooks/note?source=share-target', {
|
const res = await fetch('/api/hooks/note?source=share-target', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
url: sharedUrl,
|
url: extractedUrl,
|
||||||
text: sharedText,
|
text: cleanText,
|
||||||
title: sharedTitle,
|
title: cleanTitle,
|
||||||
tags: ['inbox', 'share'],
|
tags: isWebClip ? ['inbox', 'web-clip'] : ['inbox', 'share'],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
setShareToast('Saved note to Everclone from Share!');
|
setShareToast(isWebClip ? 'Web article clipped to Everclone!' : 'Saved note to Everclone!');
|
||||||
setTimeout(() => setShareToast(null), 4000);
|
setTimeout(() => setShareToast(null), 4000);
|
||||||
if (data.noteId) {
|
if (data.noteId) {
|
||||||
|
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||||
|
window.history.pushState({ evcMobilePane: 'editor', noteId: data.noteId }, '');
|
||||||
|
}
|
||||||
setSelectedNoteId(data.noteId);
|
setSelectedNoteId(data.noteId);
|
||||||
setMobilePane('editor');
|
setMobilePane('editor');
|
||||||
fetchNotesList(data.noteId);
|
fetchNotesList(data.noteId);
|
||||||
|
|||||||
Reference in new issue
Block a user