refactor: 重构 renderer 代码

This commit is contained in:
purocean
2021-07-05 15:34:28 +08:00
parent 1a4a07d3fc
commit 5a46264a99
86 changed files with 1165 additions and 982 deletions
+1
View File
@@ -23,6 +23,7 @@ module.exports = {
'no-console': 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'comma-dangle': 'off',
'no-unused-expressions': 'off'
},
overrides: [
{
+1
View File
@@ -91,6 +91,7 @@
"ts-node": "^10.0.0",
"turndown": "^7.0.0",
"typescript": "^4.3.2",
"utility-types": "^3.10.0",
"vite": "^2.3.7",
"vue": "^3.1.1",
"vue-router": "^4.0.0-0",
+1 -1
View File
@@ -176,7 +176,7 @@ const showSetting = () => {
}
showWindow()
win.webContents.executeJavaScript('globalBus.emit("show-setting");', true)
win.webContents.executeJavaScript('window.ctx.action.getAction("status-bar.show-setting")();', true)
}
const serve = () => {
+1 -1
View File
@@ -12,7 +12,7 @@
<script lang="ts">
import { defineComponent, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
import { Components } from '../types'
import { Components } from '@fe/support/types'
export default defineComponent({
name: 'context-menu',
+47 -144
View File
@@ -9,7 +9,6 @@
@paste-img="pasteImg"
@upload-file="uploadFile"
@change-document="val => store.commit('setDocumentInfo', val)"
@xterm-run="val => bus.emit('xterm-run', val)"
@save="saveFile"></MonacoEditor>
</div>
</template>
@@ -19,12 +18,13 @@ import { debounce } from 'lodash-es'
import dayjs from 'dayjs'
import { defineComponent, nextTick, onBeforeMount, onMounted, ref, toRefs, watch } from 'vue'
import { useStore } from 'vuex'
import { encodeMarkdownLink } from '../useful/utils'
import { useBus } from '../useful/bus'
import { useModal } from '../useful/modal'
import { useToast } from '../useful/toast'
import File from '../useful/file'
import Storage from '../useful/storage'
import { encodeMarkdownLink, fileToBase64URL } from '@fe/utils'
import { useBus } from '@fe/support/bus'
import * as api from '@fe/support/api'
import Storage from '@fe/utils/storage'
import { registerAction, removeAction } from '@fe/context/action'
import { isEncrypted, saveDoc, toUri } from '@fe/context/document'
import { Doc } from '@fe/support/types'
import MonacoEditor from './MonacoEditor.vue'
const FILE_POSITION_KEY = 'filePosition'
@@ -33,28 +33,17 @@ export default defineComponent({
name: 'editor',
components: { MonacoEditor },
setup (_, { emit }) {
let editorIsReady = false
const bus = useBus()
const store = useStore()
const modal = useModal()
const toast = useToast()
let timer: number | null = null
const refEditor = ref<any>(null)
const { currentFile, currentContent, previousContent, previousHash, passwordHash } = toRefs(store.state)
const { currentFile, currentContent } = toRefs(store.state)
const getEditor = () => refEditor.value
const revealLine = (line: number) => getEditor().revealLine(line)
const revealLineInCenter = (line: number) => getEditor().revealLineInCenter(line)
const toUri = (file: any) => File.toUri(file)
async function inputPassword (title: string, filename: string) {
const password = await modal.input({ title, type: 'password', hint: filename })
if (!password) {
throw new Error('未输入密码')
}
return password
}
function saveFileOpenPosition (top: number) {
if (currentFile.value) {
@@ -79,27 +68,6 @@ export default defineComponent({
saveFileOpenPositionDebounce(top)
}
async function createFile ({ file, content }: {file: any; content: any}) {
try {
// 加密文件内容
if (File.isEncryptedFile(file)) {
const password = await inputPassword('[创建] 请输入密码', file.name)
const encrypted = File.encrypt(content, password)
content = encrypted.content
// 储存这次解密文件密码的 hash,用于下次判断是否输入了相同密码
store.commit('setPasswordHash', { file, passwordHash: encrypted.passwordHash })
}
await File.write(file, content, 'new')
bus.emit('file-created', file)
store.commit('setCurrentFile', file)
} catch (error) {
toast.show('warning', error.message)
console.error(error)
}
}
function clearTimer () {
if (timer) {
window.clearTimeout(timer)
@@ -107,14 +75,14 @@ export default defineComponent({
}
}
async function saveFile (f: any = null) {
async function saveFile (f: Doc | null = null) {
const file = f || currentFile.value
if (!(file && file.repo && file.path)) {
if (!(file && file.repo && file.path && file.status)) {
return
}
if (previousContent.value === currentContent.value) {
if (file.content === currentContent.value) {
return
}
@@ -126,34 +94,7 @@ export default defineComponent({
return
}
try {
let content = currentContent.value
// 加密文件内容
if (File.isEncryptedFile(file)) {
const password = await inputPassword('[保存] 请输入密码', file.name)
const encrypted = File.encrypt(content, password)
const oldPasswdHash = passwordHash.value[`${file.repo}|${file.path}`]
if (oldPasswdHash !== encrypted.passwordHash) {
if (!(await modal.confirm({ title: '提示', content: '密码和上一次输入的密码不一致,是否用新密码保存?' }))) {
return
}
}
content = encrypted.content
// 储存这次解密文件密码的 hash,用于下次判断是否输入了相同密码
store.commit('setPasswordHash', { file, passwordHash: encrypted.passwordHash })
}
const { hash } = await File.write(file, content, previousHash.value)
store.commit('setPreviousHash', hash)
store.commit('setPreviousContent', currentContent.value)
store.commit('setSavedAt', new Date())
} catch (error) {
toast.show('warning', error.message)
console.error(error)
}
await saveDoc(file, currentContent.value)
}
function restartTimer () {
@@ -164,7 +105,7 @@ export default defineComponent({
}
timer = window.setTimeout(() => {
if (!currentFile.value || File.isEncryptedFile(currentFile.value)) { // 加密文件不自动保存
if (!currentFile.value || isEncrypted(currentFile.value)) { // 加密文件不自动保存
return
}
@@ -174,10 +115,10 @@ export default defineComponent({
async function pasteImg (file: any, asBase64: boolean) {
if (asBase64) {
const uri = await File.toBase64URL(file)
const uri = await fileToBase64URL(file)
getEditor().insert(`![图片](${uri})\n`)
} else {
const { relativePath } = await File.upload(currentFile.value.repo, currentFile.value.path, file)
const { relativePath } = await api.upload(currentFile.value.repo, currentFile.value.path, file)
bus.emit('file-uploaded', relativePath)
getEditor().insert(`![图片](${encodeMarkdownLink(relativePath)})\n`)
}
@@ -185,63 +126,32 @@ export default defineComponent({
async function uploadFile (file: any) {
const filename = `${dayjs().format('YYYYMMDDHHmmss')}.${file.name}`
const { relativePath } = await File.upload(currentFile.value.repo, currentFile.value.path, file, filename)
const { relativePath } = await api.upload(currentFile.value.repo, currentFile.value.path, file, filename)
bus.emit('file-uploaded', relativePath)
getEditor().insert(`附件 [${dayjs().format('YYYY-MM-DD HH:mm')}] [${file.name} (${(file.size / 1024).toFixed(2)}KiB)](${encodeMarkdownLink(relativePath)}){class=open target=_blank}\n`)
}
async function changeFile (current: any, previous?: any) {
async function changeFile (current?: Doc | null) {
clearTimer()
if (previous && previous.repo && previous.path) {
await saveFile(previous)
}
if (!current) {
store.commit('setPreviousContent', '\n')
getEditor().setModel(toUri(current), '\n')
store.commit('setCurrentFile', null)
if (!editorIsReady) {
return
}
if (current.content) { // 系统文件
store.commit('setPreviousContent', current.content)
store.commit('setSavedAt', null)
getEditor().setModel(toUri(current), current.content)
return
getEditor().setModel(toUri(current), current?.content ?? '\n')
if (current && current.status) {
// 切换文件时候保留定位
nextTick(() => {
const top = Storage.get(FILE_POSITION_KEY, {})[`${current.repo}|${current.path}`] || 1
getEditor().setScrollToTop(top)
})
}
try {
let { content, hash } = await File.read(current)
// 解密文件内容
if (File.isEncryptedFile(current)) {
const password = await inputPassword('[打开] 请输入密码', current.name)
const decrypted = File.decrypt(content, password)
content = decrypted.content
// 储存这次解密文件密码的 hash,用于下次判断是否输入了相同密码
store.commit('setPasswordHash', { file: current, passwordHash: decrypted.passwordHash })
}
store.commit('setPreviousContent', content)
store.commit('setPreviousHash', hash)
store.commit('setSavedAt', null)
getEditor().setModel(toUri(current), content)
} catch (error) {
store.commit('setCurrentFile', null)
toast.show('warning', error.message.includes('Malformed') ? '密码错误' : error.message)
console.error(error)
}
// 切换文件时候保留定位
nextTick(() => {
const top = Storage.get(FILE_POSITION_KEY, {})[`${current.repo}|${current.path}`] || 1
getEditor().setScrollToTop(top)
})
}
function editorReady () {
bus.emit('editor-ready')
bus.emit('editor.ready')
editorIsReady = true
if (currentFile.value) {
changeFile(currentFile.value)
}
@@ -253,46 +163,39 @@ export default defineComponent({
const resize = () => nextTick(() => getEditor().resize())
const insert = (text?: string) => getEditor().insert(text)
const replaceValue = (value?: { search: string; replace: string }) => {
if (value) {
getEditor().replaceValue(value.search, value.replace)
}
const replaceValue = (search: string, replace: string) => {
getEditor().replaceValue(search, replace)
}
const replaceLine = (params?: { line: number; value: string }) => {
if (params) {
getEditor().replaceLine(params.line, params.value)
}
const replaceLine = (line: number, value: string) => {
getEditor().replaceLine(line, value)
}
const getLine = (params?: {line: number; callback?: (value: string) => void}) => {
if (params) {
const { line, callback } = params
callback && callback(getEditor().getLineContent(line))
}
const getLine = (line: number) => {
return getEditor().getLineContent(line)
}
const toggleWrap = () => getEditor().toggleWrap()
onMounted(() => {
bus.on('resize', resize)
bus.on('editor-insert-value', insert)
bus.on('editor-replace-value', replaceValue)
bus.on('editor-get-line', getLine)
bus.on('editor-replace-line', replaceLine)
bus.on('editor-toggle-wrap', toggleWrap)
bus.on('file-new', createFile as any)
registerAction('editor.get-editor', getEditor)
registerAction('editor.insert-value', insert)
registerAction('editor.replace-value', replaceValue)
registerAction('editor.get-line', getLine)
registerAction('editor.replace-line', replaceLine)
registerAction('editor.toggle-wrap', toggleWrap)
restartTimer()
})
onBeforeMount(() => {
bus.off('resize', resize)
bus.off('editor-insert-value', insert)
bus.off('editor-replace-value', replaceValue)
bus.off('editor-get-line', getLine)
bus.off('editor-replace-line', replaceLine)
bus.off('editor-toggle-wrap', toggleWrap)
bus.off('file-new', createFile as any)
removeAction('editor.get-editor')
removeAction('editor.insert-value')
removeAction('editor.replace-value')
removeAction('editor.get-line')
removeAction('editor.replace-line')
removeAction('editor.toggle-wrap')
})
return {
+47 -14
View File
@@ -4,19 +4,22 @@
<script lang="ts">
import { useStore } from 'vuex'
import Tabs from './Tabs.vue'
import File from '../useful/file'
import { defineComponent, onBeforeMount, onBeforeUnmount, ref, toRefs, watch } from 'vue'
import { Components } from '../types'
import { isAction } from '../useful/shortcut'
import { isAction } from '@fe/context/shortcut'
import { Components, Doc } from '@fe/support/types'
import { useBus } from '@fe/support/bus'
import { ensureCurrentFileSaved, isEncrypted, switchDoc, toUri } from '@fe/context/document'
import { isBelongTo } from '@fe/utils/path'
import Tabs from './Tabs.vue'
const blankUri = File.toUri(null)
const blankUri = toUri(null)
export default defineComponent({
name: 'file-tabs',
components: { Tabs },
setup () {
const store = useStore()
const bus = useBus()
const { currentFile, tabs } = toRefs(store.state)
const list = ref<Components.FileTabs.Item[]>([])
@@ -27,14 +30,18 @@ export default defineComponent({
}
function switchFile (file: any) {
store.commit('setCurrentFile', file)
switchDoc(file)
}
function switchTab (item: Components.FileTabs.Item) {
switchFile(item.payload.file)
}
function removeTabs (items: Components.FileTabs.Item[]) {
async function removeTabs (items: Components.FileTabs.Item[]) {
if (items.find(x => x.key === current.value)) {
await ensureCurrentFileSaved()
}
const keys = items.map(x => x.key)
setTabs(tabs.value.filter((x: any) => keys.indexOf(x.key) === -1))
}
@@ -84,24 +91,50 @@ export default defineComponent({
}
}
function removeFile (doc?: Doc | null) {
const tab = tabs.value.find((x: Components.FileTabs.Item) => {
return x.key === toUri(doc) || (x.payload.file && doc && isBelongTo(doc.path, x.payload.file.path))
})
if (tab) {
removeTabs([tab])
}
}
function handleSwitchFailed (payload?: { doc?: Doc | null, message: string }) {
if (isEncrypted(payload?.doc) || payload?.message?.indexOf('NOENT')) {
removeFile(payload?.doc)
}
}
function handleMoved (payload?: { oldDoc: Doc }) {
if (payload) {
removeFile(payload.oldDoc)
}
}
onBeforeMount(() => {
bus.on('doc.created', switchFile)
bus.on('doc.deleted', removeFile)
bus.on('doc.switch-failed', handleSwitchFailed)
bus.on('doc.moved', handleMoved)
window.addEventListener('keydown', keydownHandler, true)
})
onBeforeUnmount(() => {
bus.off('doc.created', switchFile)
bus.off('doc.deleted', removeFile)
bus.off('doc.switch-failed', handleSwitchFailed)
bus.off('doc.moved', handleMoved)
window.removeEventListener('keydown', keydownHandler)
})
watch(currentFile, (file: any, prevFile: any) => {
if (!file && File.isEncryptedFile(prevFile)) {
setTabs(tabs.value.filter((x: any) => !File.isSameFile(x.payload.file, prevFile)))
}
const uri = File.toUri(file)
watch(currentFile, (file: any) => {
const uri = toUri(file)
const item = {
key: uri,
label: file ? file.name : '空白页',
description: file ? file.path : '空白页',
description: file ? `[${file.repo}] ${file.path}` : '空白页',
payload: { file },
}
+7 -7
View File
@@ -7,10 +7,11 @@
<script lang="ts">
import { useStore } from 'vuex'
import { computed, defineComponent, onMounted, onUnmounted, ref, toRef } from 'vue'
import { encodeMarkdownLink } from '@fe/useful/utils'
import { getCurrentAction } from '@fe/useful/shortcut'
import { useBus } from '@fe/useful/bus'
import { basename, dirname, isBelongTo, join, relative } from '@fe/useful/path'
import { encodeMarkdownLink } from '@fe/utils'
import { getCurrentAction } from '@fe/context/shortcut'
import { dirname, isBelongTo, join, relative } from '@fe/utils/path'
import { insertValue } from '@fe/context/editor'
import { switchDoc } from '@fe/context/document'
import XMask from './Mask.vue'
import QuickOpen from './QuickOpen.vue'
@@ -19,7 +20,6 @@ export default defineComponent({
components: { QuickOpen, XMask },
setup () {
const store = useStore()
const bus = useBus()
const currentFile = toRef(store.state, 'currentFile')
const callback = ref<Function | null>(null)
@@ -36,7 +36,7 @@ export default defineComponent({
? relative(cwd, f.path)
: join('/', f.path)
const fileName = f.name.replace(/\.[^.]*$/, '')
bus.emit('editor-insert-value', `[${fileName}](${encodeMarkdownLink(filePath)})`)
insertValue(`[${fileName}](${encodeMarkdownLink(filePath)})`)
}
callback.value = null
}
@@ -46,7 +46,7 @@ export default defineComponent({
break
case 'show-quick-open':
callback.value = (f: any) => {
store.commit('setCurrentFile', f)
switchDoc(f)
callback.value = null
}
withMarked.value = true
+4 -39
View File
@@ -31,10 +31,11 @@
</template>
<script lang="ts">
import { defineComponent, onBeforeUnmount, onMounted, nextTick, ref, toRefs } from 'vue'
import { defineComponent, onBeforeUnmount, onMounted, ref, toRefs } from 'vue'
import { useStore } from 'vuex'
import { useBus } from '../useful/bus'
import { $args, FLAG_DISABLE_XTERM } from '../useful/global-args'
import { useBus } from '@fe/support/bus'
import { $args, FLAG_DISABLE_XTERM } from '@fe/support/global-args'
import { emitResize } from '@fe/context/layout'
let resizeOrigin: any = null
@@ -51,34 +52,6 @@ export default defineComponent({
const terminal = ref(null)
const refs: any = { aside, editor, terminal }
function toggleSide () {
store.commit('setShowSide', !showSide.value)
nextTick(() => bus.emit('resize'))
}
function toggleView () {
store.commit('setShowView', !showView.value)
nextTick(() => bus.emit('resize'))
}
function toggleXterm (val?: boolean) {
const show = typeof val === 'boolean' ? val : !showXterm.value
store.commit('setShowXterm', show)
nextTick(() => {
nextTick(() => bus.emit('resize'))
if (showXterm.value) {
bus.emit('xterm-init')
}
})
}
function emitResize () {
bus.emit('resize')
}
function resizeFrame (e: MouseEvent) {
if (e.buttons !== 1) {
resizeOrigin = null
@@ -122,19 +95,11 @@ export default defineComponent({
}
onMounted(() => {
bus.on('toggle-view', toggleView)
bus.on('toggle-side', toggleSide)
bus.on('toggle-xterm', toggleXterm)
window.addEventListener('resize', emitResize)
window.document.addEventListener('mousemove', resizeFrame)
})
onBeforeUnmount(() => {
bus.off('toggle-side', toggleSide)
bus.off('toggle-view', toggleView)
bus.off('toggle-xterm', toggleXterm)
window.removeEventListener('resize', emitResize)
window.document.removeEventListener('mousemove', resizeFrame)
})
+1 -1
View File
@@ -18,7 +18,7 @@
<script lang="ts">
import { defineComponent, nextTick, ref } from 'vue'
import XMask from './Mask.vue'
import { Components } from '../types'
import { Components } from '@fe/support/types'
type ModalType = '' | 'confirm' | 'input'
+4 -3
View File
@@ -7,8 +7,9 @@ import dayjs from 'dayjs'
import TurndownService from 'turndown'
import * as monaco from 'monaco-editor'
import { defineComponent, onBeforeUnmount, onMounted, ref } from 'vue'
import { $args } from '../useful/global-args'
import env from '../useful/env'
import { $args } from '@fe/support/global-args'
import { getAction } from '@fe/context/action'
import env from '@fe/utils/env'
const models: {[key: string]: monaco.editor.ITextModel} = {}
@@ -91,7 +92,7 @@ export default defineComponent({
})
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_R, () => {
emit('xterm-run', getEditor().getModel()!.getValueInRange(getEditor().getSelection()!))
getAction('xterm.run')(getEditor().getModel()!.getValueInRange(getEditor().getSelection()!))
})
getEditor().addCommand(KM.CtrlCmd | KC.Enter, () => {
+8 -8
View File
@@ -43,12 +43,12 @@
import { debounce } from 'lodash-es'
import { useStore } from 'vuex'
import { computed, defineComponent, nextTick, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
import { extname } from '@fe/useful/path'
import { triggerHook } from '@fe/useful/plugin'
import { useBus } from '@fe/useful/bus'
import markdown from '@fe/useful/markdown'
import { useToast } from '@fe/useful/toast'
import { extname } from '@fe/utils/path'
import { markdown } from '@fe/context/markdown'
import { triggerHook } from '@fe/context/plugin'
import { useBus } from '@fe/support/bus'
import { useToast } from '@fe/support/toast'
import { registerAction, removeAction } from '@fe/context/action'
import Render from './Render.vue'
import 'github-markdown-css/github-markdown.css'
@@ -218,16 +218,16 @@ export default defineComponent({
onMounted(() => {
nextTick(renderDebonce)
triggerHook('ON_VIEW_MOUNTED', { getViewDom })
registerAction('view.refresh', render)
window.addEventListener('keydown', keydownHandler, true)
bus.on('resize', resizeHandler)
bus.on('view-rerender', render)
resizeHandler()
})
onBeforeUnmount(() => {
removeAction('view.refresh')
window.removeEventListener('keydown', keydownHandler)
bus.off('resize', resizeHandler)
bus.off('view-rerender', render)
})
watch(currentContent, () => autoPreview.value && renderDebonce())
+3 -3
View File
@@ -29,8 +29,8 @@
import { debounce } from 'lodash-es'
import { computed, defineComponent, nextTick, onMounted, ref, toRefs, watch } from 'vue'
import { useStore } from 'vuex'
import file from '../useful/file'
import fuzzyMatch from '../useful/fuzzy-match'
import * as api from '@fe/support/api'
import fuzzyMatch from '@fe/utils/fuzzy-match'
type TabKey = 'marked' | 'search' | 'file'
@@ -143,7 +143,7 @@ export default defineComponent({
if (repo.value && text.trim()) {
const fetchTime = new Date().getTime()
lastFetchTime.value = fetchTime
const data = await file.search(repo.value, text.trim())
const data = await api.search(repo.value, text.trim())
// 总是保证最后的搜索结果出现在列表
if (fetchTime >= lastFetchTime.value) {
call(data)
+6 -6
View File
@@ -13,9 +13,9 @@
import { useStore } from 'vuex'
import { computed, defineComponent, onMounted, ref } from 'vue'
import { JSONEditor } from '@json-editor/json-editor'
import file from '@fe/useful/file'
import { FLAG_DISABLE_XTERM } from '@fe/useful/global-args'
import { useToast } from '@fe/useful/toast'
import * as api from '@fe/support/api'
import { FLAG_DISABLE_XTERM } from '@fe/support/global-args'
import { useToast } from '@fe/support/toast'
JSONEditor.defaults.language = 'zh'
JSONEditor.defaults.languages.zh = { ...JSONEditor.defaults.languages.en }
@@ -91,7 +91,7 @@ export default defineComponent({
schema,
})
const data = await file.fetchSettings()
const data = await api.fetchSettings()
data.repos = Object.keys(data.repositories).map(name => ({
name,
path: data.repositories[name]
@@ -131,7 +131,7 @@ export default defineComponent({
const data = { repositories, shell: value.shell }
await file.writeSettings(data)
await api.writeSettings(data)
store.dispatch('fetchRepositories')
}
emit('close')
@@ -142,7 +142,7 @@ export default defineComponent({
if (target.name && target.name.endsWith('[path]')) {
const jsonPath = target.name.replace(/\]/g, '').replace(/\[/g, '.')
const field = editor.getEditor(jsonPath)
const { canceled, filePaths } = await file.choosePath({
const { canceled, filePaths } = await api.choosePath({
properties: ['openDirectory', 'createDirectory'],
})
+4 -5
View File
@@ -18,8 +18,8 @@
<script lang="ts">
import { defineComponent, onBeforeUnmount, ref } from 'vue'
import { useBus } from '../useful/bus'
import { getMenus, MenuItem } from '../useful/plugin/status-bar'
import { getMenus, MenuItem } from '@fe/context/status-bar'
import { hookAction } from '@fe/context/action'
import SvgIcon from './SvgIcon.vue'
export default defineComponent({
@@ -32,7 +32,6 @@ export default defineComponent({
}
},
setup (props) {
const bus = useBus()
const list = ref(getMenus(props.position))
const showList = ref(true)
@@ -48,9 +47,9 @@ export default defineComponent({
list.value = getMenus(props.position)
}
bus.on('status-bar-menu-update', updateMenu)
const cleanup = hookAction('before-run', 'status-bar.refresh-menu', updateMenu)
onBeforeUnmount(() => {
bus.off('status-bar-menu-update', updateMenu)
cleanup()
})
return {
+2 -2
View File
@@ -20,8 +20,8 @@
<script lang="ts">
import Sortable from 'sortablejs'
import { computed, defineComponent, nextTick, onMounted, ref, watch } from 'vue'
import { useContextMenu } from '../useful/context-menu'
import { Components } from '../types'
import { useContextMenu } from '@fe/support/context-menu'
import { Components } from '@fe/support/types'
import SvgIcon from './SvgIcon.vue'
export default defineComponent({
+23 -15
View File
@@ -31,9 +31,10 @@
<script lang="ts">
import { computed, defineComponent, onBeforeUnmount, onMounted, ref, toRefs, watch } from 'vue'
import { useStore } from 'vuex'
import env from '@fe/useful/env'
import File from '@fe/useful/file'
import env from '@fe/utils/env'
import { isEncrypted } from '@fe/context/document'
import SvgIcon from './SvgIcon.vue'
import { Doc } from '@fe/support/types'
const { isElectron, isMacOS } = env
@@ -43,8 +44,8 @@ export default defineComponent({
setup () {
const store = useStore()
const { currentFile, savedAt, currentContent, previousContent } = toRefs(store.state)
const saved = computed(() => currentContent.value === previousContent.value)
const { currentFile } = toRefs(store.state)
const { isSaved } = toRefs(store.getters)
let win: any = null
@@ -96,7 +97,7 @@ export default defineComponent({
onMounted(() => {
if (!isElectron) {
window.onbeforeunload = () => {
return !saved.value || null
return !isSaved.value || null
}
}
@@ -135,20 +136,27 @@ export default defineComponent({
const statusText = computed(() => {
let status = ''
if (savedAt.value === null && currentFile.value) {
status = '加载完毕'
} else if (savedAt.value) {
status = saved.value ? '已保存' : '未保存'
}
const file: Doc = currentFile.value
const file = currentFile.value
if (file) {
if (file.repo === '__help__') {
return file.title
}
if (!isSaved.value) {
status = '未保存'
} else if (file.status === 'saved') {
status = '已保存'
} else if (file.status === 'save-failed') {
status = '保存失败!'
} else if (file.status === 'loaded') {
status = '加载完毕'
} else {
status = '加载中…'
}
if (file.path && file.repo) {
return `${file.path}-${status} [${file.repo}]`
return `${isSaved.value ? '' : '*'}${file.path}-${status} [${file.repo}]`
} else {
return file.name
}
@@ -162,7 +170,7 @@ export default defineComponent({
return { background: '#818181' }
}
if (!saved.value && File.isEncryptedFile(currentFile.value)) {
if (currentFile.value && !isSaved.value && (isEncrypted(currentFile.value) || currentFile.value.status === 'save-failed')) {
return { background: '#ff9800ad' }
}
@@ -173,9 +181,9 @@ export default defineComponent({
document.title = currentFile.value ? (currentFile.value.name || currentFile.value.title || 'Yank Note') : '未打开文件'
}, { immediate: true })
watch(saved, val => {
watch(isSaved, (val: boolean) => {
// 暴露文档保存状态给 Electron 用
(window as any).documentSaved = val
window.documentSaved = val
}, { immediate: true })
return {
+1 -1
View File
@@ -4,7 +4,7 @@
<script lang="ts">
import { defineComponent, ref } from 'vue'
import { Components } from '../types'
import { Components } from '@fe/support/types'
interface ToastData {
type: Components.Toast.ToastType;
+5 -31
View File
@@ -14,31 +14,22 @@
</template>
<script lang="ts">
import { computed, defineComponent, onBeforeMount, onBeforeUnmount, toRefs, watch } from 'vue'
import { computed, defineComponent, onBeforeMount, toRefs, watch } from 'vue'
import { useStore } from 'vuex'
import { useBus } from '../useful/bus'
import { useContextMenu } from '../useful/context-menu'
import { useContextMenu } from '@fe/support/context-menu'
import { getAction } from '@fe/context/action'
import { refreshRepo, refreshTree } from '@fe/context/tree'
import TreeNode from './TreeNode.vue'
export default defineComponent({
name: 'tree',
components: { TreeNode },
setup () {
const bus = useBus()
const store = useStore()
const contextMenu = useContextMenu()
const { currentRepo, tree, repositories } = toRefs(store.state)
function refreshTree () {
store.dispatch('fetchTree', currentRepo.value)
}
function refreshRepo () {
refreshTree()
store.dispatch('fetchRepositories')
}
function showContextMenu () {
contextMenu.show([
{
@@ -50,30 +41,13 @@ export default defineComponent({
}
function showSetting () {
bus.emit('show-setting')
getAction('status-bar.show-setting')()
}
onBeforeMount(() => {
bus.on('file-created', refreshTree)
bus.on('file-moved', refreshTree)
bus.on('file-deleted', refreshTree)
bus.on('file-uploaded', refreshTree)
bus.on('file-marked', refreshTree)
bus.on('file-unmarked', refreshTree)
bus.on('tree-refresh', refreshTree)
refreshTree()
})
onBeforeUnmount(() => {
bus.off('file-created', refreshTree)
bus.off('file-moved', refreshTree)
bus.off('file-deleted', refreshTree)
bus.off('file-uploaded', refreshTree)
bus.off('file-marked', refreshTree)
bus.off('file-unmarked', refreshTree)
bus.off('tree-refresh', refreshTree)
})
watch(currentRepo, refreshTree)
const hasRepo = computed(() => Object.keys(repositories.value).length > 0)
+29 -155
View File
@@ -33,17 +33,14 @@
<script lang="ts">
import { computed, defineComponent, nextTick, ref, toRefs, watch } from 'vue'
import { useStore } from 'vuex'
import { useBus } from '@fe/useful/bus'
import { useToast } from '@fe/useful/toast'
import { useModal } from '@fe/useful/modal'
import { useContextMenu } from '@fe/useful/context-menu'
import File from '@fe/useful/file'
import { basename, dirname, isBelongTo } from '@fe/useful/path'
import Extensions from '@fe/useful/extensions'
import { triggerHook } from '@fe/useful/plugin'
import { getContextMenuItems } from '@fe/useful/plugin/tree'
import { FLAG_DISABLE_XTERM } from '@fe/useful/global-args'
import { Components } from '@fe/types'
import { useContextMenu } from '@fe/support/context-menu'
import Extensions from '@fe/support/extensions'
import { triggerHook } from '@fe/context/plugin'
import { getContextMenuItems, refreshTree } from '@fe/context/tree'
import { FLAG_DISABLE_XTERM } from '@fe/support/global-args'
import { Components } from '@fe/support/types'
import { getAction } from '@fe/context/action'
import { createDoc, deleteDoc, duplicateDoc, isEncrypted, markDoc, moveDoc, openInOS, switchDoc, unmarkDoc } from '@fe/context/document'
import SvgIcon from './SvgIcon.vue'
export default defineComponent({
@@ -52,9 +49,6 @@ export default defineComponent({
props: ['item'],
setup (props) {
const store = useStore()
const bus = useBus()
const toast = useToast()
const modal = useModal()
const contextMenu = useContextMenu()
const refDir = ref<any>(null)
@@ -67,181 +61,68 @@ export default defineComponent({
const { currentFile, currentRepo } = toRefs(store.state)
async function createFile (path: string | null = null, content: string | null = null) {
if (path === null) {
const currentPath = props.item.type === 'dir' ? props.item.path : dirname(props.item.path)
let filename = await modal.input({
title: '创建文件(加密文件以 .c.md 结尾)',
hint: '文件路径',
content: '当前路径:' + currentPath,
value: 'new.md',
select: true
})
if (!filename) {
return
}
if (!filename.endsWith('.md')) {
filename = filename.replace(/\/$/, '') + '.md'
}
path = currentPath.replace(/\/$/, '') + '/' + filename
}
if (!path) {
return
}
const filename = basename(path)
const file = {
type: 'file',
repo: props.item.repo,
path,
name: filename,
}
if (content === null) {
content = `# ${filename.replace(/\.md$/i, '')}\n`
}
bus.emit('file-new', { file, content })
async function createFile () {
await createDoc({ repo: props.item.repo }, props.item)
}
async function duplicateFile () {
let newPath = await modal.input({
title: '重复文件',
hint: '目标路径',
content: '当前路径:' + props.item.path,
value: props.item.path,
// 默认选中文件名
select: [props.item.path.lastIndexOf('/') + 1, props.item.name.lastIndexOf('.') > -1 ? props.item.path.lastIndexOf('.') : props.item.path.length, 'forward']
})
if (!newPath) {
return
}
newPath = newPath.replace(/\/$/, '')
const { content } = await File.read({ path: props.item.path, repo: props.item.repo })
await createFile(newPath, content)
await duplicateDoc(props.item)
}
async function toggleMark () {
if (props.item.marked) {
localMarked.value = false
await File.unmark(props.item)
bus.emit('file-unmarked', props.item)
await unmarkDoc(props.item)
} else {
localMarked.value = true
await File.mark(props.item)
bus.emit('file-marked', props.item)
await markDoc(props.item)
}
}
async function select (item: any) {
if (item.type !== 'dir') {
if (Extensions.supported(item.name)) {
store.commit('setCurrentFile', item)
switchDoc(item)
} else {
if (!(await triggerHook('ON_TREE_NODE_SELECT', item))) {
File.openInOS(props.item)
openInOS(item)
}
}
}
}
function revealInExplorer () {
File.openInOS(props.item)
openInOS(props.item)
}
function revealInXterminal () {
const path = currentRepo.value ? currentRepo.value.path + '/' + props.item.path : ''
bus.emit('xterm-run', `--yank-note-run-command-cd-- ${path}`)
getAction('xterm.run')(`--yank-note-run-command-cd-- ${path}`)
}
async function renameFile () {
if (props.item.path === '/') {
toast.show('warning', '不能移动根目录')
return
}
let newPath = await modal.input({
title: '移动文件',
hint: '新的路径',
content: '当前路径:' + props.item.path,
value: props.item.path,
// 默认选中文件名
select: [props.item.path.lastIndexOf('/') + 1, props.item.name.lastIndexOf('.') > -1 ? props.item.path.lastIndexOf('.') : props.item.path.length, 'forward']
})
if (!newPath) {
return
}
newPath = newPath.replace(/\/$/, '')
const oldPath = props.item.path.replace(/\/$/, '')
if (newPath === oldPath) {
return
}
// TODO 文件统一处理
// bus.emit('file-move', {file, newPath})
const newFile = {
name: basename(newPath),
path: newPath,
repo: props.item.repo,
type: props.item.type
}
await File.move(props.item, newPath)
// 重命名当前文件或父目录后,切换到新位置
if (currentFile.value && (File.isSameFile(props.item, currentFile.value) || (props.item.type === 'dir' && isBelongTo(props.item.path, currentFile.value.path)))) {
if (newFile.type === 'file') {
store.commit('setCurrentFile', newFile)
} else {
// TODO 切换到新位置
store.commit('setCurrentFile', null)
}
}
bus.emit('file-moved', newFile)
async function moveFile () {
await moveDoc(props.item)
}
async function deleteFile () {
if (props.item.path === '/') {
toast.show('warning', '不能删除根目录')
return
}
const confirm = await modal.confirm({ title: '删除文件', content: `确定要删除 [${props.item.path}] 吗?` })
if (confirm) {
await File.delete(props.item)
// 删除当前文件或父目录后,关闭当前文件
if (currentFile.value && (File.isSameFile(props.item, currentFile.value) || (props.item.type === 'dir' && isBelongTo(props.item.path, currentFile.value.path)))) {
store.commit('setCurrentFile', null)
}
bus.emit('file-deleted', props.item)
}
await deleteDoc(props.item)
}
function buildContextMenu (item: any) {
const menu: Components.ContextMenu.Item[] = [
{ id: 'delete', label: '删除', onClick: () => deleteFile() },
{ id: 'rename', label: '重命名 / 移动', onClick: () => renameFile() },
{ type: 'separator' },
{ id: 'openInOS', label: '在系统中打开', onClick: () => revealInExplorer() },
{ id: 'refreshTree', label: '刷新目录树', onClick: () => bus.emit('tree-refresh') },
{ id: 'refreshTree', label: '刷新目录树', onClick: refreshTree },
]
if (item.path !== '/') {
menu.unshift(...[
{ id: 'rename', label: '重命名 / 移动', onClick: () => moveFile() },
{ id: 'delete', label: '删除', onClick: () => deleteFile() },
] as Components.ContextMenu.Item[])
}
if (item.type === 'dir') {
const other = FLAG_DISABLE_XTERM ? [] : [{ id: 'openInTerminal', label: '在终端中打开', onClick: () => revealInXterminal() }]
@@ -258,7 +139,7 @@ export default defineComponent({
]
// 非加密文件增加复制重复菜单
if (!File.isEncryptedFile(props.item)) {
if (!isEncrypted(props.item)) {
additional.push({ id: 'duplicate', label: '重复文件', onClick: () => duplicateFile() })
}
@@ -326,16 +207,9 @@ export default defineComponent({
currentRepoName,
selected,
marked,
showContextMenu,
duplicateFile,
toggleMark,
select,
revealInExplorer,
revealInXterminal,
createFile,
renameFile,
deleteFile
}
},
})
+13 -11
View File
@@ -10,9 +10,11 @@ import { defineComponent, nextTick, onBeforeMount, onBeforeUnmount, ref } from '
import { useStore } from 'vuex'
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import { useBus } from '../useful/bus'
import { $args, FLAG_DEMO, FLAG_DISABLE_XTERM } from '../useful/global-args'
import { getLogger } from '../useful/utils'
import { useBus } from '@fe/support/bus'
import { $args, FLAG_DEMO, FLAG_DISABLE_XTERM } from '@fe/support/global-args'
import { toggleXterm } from '@fe/context/layout'
import { registerAction, removeAction } from '@fe/context/action'
import { getLogger } from '@fe/utils'
import 'xterm/css/xterm.css'
const logger = getLogger('component-x-term')
@@ -88,7 +90,7 @@ export default defineComponent({
socket.on('output', (arrayBuffer: any) => xterm!.write(arrayBuffer))
socket.on('disconnect', () => {
xterm!.clear()
bus.emit('toggle-xterm', false)
toggleXterm(false)
})
}
@@ -105,7 +107,7 @@ export default defineComponent({
return
}
bus.emit('toggle-xterm', true)
toggleXterm(true)
nextTick(() => {
init()
@@ -151,15 +153,15 @@ export default defineComponent({
}
onBeforeMount(() => {
(window as any).runInXterm = runInXterm
bus.on('xterm-run', handleRunInXterm)
bus.on('xterm-init', init)
registerAction('xterm.run-code', runInXterm)
registerAction('xterm.run', handleRunInXterm)
registerAction('xterm.init', init)
})
onBeforeUnmount(() => {
(window as any).runInXterm = null
bus.off('xterm-init', init)
bus.off('xterm-run', handleRunInXterm)
removeAction('xterm.run-code')
removeAction('xterm.run')
removeAction('xterm.init')
})
return {
+50
View File
@@ -0,0 +1,50 @@
import { getLogger } from '@fe/utils'
import { useBus } from '@fe/support/bus'
import type { ActionName as ViewActionName } from './view'
import type { ActionName as LayoutActionName } from './layout'
import type { ActionName as StatusBarActionName } from './status-bar'
import type { ActionName as EditorActionName } from './editor'
import type { ActionName as TreeActionName } from './tree'
import type { ActionName as DocActionName } from './document'
const logger = getLogger('action')
const bus = useBus()
export type ActionFun = (...args: any[]) => any
export type ActionName = ViewActionName | LayoutActionName | StatusBarActionName | EditorActionName | TreeActionName | DocActionName
export type HookType = 'before-run' | 'after-run'
export const actions: { [key: string]: ActionFun } = {}
export function hookAction (type: HookType, name: ActionName, handler: (payload: any) => void): () => void
export function hookAction (type: HookType, name: string, handler: (payload: any) => void): () => void
export function hookAction (type: HookType, name: string, handler: (payload: any) => void) {
bus.on(`action.${type}.${name}`, handler)
return () => bus.off(`action.${type}.${name}`, handler)
}
export function registerAction (name: ActionName, handler: ActionFun): void
export function registerAction (name: string, handler: ActionFun): void
export function registerAction (name: string, handler: ActionFun) {
logger.debug('registerAction', name)
actions[name] = handler
}
export function getAction (name: ActionName): ActionFun
export function getAction (name: string): ActionFun
export function getAction (name: string) {
logger.debug('getAction', name)
return (...args: any[]) => {
bus.emit(`action.before-run.${name}`, args)
const result = actions[name]?.(...args)
bus.emit(`action.after-run.${name}`, result)
return result
}
}
export function removeAction (name: ActionName): void
export function removeAction (name: string): void
export function removeAction (name: string) {
logger.debug('removeAction', name)
delete actions[name]
}
+339
View File
@@ -0,0 +1,339 @@
import { Optional } from 'utility-types'
import Crypto from '@fe/utils/crypto'
import { useModal } from '@fe/support/modal'
import { useToast } from '@fe/support/toast'
import store from '@fe/support/store'
import type { Doc } from '@fe/support/types'
import { basename, dirname, join } from '@fe/utils/path'
import { useBus } from '@fe/support/bus'
import * as api from '@fe/support/api'
import { getLogger } from '@fe/utils'
import { registerAction } from './action'
export type ActionName = 'doc.create'
| 'doc.create'
| 'doc.delete'
| 'doc.move'
| 'doc.mark'
| 'doc.unmark'
| 'doc.open-in-os'
| 'doc.save'
| 'doc.switch'
| 'doc.show-help'
const logger = getLogger('document')
const bus = useBus()
function decrypt (content: any, password: string) {
if (!password) {
throw new Error('未输入解密密码')
}
return Crypto.decrypt(content, password)
}
function encrypt (content: any, password: string) {
if (!password) {
throw new Error('未输入解密密码')
}
return Crypto.encrypt(content, password)
}
async function inputPassword (title: string, filename: string) {
const password = await useModal().input({ title, type: 'password', hint: filename })
if (!password) {
throw new Error('未输入密码')
}
return password
}
export function isEncrypted (doc?: Pick<Doc, 'path'> | null) {
return doc && doc.path.toLowerCase().endsWith('.c.md')
}
export function isSameFile (a?: Doc | null, b?: Doc | null) {
return a && b && a.repo === b.repo && a.path === b.path
}
export function toUri (doc?: Doc | null) {
if (doc && doc.repo && doc.path) {
return encodeURI(`yank-note://${doc.repo}/${doc.path.replace(/^\//, '')}`)
} else {
return 'yank-note://system/blank.md'
}
}
export async function createDoc (doc: Pick<Doc, 'repo' | 'path' | 'content'>, baseDoc: Doc): Promise<Doc>
export async function createDoc (doc: Optional<Pick<Doc, 'repo' | 'path' | 'content'>, 'path'>, baseDoc?: Doc): Promise<Doc>
export async function createDoc (doc: Optional<Pick<Doc, 'repo' | 'path' | 'content'>, 'path'>, baseDoc?: Doc) {
if (!doc.path) {
if (baseDoc) {
const currentPath = baseDoc.type === 'dir' ? baseDoc.path : dirname(baseDoc.path)
let filename = await useModal().input({
title: '创建文件(加密文件以 .c.md 结尾)',
hint: '文件路径',
content: '当前路径:' + currentPath,
value: 'new.md',
select: true
})
if (!filename) {
return
}
if (!filename.endsWith('.md')) {
filename = filename.replace(/\/$/, '') + '.md'
}
doc.path = join(currentPath, filename).replace(/^\//, '')
}
}
if (!doc.path) {
throw new Error('需要传入文件路径')
}
const filename = basename(doc.path)
const file: Doc = { ...doc, path: doc.path, type: 'file', name: filename, contentHash: 'new' }
if (typeof file.content !== 'string') {
file.content = `# ${filename.replace(/\.md$/i, '')}\n`
}
try {
// 加密文件内容
if (isEncrypted(file)) {
const password = await inputPassword('[创建] 请输入密码', file.name)
const encrypted = encrypt(file.content, password)
file.content = encrypted.content
}
await api.writeFile(file, file.content)
bus.emit('doc.created', file)
} catch (error) {
useToast().show('warning', error.message)
console.error(error)
}
return file
}
export async function duplicateDoc (origin: Doc) {
let newPath = await useModal().input({
title: '重复文件',
hint: '目标路径',
content: '当前路径:' + origin.path,
value: origin.path,
// 默认选中文件名
select: [
origin.path.lastIndexOf('/') + 1,
origin.name.lastIndexOf('.') > -1 ? origin.path.lastIndexOf('.') : origin.path.length,
'forward'
]
})
if (!newPath) {
return
}
newPath = newPath.replace(/\/$/, '')
const { content } = await api.readFile(origin)
await createDoc({ repo: origin.repo, path: newPath, content })
}
export async function deleteDoc (doc: Doc) {
if (doc.path === '/') {
throw new Error('不能删除根目录')
}
const confirm = await useModal().confirm({ title: '删除文件', content: `确定要删除 [${doc.path}] 吗?` })
if (confirm) {
await api.deleteFile(doc)
bus.emit('doc.deleted', doc)
}
}
export async function moveDoc (doc: Doc, newPath?: string) {
if (doc.path === '/') {
throw new Error('不能移动根目录')
}
newPath = newPath ?? await useModal().input({
title: '移动文件',
hint: '新的路径',
content: '当前路径:' + doc.path,
value: doc.path,
// 默认选中文件名
select: [
doc.path.lastIndexOf('/') + 1,
doc.name.lastIndexOf('.') > -1 ? doc.path.lastIndexOf('.') : doc.path.length,
'forward'
]
})
if (!newPath) {
return
}
newPath = newPath.replace(/\/$/, '')
const oldPath = doc.path.replace(/\/$/, '')
if (newPath === oldPath) {
return
}
const newDoc: Doc = {
name: basename(newPath),
path: newPath,
repo: doc.repo,
type: doc.type
}
await api.moveFile(doc, newPath)
bus.emit('doc.moved', { oldDoc: doc, newDoc })
}
export async function saveDoc (doc: Doc, content: string) {
logger.debug('saveDoc', doc)
try {
let sendContent = content
let passwordHash = ''
// 加密文件内容
if (isEncrypted(doc)) {
const password = await inputPassword('[保存] 请输入密码', doc.name)
const encrypted = encrypt(sendContent, password)
if (doc.passwordHash !== encrypted.passwordHash) {
if (!(await useModal().confirm({ title: '提示', content: '密码和上一次输入的密码不一致,是否用新密码保存?' }))) {
return
}
}
sendContent = encrypted.content
passwordHash = encrypted.passwordHash
}
const { hash } = await api.writeFile(doc, sendContent)
Object.assign(doc, {
content,
passwordHash,
contentHash: hash,
status: 'saved'
})
bus.emit('doc.saved', store.state.currentFile)
} catch (error) {
store.commit('setCurrentFile', { ...doc, status: 'save-failed' })
useToast().show('warning', error.message)
throw error
}
}
export async function ensureCurrentFileSaved () {
const { currentFile, currentContent } = store.state
if (!currentFile || !currentFile.status) {
return
}
try {
if (isEncrypted(currentFile)) {
if (!store.getters.isSaved && !(await useModal().confirm({ title: '未保存文件', content: '确定要离开吗?' }))) {
throw new Error('请先保存文件')
} else {
store.commit('setCurrentFile', null)
}
} else {
if (currentFile && currentContent && currentFile.content !== currentContent && currentFile.repo !== '__help__') {
await saveDoc(currentFile, currentContent)
}
}
} catch (error) {
useToast().show('warning', error.message)
throw error
}
}
export async function switchDoc (doc: Doc | null) {
logger.debug('switchDoc', doc)
await ensureCurrentFileSaved()
try {
if (!doc) {
store.commit('setCurrentFile', null)
bus.emit('doc.switched', null)
return
}
store.commit('setCurrentFile', { ...doc, status: undefined })
let passwordHash = ''
let { content, hash } = await api.readFile(doc)
// 解密文件内容
if (isEncrypted(doc)) {
const password = await inputPassword('[打开] 请输入密码', doc.name)
const decrypted = decrypt(content, password)
content = decrypted.content
passwordHash = decrypted.passwordHash
}
store.commit('setCurrentFile', {
...doc,
content,
passwordHash,
contentHash: hash,
status: 'loaded'
})
bus.emit('doc.switched', store.state.currentFile)
} catch (error) {
bus.emit('doc.switch-failed', { doc, message: error.message })
useToast().show('warning', error.message.includes('Malformed') ? '密码错误' : error.message)
throw error
}
}
export async function markDoc (doc: Doc) {
await api.markFile(doc)
bus.emit('doc.changed', doc)
}
export async function unmarkDoc (doc: Doc) {
await api.unmarkFile(doc)
bus.emit('doc.changed', doc)
}
export async function openInOS (doc: Doc) {
await api.openInOS(doc)
}
export async function showHelp (doc: string) {
switchDoc({
type: 'file',
repo: '__help__',
title: doc,
name: doc,
path: doc,
})
}
registerAction('doc.create', createDoc)
registerAction('doc.create', duplicateDoc)
registerAction('doc.delete', deleteDoc)
registerAction('doc.move', moveDoc)
registerAction('doc.mark', markDoc)
registerAction('doc.unmark', unmarkDoc)
registerAction('doc.open-in-os', openInOS)
registerAction('doc.save', saveDoc)
registerAction('doc.switch', switchDoc)
registerAction('doc.show-help', showHelp)
+15
View File
@@ -0,0 +1,15 @@
import { getAction } from './action'
export type ActionName = 'editor.get-editor'
| 'editor.insert-value'
| 'editor.replace-value'
| 'editor.get-line'
| 'editor.replace-line'
| 'editor.toggle-wrap'
export const getEditor = () => getAction('editor.get-editor')()
export const insertValue = (str: string) => getAction('editor.insert-value')(str)
export const replaceValue = (search: string, replace: string) => getAction('editor.replace-value')(search, replace)
export const getLine = (line: number) => getAction('editor.get-line')(line) as string
export const replaceLine = (line: number, str: string) => getAction('editor.replace-line')(line, str)
export const toggleWrap = () => getAction('editor.toggle-wrap')()
+32
View File
@@ -0,0 +1,32 @@
import { useBus } from '@fe/support/bus'
import { useToast } from '@fe/support/toast'
import store from '@fe/support/store'
import * as action from './action'
import * as shortcut from './shortcut'
import * as view from './view'
import * as tree from './tree'
import * as markdown from './markdown'
import * as statusBar from './status-bar'
import * as layout from './layout'
import * as editor from './editor'
export type CtxHookType = view.HookType | tree.HookType
const bus = useBus()
const ctx = {
bus,
store,
action,
shortcut,
ui: { useToast },
tree,
markdown,
statusBar,
layout,
editor
}
window.ctx = ctx
export default ctx
+43
View File
@@ -0,0 +1,43 @@
import { nextTick } from 'vue'
import store from '@fe/support/store'
import { useBus } from '@fe/support/bus'
import { getAction, registerAction } from './action'
export type ActionName = 'layout.toggle-view'
| 'layout.toggle-side'
| 'layout.toggle-xterm'
const bus = useBus()
export function emitResize () {
nextTick(() => bus.emit('resize'))
}
export function toggleSide () {
store.commit('setShowSide', !store.state.showSide)
emitResize()
}
export function toggleView () {
store.commit('setShowView', !store.state.showView)
emitResize()
}
export function toggleXterm (val?: boolean) {
const showXterm = store.state.showXterm
const show = typeof val === 'boolean' ? val : !showXterm
store.commit('setShowXterm', show)
nextTick(() => {
emitResize()
if (showXterm) {
getAction('xterm.init')()
}
})
}
registerAction('layout.toggle-view', toggleView)
registerAction('layout.toggle-side', toggleSide)
registerAction('layout.toggle-xterm', toggleXterm)
@@ -1,15 +1,12 @@
import Markdown from 'markdown-it'
import MarkdownItAttrs from 'markdown-it-attrs'
import MultimdTable from 'markdown-it-multimd-table'
import { getPlugins as getMarkdownItPlugins } from '@fe/useful/plugin/markdown'
const markdown = Markdown({ linkify: true, breaks: true, html: true })
export const markdown = Markdown({ linkify: true, breaks: true, html: true })
getMarkdownItPlugins().forEach(({ plugin, params }) => {
export function registerPlugin (plugin: (md: Markdown, ...args: any) => void, params?: any) {
markdown.use(plugin, params)
})
}
markdown.use(MarkdownItAttrs)
markdown.use(MultimdTable, { multiline: true })
export default markdown
+66
View File
@@ -0,0 +1,66 @@
import { getLogger } from '@fe/utils'
import buildInPlugins from './build-in-plugins'
import context, { CtxHookType } from './index'
const logger = getLogger('plugin')
const ctx = { ...context, registerHook, removeHook, triggerHook }
export type Ctx = typeof ctx;
export type HookType = 'ON_STARTUP' | CtxHookType
export type HookFun = (...args: any[]) => boolean | void | Promise<boolean | void>
export interface Plugin {
name: string;
register?: (ctx: Ctx) => void;
}
const hooks: { [key in HookType]?: {fun: HookFun; once: boolean}[] } = {}
const plugins: {[name: string]: Plugin} = {}
export function registerHook (type: HookType, fun: HookFun, once = false) {
if (Array.isArray(hooks[type])) {
hooks[type] = [...hooks[type]!, { fun, once }]
} else {
hooks[type] = [{ fun, once }]
}
}
export function removeHook (type: HookType, fun: HookFun) {
if (Array.isArray(hooks[type])) {
hooks[type] = hooks[type]!.filter(x => x.fun !== fun)
}
}
export async function triggerHook (type: HookType, ...args: any[]) {
logger.debug('triggerHook', type, args)
for (const { fun, once } of (hooks[type] || [])) {
once && ctx.removeHook(type, fun)
if (await fun(...args)) {
return true
}
}
return false
}
export function register (plugin: Plugin) {
logger.debug('register', plugin)
plugins[plugin.name] = plugin
plugin.register && plugin.register(ctx)
}
function init () {
logger.debug('init')
buildInPlugins.forEach((plugin) => {
register(plugin)
})
window.registerPlugin = register
const script = window.document.createElement('script')
script.src = '/api/plugins'
window.document.body.appendChild(script)
}
init()
@@ -1,5 +1,5 @@
import { getLogger } from './utils'
import env from './env'
import { getLogger } from '@fe/utils'
import env from '@fe/utils/env'
const logger = getLogger('shortcut')
@@ -1,21 +1,23 @@
import { useStore } from 'vuex'
import { useBus } from './bus'
import env from './env'
import { useBus } from '@fe/support/bus'
import env from '@fe/utils/env'
import { triggerHook } from '@fe/context/plugin'
import store, { getLastOpenFile } from '@fe/support/store'
import { Doc } from '@fe/support/types'
import { hasCtrlCmd } from './shortcut'
import { triggerHook } from './plugin'
import { showHelp, switchDoc, unmarkDoc } from './document'
import { refreshTree } from './tree'
const bus = useBus()
export default function startup () {
const bus = useBus()
const store = useStore()
triggerHook('ON_STARTUP')
bus.on('editor-ready', () => {
bus.on('editor.ready', () => {
const { currentFile } = store.state
if (!currentFile) {
// 当前没打开文件,直接打开 README
store.dispatch('showHelp', 'README.md')
showHelp('README.md')
}
})
@@ -53,3 +55,18 @@ export default function startup () {
})
}
}
const doc = getLastOpenFile()
switchDoc(doc)
bus.on('doc.created', refreshTree)
bus.on('doc.deleted', refreshTree)
bus.on('doc.moved', refreshTree)
bus.on('doc.changed', refreshTree)
bus.on('doc.switch-failed', refreshTree)
bus.on('doc.switch-failed', (payload?: { doc?: Doc | null, message: string }) => {
if (payload && payload.doc && payload?.message?.indexOf('NOENT')) {
unmarkDoc(payload.doc)
}
})
@@ -1,6 +1,6 @@
import { useBus } from '@fe/useful/bus'
import { getAction } from './action'
const bus = useBus()
export type ActionName = 'status-bar.show-setting' | 'status-bar.refresh-menu'
export type MenuItem = {
id: string;
@@ -26,9 +26,13 @@ const menus: { [key: string]: Menu } = {}
export const getMenus = (position: string) =>
Object.values(menus).filter(x => x.position === position)
export function refreshMenu () {
getAction('status-bar.refresh-menu')()
}
export function updateMenu (menu: Menu) {
menus[menu.id] = menu
bus.emit('status-bar-menu-update', menu)
refreshMenu()
}
export function tapMenu (id: string, fn: (menu: Menu) => Menu) {
@@ -37,12 +41,5 @@ export function tapMenu (id: string, fn: (menu: Menu) => Menu) {
export function removeMenu (id: string) {
delete menus[id]
bus.emit('status-bar-menu-update')
}
export const ctx = {
getMenus,
tapMenu,
updateMenu,
removeMenu,
refreshMenu()
}
+29
View File
@@ -0,0 +1,29 @@
import { Components } from '@fe/support/types'
import store from '@fe/support/store'
import { registerAction } from './action'
export type ActionName = 'tree.refresh'
export type HookType = 'ON_TREE_NODE_SELECT'
export type MenuItem = Components.ContextMenu.Item
export type BuildContextMenu = (items: MenuItem[], node: any) => MenuItem[]
const contextMenuFunList: BuildContextMenu[] = []
export const registerContextMenu = (fun: BuildContextMenu) => {
contextMenuFunList.push(fun)
}
export const getContextMenuItems = (node: any) => {
return contextMenuFunList.reduce((items, fun) => fun(items, node), [] as MenuItem[])
}
export function refreshTree () {
store.dispatch('fetchTree')
}
export function refreshRepo () {
refreshTree()
store.dispatch('fetchRepositories')
}
registerAction('tree.refresh', refreshTree)
@@ -6,3 +6,5 @@ export type HookType = 'ON_VIEW_ELEMENT_CLICK'
| 'ON_VIEW_MOUNTED'
| 'ON_VIEW_FILE_CHANGE'
| 'ON_VIEW_BEFORE_CONVERT'
export type ActionName = 'view.refresh'
+7 -6
View File
@@ -1,13 +1,14 @@
import './support/demo'
import { createApp } from 'vue'
import './demo'
import App from './App.vue'
import router from './router'
import store from './store'
import bus from './useful/bus'
import toast from './useful/toast'
import modal from './useful/modal'
import contextmenu from './useful/context-menu'
import store from './support/store'
import bus from './support/bus'
import toast from './support/toast'
import modal from './support/modal'
import contextmenu from './support/context-menu'
const app = createApp(App)
+11 -14
View File
@@ -1,21 +1,16 @@
import { Plugin, Ctx } from '@fe/useful/plugin'
import { hasCtrlCmd } from '@fe/useful/shortcut'
import { encodeMarkdownLink } from '@fe/useful/utils'
import { useBus } from '@fe/useful/bus'
import { copyText } from '@fe/useful/copy-text'
import store from '@fe/store'
import { Plugin, Ctx } from '@fe/context/plugin'
import { hasCtrlCmd } from '@fe/context/shortcut'
import { encodeMarkdownLink } from '@fe/utils'
import { copyText } from '@fe/support/copy-text'
import store from '@fe/support/store'
export default {
name: 'copy-text',
register: (ctx: Ctx) => {
ctx.registerHook('ON_STARTUP', () => {
const bus = useBus()
bus.on('copy-text', copyText)
})
ctx.action.registerAction('app.copy-text', copyText)
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => {
const target = e.target as HTMLElement
const bus = useBus()
const preventEvent = () => {
e.preventDefault()
@@ -26,13 +21,15 @@ export default {
// 复制标题链接
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && hasCtrlCmd(e)) {
const { currentFile } = store.state
bus.emit('copy-text', encodeMarkdownLink(currentFile.path) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
return preventEvent()
if (currentFile) {
copyText(encodeMarkdownLink(currentFile.path) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
return preventEvent()
}
}
// 复制内容
if (target.classList.contains('copy-inner-text') && hasCtrlCmd(e)) {
bus.emit('copy-text', target.innerText)
copyText(target.innerText)
return preventEvent()
}
+3 -3
View File
@@ -1,10 +1,10 @@
import { defineComponent, h, onMounted, ref, watch } from 'vue'
import CryptoJS from 'crypto-js'
import { debounce } from 'lodash-es'
import dayjs from 'dayjs'
import Markdown from 'markdown-it'
import { useBus } from '@fe/useful/bus'
import { Plugin } from '@fe/useful/plugin'
import { defineComponent, h, onMounted, ref, watch } from 'vue'
import { useBus } from '@fe/support/bus'
import { Plugin } from '@fe/context/plugin'
const Applet = defineComponent({
name: 'applet',
+3 -3
View File
@@ -1,7 +1,7 @@
import Highlight from 'highlight.js'
import { getKeyLabel } from '@fe/useful/shortcut'
import { Plugin, Ctx } from '@fe/useful/plugin'
import { getLogger } from '../useful/utils'
import { getKeyLabel } from '@fe/context/shortcut'
import { Plugin, Ctx } from '@fe/context/plugin'
import { getLogger } from '@fe/utils'
const logger = getLogger('markdown-code')
+5 -5
View File
@@ -1,9 +1,9 @@
import Markdown from 'markdown-it'
import { Plugin } from '@fe/useful/plugin'
import file from '@fe/useful/file'
import { useBus } from '@fe/useful/bus'
import { openInNewWindow } from '@fe/useful/utils'
import { defineComponent, h, ref, watch } from 'vue'
import { Plugin } from '@fe/context/plugin'
import * as api from '@fe/support/api'
import { useBus } from '@fe/support/bus'
import { openInNewWindow } from '@fe/utils'
const Drawio = defineComponent({
name: 'drawio',
@@ -102,7 +102,7 @@ async function buildSrcdoc ({ repo, path, content, url }: F) {
if (url) {
content = await (await fetch(url)).text()
} else if (!content && repo && path) {
content = (await file.read({ repo, path })).content
content = (await api.readFile({ repo, path })).content
}
content = content.replace(/<!--.*?-->/gs, '').trim()
+2 -2
View File
@@ -1,8 +1,8 @@
import { h } from 'vue'
import * as echarts from 'echarts'
import CryptoJS from 'crypto-js'
import Markdown from 'markdown-it'
import { Plugin } from '@fe/useful/plugin'
import { h } from 'vue'
import { Plugin } from '@fe/context/plugin'
const render = (code: string) => {
const id = `echart-${CryptoJS.MD5(code).toString()}-${Math.random().toString(36).substr(2)}`
+1 -1
View File
@@ -6,7 +6,7 @@
// https://github.com/markdown-it/markdown-it-footnote/blob/master/index.js
import { createVNode, Fragment } from 'vue'
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
// Process footnotes
//
+1 -1
View File
@@ -1,4 +1,4 @@
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
import StateBlock from 'markdown-it/lib/rules_block/state_block'
import StateInline from 'markdown-it/lib/rules_inline/state_inline'
import Token from 'markdown-it/lib/token'
+1 -1
View File
@@ -13,8 +13,8 @@ for rendering output.
*/
import katex from 'katex'
import { Plugin } from '@fe/useful/plugin'
import { h } from 'vue'
import { Plugin } from '@fe/context/plugin'
// Test if potential opening or closing delimieter
// Assumes that there is a "$" at state.src[pos]
+10 -5
View File
@@ -1,12 +1,17 @@
import StateCore from 'markdown-it/lib/rules_core/state_core'
import Token from 'markdown-it/lib/token'
import { Plugin, Ctx } from '@fe/useful/plugin'
import store from '@fe/store'
import env from '@fe/useful/env'
import { basename, dirname, join } from '@fe/useful/path'
import { Plugin, Ctx } from '@fe/context/plugin'
import store from '@fe/support/store'
import env from '@fe/utils/env'
import { basename, dirname, join } from '@fe/utils/path'
import { switchDoc } from '@fe/context/document'
const handleLink = (link: HTMLAnchorElement, ctx: Ctx) => {
const { currentFile } = store.state
if (!currentFile) {
return
}
const { repo: fileRepo, path: filePath } = currentFile
// 系统中打开附件
@@ -33,7 +38,7 @@ const handleLink = (link: HTMLAnchorElement, ctx: Ctx) => {
}
// 打开文件
store.commit('setCurrentFile', {
switchDoc({
path,
name: basename(path),
repo: fileRepo,
+1 -1
View File
@@ -1,6 +1,6 @@
import Markdown from 'markdown-it'
import mermaid from 'mermaid/dist/mermaid.js'
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
import { h } from 'vue'
const mermaidChart: any = (code: string) => {
+3 -3
View File
@@ -1,9 +1,9 @@
import { defineComponent, h, onMounted, ref, watch } from 'vue'
import { debounce } from 'lodash-es'
import Renderer from 'markdown-it/lib/renderer'
import { Plugin } from '@fe/useful/plugin'
import crypto from '@fe/useful/crypto'
import { dataURItoBlobLink, openInNewWindow } from '@fe/useful/utils'
import { Plugin } from '@fe/context/plugin'
import crypto from '@fe/utils/crypto'
import { dataURItoBlobLink, openInNewWindow } from '@fe/utils'
const layoutStorageKey = 'mind-map-layout'
+1 -1
View File
@@ -3,7 +3,7 @@ import { debounce } from 'lodash-es'
import { defineComponent, h, nextTick, onMounted, reactive, ref, watch } from 'vue'
import Markdown from 'markdown-it'
import Token from 'markdown-it/lib/token'
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
const emptySrc = 'data:image/gif;base64,R0lGODlhAQABAAD/ACwAAAAAAQABAAACADs='
@@ -2,7 +2,7 @@ import { createVNode, Fragment, Text, VNode } from 'vue'
import Token from 'markdown-it/lib/token'
import Renderer from 'markdown-it/lib/renderer'
import { escapeHtml, unescapeAll } from 'markdown-it/lib/common/utils'
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
const attrNameReg = /^[a-zA-Z_:][a-zA-Z0-9:._-]*/
const defaultRules = {} as any
+3 -2
View File
@@ -1,7 +1,8 @@
import { computed, defineComponent, h, ref, VNode } from 'vue'
import CryptoJS from 'crypto-js'
import Markdown from 'markdown-it'
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
import { getAction } from '@fe/context/action'
const cachePrefix = 'run_code_result_'
@@ -48,7 +49,7 @@ const RunCode = defineComponent({
}
const runInXterm = (e: MouseEvent) => {
(window as any).runInXterm(props.language, props.code, e.ctrlKey)
getAction('xterm.run-code')(props.language, props.code, e.ctrlKey)
}
return () => {
+1 -1
View File
@@ -1,4 +1,4 @@
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
import Renderer from 'markdown-it/lib/renderer'
const buildFunction = (className: string): Renderer.RenderRule => {
+1 -1
View File
@@ -8,7 +8,7 @@
// https://github.com/blog/1825-task-lists-in-all-markdown-documents
import Markdown from 'markdown-it'
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
let disableCheckboxes = true
+2 -2
View File
@@ -2,8 +2,8 @@
import Markdown from 'markdown-it'
import StateInline from 'markdown-it/lib/rules_inline/state_inline'
import { getKeyLabel } from '@fe/useful/shortcut'
import { Plugin } from '@fe/useful/plugin'
import { getKeyLabel } from '@fe/context/shortcut'
import { Plugin } from '@fe/context/plugin'
import { injectLineNumbers } from './markdown-source-line'
+2 -2
View File
@@ -1,5 +1,5 @@
import { FLAG_DEMO, URL_GITHUB, URL_MAS } from '@fe/useful/global-args'
import { Plugin } from '@fe/useful/plugin'
import { FLAG_DEMO, URL_GITHUB, URL_MAS } from '@fe/support/global-args'
import { Plugin } from '@fe/context/plugin'
export default {
name: 'status-bar-github',
+5 -5
View File
@@ -1,6 +1,6 @@
import { Plugin } from '@fe/useful/plugin'
import { Alt, getActionLabel, isAction } from '@fe/useful/shortcut'
import store from '@fe/store'
import { showHelp } from '@fe/context/document'
import { Plugin } from '@fe/context/plugin'
import { Alt, getActionLabel, isAction } from '@fe/context/shortcut'
export default {
name: 'status-bar-view',
@@ -8,12 +8,12 @@ export default {
const actions = {
'toggle-readme': {
title: 'README',
action: () => store.dispatch('showHelp', 'README.md'),
action: () => showHelp('README.md'),
shortcut: [Alt, 'h'],
},
'toggle-features': {
title: '特色功能说明',
action: () => store.dispatch('showHelp', 'FEATURES.md'),
action: () => showHelp('FEATURES.md'),
shortcut: null,
},
}
+4 -6
View File
@@ -1,12 +1,10 @@
import { Plugin } from '@fe/useful/plugin'
import store from '@fe/store'
import { useBus } from '@fe/useful/bus'
import { Plugin } from '@fe/context/plugin'
import store from '@fe/support/store'
import { getAction } from '@fe/context/action'
export default {
name: 'status-bar-refresh',
register: ctx => {
const bus = useBus()
const updateMenu = (autoPreview: boolean) => {
ctx.statusBar.updateMenu({
id: 'status-bar-refresh-status',
@@ -21,7 +19,7 @@ export default {
tips: '强制渲染',
icon: 'sync-alt-solid',
hidden: autoPreview,
onClick: () => bus.emit('view-rerender')
onClick: () => getAction('view.refresh')()
})
}
@@ -1,9 +1,10 @@
import { Plugin } from '@fe/useful/plugin'
import { Menu as StatusBarMenu } from '@fe/useful/plugin/status-bar'
import store from '@fe/store'
import { useBus } from '@fe/useful/bus'
import { $args } from '@fe/useful/global-args'
import { basename } from '@fe/useful/path'
import { Plugin } from '@fe/context/plugin'
import { Menu as StatusBarMenu } from '@fe/context/status-bar'
import store from '@fe/support/store'
import { useBus } from '@fe/support/bus'
import { $args } from '@fe/support/global-args'
import { basename } from '@fe/utils/path'
import { switchDoc } from '@fe/context/document'
export default {
name: 'status-bar-repository-switch',
@@ -35,7 +36,7 @@ export default {
}
if (initFilePath) {
store.commit('setCurrentFile', { repo: currentRepo.name, name: basename(initFilePath), path: initFilePath })
switchDoc({ type: 'file', repo: currentRepo.name, name: basename(initFilePath), path: initFilePath })
}
}
@@ -66,8 +67,7 @@ export default {
ctx.statusBar.updateMenu(menu)
bus.on('switch-repo-by-name', chooseRepoByName)
bus.on('editor-ready', initRepo)
bus.on('editor.ready', initRepo)
store.dispatch('fetchRepositories')
store.watch(() => store.state.repositories, updateMenu)
+5 -3
View File
@@ -1,11 +1,13 @@
import store from '@fe/store'
import { Plugin } from '@fe/useful/plugin'
import store from '@fe/support/store'
import { Plugin } from '@fe/context/plugin'
export default {
name: 'status-bar-setting',
register: ctx => {
const showSetting = () => store.commit('setShowSetting', true)
ctx.bus.on('show-setting', showSetting)
ctx.action.registerAction('status-bar.show-setting', showSetting)
ctx.statusBar.updateMenu({
id: 'status-bar-setting',
position: 'left',
+1 -1
View File
@@ -1,4 +1,4 @@
import { Plugin } from '@fe/useful/plugin'
import { Plugin } from '@fe/context/plugin'
export default {
name: 'status-bar-tool',
+9 -10
View File
@@ -1,32 +1,31 @@
import { Plugin } from '@fe/useful/plugin'
import { useBus } from '@fe/useful/bus'
import { Alt, getActionLabel, isAction } from '@fe/useful/shortcut'
import { FLAG_DISABLE_XTERM } from '@fe/useful/global-args'
import { Plugin } from '@fe/context/plugin'
import { Alt, getActionLabel, isAction } from '@fe/context/shortcut'
import { FLAG_DISABLE_XTERM } from '@fe/support/global-args'
import { toggleSide, toggleView, toggleXterm } from '@fe/context/layout'
import { toggleWrap } from '@fe/context/editor'
export default {
name: 'status-bar-view',
register: ctx => {
const bus = useBus()
const actions = {
'toggle-side': {
title: '切换侧栏',
action: () => bus.emit('toggle-side'),
action: toggleSide,
shortcut: [Alt, 'e'],
},
'toggle-view': {
title: '切换预览',
action: () => bus.emit('toggle-view'),
action: toggleView,
shortcut: [Alt, 'v'],
},
'toggle-xterm': {
title: '切换终端',
action: () => bus.emit('toggle-xterm'),
action: () => toggleXterm(),
shortcut: [Alt, 't'],
},
'toggle-wrap': {
title: '切换换行',
action: () => bus.emit('editor-toggle-wrap'),
action: toggleWrap,
shortcut: [Alt, 'w'],
},
}
+5 -6
View File
@@ -1,13 +1,12 @@
import { Plugin, Ctx } from '@fe/useful/plugin'
import { useBus } from '@fe/useful/bus'
import dayjs from 'dayjs'
import { getLine, replaceLine } from '@fe/context/editor'
import { Plugin, Ctx } from '@fe/context/plugin'
export default {
name: 'switch-todo',
register: (ctx: Ctx) => {
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', async (e: MouseEvent) => {
const target = e.target as HTMLElement
const bus = useBus()
const preventEvent = () => {
e.preventDefault()
@@ -16,13 +15,13 @@ export default {
}
function switchTodo (line: number, checked: boolean) {
let lineText = ''
bus.emit('editor-get-line', { line, callback: (val: string) => { lineText = val } })
const lineText = getLine(line)
const value = checked
? lineText.replace('[ ]', `[x] ~~${dayjs().format('YYYY-MM-DD HH:mm')}~~`)
: lineText.replace(/(\[x\] ~~[\d-: ]+~~|\[x\])/, '[ ]')
bus.emit('editor-replace-line', { line, value })
replaceLine(line, value)
}
if (target.tagName === 'INPUT' && target.parentElement!.classList.contains('source-line')) {
+9 -10
View File
@@ -1,13 +1,13 @@
import { useBus } from '@fe/useful/bus'
import { useModal } from '@fe/useful/modal'
import { Plugin, Ctx } from '@fe/useful/plugin'
import { hasCtrlCmd } from '@fe/useful/shortcut'
import { useToast } from '@fe/useful/toast'
import { useModal } from '@fe/support/modal'
import { Plugin, Ctx } from '@fe/context/plugin'
import { hasCtrlCmd } from '@fe/context/shortcut'
import { getAction } from '@fe/context/action'
import { useToast } from '@fe/support/toast'
import { getLine, replaceLine } from '@fe/context/editor'
const editTableCell = async (start: number, end: number, cellIndex: number, input: HTMLTextAreaElement | null) => {
const toast = useToast()
const modal = useModal()
const bus = useBus()
if (end - start !== 1) {
toast.show('warning', '暂只支持编辑单行文本')
@@ -52,8 +52,7 @@ const editTableCell = async (start: number, end: number, cellIndex: number, inpu
return result
}
let text = ''
bus.emit('editor-get-line', { line: start, callback: (val: string) => { text = val.trim() } })
const text = getLine(start).trim()
const columns = escapedSplit(text)
const cellText = columns[cellIndex].trim()
@@ -76,7 +75,7 @@ const editTableCell = async (start: number, end: number, cellIndex: number, inpu
const ok = () => {
if (input.value !== cellText) {
resolve(input.value)
bus.emit('view-rerender')
getAction('view.refresh')()
}
cancel()
@@ -119,7 +118,7 @@ const editTableCell = async (start: number, end: number, cellIndex: number, inpu
if (text.startsWith('|')) content = '| ' + content
if (text.endsWith('|')) content += ' |'
bus.emit('editor-replace-line', { line: start, value: content })
replaceLine(start, content)
}
export default {
+20 -17
View File
@@ -1,15 +1,20 @@
import mime from 'mime-types'
import file from '@fe/useful/file'
import { Plugin, Ctx } from '@fe/useful/plugin'
import { CtrlCmd, getActionLabel, isAction, LeftClick, Shift } from '@fe/useful/shortcut'
import { encodeMarkdownLink } from '@fe/useful/utils'
import { useBus } from '@fe/useful/bus'
import env from '@fe/useful/env'
import { useToast } from '@fe/useful/toast'
import store from '@fe/store'
import * as api from '@fe/support/api'
import { Plugin, Ctx } from '@fe/context/plugin'
import { CtrlCmd, getActionLabel, isAction, LeftClick, Shift } from '@fe/context/shortcut'
import { encodeMarkdownLink } from '@fe/utils'
import env from '@fe/utils/env'
import { useToast } from '@fe/support/toast'
import store from '@fe/support/store'
import { replaceValue } from '@fe/context/editor'
import { refreshTree } from '@fe/context/tree'
async function transformImgOutLink (img: HTMLImageElement) {
const { currentFile } = store.state
if (!currentFile) {
return
}
const { repo, path } = currentFile
const transform = (ximg: HTMLImageElement): Promise<string> => {
@@ -21,7 +26,7 @@ async function transformImgOutLink (img: HTMLImageElement) {
canvas.toBlob(async blob => {
try {
const imgFile = new File([blob!], 'file.png')
const { relativePath } = await file.upload(repo, path, imgFile)
const { relativePath } = await api.upload(repo, path, imgFile)
resolve(relativePath)
} catch (error) {
reject(error)
@@ -38,7 +43,7 @@ async function transformImgOutLink (img: HTMLImageElement) {
const res = await window.fetch(`api/proxy?url=${encodeURIComponent(img.src)}&headers=${img.getAttribute('headers') || ''}`)
const blob = await res.blob()
const imgFile = new File([blob!], 'file.' + mime.extension(res.headers.get('content-type')!))
const { relativePath } = await file.upload(repo, path, imgFile)
const { relativePath } = await api.upload(repo, path, imgFile)
replacedLink = relativePath
}
@@ -59,7 +64,6 @@ async function transformAll () {
}
const toast = useToast()
const bus = useBus()
const result = []
const imgList = refView.querySelectorAll('img')
@@ -72,8 +76,8 @@ async function transformAll () {
result.push(data)
}
}
result.forEach(data => bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink }))
bus.emit('tree-refresh')
result.forEach(data => replaceValue(data.oldLink, data.replacedLink))
refreshTree()
}
async function handleKeydown (e: KeyboardEvent) {
@@ -85,7 +89,6 @@ async function handleKeydown (e: KeyboardEvent) {
}
async function handleClick (e: MouseEvent) {
const bus = useBus()
const target = e.target as HTMLElement
if (target.tagName !== 'IMG') {
return false
@@ -95,8 +98,8 @@ async function handleClick (e: MouseEvent) {
if (isAction(e, actionClick)) { // 转换外链图片到本地
const data = await transformImgOutLink(img)
if (data) {
bus.emit('tree-refresh')
bus.emit('editor-replace-value', { search: data.oldLink, replace: data.replacedLink })
refreshTree()
replaceValue(data.oldLink, data.replacedLink)
}
} else {
env.openAlwaysOnTopWindow(img.src)
@@ -115,7 +118,7 @@ export default {
ctx.shortcut.addAction(actionClick, [CtrlCmd, Shift, LeftClick])
ctx.registerHook('ON_VIEW_KEY_DOWN', handleKeydown)
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', handleClick )
ctx.registerHook('ON_VIEW_ELEMENT_CLICK', handleClick)
ctx.registerHook('ON_VIEW_RENDERED', ({ getViewDom }) => {
refView = getViewDom()
})
+7
View File
@@ -4,6 +4,13 @@ declare module '*.vue' {
export default component
}
declare interface Window {
ctx: any;
globalBus: any;
registerPlugin: any;
documentSaved: boolean;
}
declare module 'markdown-it-task-lists'
declare module 'markdown-it-attrs'
declare module 'markdown-it-multimd-table'
+141
View File
@@ -0,0 +1,141 @@
import { slugify } from 'transliteration'
import { basename, relative, extname, join, dirname } from '@fe/utils/path'
import { fileToBase64URL } from '@fe/utils'
import env from '@fe/utils/env'
import Crypto from '@fe/utils/crypto'
import { Doc } from './types'
import { FLAG_DEMO } from './global-args'
async function fetchHttp (input: RequestInfo, init?: RequestInit) {
const response = await fetch(input, init)
const result = await response.json()
if (result.status !== 'ok') {
throw new Error(result.message)
}
return result
}
async function fetchHelpContent (doc: string) {
const result = await fetchHttp('/api/help?doc=' + doc)
return { content: result.data.content, hash: '' }
}
export async function readFile ({ path, repo }: Pick<Doc, 'path' | 'repo'>) {
if (repo === '__help__') {
return await fetchHelpContent(path)
}
const result = await fetchHttp(`/api/file?path=${encodeURIComponent(path)}&repo=${encodeURIComponent(repo)}`)
const hash = result.data.hash
const content = result.data.content
return { content, hash }
}
export async function writeFile ({ repo, path, contentHash }: Doc, content: string) {
const result = await fetchHttp('/api/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo, path, content, old_hash: contentHash })
})
return { hash: result.data }
}
export async function moveFile ({ repo, path }: Doc, newPath: string) {
return fetchHttp('/api/file', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo: repo, oldPath: path, newPath })
})
}
export async function deleteFile ({ path, repo }: Doc) {
return fetchHttp(`/api/file?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'DELETE' })
}
export async function markFile ({ path, repo }: Doc) {
return fetchHttp(`/api/mark?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'POST' })
}
export async function unmarkFile ({ path, repo }: Doc) {
return fetchHttp(`/api/mark?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'DELETE' })
}
export async function fetchMarkedFiles () {
const { data } = await fetchHttp('/api/mark')
return data
}
export async function fetchTree (repo: string) {
const result = await fetchHttp(`/api/tree?repo=${repo}`)
return result.data
}
export async function fetchRepositories () {
const result = await fetchHttp('/api/repositories')
return result.data
}
export async function fetchSettings () {
const result = await fetchHttp('/api/settings')
return result.data
}
export async function writeSettings (body: any) {
return fetchHttp('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
}
export async function choosePath (args: any) {
const from = env.isElectron ? 'electron' : 'browser'
const result = await fetchHttp(`/api/choose?from=${from}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args)
})
return result.data
}
export async function search (repo: string, text: string) {
const result = await fetchHttp(`/api/search?repo=${repo}&search=${encodeURIComponent(text)}`)
return result.data
}
export async function upload (repo: string, belongPath: string, uploadFile: any, name: string | null = null) {
if (FLAG_DEMO) {
return Promise.resolve({
repo,
path: belongPath,
relativePath: URL.createObjectURL(uploadFile)
})
}
const fileBase64Url = await fileToBase64URL(uploadFile)
const filename = name || Crypto.binMd5(fileBase64Url).substr(0, 8) + extname(uploadFile.name)
const formData = new FormData()
const dirName = slugify(basename(belongPath))
const parentPath = dirname(belongPath)
const path: string = join(
parentPath,
'FILES',
dirName.startsWith('.') ? 'upload' : dirName, filename
)
formData.append('repo', repo)
formData.append('path', path)
formData.append('attachment', fileBase64Url)
await fetchHttp('/api/attachment', { method: 'POST', body: formData })
const relativePath: string = relative(parentPath, path)
return { repo, path, relativePath }
}
export async function openInOS ({ repo, path }: Doc) {
return fetchHttp(`/api/open?repo=${encodeURIComponent(repo)}&path=${encodeURIComponent(path)}`)
}
@@ -25,5 +25,5 @@ export function useBus () {
}
export default function install () {
(window as any).globalBus = emitter
window.globalBus = emitter
}
@@ -1,5 +1,5 @@
import { App, ComponentPublicInstance, createApp } from 'vue'
import { Components } from '@fe/types'
import { Components } from '@fe/support/types'
import Modal from '@fe/components/ContextMenu.vue'
interface Instance extends ComponentPublicInstance {
@@ -1,4 +1,4 @@
import { useToast } from '@fe/useful/toast'
import { useToast } from '@fe/support/toast'
export function copyText (text?: string) {
if (text === undefined) {
@@ -1,6 +1,5 @@
import { FLAG_DEMO } from './useful/global-args'
import { useToast } from './useful/toast'
import file from './useful/file'
import { FLAG_DEMO } from './global-args'
import { useToast } from './toast'
if (FLAG_DEMO) {
localStorage.clear()
@@ -8,12 +7,6 @@ if (FLAG_DEMO) {
const xFetch = window.fetch
const cache: {[key: string]: string} = {}
file.upload = (repo, path, fileData: File) => Promise.resolve({
repo,
path,
relativePath: URL.createObjectURL(fileData)
})
window.addEventListener('click', e => {
const target = e.target as HTMLElement
if (target.parentElement?.tagName === 'FORM' && target.innerText.includes('docx')) {
@@ -49,6 +42,10 @@ if (FLAG_DEMO) {
const url = new URL(location.origin + uri)
const method = (init && init.method) || 'GET'
if (uri.startsWith('/api/mark')) {
return Promise.resolve({ status: 'ok', message: '操作成功', data: {} })
}
if (uri.startsWith('/api/repositories')) {
return Promise.resolve({ status: 'ok', message: '获取成功', data: { test: '/test' } })
}
@@ -1,4 +1,4 @@
import { extname } from './path'
import { extname } from '@fe/utils/path'
const extensions = [
'.txt',
'.gitignore',
@@ -1,5 +1,5 @@
import { App, ComponentPublicInstance, createApp } from 'vue'
import { Components } from '@fe/types'
import { Components } from '@fe/support/types'
import Modal from '@fe/components/ModalUi.vue'
interface Instance extends ComponentPublicInstance {
@@ -1,16 +1,16 @@
import { omit } from 'lodash-es'
import { createStore } from 'vuex'
import Storage from '@fe/useful/storage'
import file from '@fe/useful/file'
import { basename } from '@fe/useful/path'
import Storage from '@fe/utils/storage'
import { basename } from '@fe/utils/path'
import * as api from '@fe/support/api'
import { Doc } from './types'
/**
* 获取上一次打开的文件
* @param repoName 仓库名称
*/
const getLastOpenFile = (repoName?: string) => {
export const getLastOpenFile = (repoName?: string) => {
const currentFile = Storage.get('currentFile')
const recentOpenTime = Storage.get('recentOpenTime', {}) as {[key: string]: number}
repoName ??= Storage.get('currentRepo')?.name
if (!repoName) {
return null
}
@@ -44,15 +44,11 @@ export default createStore({
showXterm: false,
autoPreview: true,
showSetting: false,
savedAt: null,
currentContent: '',
previousContent: '',
previousHash: '',
currentRepo: Storage.get('currentRepo'),
currentFile: getLastOpenFile(Storage.get('currentRepo')?.name),
currentFile: null as Doc | null,
recentOpenTime: Storage.get('recentOpenTime', {}),
tabs: Storage.get('tabs', []),
passwordHash: {},
documentInfo: {
textLength: 0,
selectedLength: 0,
@@ -66,11 +62,6 @@ export default createStore({
setMarkedFiles (state, files) {
state.markedFiles = files
},
setPasswordHash (state, { file, passwordHash }) {
if (file && passwordHash) {
state.passwordHash = { ...state.passwordHash, [`${file.repo}|${file.path}`]: passwordHash }
}
},
setRepositories (state, data) {
state.repositories = data
},
@@ -98,15 +89,6 @@ export default createStore({
setShowXterm (state, data) {
state.showXterm = data
},
setPreviousHash (state, data) {
state.previousHash = data
},
setPreviousContent (state, data) {
state.previousContent = data
},
setSavedAt (state, data) {
state.savedAt = data
},
setDocumentInfo (state, data) {
state.documentInfo = data
},
@@ -115,13 +97,13 @@ export default createStore({
},
setCurrentRepo (state, data) {
state.currentRepo = data
state.currentFile = getLastOpenFile(data?.name)
state.tree = null
Storage.set('currentRepo', data)
},
setCurrentFile (state, data) {
setCurrentFile (state, data: Doc | null) {
state.currentFile = data
Storage.set('currentFile', data)
Storage.set('currentFile', omit(data, 'content'))
if (data) {
state.recentOpenTime = { ...(state.recentOpenTime || {}), [`${data.repo}|${data.path}`]: new Date().valueOf() }
@@ -130,34 +112,32 @@ export default createStore({
},
},
getters: {
isSaved (state) {
if (!state.currentFile?.status) {
return true
}
return state.currentContent === state.currentFile?.content
}
},
actions: {
async fetchMarkedFiles ({ commit }) {
const files = (await file.markedFiles()).map((x: any) => ({ ...x, name: basename(x.path) }))
const files = (await api.fetchMarkedFiles()).map((x: any) => ({ ...x, name: basename(x.path) }))
commit('setMarkedFiles', files)
},
async fetchRepositories ({ commit }) {
const repos = await file.fetchRepositories()
const repos = await api.fetchRepositories()
commit('setRepositories', repos)
},
async fetchTree ({ commit }, repo) {
async fetchTree ({ commit, state }) {
const repo = state.currentRepo
if (!repo) {
console.warn('未选择仓库')
return
}
const tree = await file.fetchTree(repo.name)
const tree = await api.fetchTree(repo.name)
commit('setTree', tree)
},
async showHelp ({ commit }, doc) {
const content = await file.fetchHelpContent(doc)
commit('setCurrentFile', {
repo: '__help__',
title: doc,
name: doc,
path: '/' + doc,
content
})
},
}
})
@@ -1,6 +1,6 @@
import { App, ComponentPublicInstance, createApp } from 'vue'
import Toast from '@fe/components/Toast.vue'
import { Components } from '@fe/types'
import { Components } from '@fe/support/types'
interface ToastInstance extends ComponentPublicInstance {
show: (type: Components.Toast.ToastType, content: string, timeout?: number) => void;
@@ -1,3 +1,15 @@
export interface Doc {
type: 'file' | 'dir';
name: string;
repo: string;
path: string;
content?: string;
title?: string;
passwordHash?: string;
contentHash?: string;
status?: 'loaded' | 'save-failed' | 'saved';
}
export namespace Components {
export namespace Modal {
export type ConfirmModalParams = { title?: string; content?: string }
@@ -43,7 +55,7 @@ export namespace Components {
export namespace FileTabs {
export interface Item extends Tabs.Item {
payload: {
file: any; // TODO 文件类型
file: Doc | null;
};
}
}
-202
View File
@@ -1,202 +0,0 @@
import Crypto from './crypto'
import { slugify } from 'transliteration'
import env from './env'
import { basename, relative, extname, join, dirname } from './path'
// TODO 文件类型
type F = { repo: string; path: string };
const fetchHttp = async (input: RequestInfo, init?: RequestInit) => {
const response = await fetch(input, init)
const result = await response.json()
if (result.status !== 'ok') {
throw new Error(result.message)
}
return result
}
const isEncryptedFile = (file: any) => {
return file && file.path.endsWith('.c.md')
}
const isSameFile = (a?: F | null, b?: F | null) => {
return a && b && a.repo === b.repo && a.path === b.path
}
const toUri = (file?: F | null) => {
if (file && file.repo && file.path) {
return encodeURI(`yank-note://${file.repo}/${file.path.replace(/^\//, '')}`)
} else {
return 'yank-note://system/blank.md'
}
}
const decrypt = (content: any, password: string) => {
if (!password) {
throw new Error('未输入解密密码')
}
return Crypto.decrypt(content, password)
}
const encrypt = (content: any, password: string) => {
if (!password) {
throw new Error('未输入解密密码')
}
return Crypto.encrypt(content, password)
}
const read = async ({ path, repo }: F) => {
const result = await fetchHttp(`/api/file?path=${encodeURIComponent(path)}&repo=${encodeURIComponent(repo)}`)
const hash = result.data.hash
const content = result.data.content
return { content, hash }
}
const write = async ({ repo, path }: F, content: any, oldHash: string) => {
const result = await fetchHttp('/api/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo, path, content, old_hash: oldHash })
})
return { hash: result.data }
}
const move = async ({ repo, path }: F, newPath: string) => {
return fetchHttp('/api/file', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ repo: repo, oldPath: path, newPath })
})
}
const deleteFile = async ({ path, repo }: F) => {
return fetchHttp(`/api/file?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'DELETE' })
}
const mark = async ({ path, repo }: F) => {
return fetchHttp(`/api/mark?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'POST' })
}
const unmark = async ({ path, repo }: F) => {
return fetchHttp(`/api/mark?path=${encodeURIComponent(path)}&repo=${repo}`, { method: 'DELETE' })
}
const markedFiles = async () => {
const { data } = await fetchHttp('/api/mark')
return data
}
const fetchTree = async (repo: string) => {
const result = await fetchHttp(`/api/tree?repo=${repo}`)
return result.data
}
const fetchHelpContent = async (doc: string) => {
const result = await fetchHttp('/api/help?doc=' + doc)
return result.data.content
}
const fetchRepositories = async () => {
const result = await fetchHttp('/api/repositories')
return result.data
}
const fetchSettings = async () => {
const result = await fetchHttp('/api/settings')
return result.data
}
const writeSettings = (body: any) => {
return fetchHttp('/api/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
}
const choosePath = async (args: any) => {
const from = env.isElectron ? 'electron' : 'browser'
const result = await fetchHttp(`/api/choose?from=${from}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(args)
})
return result.data
}
const search = async (repo: string, text: string) => {
const result = await fetchHttp(`/api/search?repo=${repo}&search=${encodeURIComponent(text)}`)
return result.data
}
const upload = async (repo: string, belongPath: string, uploadFile: any, name: string | null = null): Promise<{repo: string; path: string; relativePath: string}> => {
return new Promise((resolve, reject) => {
const fr = new FileReader()
fr.readAsDataURL(uploadFile)
fr.onloadend = async () => {
try {
const filename = name || Crypto.binMd5(fr.result).substr(0, 8) + extname(uploadFile.name)
const formData = new FormData()
const dirName = slugify(basename(belongPath))
const parentPath = dirname(belongPath)
const path = join(
parentPath,
'FILES',
dirName.startsWith('.') ? 'upload' : dirName, filename
)
formData.append('repo', repo)
formData.append('path', path)
formData.append('attachment', fr.result as string)
await fetchHttp('/api/attachment', { method: 'POST', body: formData })
const relativePath = relative(parentPath, path)
resolve({ repo, path, relativePath })
} catch (error) {
reject(error)
}
}
})
}
const openInOS = async ({ repo, path }: F) => {
return fetchHttp(`/api/open?repo=${encodeURIComponent(repo)}&path=${encodeURIComponent(path)}`)
}
const toBase64URL = async (file: any) => new Promise((resolve, reject) => {
const fr = new FileReader()
fr.readAsDataURL(file)
fr.onload = () => resolve(fr.result)
fr.onerror = error => reject(error)
})
export default {
isSameFile,
isEncryptedFile,
toUri,
decrypt,
encrypt,
read,
write,
move,
delete: deleteFile,
fetchTree,
fetchRepositories,
fetchHelpContent,
fetchSettings,
writeSettings,
choosePath,
openInOS,
search,
upload,
mark,
unmark,
markedFiles,
toBase64URL,
}
-86
View File
@@ -1,86 +0,0 @@
import * as shortcut from '@fe/useful/shortcut'
import { getLogger } from '@fe/useful/utils'
import { useBus } from '@fe/useful/bus'
import { useToast } from '@fe/useful/toast'
import buildInPlugins from './build-in'
import store from '@fe/store'
import * as tree from './tree'
import * as statusBar from './status-bar'
import * as view from './view'
import * as markdown from './markdown'
export type HookType = 'ON_STARTUP' | tree.HookType | view.HookType
export type HookFun = (...args: any[]) => boolean | void | Promise<boolean | void>
const logger = getLogger('plugin')
const bus = useBus()
const hooks: { [key in HookType]?: {fun: HookFun; once: boolean}[] } = {}
const ctx = {
bus,
store,
shortcut,
ui: {
useToast,
},
registerHook: (type: HookType, fun: HookFun, once = false) => {
if (Array.isArray(hooks[type])) {
hooks[type] = [...hooks[type]!, { fun, once }]
} else {
hooks[type] = [{ fun, once }]
}
},
removeHook: (type: HookType, fun: HookFun) => {
if (Array.isArray(hooks[type])) {
hooks[type] = hooks[type]!.filter(x => x.fun !== fun)
}
},
triggerHook: async (type: HookType, ...args: any[]) => {
logger.debug('triggerHook', type, args)
for (const { fun, once } of (hooks[type] || [])) {
once && ctx.removeHook(type, fun)
if (await fun(...args)) {
return true
}
}
return false
},
tree: tree.ctx,
markdown: markdown.ctx,
statusBar: statusBar.ctx
}
export type Ctx = typeof ctx;
export interface Plugin {
name: string;
register?: (ctx: Ctx) => void;
}
const plugins: {[name: string]: Plugin} = {}
export const register = (plugin: Plugin) => {
logger.debug('register', plugin)
plugins[plugin.name] = plugin
plugin.register && plugin.register(ctx)
}
export const init = () => {
logger.debug('init')
buildInPlugins.forEach((plugin) => {
register(plugin)
})
;(window as any).registerPlugin = register
const script = window.document.createElement('script')
script.src = '/api/plugins'
window.document.body.appendChild(script)
}
export const triggerHook = ctx.triggerHook
init()
-11
View File
@@ -1,11 +0,0 @@
import Markdown from 'markdown-it'
const markdownItPlugins: {plugin: any; params: any}[] = []
export const getPlugins = () => markdownItPlugins
export const ctx = {
registerPlugin: (plugin: (md: Markdown, ...args: any) => void, params?: any) => {
markdownItPlugins.push({ plugin, params })
}
}
-21
View File
@@ -1,21 +0,0 @@
import { Components } from '@fe/types'
export type HookType = 'ON_TREE_NODE_SELECT'
type MenuItem = Components.ContextMenu.Item
type BuildContextMenu = (items: MenuItem[], node: any) => MenuItem[]
const contextMenuFunList: BuildContextMenu[] = []
export const registerContextMenu = (fun: BuildContextMenu) => {
contextMenuFunList.push(fun)
}
export const getContextMenuItems = (node: any) => {
return contextMenuFunList.reduce((items, fun) => fun(items, node), [] as MenuItem[])
}
export const ctx = {
registerContextMenu
}
@@ -1,4 +1,4 @@
import env from '@fe/useful/env'
import env from './env'
export const encodeMarkdownLink = (path: string) => {
return path
@@ -45,6 +45,15 @@ export const dataURItoBlobLink = (dataURI: string) => {
return window.URL.createObjectURL(blob)
}
export function fileToBase64URL (file: File) {
return new Promise<string>((resolve, reject) => {
const fr = new FileReader()
fr.readAsDataURL(file)
fr.onload = () => resolve(fr.result as string)
fr.onerror = error => reject(error)
})
}
export const getLogger = (subject: string) => {
const logger = (level: string) => (...args: any) => {
const time = `${new Date().toLocaleString()}.${Date.now() % 1000}`
+2 -2
View File
@@ -28,8 +28,8 @@
<script lang="ts">
import { defineComponent, onMounted, ref } from 'vue'
import startup from '@fe/useful/startup'
import { FLAG_DISABLE_XTERM } from '@fe/useful/global-args'
import startup from '@fe/context/startup'
import { FLAG_DISABLE_XTERM } from '@fe/support/global-args'
import Layout from '@fe/components/Layout.vue'
import TitleBar from '@fe/components/TitleBar.vue'
import StatusBar from '@fe/components/StatusBar.vue'
+2 -2
View File
@@ -11,9 +11,9 @@
"lib": ["esnext", "dom", "dom.iterable"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@fe/*": ["src/renderer/*"],
"@main/*": ["src/main/*"]
"@main/*": ["src/main/*"],
"@/*": ["src/*"]
},
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
+5
View File
@@ -5927,6 +5927,11 @@ util@^0.11.0:
dependencies:
inherits "2.0.3"
utility-types@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.10.0.tgz#ea4148f9a741015f05ed74fd615e1d20e6bed82b"
integrity sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==
uuid@^3.3.2:
version "3.4.0"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"