mirror of
https://github.com/purocean/yn.git
synced 2026-10-24 12:11:43 +08:00
refactor: 重构 renderer 代码
This commit is contained in:
@@ -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: [
|
||||
{
|
||||
|
||||
@@ -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
@@ -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 = () => {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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(`\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(`})\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 {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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, () => {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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'],
|
||||
})
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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')()
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)}`
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,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) => {
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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 => {
|
||||
|
||||
@@ -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,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'
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
import { Plugin } from '@fe/useful/plugin'
|
||||
import { Plugin } from '@fe/context/plugin'
|
||||
|
||||
export default {
|
||||
name: 'status-bar-tool',
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
Vendored
+7
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
@@ -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}`
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user