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);
|
||||
}
|
||||
|
||||
// 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) {
|
||||
const match = /(https?:\/\/[^\s]+)/i.exec(content);
|
||||
if (match) {
|
||||
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 = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
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())) {
|
||||
url = title.trim();
|
||||
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()
|
||||
? 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!
|
||||
if (url && !content && !markdown && !html && uploadedFiles.length === 0) {
|
||||
const clippedId = await clipUrlToNote(url, {
|
||||
title: title || undefined,
|
||||
notebookId,
|
||||
tags,
|
||||
isShortcut,
|
||||
});
|
||||
try {
|
||||
const clipTags = [...tags];
|
||||
if (!clipTags.includes('web-clip')) {
|
||||
clipTags.push('web-clip');
|
||||
}
|
||||
const clippedId = await clipUrlToNote(url, {
|
||||
title: title || undefined,
|
||||
notebookId,
|
||||
tags: clipTags,
|
||||
isShortcut,
|
||||
});
|
||||
|
||||
if (isShareTarget) {
|
||||
return NextResponse.redirect(getRedirectUrl(req, clippedId), 303);
|
||||
await applyAutoSortToNote(clippedId, userId).catch(() => {});
|
||||
|
||||
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;
|
||||
@@ -309,7 +359,11 @@ export async function POST(req: NextRequest) {
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,10 +56,35 @@ export async function GET(req: NextRequest) {
|
||||
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)) {
|
||||
url = 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;
|
||||
if (url && !text) {
|
||||
@@ -150,10 +175,39 @@ export async function POST(req: NextRequest) {
|
||||
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)) {
|
||||
url = 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
|
||||
if (url && !text && uploadedFiles.length === 0) {
|
||||
@@ -164,6 +218,9 @@ export async function POST(req: NextRequest) {
|
||||
tags: ['inbox', 'web-clip'],
|
||||
});
|
||||
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);
|
||||
} catch (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
|
||||
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
|
||||
return NextResponse.redirect(getRedirectUrl(req, note.id), 303);
|
||||
} catch (err) {
|
||||
|
||||
@@ -471,30 +471,76 @@ export default function EvercloneWorkspace() {
|
||||
}
|
||||
|
||||
// Android PWA Web Share Target GET Handler (/?url=... or /?text=... or /?title=...)
|
||||
const sharedUrl = sp.get('url') || '';
|
||||
const sharedText = sp.get('text') || '';
|
||||
const sharedTitle = sp.get('title') || '';
|
||||
if (sharedUrl || sharedText || sharedTitle) {
|
||||
const rawSharedUrl = sp.get('url') || '';
|
||||
const rawSharedText = sp.get('text') || '';
|
||||
const rawSharedTitle = sp.get('title') || '';
|
||||
if (rawSharedUrl || rawSharedText || rawSharedTitle) {
|
||||
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 () => {
|
||||
try {
|
||||
const res = await fetch('/api/hooks/note?source=share-target', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
url: sharedUrl,
|
||||
text: sharedText,
|
||||
title: sharedTitle,
|
||||
tags: ['inbox', 'share'],
|
||||
url: extractedUrl,
|
||||
text: cleanText,
|
||||
title: cleanTitle,
|
||||
tags: isWebClip ? ['inbox', 'web-clip'] : ['inbox', 'share'],
|
||||
}),
|
||||
});
|
||||
if (res.ok) {
|
||||
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);
|
||||
if (data.noteId) {
|
||||
if (typeof window !== 'undefined' && window.innerWidth < 768) {
|
||||
window.history.pushState({ evcMobilePane: 'editor', noteId: data.noteId }, '');
|
||||
}
|
||||
setSelectedNoteId(data.noteId);
|
||||
setMobilePane('editor');
|
||||
fetchNotesList(data.noteId);
|
||||
|
||||
Reference in new issue
Block a user