Convert pasted web content to Markdown and import remote images
This commit is contained in:
1 parent
84e89c8fe4
commit
8c13dfeaec
13 files changed
+820
-15
No files matched your search
@@ -15,7 +15,7 @@ import { noteTitle } from './paths'
|
||||
import { SIDEBAR_WIDTH, settings, updateSettings } from './settings'
|
||||
import Sidebar from './Sidebar.vue'
|
||||
import TabBar from './TabBar.vue'
|
||||
import { activeTab, flushAll, startWorkspace, stopWorkspace, toast, ws } from './workspace'
|
||||
import { activeTab, flushAll, readyToReload, startWorkspace, stopWorkspace, toast, ws } from './workspace'
|
||||
|
||||
const emit = defineEmits<{ logout: [] }>()
|
||||
const title = computed(() => (ws.active ? noteTitle(ws.active) : 'RawMark'))
|
||||
@@ -43,6 +43,10 @@ function startResize(e: PointerEvent) {
|
||||
|
||||
async function logout() {
|
||||
await flushAll()
|
||||
if (!readyToReload()) {
|
||||
toast('仍有未保存内容或正在处理粘贴、上传与文件操作,请完成后再退出')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.logout()
|
||||
} catch {
|
||||
|
||||
@@ -159,6 +159,9 @@ export const api = {
|
||||
return (await json<{ link: string }>(request('POST', '/api/v1/assets?note=' + encodeURIComponent(note), { body: form }))).link
|
||||
},
|
||||
|
||||
importImage: async (note: string, url: string): Promise<string> =>
|
||||
(await json<{ link: string }>(request('POST', '/api/v1/assets/import?note=' + encodeURIComponent(note), { json: { url } }))).link,
|
||||
|
||||
shares: async (path?: string) =>
|
||||
(await json<{ shares: Share[] }>(request('GET', '/api/v1/shares' + (path ? '?path=' + encodeURIComponent(path) : '')))).shares,
|
||||
// expiresIn 为有效时长(秒),0 为永久
|
||||
|
||||
@@ -6,7 +6,7 @@ import { api, ApiError } from './api'
|
||||
import { drafts, type Draft } from './drafts'
|
||||
import { connectEvents } from './events'
|
||||
import { pluginEvents } from '../plugins/registry'
|
||||
import { attachView, closeTab, editorStateOf, flush, keepLocalVersion, openNote, startWorkspace, stopWorkspace, ws } from './workspace'
|
||||
import { attachView, closeTab, editorStateOf, flush, keepLocalVersion, openNote, startWorkspace, stopWorkspace, storeViewState, ws } from './workspace'
|
||||
|
||||
vi.mock('./events', () => ({ connectEvents: vi.fn(() => () => {}) }))
|
||||
|
||||
@@ -136,6 +136,32 @@ describe('保存与关闭', () => {
|
||||
await closeTab('race.md')
|
||||
})
|
||||
|
||||
it('网页图片导入期间切换标签,转换内容仍保存到原笔记', async () => {
|
||||
const v = await edit('网页.md')
|
||||
vi.spyOn(drafts, 'put').mockImplementation(async (draft) => draft.path)
|
||||
const contents = new Map<string, string>()
|
||||
vi.spyOn(api, 'writeFile').mockImplementation(async (path, content) => { contents.set(path, content); return 'r2' })
|
||||
let finish!: (link: string) => void
|
||||
vi.spyOn(api, 'importImage').mockImplementation(() => new Promise<string>((resolve) => { finish = resolve }))
|
||||
const data = new DataTransfer()
|
||||
data.setData('text/plain', '网页标题')
|
||||
data.setData('text/html', '<h1>网页标题</h1><img src="https://example.com/a.png">')
|
||||
v.contentDOM.dispatchEvent(new ClipboardEvent('paste', { clipboardData: data, bubbles: true, cancelable: true }))
|
||||
await vi.waitFor(() => expect(finish).toBeTypeOf('function'))
|
||||
expect(await closeTab('网页.md')).toBe(false)
|
||||
await openNote('other.md')
|
||||
storeViewState('网页.md', v.state)
|
||||
v.setState(editorStateOf('other.md')!)
|
||||
finish('网页.assets/hash.png')
|
||||
await vi.waitFor(() => expect(editorStateOf('网页.md')!.doc.toString()).toContain('网页.assets/hash.png'))
|
||||
await flush('网页.md')
|
||||
expect(contents.get('网页.md')).toContain('# 网页标题')
|
||||
expect(contents.get('网页.md')).toContain('网页.assets/hash.png')
|
||||
expect(v.state.doc.toString()).toBe('原文')
|
||||
await closeTab('网页.md')
|
||||
await closeTab('other.md')
|
||||
})
|
||||
|
||||
it('切换标签后上传结果仍写回原笔记,上传期间不能关闭', async () => {
|
||||
const v = await edit('image.md')
|
||||
vi.spyOn(drafts, 'put').mockImplementation(async (draft) => draft.path)
|
||||
|
||||
@@ -188,21 +188,23 @@ function buildState(path: string, doc: string): EditorState {
|
||||
}),
|
||||
pasteImages({
|
||||
upload: (file) => api.upload(ref.path, file),
|
||||
replace: (text, replacement) => {
|
||||
importImage: (url) => api.importImage(ref.path, url),
|
||||
update: (change) => {
|
||||
const shown = view?.state.facet(notePath) === ref
|
||||
const state = shown ? view!.state : editorStates.get(ref.path)
|
||||
if (!state) throw new Error('上传所属笔记的编辑状态不存在')
|
||||
const at = state.doc.toString().indexOf(text)
|
||||
if (at < 0) return // 用户已删除占位
|
||||
const spec = { changes: { from: at, to: at + text.length, insert: replacement } }
|
||||
if (!state) throw new Error('粘贴所属笔记的编辑状态不存在')
|
||||
const spec = change(state)
|
||||
if (!spec) return
|
||||
if (shown) view!.dispatch(spec)
|
||||
else {
|
||||
editorStates.set(ref.path, state.update(spec).state)
|
||||
scheduleSave(ref.path)
|
||||
const transaction = state.update(spec)
|
||||
editorStates.set(ref.path, transaction.state)
|
||||
if (transaction.docChanged) scheduleSave(ref.path)
|
||||
}
|
||||
},
|
||||
onError: (err) => toast('图片上传失败:' + errorText(err)),
|
||||
onError: (err) => toast('粘贴或图片导入失败:' + (err instanceof Error ? err.message : errorText(err))),
|
||||
onPending: (d) => {
|
||||
if (d === 1 && slot(ref.path).uploads === 0) toast('正在处理粘贴或图片导入…')
|
||||
slot(ref.path).uploads += d
|
||||
},
|
||||
}),
|
||||
@@ -911,6 +913,9 @@ export async function startWorkspace() {
|
||||
if (document.visibilityState === 'hidden') void flushAll(true)
|
||||
})
|
||||
window.addEventListener('pagehide', () => void flushAll(true))
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
if (!readyToReload()) event.preventDefault()
|
||||
})
|
||||
window.addEventListener('keydown', handleShortcut)
|
||||
// 软键盘弹出、编辑区变矮后,让光标保持可见
|
||||
trackVisualViewport(() => {
|
||||
|
||||
Reference in new issue
Block a user