Merge branch 'release/2.5.0'

This commit is contained in:
purocean
2021-01-17 14:49:17 +08:00
32 changed files with 284 additions and 251 deletions
+3 -1
View File
@@ -8,8 +8,10 @@ jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
os: [macos-latest, ubuntu-latest, windows-latest]
include:
- os: macos-latest
platform: 'mac'
- os: windows-latest
platform: 'win'
- os: ubuntu-latest
+1
View File
@@ -9,3 +9,4 @@ bin
# .yarnrc
b.bat
*.pfx
.DS_Store
+15 -81
View File
@@ -34,26 +34,28 @@
+ [ ] 可以自定义快捷键
## 特色功能
部分功能需要相关快捷键配合,可参考[特色功能说明](./help/FEATURE.md)
+ 同步滚动:编辑区和预览区同步滚动,预览区可独立滚动
+ 目录大纲:预览区目录大纲快速跳转
+ 文件加密:以 `.c.md` 结尾的文件视为加密文件
+ 自动保存:文件编辑后自动保存,未保存文件橙色标题栏提醒(加密文档不自动保存)
+ 编辑优化:列表自动补全
+ 粘贴图片:`Ctrl + V` 粘贴剪切板里面的图片,使用快捷键 `Ctrl + B + V` 作为 Base64 形式粘贴
+ 粘贴图片:可快速粘贴剪切板里面的图片,可作为文件或 Base64 形式插入
+ 嵌入附件:可以添加附件到文档,点击在系统中打开
+ 代码运行:支持运行 PHP,nodejs,Python,bash 代码,示例见 FEATURE.md
+ 待办列表:支持显示文档中的待办进度,点击可快速切换待办状态
+ 快速打开:可使用快捷键 `Ctrl + P` 打开文件切换面板,以便快捷打开文件,标记的文件,全文搜索文件内容
+ 快速打开:可使用快捷键打开文件切换面板,以便快捷打开文件,标记的文件,全文搜索文件内容
+ 内置终端:支持在编辑器打开终端,快速切换当前工作目录
+ 公式解析:支持输入 katex 公式代码
+ 样式风格:Markdown 使用 GitHub 风格样式和特性
+ 数据仓库:可定义多个数据位置以便文档分类
+ 外链转换:将外链或 BASE64 图片转换为本地图片:`Ctrl + Shift + 单击图片` `Ctrl + Alt + L` 转换所有外链图片到本地
+ HTML 解析:可以直接在文档里面使用 HTML 代码,也可以使用快捷键 `Ctrl + M + V` 粘贴复制 HTML 为 Markdown
+ 外链转换:将外链或 BASE64 图片转换为本地图片
+ HTML 解析:可以直接在文档里面使用 HTML 代码,也可以使用快捷键粘贴复制 HTML 为 Markdown
+ docx 导出:后端使用 pandoc 做转换器
+ TOC 支持:生成 TOC 在需要生成目录的地方写入 `[toc]{type: "ol", level: [1,2,3]}` 即可,,示例见 FEATURE.md
+ 编辑表格单元格:`Ctrl + 单击单元格` 快捷编辑表格单元格
+ 复制标题链接:`Ctrl + 单击标题` 复制标题链接路径到剪切板,便于插入到其他文件
+ 编辑表格单元格:快捷编辑表格单元格
+ 复制标题链接:复制标题链接路径到剪切板,便于插入到其他文件
+ 嵌入小工具:文档支持内嵌 HTML 小工具,示例见 FEATURE.md
+ 嵌入 Plantuml 图形:需要安装 Java,graphviz ,示例见 FEATURE.md
+ 嵌入 drawio 图形:文档支持内嵌 drawio 图形,示例见 FEATURE.md
@@ -65,78 +67,6 @@
+ 文档交叉链接跳转:支持在文档中引入其他文档,互相跳转
+ 脚注功能:支持在文档中书写脚注
## 上手使用
+ 在 [最新版本](https://github.com/purocean/yn/releases) 下载对应平台应用即可
+ 默认快捷键参考 [vscode](https://code.visualstudio.com/)
+ `Ctrl + X` 剪切所选/当前行
+ `Ctrl + C` 复制所选/当前行
+ `Ctrl + Z` 撤消
+ `Ctrl + Shift + Z` 反撤消
+ `Ctrl + /` 注释行
+ `Ctrl + Shift + A` 注释段
+ `Ctrl + K, Ctrl + X` 删除尾部空格
+ `Alt + Click` 插入光标
+ `Shift + Alt + ↑/↓` 在相邻行插入光标
+ `Ctrl + U` 取消最后添加的光标
+ `Shift + Alt + I` 在选中区的所有行的最后添加光标
+ `Ctrl + D` 为下一个匹配项添加光标
+ `Ctrl + F` 查找
+ `Ctrl + H` 替换
+ 自定义编辑器快捷键
+ `Ctrl + P` 打开文件快速跳转面板
+ `Ctrl + S` 保存文档
+ `Ctrl + Enter` 强制插入新行,忽略预置补全规则
+ `Shift + Enter` 强制插入 Tab,忽略预置补全规则
+ `Ctrl + Shift + Up` 当前行上移
+ `Ctrl + Shift + Down` 当前行下移
+ `Ctrl + Shift + D` 重复当前行
+ `Ctrl + Alt + D` 插入当前日期
+ `Ctrl + Alt + T` 插入当前时间
+ `Ctrl + Alt + F` 插入文件附件
+ `Ctrl + Alt + I` 插入文档链接
+ `Ctrl + J` 连接行 join lines
+ `Ctrl + K, Ctrl + U` 转换大写
+ `Ctrl + K, Ctrl + L` 转换小写
+ `Ctrl + Alt + R` 在内置终端里面运行选中内容
+ `Ctrl + M + V` 粘贴 html 富文本
+ `Ctrl + B + V` 粘贴图片为 Base64
+ `Ctrl + Alt + Left/Right` 切换编辑器 Tab
## 开发
依赖安装和编译比较麻烦,请保证自己有足够耐心且有科学上网的手段。
### Windows
Windows 上安装 node-pty 需要费一番功夫,请参考 https://github.com/microsoft/node-pty#windows 配置编译环境。
```bat
REM Node 版本要求 12 v12.7.0
node -v
REM 前端
cd frontend
REM 安装依赖
yarn
REM 打包
yarn run dist
REM 开发
yarn run serve
REM Electron 端
cd ../
REM 安装依赖
win-install.bat
REM 打包 打包前请在前端目录运行打包命令
win-build.bat
REM 开发
yarn run start
```
### Liunx
TODO 手头没有环境,待完善
### OSX
TODO 手头没有环境,待完善
## 界面截图
![截图](./help/3.png)
![截图](./help/5.png)
@@ -147,13 +77,17 @@ yarn run start
## 更新日志
[最新发布](https://github.com/purocean/yn/releases)
### [v2.4.11](https://github.com/purocean/yn/releases/tag/v2.4.11) 2020-12-21
1. 修复不能导出 docx 问题
1. 修复大纲目录高度不正确
### [v2.5.0](https://github.com/purocean/yn/releases/tag/v2.5.0) 2021-01-17
1. 支持 macOS
2. 调整部分快捷键
<details>
<summary>展开查看更多版本记录</summary>
### [v2.4.11](https://github.com/purocean/yn/releases/tag/v2.4.11) 2020-12-21
1. 修复不能导出 docx 问题
2. 修复大纲目录高度不正确
### [v2.4.10](https://github.com/purocean/yn/releases/tag/v2.4.10) 2020-12-16
1. 优化脑图使用体验
+1 -1
View File
@@ -16,5 +16,5 @@ if (fs.existsSync(filePath)) {
console.warn('Pandoc exists. Skip download.', filePath)
} else {
console.info('Download pandoc', downloadUrl, filename, filePath)
request(downloadUrl).pipe(fs.createWriteStream(filePath))
request(downloadUrl).pipe(fs.createWriteStream(filePath, { mode: 0o755 }))
}
+5
View File
@@ -32,6 +32,11 @@
"deb"
]
},
"mac": {
"target": [
"dmg"
]
},
"nsis": {
"perMachine": false
},
+9 -8
View File
@@ -8,6 +8,7 @@ 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'
const blankUri = File.toUri(null)
@@ -72,14 +73,14 @@ export default defineComponent({
}
// 快捷键切换最近文档
if (e.altKey && e.ctrlKey) {
if (e.key === 'ArrowLeft') {
const prev = findTab(-1)
prev && switchTab(prev)
} else if (e.key === 'ArrowRight') {
const next = findTab(1)
next && switchTab(next)
}
if (isAction(e, 'file-tabs-switch-left')) {
const prev = findTab(-1)
prev && switchTab(prev)
}
if (isAction(e, 'file-tabs-switch-right')) {
const next = findTab(1)
next && switchTab(next)
}
}
+23 -19
View File
@@ -8,6 +8,7 @@
import { useStore } from 'vuex'
import { computed, defineComponent, onMounted, onUnmounted, ref, toRef } from 'vue'
import { encodeMarkdownLink } from '../useful/utils'
import { getCurrentAction } from '../useful/shortcut'
import { useBus } from '../useful/bus'
import XMask from './Mask.vue'
import QuickOpen from './QuickOpen.vue'
@@ -24,26 +25,29 @@ export default defineComponent({
const withMarked = ref(true)
function keydownHandler (e: KeyboardEvent) {
if (e.key === 'i' && e.ctrlKey && e.altKey) {
callback.value = (f: any) => {
const file = currentFile.value
if (file) {
const relativePath = f.path.replace(file.path.substr(0, file.path.lastIndexOf('/')), '.')
bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`)
switch (getCurrentAction(e)) {
case 'insert-document':
callback.value = (f: any) => {
const file = currentFile.value
if (file) {
const relativePath = f.path.replace(file.path.substr(0, file.path.lastIndexOf('/')), '.')
bus.emit('editor-insert-value', `[${f.name.replace(/\.[^.]+$/, '')}](${encodeMarkdownLink(relativePath)})`)
}
callback.value = null
}
callback.value = null
}
withMarked.value = false
e.preventDefault()
e.stopPropagation()
} else if (e.key === 'p' && e.ctrlKey) {
callback.value = (f: any) => {
store.commit('setCurrentFile', f)
callback.value = null
}
withMarked.value = true
e.preventDefault()
e.stopPropagation()
withMarked.value = false
e.preventDefault()
e.stopPropagation()
break
case 'show-quick-open':
callback.value = (f: any) => {
store.commit('setCurrentFile', f)
callback.value = null
}
withMarked.value = true
e.preventDefault()
e.stopPropagation()
break
}
}
+8 -8
View File
@@ -12,7 +12,7 @@ import env from '../useful/env'
const models: {[key: string]: monaco.editor.ITextModel} = {}
const keys: {[key: string]: boolean} = {}
let keys: {[key: string]: boolean} = {}
export default defineComponent({
name: 'monaco-editor',
@@ -80,17 +80,17 @@ export default defineComponent({
const KM = getMonaco().KeyMod
const KC = getMonaco().KeyCode
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_F, addAttachment)
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_F, addAttachment)
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_D, () => {
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_D, () => {
insert(dayjs().format('YYYY-MM-DD'))
})
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_T, () => {
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_T, () => {
insert(dayjs().format('HH:mm:ss'))
})
getEditor().addCommand(KM.CtrlCmd | KM.Alt | KC.KEY_R, () => {
getEditor().addCommand(KM.Shift | KM.Alt | KC.KEY_R, () => {
emit('xterm-run', getEditor().getModel()!!.getValueInRange(getEditor().getSelection()!!))
})
@@ -156,7 +156,7 @@ export default defineComponent({
function paste (e: ClipboardEvent) {
if (getEditor().hasTextFocus()) {
const items = e.clipboardData!!.items
if (keys.m || keys.M) { // 粘贴 HTML 转为 markdown
if (keys.d || keys.D) { // 粘贴 HTML 转为 markdown
for (let i = 0; i < items.length; i++) {
if (items[i].type.match(/^text\/html$/i)) {
items[i].getAsString(str => {
@@ -172,7 +172,7 @@ export default defineComponent({
for (let i = 0; i < items.length; i++) {
const fileType = items[i].type
if (fileType.match(/^image\/(png|jpg|jpeg|gif)$/i)) {
const asBase64 = keys.b || keys.B // Ctrl + v 粘贴的同时 按下了 B 键,就粘贴 base64 图像
const asBase64 = keys.b || keys.B // 粘贴的同时 按下了 B 键,就粘贴 base64 图像
emit('paste-img', items[i].getAsFile(), asBase64)
}
}
@@ -243,7 +243,7 @@ export default defineComponent({
if (e.type === 'keydown') {
keys[e.key] = true
} else {
keys[e.key] = false
keys = {}
}
}
+11 -4
View File
@@ -71,6 +71,8 @@ import 'highlight.js/styles/atom-one-dark.css'
import 'katex/dist/katex.min.css'
import { useBus } from '../useful/bus'
import { useToast } from '../useful/toast'
import { hasCtrlCmd, isAction } from '../useful/shortcut'
HighlightLineNumber.addStyles()
const markdown = Markdown({
@@ -311,7 +313,7 @@ export default defineComponent({
async function keydownHandler (e: KeyboardEvent) {
// 转换所有外链图片到本地
if (e.key === 'l' && e.ctrlKey && e.altKey) {
if (isAction(e, 'transform-img-link')) {
e.preventDefault()
e.stopPropagation()
const result = []
@@ -420,14 +422,19 @@ export default defineComponent({
}
// 复制标题链接
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && e.ctrlKey) {
if (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].indexOf(target.tagName) > -1 && target.id && hasCtrlCmd(e)) {
bus.emit('copy-text', encodeMarkdownLink(filePath.value) + '#' + encodeMarkdownLink(decodeURIComponent(target.id)))
return preventEvent()
}
// 复制内容
if (target.classList.contains('copy-inner-text')) {
bus.emit('copy-text', target.innerText)
}
if (target.tagName === 'IMG') {
const img = target as HTMLImageElement
if (e.ctrlKey && e.shiftKey) { // 转换外链图片到本地
if (isAction(e, 'transform-img-link-by-click')) { // 转换外链图片到本地
const data = await transformImgOutLink(img)
if (data) {
bus.emit('tree-refresh')
@@ -450,7 +457,7 @@ export default defineComponent({
return
}
if (target.tagName === 'TD' && target.classList.contains('yank-td') && e.ctrlKey) {
if (target.tagName === 'TD' && target.classList.contains('yank-td') && hasCtrlCmd(e)) {
const start = parseInt(target.dataset.sourceLine || '0')
const end = parseInt(target.dataset.sourceLineEnd || '0')
const td = target as HTMLTableDataCellElement
+12 -10
View File
@@ -9,11 +9,11 @@
<span>字符数:{{documentInfo.textLength}}</span>
<span v-if="documentInfo.selectedLength > 0">已选中:{{documentInfo.selectedLength}}</span>
</div>
<div class="action" @click="toggleSide" title="Alt + e">切换侧栏</div>
<div class="action" @click="toggleWrap" title="Alt + w">切换换行</div>
<div class="action" @click="toggleView" title="Alt + v">切换预览</div>
<div class="action" @click="toggleXterm" title="Alt + o">切换终端</div>
<div class="action" @click="toggleReadme" title="Alt + h">README</div>
<div class="action" @click="toggleSide" :title="getActionLabel('toggle-side')">切换侧栏</div>
<div class="action" @click="toggleWrap" :title="getActionLabel('toggle-wrap')">切换换行</div>
<div class="action" @click="toggleView" :title="getActionLabel('toggle-view')">切换预览</div>
<div class="action" @click="toggleXterm" :title="getActionLabel('toggle-xterm')">切换终端</div>
<div class="action" @click="toggleReadme" :title="getActionLabel('toggle-readme')">README</div>
<div class="action" @click="toggleFeature">特色功能说明</div>
<div class="action" @click="toggleRender">{{autoPreview ? '同步渲染-已开启' : '同步渲染-已关闭'}}</div>
<svg-icon v-if="!autoPreview" class="action action-icon" name="sync-alt-solid" @click="rerenderView" title="强制重新渲染" />
@@ -27,6 +27,7 @@ import { useStore } from 'vuex'
import { useBus } from '../useful/bus'
import RepositorySwitch from './RepositorySwitch.vue'
import SvgIcon from '../components/SvgIcon.vue'
import { isAction, getActionLabel } from '../useful/shortcut'
export default defineComponent({
name: 'status-bar',
@@ -69,31 +70,31 @@ export default defineComponent({
}
function keydownHandler (e: KeyboardEvent) {
if (e.key === 'e' && e.altKey) {
if (isAction(e, 'toggle-side')) {
toggleSide()
e.preventDefault()
e.stopPropagation()
}
if (e.key === 'v' && e.altKey) {
if (isAction(e, 'toggle-view')) {
toggleView()
e.preventDefault()
e.stopPropagation()
}
if (e.key === 'o' && e.altKey) {
if (isAction(e, 'toggle-xterm')) {
toggleXterm()
e.preventDefault()
e.stopPropagation()
}
if (e.key === 'w' && e.altKey) {
if (isAction(e, 'toggle-wrap')) {
toggleWrap()
e.preventDefault()
e.stopPropagation()
}
if (e.key === 'h' && e.altKey) {
if (isAction(e, 'toggle-readme')) {
toggleReadme()
e.preventDefault()
e.stopPropagation()
@@ -119,6 +120,7 @@ export default defineComponent({
toggleFeature,
toggleRender,
rerenderView,
getActionLabel,
}
},
})
+3 -23
View File
@@ -1,14 +1,10 @@
<template>
<div class="tree-node">
<details ref="refDir" @keydown.enter.prevent v-if="item.type === 'dir'" class="name" :title="item.name + '\n\n' + dirTitle" :open="item.path === '/'">
<details ref="refDir" @keydown.enter.prevent v-if="item.type === 'dir'" class="name" :title="item.name" :open="item.path === '/'">
<summary
class="folder"
:style="{background: selected ? '#313131' : 'none'}"
@click.ctrl.exact.prevent="revealInExplorer"
@click.ctrl.alt.exact.prevent="revealInXterminal"
@contextmenu.exact.prevent.stop="showContextMenu(item)"
@contextmenu.ctrl.exact.prevent.stop="renameFile"
@contextmenu.shift.exact.prevent.stop="deleteFile">
@contextmenu.exact.prevent.stop="showContextMenu(item)">
<div class="item">
<div class="item-label">
{{ item.name === '/' ? currentRepoName : item.name }} <span class="count">({{item.children.length}})</span>
@@ -26,10 +22,7 @@
:class="{name: true, 'file-name': true, selected}"
:title="item.name + '\n\n' + fileTitle"
@click.exact.prevent="select(item)"
@click.ctrl.exact.prevent="revealInExplorer"
@contextmenu.exact.prevent.stop="showContextMenu(item)"
@contextmenu.ctrl.exact.prevent.stop="renameFile"
@contextmenu.shift.exact.prevent.stop="deleteFile">
@contextmenu.exact.prevent.stop="showContextMenu(item)">
<div class="item">
<div :class="{'item-label': true, marked}"> {{ item.name }} </div>
</div>
@@ -313,27 +306,14 @@ export default defineComponent({
}
}, { immediate: true })
const dirTitle = computed(() => [
'"Ctrl + 右键" 重命名目录',
'"Shift + 右键" 删除目录',
'"Ctrl + 单击" 在操作系统中打开目录',
'"Ctrl + Alt + 单击" 在终端中打开'
].join('\n'))
const fileTitle = computed(() => [
`创建于:${props.item.birthtime ? new Date(props.item.birthtime).toLocaleString() : '无'}`,
`更新于:${props.item.mtime ? new Date(props.item.mtime).toLocaleString() : '无'}`,
'',
'"Ctrl + 右键" 重命名文件',
'"Shift + 右键" 删除文件',
'"Ctrl + 单击" 使用系统程序打开文件',
'".c.md" 结尾的文件为加密文件'
].join('\n'))
return {
refDir,
refFile,
dirTitle,
fileTitle,
currentRepoName,
selected,
+1 -1
View File
@@ -46,7 +46,7 @@ export default defineComponent({
rows: 24,
fontSize: 18,
cursorStyle: 'underline',
fontFamily: 'Consolas',
// fontFamily: 'Consolas',
fontWeightBold: '500',
theme: {
background: 'rgb(30, 31, 32)'
+3 -2
View File
@@ -1,10 +1,11 @@
import { getKeyLabel } from '@/useful/shortcut'
import Markdown from 'markdown-it'
export default (md: Markdown) => {
const Fun = (fn: Function) => (tokens: any, idx: any, options: any, env: any, slf: any) => {
if (tokens[idx].attrIndex('title') < 0) {
tokens[idx].attrPush(['title', 'Ctrl + 单击复制代码'])
tokens[idx].attrPush(['onclick', "event.ctrlKey && window.globalBus.emit('copy-text', this.innerText)"])
tokens[idx].attrJoin('class', 'copy-inner-text')
tokens[idx].attrPush(['title', getKeyLabel('CtrlCmd') + ' + 单击复制'])
}
return (fn)(tokens, idx, options, env, slf)
+2 -1
View File
@@ -1,6 +1,7 @@
// https://github.com/Oktavilla/markdown-it-table-of-contents
import Markdown from 'markdown-it'
import { getKeyLabel } from '@/useful/shortcut'
import { injectLineNumbers } from './SourceLinePlugin'
@@ -185,7 +186,7 @@ export default (md: Markdown, o: any) => {
}
if (header.attrIndex('title') < 0) {
header.attrSet('title', 'Ctrl + 单击复制链接')
header.attrSet('title', getKeyLabel('CtrlCmd') + ' + 单击复制链接')
}
return injectLineNumbers(tokens, idx, opt, env, slf)
+2 -2
View File
@@ -28,7 +28,7 @@ const openAlwaysOnTopWindow = (url: string, target = '_blank') => {
}
window.addEventListener('keydown', e => {
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
if (e.key === '0') {
webContents.setZoomFactor(1)
} else if (e.key === '=') {
@@ -40,7 +40,7 @@ const openAlwaysOnTopWindow = (url: string, target = '_blank') => {
})
window.addEventListener('mousewheel', e => {
if (e.ctrlKey) {
if (e.ctrlKey || e.metaKey) {
changeZoomFactor(e.deltaY < 0)
}
})
+85
View File
@@ -0,0 +1,85 @@
import { getLogger } from './utils'
const logger = getLogger('shortcut')
const isMacOS = /macintosh|mac os x/i.test(navigator.userAgent)
const Ctrl = 'Ctrl'
const CtrlCmd = 'CtrlCmd'
const Alt = 'Alt'
const Shift = 'Shift'
const LeftClick = 0
type XKey = typeof Ctrl | typeof CtrlCmd | typeof Alt | typeof Shift
const actions = {
'toggle-side': [Alt, 'e'],
'toggle-wrap': [Alt, 'w'],
'toggle-view': [Alt, 'v'],
'toggle-xterm': [Alt, 't'],
'toggle-readme': [Alt, 'h'],
'insert-document': [CtrlCmd, Alt, 'i'],
'show-quick-open': [CtrlCmd, 'p'],
'transform-img-link-by-click': [CtrlCmd, Shift, LeftClick],
'transform-img-link': [CtrlCmd, Shift, 'l'],
'file-tabs-switch-left': [Ctrl, Alt, 'ArrowLeft'],
'file-tabs-switch-right': [Ctrl, Alt, 'ArrowRight'],
}
type ActionName = keyof typeof actions
export const hasCtrlCmd = (e: KeyboardEvent | MouseEvent) => isMacOS ? e.metaKey : e.ctrlKey
export const getKeyLabel = (key: XKey | string | number) => {
switch (key) {
case CtrlCmd:
return isMacOS ? 'Cmd' : 'Ctrl'
case Alt:
return isMacOS ? 'Option' : 'Ctrl'
default:
return key.toString().toUpperCase()
}
}
export const getActionLabel = (name: ActionName) => {
const keys: any[] = actions[name]
return keys.map(getKeyLabel).join(' + ')
}
export const matchKeys = (e: KeyboardEvent | MouseEvent, keys: (string | number)[]) => {
for (const key of keys) {
switch (key) {
case CtrlCmd:
if (!hasCtrlCmd(e)) return false
break
case Alt:
if (!e.altKey) return false
break
case Ctrl:
if (!e.ctrlKey) return false
break
case Shift:
if (!e.shiftKey) return false
break
default:
if (e instanceof KeyboardEvent) {
if (key !== e.key && `Key${key.toString().toUpperCase()}` !== e.code) return false
} else {
if (key !== e.button) return false
}
}
}
return true
}
export const getCurrentAction = (e: KeyboardEvent | MouseEvent) => {
logger.debug('getCurrentAction', { e })
return (Object.keys(actions) as ActionName[]).find(action => matchKeys(e, actions[action]))
}
export const isAction = (e: KeyboardEvent | MouseEvent, name: ActionName) => {
logger.debug('isAction', name, e)
return matchKeys(e, actions[name])
}
+3 -2
View File
@@ -3,6 +3,7 @@ import { useBus } from './bus'
import RunPlugin from '@/plugins/RunPlugin'
import { copyText } from './copy-text'
import env from './env'
import { hasCtrlCmd } from './shortcut'
export default function startup () {
const bus = useBus()
@@ -35,7 +36,7 @@ export default function startup () {
}
window.addEventListener('keydown', e => {
if (e.ctrlKey) {
if (hasCtrlCmd(e)) {
if (e.key === '0') {
webContents.setZoomFactor(1)
} else if (e.key === '=') {
@@ -49,7 +50,7 @@ export default function startup () {
window.addEventListener('mousewheel', event => {
const e = event as WheelEvent
if (e.ctrlKey) {
if (hasCtrlCmd(e)) {
changeZoomFactor(e.deltaY < 0)
}
})
+15
View File
@@ -44,3 +44,18 @@ export const dataURItoBlobLink = (dataURI: string) => {
const blob = new Blob([ab], { type: mimeString })
return window.URL.createObjectURL(blob)
}
export const getLogger = (subject: string) => {
const logger = (level: string) => (...args: any) => {
const time = `${new Date().toLocaleString()}.${Date.now() % 1000}`
;(console as any)[level](`[${time}] ${subject} >`, ...args)
}
return {
debug: logger('debug'),
log: logger('log'),
info: logger('info'),
warn: logger('warn'),
error: logger('error')
}
}
+47 -56
View File
@@ -14,12 +14,8 @@
1. 在配置文件 `<home/yank-note/config.json>` 中可以配置多个仓库,或者自定义 `main` 仓库路径
## 文件管理
Yank-Note 很多功能可以直接使用的快捷键操作
1. 在系统中打开文件/目录:`Ctrl + 单击文件/目录`
1. 删除文件/目录:`Shift + 右键文件/目录`。文件并没有真正删除,还可以从 `<home>/yank-note/trash` 目录下面恢复
1. 重命名文件/目录:`Ctrl + 右键文件/目录`
1. 刷新目录树 `双击目录空白区域`
1. 文件标记 `右键菜单点击标记`
右键目录树可看到文件相关操作选项。
进行删除文件/目录操作后,文件并没有真正删除,还可以从 `<home>/yank-note/trash` 目录下面恢复
## 待办切换
在预览界面打勾试试
@@ -126,7 +122,7 @@ a -> b
此功能由 [markdown-it-multimd-table](https://github.com/RedBug312/markdown-it-multimd-table) 插件提供
支持在表格中使用多行文本和列表。支持表格说明渲染
另外您可以使用:`Ctrl + 单击单元格` 快捷编辑表格单元格内容
另外您可以使用:`Ctrl/Cmd + 单击单元格` 快捷编辑表格单元格内容
First header | Second header
-------------|---------------
@@ -184,9 +180,9 @@ REM --run--
```
## 集成终端
1. 使用 `Alt + O` 或者点击状态栏 **切换终端** 菜单唤起集成终端
1. 支持在编辑器中选中一段代码后按下 `Ctrl+Alt+R` 直接在终端中运行命令。免去复制粘贴。
1. 切换内置终端工作目录到当前目录 `Ctrl + Alt + 单击目录`
1. 使用 `Alt/Option + T` 或者点击状态栏 **切换终端** 菜单唤起集成终端
1. 支持在编辑器中选中一段代码后按下 `Shift + Alt/Option + R` 直接在终端中运行命令。免去复制粘贴。
1. 切换内置终端工作目录到当前目录 `右键目录`
## 小工具
支持在文档中嵌入 HTML 小工具。
@@ -325,40 +321,47 @@ xml 代码块 第一行注释需要有 `--drawio--` 文字
[drawio](./test.drawio){link-type="drawio"}
```
## 编辑器快捷键
+ 默认快捷键参考 [vscode](https://code.visualstudio.com/)
+ `Ctrl + X` 剪切所选/当前行
+ `Ctrl + C` 复制所选/当前行
+ `Ctrl + Z` 撤消
+ `Ctrl + Shift + Z` 反撤消
+ `Ctrl + /` 注释行
+ `Ctrl + Shift + A` 注释段
+ `Ctrl + K, Ctrl + X` 删除尾部空格
+ `Alt + Click` 插入光标
+ `Shift + Alt + ↑/↓` 在相邻行插入光标
+ `Ctrl + U` 取消最后添加的光标
+ `Shift + Alt + I` 在选中区的所有行的最后添加光标
+ `Ctrl + D` 为下一个匹配项添加光标
+ `Ctrl + F` 查找
+ `Ctrl + H` 替换
+ 自定义编辑器快捷键
+ `Ctrl + P` 打开文件快速跳转面板
+ `Ctrl + S` 保存文档
+ `Ctrl + Enter` 强制插入新行,忽略预置补全规则
+ `Shift + Enter` 强制插入 Tab,忽略预置补全规则
+ `Ctrl + Shift + Up` 当前行上移
+ `Ctrl + Shift + Down` 当前行下移
+ `Ctrl + Shift + D` 重复当前行
+ `Ctrl + Alt + D` 插入当前日期
+ `Ctrl + Alt + T` 插入当前时间
+ `Ctrl + Alt + F` 插入文件附件
+ `Ctrl + Alt + I` 插入文档链接
+ `Ctrl + J` 连接行 join lines
+ `Ctrl + K, Ctrl + U` 转换大写
+ `Ctrl + K, Ctrl + L` 转换小写
+ `Ctrl + Alt + R` 在内置终端里面运行选中内容
+ `Ctrl + M + V` 粘贴 HTML 富文本
+ `Ctrl + B + V` 粘贴图片为 base64 链接
## 快捷键
这里仅列出部分常用快捷键和自定义快捷键,默认编辑器快捷键参考 [vscode](https://code.visualstudio.com/)。
功能 | Windows | macOS
---- | ------ | -----
剪切所选/当前行 | Ctrl + X | Cmd + X
复制所选/当前行 | Ctrl + C | Cmd + X
撤销 | Ctrl + Z | Cmd + Z
重做 | Ctrl + Shift + Z | Cmd + Shift + Z
在选中区的所有行的最后添加光标 | Shift + Alt + I | Shift + Option + I
在相邻行插入光标 | Ctrl + Alt + ↑/↓ | Cmd + Option + ↑/↓
为下一个匹配项添加光标 | Ctrl + D | Cmd + D
查找 | Ctrl + F | Cmd + F
打开文件快速跳转面板 | Ctrl + P | Cmd + P
插入文档链接 | Ctrl + Alt + I | Cmd + Option + I
保存文档 | Ctrl + S | Cmd + S
连接行 | Ctrl + J | Cmd + J
强制插入新行,忽略预置补全规则 | Ctrl + Enter | Cmd + Enter
强制插入 Tab,忽略预置补全规则 | Shift + Enter | Shift + Enter
当前行上移 | Ctrl + Shift + Up | Cmd + Shift + Up
当前行下移 | Ctrl + Shift + Down | Cmd + Shift + Down
重复当前行 | Ctrl + Shift + D | Cmd + Shift + D
插入当前日期 | Shift + Alt + D | Shift + Option + D
插入当前时间 | Shift + Alt + T | Shift + Option + T
插入文件附件 | Shift + Alt + F | Shift + Option + F
在内置终端里面运行选中内容 | Shift + Alt + R | Shift + Option + R
转换大写 | Ctrl + K, Ctrl + U | Cmd + K, Cmd + U
转换小写 | Ctrl + K, Ctrl + L | Cmd + K, Cmd + K
粘贴 HTML 富文本 | Ctrl + D + V | Cmd + D + V
粘贴图片为 base64 链接 | Ctrl + B + V | Cmd + B + V
转换单个外链图片到本地 | Ctrl + Shift + 单击图片 | Cmd + Shift + 单击图片
转换所有外链图片到本地 | Ctrl + Shift + L | Cmd + Shift + L
编辑表格单元格 | Ctrl + 单击单元格 | Cmd + 单击单元格
复制文档标题链接 | Ctrl + 单击标题 | Cmd + 单击标题
切换编辑器 Tab | Ctrl + Alt + Left/Right | Ctrl + Option + Left/Rig
切换侧栏 | Alt + E | Option + E
切换编辑器自动换行 | Alt + W | Option + W
切换文档预览显示 | Alt + V | Option + V
切换终端 | Alt + T | Option + T
查看 Readme | Alt + H | Option + H
## 元素属性书写
此功能使用如下库实现 [markdown-it-attrs](https://github.com/arve0/markdown-it-attrs)
@@ -378,17 +381,5 @@ xml 代码块 第一行注释需要有 `--drawio--` 文字
--init-repo | 初始仓库名 | 无 | 字符串 | --init-repo='test'
--init-file | 加载文件路径 | 无 | 文件路径,相对于仓库路径 | --init-file='/1.md'
## 其他
1. 转换单个外链图片到本地:`Ctrl + Shift + 单击图片`
1. 转换所有外链图片到本地 `Ctrl + Alt + L`
1. 切换文档预览显示:`Alt + V` 或点击状态栏 `切换预览` 按钮
1. 切换编辑器自动换行:`Alt + W` 或点击状态栏 `切换换行` 按钮
1. 粘贴 HTML 富文本为 Markdown `Ctrl + M + V`
1. 粘贴图片为 Base64 `Ctrl + B + V`
1. 打开文件快速切换面板 `Ctrl + P`
1. 复制文档标题链接 `Ctrl + 单击标题`
1. 切换编辑器 Tab `Ctrl + Alt + Left/Right`
1. 编辑表格单元格:`Ctrl + 单击单元格`
[^1]: 这是一个脚注
[^2]: 这也是一个脚注
-13
View File
@@ -1,13 +0,0 @@
# Electron's version.
export npm_config_target=6.0.0
# The architecture of Electron, can be ia32 or x64.
export npm_config_arch=x64
export npm_config_target_arch=x64
# Download headers for Electron.
export npm_config_disturl=https://atom.io/download/electron
# Tell node-pre-gyp that we are building for Electron.
export npm_config_runtime=electron
# Tell node-pre-gyp to build module from source code.
export npm_config_build_from_source=true
# Install all dependencies, and store cache to ~/.electron-gyp.
HOME=~/.electron-gyp yarn && yarn run download-pandoc
-2
View File
@@ -1,2 +0,0 @@
yarn run build
./node_modules/.bin/electron-builder --linux
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "yank.note.yang",
"version": "2.4.11",
"version": "2.5.0",
"description": "Yank Note 一款面向程序员的 Markdown 笔记应用",
"main": "dist/app.js",
"license": "Apache-2.0",
+12 -2
View File
@@ -1,5 +1,6 @@
import { app, BrowserWindow, Menu, Tray } from 'electron'
import { app, BrowserWindow, Menu, Tray, powerMonitor } from 'electron'
import * as path from 'path'
import * as os from 'os'
import { dialog } from 'electron'
import * as yargs from 'yargs'
import server from './server/main'
@@ -8,6 +9,8 @@ import * as updater from './updater'
import { getAccelerator, registerShortcut } from './shortcut'
const opn = require('opn')
const isMacos = os.platform() === 'darwin'
let isDev = false
const showTray = !(yargs.argv['disable-tray'])
@@ -105,6 +108,8 @@ const createWindow = () => {
const showWindow = () => {
if (win) {
const show = () => {
// macos 上隐藏图标
isMacos && app.dock.hide()
win.setSkipTaskbar(false)
win.show()
}
@@ -164,6 +169,9 @@ if (!gotTheLock) {
})
app.on('ready', () => {
// macos 上隐藏图标
isMacos && app.dock.hide()
// 打开后端服务器
try {
server(backendPort)
@@ -179,6 +187,8 @@ if (!gotTheLock) {
'open-in-browser': () => opn(getUrl())
})
powerMonitor.on('shutdown', quit)
if (showTray) {
const contextMenu = Menu.buildFromTemplate([
{
@@ -295,7 +305,7 @@ if (!gotTheLock) {
},
])
tray = new Tray(path.join(__dirname, './assets/icon.png'))
tray = new Tray(path.join(__dirname, './assets/tray.png'))
tray.setToolTip('Yank Note 一款面向程序员的 Markdown 编辑器')
tray.on('click', showWindow)
tray.setContextMenu(contextMenu)
Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 908 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+14 -3
View File
@@ -6,6 +6,7 @@ import * as wsl from './wsl'
import config from './config'
const isWsl = wsl.isWsl
const isWin = os.platform() === 'win32'
const runCode = (language: string, code: string) => {
try {
@@ -17,7 +18,7 @@ const runCode = (language: string, code: string) => {
js: { cmd: 'node', args: ['-e'] },
} as {[key: string]: {cmd: string, args: string[]}}
if (language === 'bat') {
if (language === 'bat' && isWin) {
const fileName = 'yn-run-cmd.bat'
code = code.split('\n').slice(1).join('\n') // 去掉第一行的注释
if (isWsl) {
@@ -36,12 +37,22 @@ const runCode = (language: string, code: string) => {
return `不支持 ${language} 语言`
}
const useWsl = os.platform() === 'win32' && config.get('runCodeUseWsl', false)
const useWsl = isWin && config.get('runCodeUseWsl', false)
if (useWsl) {
return execFileSync('wsl.exe', ['--', runParams.cmd].concat(runParams.args).concat([code])).toString()
}
return execFileSync(runParams.cmd, runParams.args.concat([code])).toString()
const env = { ...process.env }
const extPath = '/usr/local/bin'
if (!isWin && env.PATH.indexOf(extPath) < 0) {
env.PATH = `${extPath}:${env.PATH}`
}
return execFileSync(
runParams.cmd,
runParams.args.concat([code]),
{ env }
).toString()
} catch (e) {
return e.message
}
+1 -1
View File
@@ -5,7 +5,7 @@ import { toWslPath } from './wsl'
const configKey = 'shell'
const CD_COMMAND_PREFIX = '--yank-note-run-command-cd--'
const defaultShell = os.platform() === 'win32' ? 'cmd.exe' : 'bash'
const defaultShell = os.platform() === 'win32' ? 'cmd.exe' : (process.env.SHELL || 'bash')
const getShell = () => {
const shell = config.get(configKey, defaultShell)
+7 -1
View File
@@ -1,9 +1,11 @@
import * as os from 'os'
import { globalShortcut, dialog } from 'electron'
const platform = os.platform()
type AcceleratorType = 'show-main-window' | 'open-in-browser'
export const getAccelerator = (type: AcceleratorType) => {
return {
'show-main-window': 'Super+N',
'show-main-window': platform === 'darwin' ? null : 'Super+N',
'open-in-browser': 'Super+Shift+B'
}[type]
}
@@ -11,6 +13,10 @@ export const getAccelerator = (type: AcceleratorType) => {
export const registerShortcut = (shortcuts: {[key in AcceleratorType]: () => void}) => {
Object.keys(shortcuts).forEach((key: AcceleratorType) => {
const accelerator = getAccelerator(key)
if (!accelerator) {
return
}
globalShortcut.register(accelerator, shortcuts[key])
if (!globalShortcut.isRegistered(accelerator)) {
-1
View File
@@ -1 +0,0 @@
@yarn run build && yarn run electron-builder --win -p never
-8
View File
@@ -1,8 +0,0 @@
@echo off
setlocal
set npm_config_disturl=https://atom.io/download/electron
set npm_config_target=9.3.1
set npm_config_runtime="electron"
set npm_config_cache=~\.npm-electron
yarn && yarn run download-pandoc
endlocal