mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 22:10:24 +08:00
Line ending (#234)
* Prepare line ending feature * Detect document line ending * Line ending conversion * Add "endOfLine" settings option * Add line ending menu * Notify user about mixed endings * Fixes * Change line ending menu entries to radio style
This commit is contained in:
parent
6cad091f6d
commit
dfffc73e69
3
.github/CHANGELOG.md
vendored
3
.github/CHANGELOG.md
vendored
@ -4,7 +4,8 @@
|
|||||||
|
|
||||||
- feature: add editorFont setting in user preference. (#175) - Anderson
|
- feature: add editorFont setting in user preference. (#175) - Anderson
|
||||||
- feature: line break, support event and import and export markdown - Jocs
|
- feature: line break, support event and import and export markdown - Jocs
|
||||||
- feat: unindent list item - Jocs
|
- feature: unindent list item - Jocs
|
||||||
|
- feature: Support for CRLF and LF line endings
|
||||||
|
|
||||||
**:butterfly:Optimization**
|
**:butterfly:Optimization**
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import path from 'path'
|
|||||||
import { dialog, ipcMain, BrowserWindow } from 'electron'
|
import { dialog, ipcMain, BrowserWindow } from 'electron'
|
||||||
import { IMAGE_EXTENSIONS } from '../config'
|
import { IMAGE_EXTENSIONS } from '../config'
|
||||||
import { searchFilesAndDir } from '../imagePathAutoComplement'
|
import { searchFilesAndDir } from '../imagePathAutoComplement'
|
||||||
|
import { updateLineEndingnMenu } from '../menu'
|
||||||
import { log } from '../utils'
|
import { log } from '../utils'
|
||||||
|
|
||||||
const getAndSendImagePath = (win, type) => {
|
const getAndSendImagePath = (win, type) => {
|
||||||
@ -37,10 +38,18 @@ ipcMain.on('AGANI::ask-for-image-auto-path', (e, { pathname, src }) => {
|
|||||||
.catch(log)
|
.catch(log)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.on('AGANI::update-line-ending-menu', (e, lineEnding) => {
|
||||||
|
updateLineEndingnMenu(lineEnding)
|
||||||
|
})
|
||||||
|
|
||||||
export const edit = (win, type) => {
|
export const edit = (win, type) => {
|
||||||
win.webContents.send('AGANI::edit', { type })
|
win.webContents.send('AGANI::edit', { type })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const lineEnding = (win, lineEnding) => {
|
||||||
|
win.webContents.send('AGANI::set-line-ending', { lineEnding })
|
||||||
|
}
|
||||||
|
|
||||||
export const insertImage = (win, type) => {
|
export const insertImage = (win, type) => {
|
||||||
if (type === 'absolute' || type === 'relative') {
|
if (type === 'absolute' || type === 'relative') {
|
||||||
getAndSendImagePath(win, type)
|
getAndSendImagePath(win, type)
|
||||||
|
@ -3,13 +3,15 @@
|
|||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
// import chokidar from 'chokidar'
|
// import chokidar from 'chokidar'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { app, BrowserWindow, dialog, ipcMain } from 'electron'
|
import { BrowserWindow, dialog, ipcMain } from 'electron'
|
||||||
import createWindow, { windows } from '../createWindow'
|
import createWindow, { forceClose, windows } from '../createWindow'
|
||||||
import { EXTENSION_HASN, EXTENSIONS } from '../config'
|
import { EXTENSION_HASN, EXTENSIONS } from '../config'
|
||||||
|
import { writeFile, writeMarkdownFile } from '../filesystem'
|
||||||
import { clearRecentlyUsedDocuments } from '../menu'
|
import { clearRecentlyUsedDocuments } from '../menu'
|
||||||
import { getPath, isMarkdownFile, log, isFile } from '../utils'
|
import { getPath, isMarkdownFile, log, isFile } from '../utils'
|
||||||
import userPreference from '../preference'
|
import userPreference from '../preference'
|
||||||
|
|
||||||
|
// TODO(fxha): Do we still need this?
|
||||||
const watchAndReload = (pathname, win) => { // when i build, and failed.
|
const watchAndReload = (pathname, win) => { // when i build, and failed.
|
||||||
// const watcher = chokidar.watch(pathname, {
|
// const watcher = chokidar.watch(pathname, {
|
||||||
// persistent: true
|
// persistent: true
|
||||||
@ -27,43 +29,6 @@ const watchAndReload = (pathname, win) => { // when i build, and failed.
|
|||||||
// })
|
// })
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeFile = (pathname, content, extension, e, callback = null) => {
|
|
||||||
if (pathname) {
|
|
||||||
pathname = !extension || pathname.endsWith(extension) ? pathname : `${pathname}${extension}`
|
|
||||||
fs.writeFile(pathname, content, 'utf-8', err => {
|
|
||||||
if (err) log(err)
|
|
||||||
if (callback) callback(err, pathname)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log('[ERROR] Cannot save file without path.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const writeMarkdownFile = (pathname, content, extension, isUtf8BomEncoded, win, e, quitAfterSave = false) => {
|
|
||||||
if (isUtf8BomEncoded) {
|
|
||||||
// js is call-by-value, so we can insert BOM
|
|
||||||
content = '\uFEFF' + content
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFile(pathname, content, extension, e, (err, filePath) => {
|
|
||||||
if (!err) e.sender.send('AGANI::file-saved-successfully')
|
|
||||||
const filename = path.basename(filePath)
|
|
||||||
if (e && filePath) e.sender.send('AGANI::set-pathname', { pathname: filePath, filename })
|
|
||||||
if (!err && quitAfterSave) forceClose(win)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const forceClose = win => {
|
|
||||||
if (!win) return
|
|
||||||
if (windows.has(win.id)) {
|
|
||||||
windows.delete(win.id)
|
|
||||||
}
|
|
||||||
win.destroy() // if use win.close(), it will cause a endless loop.
|
|
||||||
if (windows.size === 0) {
|
|
||||||
app.quit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handle the response from render process.
|
// handle the response from render process.
|
||||||
const handleResponseForExport = (e, { type, content, filename, pathname }) => {
|
const handleResponseForExport = (e, { type, content, filename, pathname }) => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
@ -85,27 +50,27 @@ const handleResponseForExport = (e, { type, content, filename, pathname }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleResponseForSave = (e, { markdown, pathname, isUtf8BomEncoded, quitAfterSave = false }) => {
|
const handleResponseForSave = (e, { markdown, pathname, options, quitAfterSave = false }) => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
if (pathname) {
|
if (pathname) {
|
||||||
writeMarkdownFile(pathname, markdown, '', isUtf8BomEncoded, win, e, quitAfterSave)
|
writeMarkdownFile(pathname, markdown, '', options, win, e, quitAfterSave)
|
||||||
} else {
|
} else {
|
||||||
const filePath = dialog.showSaveDialog(win, {
|
const filePath = dialog.showSaveDialog(win, {
|
||||||
defaultPath: getPath('documents') + '/Untitled.md'
|
defaultPath: getPath('documents') + '/Untitled.md'
|
||||||
})
|
})
|
||||||
writeMarkdownFile(filePath, markdown, '.md', isUtf8BomEncoded, win, e, quitAfterSave)
|
writeMarkdownFile(filePath, markdown, '.md', options, win, e, quitAfterSave)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on('AGANI::response-file-save-as', (e, { markdown, pathname, isUtf8BomEncoded }) => {
|
ipcMain.on('AGANI::response-file-save-as', (e, { markdown, pathname, options }) => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
let filePath = dialog.showSaveDialog(win, {
|
let filePath = dialog.showSaveDialog(win, {
|
||||||
defaultPath: pathname || getPath('documents') + '/Untitled.md'
|
defaultPath: pathname || getPath('documents') + '/Untitled.md'
|
||||||
})
|
})
|
||||||
writeMarkdownFile(filePath, markdown, '.md', isUtf8BomEncoded, win, e)
|
writeMarkdownFile(filePath, markdown, '.md', options, win, e)
|
||||||
})
|
})
|
||||||
|
|
||||||
ipcMain.on('AGANI::response-close-confirm', (e, { filename, pathname, markdown, isUtf8BomEncoded }) => {
|
ipcMain.on('AGANI::response-close-confirm', (e, { filename, pathname, markdown, options }) => {
|
||||||
const win = BrowserWindow.fromWebContents(e.sender)
|
const win = BrowserWindow.fromWebContents(e.sender)
|
||||||
dialog.showMessageBox(win, {
|
dialog.showMessageBox(win, {
|
||||||
type: 'warning',
|
type: 'warning',
|
||||||
@ -122,7 +87,7 @@ ipcMain.on('AGANI::response-close-confirm', (e, { filename, pathname, markdown,
|
|||||||
break
|
break
|
||||||
case 0:
|
case 0:
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
handleResponseForSave(e, { pathname, markdown, isUtf8BomEncoded, quitAfterSave: true })
|
handleResponseForSave(e, { pathname, markdown, options, quitAfterSave: true })
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -35,3 +35,7 @@ export const VIEW_MENU_ITEM = {
|
|||||||
'Typewriter Mode': false,
|
'Typewriter Mode': false,
|
||||||
'Focus Mode': false
|
'Focus Mode': false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const LINE_ENDING_REG = /(?:\r\n|\n)/g
|
||||||
|
export const LF_LINE_ENDING_REG = /(?:[^\r]\n)|(?:^\n$)/
|
||||||
|
export const CRLF_LINE_ENDING_REG = /\r\n/
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
'use strict'
|
'use strict'
|
||||||
|
|
||||||
import fs from 'fs'
|
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { BrowserWindow, screen } from 'electron'
|
import { app, BrowserWindow, screen } from 'electron'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import windowStateKeeper from 'electron-window-state'
|
||||||
import { addRecentlyUsedDocuments } from './menu'
|
import { getOsLineEndingName, loadMarkdownFile } from './filesystem'
|
||||||
import { isMarkdownFile, log } from './utils'
|
import { addRecentlyUsedDocuments, updateLineEndingnMenu } from './menu'
|
||||||
|
import { isMarkdownFile } from './utils'
|
||||||
|
|
||||||
|
let focusedWindowId = -1
|
||||||
export const windows = new Map()
|
export const windows = new Map()
|
||||||
|
|
||||||
const ensureWindowPosition = mainWindowState => {
|
const ensureWindowPosition = mainWindowState => {
|
||||||
@ -66,31 +67,24 @@ const createWindow = (pathname, options = {}) => {
|
|||||||
|
|
||||||
if (pathname && isMarkdownFile(pathname)) {
|
if (pathname && isMarkdownFile(pathname)) {
|
||||||
addRecentlyUsedDocuments(pathname)
|
addRecentlyUsedDocuments(pathname)
|
||||||
const filename = path.basename(pathname)
|
loadMarkdownFile(win, pathname)
|
||||||
fs.readFile(path.resolve(pathname), 'utf-8', (err, file) => {
|
} else {
|
||||||
if (err) {
|
const lineEnding = getOsLineEndingName()
|
||||||
log(err)
|
win.webContents.send('AGANI::set-line-ending', {
|
||||||
return
|
lineEnding,
|
||||||
}
|
ignoreSaveStatus: true
|
||||||
|
|
||||||
// check UTF-8 BOM (EF BB BF) encoding
|
|
||||||
let isUtf8BomEncoded = file.length >= 1 && file.charCodeAt(0) === 0xFEFF
|
|
||||||
if (isUtf8BomEncoded) {
|
|
||||||
file = file.slice(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
win.webContents.send('AGANI::file-loaded', {
|
|
||||||
file,
|
|
||||||
filename,
|
|
||||||
pathname,
|
|
||||||
isUtf8BomEncoded
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
updateLineEndingnMenu(lineEnding)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('focus', () => {
|
win.on('focus', () => {
|
||||||
win.webContents.send('AGANI::window-active-status', { status: true })
|
win.webContents.send('AGANI::window-active-status', { status: true })
|
||||||
|
|
||||||
|
if (win.id !== focusedWindowId) {
|
||||||
|
focusedWindowId = win.id
|
||||||
|
win.webContents.send('AGANI::req-update-line-ending-menu')
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
win.on('blur', () => {
|
win.on('blur', () => {
|
||||||
@ -113,4 +107,15 @@ const createWindow = (pathname, options = {}) => {
|
|||||||
return win
|
return win
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const forceClose = win => {
|
||||||
|
if (!win) return
|
||||||
|
if (windows.has(win.id)) {
|
||||||
|
windows.delete(win.id)
|
||||||
|
}
|
||||||
|
win.destroy() // if use win.close(), it will cause a endless loop.
|
||||||
|
if (windows.size === 0) {
|
||||||
|
app.quit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default createWindow
|
export default createWindow
|
||||||
|
113
src/main/filesystem.js
Normal file
113
src/main/filesystem.js
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import fs from 'fs'
|
||||||
|
import path from 'path'
|
||||||
|
import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG } from './config'
|
||||||
|
import { forceClose } from './createWindow'
|
||||||
|
import userPreference from './preference'
|
||||||
|
import { log } from './utils'
|
||||||
|
|
||||||
|
const isWin = process.platform === 'win32'
|
||||||
|
|
||||||
|
const convertLineEndings = (text, lineEnding) => {
|
||||||
|
return text.replace(LINE_ENDING_REG, getLineEnding(lineEnding))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOsLineEndingName = () => {
|
||||||
|
const { endOfLine } = userPreference.getAll()
|
||||||
|
if (endOfLine === 'lf') {
|
||||||
|
return 'lf'
|
||||||
|
}
|
||||||
|
return endOfLine === 'crlf' || isWin ? 'crlf' : 'lf'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getLineEnding = lineEnding => {
|
||||||
|
if (lineEnding === 'lf') {
|
||||||
|
return '\n'
|
||||||
|
} else if (lineEnding === 'crlf') {
|
||||||
|
return '\r\n'
|
||||||
|
}
|
||||||
|
return getOsLineEndingName() === 'crlf' ? '\r\n' : '\n'
|
||||||
|
}
|
||||||
|
|
||||||
|
export const writeFile = (pathname, content, extension, e, callback = null) => {
|
||||||
|
if (pathname) {
|
||||||
|
pathname = !extension || pathname.endsWith(extension) ? pathname : `${pathname}${extension}`
|
||||||
|
fs.writeFile(pathname, content, 'utf-8', err => {
|
||||||
|
if (err) log(err)
|
||||||
|
if (callback) callback(err, pathname)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
log('[ERROR] Cannot save file without path.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const writeMarkdownFile = (pathname, content, extension, options, win, e, quitAfterSave = false) => {
|
||||||
|
const { adjustLineEndingOnSave, isUtf8BomEncoded, lineEnding } = options
|
||||||
|
if (isUtf8BomEncoded) {
|
||||||
|
content = '\uFEFF' + content
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adjustLineEndingOnSave) {
|
||||||
|
content = convertLineEndings(content, lineEnding)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFile(pathname, content, extension, e, (err, filePath) => {
|
||||||
|
if (!err) e.sender.send('AGANI::file-saved-successfully')
|
||||||
|
const filename = path.basename(filePath)
|
||||||
|
if (e && filePath) e.sender.send('AGANI::set-pathname', { pathname: filePath, filename })
|
||||||
|
if (!err && quitAfterSave) forceClose(win)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadMarkdownFile = (win, pathname) => {
|
||||||
|
fs.readFile(path.resolve(pathname), 'utf-8', (err, file) => {
|
||||||
|
if (err) {
|
||||||
|
log(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check UTF-8 BOM (EF BB BF) encoding
|
||||||
|
const isUtf8BomEncoded = file.length >= 1 && file.charCodeAt(0) === 0xFEFF
|
||||||
|
if (isUtf8BomEncoded) {
|
||||||
|
file = file.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect line ending
|
||||||
|
const isLf = LF_LINE_ENDING_REG.test(file)
|
||||||
|
const isCrlf = CRLF_LINE_ENDING_REG.test(file)
|
||||||
|
const isMixed = isLf && isCrlf
|
||||||
|
const isUnknownEnding = !isLf && !isCrlf
|
||||||
|
let lineEnding = getOsLineEndingName()
|
||||||
|
if (isLf && !isCrlf) {
|
||||||
|
lineEnding = 'lf'
|
||||||
|
} else if (isCrlf && !isLf) {
|
||||||
|
lineEnding = 'crlf'
|
||||||
|
}
|
||||||
|
|
||||||
|
let adjustLineEndingOnSave = false
|
||||||
|
if (isMixed || isUnknownEnding || lineEnding !== 'lf') {
|
||||||
|
adjustLineEndingOnSave = lineEnding !== 'lf'
|
||||||
|
// Convert to LF for internal use.
|
||||||
|
file = convertLineEndings(file, 'lf')
|
||||||
|
}
|
||||||
|
|
||||||
|
const filename = path.basename(pathname)
|
||||||
|
win.webContents.send('AGANI::file-loaded', {
|
||||||
|
file,
|
||||||
|
filename,
|
||||||
|
pathname,
|
||||||
|
options: {
|
||||||
|
isUtf8BomEncoded,
|
||||||
|
lineEnding,
|
||||||
|
adjustLineEndingOnSave
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Notify user about mixed endings
|
||||||
|
if (isMixed) {
|
||||||
|
win.webContents.send('AGANI::show-info-notification', {
|
||||||
|
msg: `The document has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
||||||
|
timeout: 20000
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
@ -84,3 +84,14 @@ export const updateApplicationMenu = (recentUsedDocuments) => {
|
|||||||
}
|
}
|
||||||
initMacDock = true
|
initMacDock = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const updateLineEndingnMenu = lineEnding => {
|
||||||
|
const menus = Menu.getApplicationMenu()
|
||||||
|
const crlfMenu = menus.getMenuItemById('crlfLineEndingMenuEntry')
|
||||||
|
const lfMenu = menus.getMenuItemById('lfLineEndingMenuEntry')
|
||||||
|
if (lineEnding === 'crlf') {
|
||||||
|
crlfMenu.checked = true
|
||||||
|
} else {
|
||||||
|
lfMenu.checked = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -88,5 +88,24 @@ export default {
|
|||||||
actions.insertImage(browserWindow, 'upload')
|
actions.insertImage(browserWindow, 'upload')
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
|
}, {
|
||||||
|
type: 'separator'
|
||||||
|
}, {
|
||||||
|
label: 'Line Ending',
|
||||||
|
submenu: [{
|
||||||
|
id: 'crlfLineEndingMenuEntry',
|
||||||
|
label: 'Carriage return and line feed (CRLF)',
|
||||||
|
type: 'radio',
|
||||||
|
click (menuItem, browserWindow) {
|
||||||
|
actions.lineEnding(browserWindow, 'crlf')
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
id: 'lfLineEndingMenuEntry',
|
||||||
|
label: 'Line feed (LF)',
|
||||||
|
type: 'radio',
|
||||||
|
click (menuItem, browserWindow) {
|
||||||
|
actions.lineEnding(browserWindow, 'lf')
|
||||||
|
}
|
||||||
|
}]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ export const getPath = directory => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getMenuItem = menuName => {
|
export const getMenuItem = menuName => {
|
||||||
|
// TODO(fxha): Please use menu id attribute to find menu entries. This will
|
||||||
|
// cause problems with internationalization later!
|
||||||
const menus = Menu.getApplicationMenu()
|
const menus = Menu.getApplicationMenu()
|
||||||
return menus.items.find(menu => menu.label === menuName)
|
return menus.items.find(menu => menu.label === menuName)
|
||||||
}
|
}
|
||||||
|
@ -98,6 +98,8 @@
|
|||||||
dispatch('LISTEN_FOR_RENAME')
|
dispatch('LISTEN_FOR_RENAME')
|
||||||
dispatch('LISTEN_FOR_IMAGE_PATH')
|
dispatch('LISTEN_FOR_IMAGE_PATH')
|
||||||
dispatch('LISTEN_FOR_FILE_SAVED_SUCCESSFULLY')
|
dispatch('LISTEN_FOR_FILE_SAVED_SUCCESSFULLY')
|
||||||
|
dispatch('LINTEN_FOR_SET_LINE_ENDING')
|
||||||
|
dispatch('LISTEN_FOR_NOTIFICATION')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="bottom-status"
|
class="bottom-status"
|
||||||
|
:class="{'error': error}"
|
||||||
v-show="showStatus"
|
v-show="showStatus"
|
||||||
>
|
>
|
||||||
<div class="status-wrapper">
|
<div class="status-wrapper">
|
||||||
<span class="message" :class="{'error': error}">{{ message }}</span>
|
<span class="message" :title="message">{{ message }}</span>
|
||||||
<span class="yes" v-show="showYes" @click="handleYesClick">[ Y ]</span>
|
<span class="yes" v-show="showYes" @click="handleYesClick">[ Y ]</span>
|
||||||
<span class="no" @click="close(true)">[ X ]</span>
|
<span class="no" @click="close(true)">[ X ]</span>
|
||||||
</div>
|
</div>
|
||||||
@ -31,10 +32,15 @@
|
|||||||
this.error = true
|
this.error = true
|
||||||
this.message = msg
|
this.message = msg
|
||||||
})
|
})
|
||||||
bus.$on('status-message', msg => {
|
bus.$on('status-message', (msg, timeout) => {
|
||||||
this.showStatus = true
|
this.showStatus = true
|
||||||
this.error = false
|
this.error = false
|
||||||
this.message = msg
|
this.message = msg
|
||||||
|
if (timeout) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.close(true)
|
||||||
|
}, timeout)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
bus.$on('status-promote', (msg, eventId) => {
|
bus.$on('status-promote', (msg, eventId) => {
|
||||||
this.showStatus = true
|
this.showStatus = true
|
||||||
@ -72,12 +78,17 @@
|
|||||||
.bottom-status {
|
.bottom-status {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 25px;
|
height: 25px;
|
||||||
|
background-color: #2196F3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.bottom-status.error {
|
||||||
|
background-color: #F44336;
|
||||||
}
|
}
|
||||||
.status-wrapper {
|
.status-wrapper {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 25px;
|
line-height: 25px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
color: rgb(136, 170, 204);
|
color: #fff;
|
||||||
}
|
}
|
||||||
.message {
|
.message {
|
||||||
max-width: 70%;
|
max-width: 70%;
|
||||||
@ -89,12 +100,9 @@
|
|||||||
.message, .yes {
|
.message, .yes {
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
.message.error {
|
|
||||||
color: #E6A23C;
|
|
||||||
}
|
|
||||||
.yes, .no {
|
.yes, .no {
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
color: rgb(79, 183, 221);
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -5,8 +5,8 @@ export const error = msg => {
|
|||||||
bus.$emit('status-error', msg)
|
bus.$emit('status-error', msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const message = msg => {
|
export const message = (msg, timeout) => {
|
||||||
bus.$emit('status-message', msg)
|
bus.$emit('status-message', msg, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const promote = msg => {
|
export const promote = msg => {
|
||||||
|
@ -30,6 +30,8 @@ const state = {
|
|||||||
isSaved: true,
|
isSaved: true,
|
||||||
markdown: '',
|
markdown: '',
|
||||||
isUtf8BomEncoded: false,
|
isUtf8BomEncoded: false,
|
||||||
|
lineEnding: 'lf', // lf or crlf
|
||||||
|
adjustLineEndingOnSave: false,
|
||||||
cursor: null,
|
cursor: null,
|
||||||
windowActive: true,
|
windowActive: true,
|
||||||
wordCount: {
|
wordCount: {
|
||||||
@ -41,6 +43,11 @@ const state = {
|
|||||||
platform: process.platform
|
platform: process.platform
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getOptionsFromState = state => {
|
||||||
|
const { isUtf8BomEncoded, lineEnding, adjustLineEndingOnSave } = state
|
||||||
|
return { isUtf8BomEncoded, lineEnding, adjustLineEndingOnSave }
|
||||||
|
}
|
||||||
|
|
||||||
const mutations = {
|
const mutations = {
|
||||||
SET_MODE (state, { type, checked }) {
|
SET_MODE (state, { type, checked }) {
|
||||||
state[type] = checked
|
state[type] = checked
|
||||||
@ -67,6 +74,12 @@ const mutations = {
|
|||||||
SET_IS_UTF8_BOM_ENCODED (state, isUtf8BomEncoded) {
|
SET_IS_UTF8_BOM_ENCODED (state, isUtf8BomEncoded) {
|
||||||
state.isUtf8BomEncoded = isUtf8BomEncoded
|
state.isUtf8BomEncoded = isUtf8BomEncoded
|
||||||
},
|
},
|
||||||
|
SET_LINE_ENDING (state, lineEnding) {
|
||||||
|
state.lineEnding = lineEnding
|
||||||
|
},
|
||||||
|
SET_ADJUST_LINE_ENDING_ON_SAVE (state, adjustLineEndingOnSave) {
|
||||||
|
state.adjustLineEndingOnSave = adjustLineEndingOnSave
|
||||||
|
},
|
||||||
SET_WORD_COUNT (state, wordCount) {
|
SET_WORD_COUNT (state, wordCount) {
|
||||||
state.wordCount = wordCount
|
state.wordCount = wordCount
|
||||||
},
|
},
|
||||||
@ -115,11 +128,11 @@ const actions = {
|
|||||||
|
|
||||||
// handle autoSave
|
// handle autoSave
|
||||||
if (autoSave) {
|
if (autoSave) {
|
||||||
const { pathname, markdown, isUtf8BomEncoded } = state
|
const { pathname, markdown } = state
|
||||||
|
const options = getOptionsFromState(state)
|
||||||
if (autoSave && pathname) {
|
if (autoSave && pathname) {
|
||||||
commit('SET_SAVE_STATUS', true)
|
commit('SET_SAVE_STATUS', true)
|
||||||
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, isUtf8BomEncoded })
|
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, options })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -141,31 +154,38 @@ const actions = {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LINTEN_WIN_STATUS ({ commit }) {
|
LINTEN_WIN_STATUS ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::window-active-status', (e, { status }) => {
|
ipcRenderer.on('AGANI::window-active-status', (e, { status }) => {
|
||||||
commit('SET_WIN_STATUS', status)
|
commit('SET_WIN_STATUS', status)
|
||||||
})
|
})
|
||||||
|
ipcRenderer.on('AGANI::req-update-line-ending-menu', e => {
|
||||||
|
const { lineEnding } = state
|
||||||
|
ipcRenderer.send('AGANI::update-line-ending-menu', lineEnding)
|
||||||
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_SAVE ({ commit, state }) {
|
LISTEN_FOR_SAVE ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::ask-file-save', () => {
|
ipcRenderer.on('AGANI::ask-file-save', () => {
|
||||||
const { pathname, markdown, isUtf8BomEncoded } = state
|
const { pathname, markdown } = state
|
||||||
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, isUtf8BomEncoded })
|
const options = getOptionsFromState(state)
|
||||||
|
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, options })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_SAVE_AS ({ commit, state }) {
|
LISTEN_FOR_SAVE_AS ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::ask-file-save-as', () => {
|
ipcRenderer.on('AGANI::ask-file-save-as', () => {
|
||||||
const { pathname, markdown, isUtf8BomEncoded } = state
|
const { pathname, markdown } = state
|
||||||
ipcRenderer.send('AGANI::response-file-save-as', { pathname, markdown, isUtf8BomEncoded })
|
const options = getOptionsFromState(state)
|
||||||
|
ipcRenderer.send('AGANI::response-file-save-as', { pathname, markdown, options })
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_MOVE_TO ({ commit, state }) {
|
LISTEN_FOR_MOVE_TO ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::ask-file-move-to', () => {
|
ipcRenderer.on('AGANI::ask-file-move-to', () => {
|
||||||
const { pathname, markdown, isUtf8BomEncoded } = state
|
const { pathname, markdown } = state
|
||||||
|
const options = getOptionsFromState(state)
|
||||||
if (!pathname) {
|
if (!pathname) {
|
||||||
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, isUtf8BomEncoded })
|
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, options })
|
||||||
} else {
|
} else {
|
||||||
ipcRenderer.send('AGANI::response-file-move-to', { pathname })
|
ipcRenderer.send('AGANI::response-file-move-to', { pathname })
|
||||||
}
|
}
|
||||||
@ -174,9 +194,10 @@ const actions = {
|
|||||||
|
|
||||||
LISTEN_FOR_RENAME ({ commit, state }) {
|
LISTEN_FOR_RENAME ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::ask-file-rename', () => {
|
ipcRenderer.on('AGANI::ask-file-rename', () => {
|
||||||
const { pathname, markdown, isUtf8BomEncoded } = state
|
const { pathname, markdown } = state
|
||||||
|
const options = getOptionsFromState(state)
|
||||||
if (!pathname) {
|
if (!pathname) {
|
||||||
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, isUtf8BomEncoded })
|
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, options })
|
||||||
} else {
|
} else {
|
||||||
bus.$emit('rename')
|
bus.$emit('rename')
|
||||||
}
|
}
|
||||||
@ -200,13 +221,17 @@ const actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_FILE_LOAD ({ commit, state }) {
|
LISTEN_FOR_FILE_LOAD ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::file-loaded', (e, { file, filename, pathname, isUtf8BomEncoded }) => {
|
ipcRenderer.on('AGANI::file-loaded', (e, { file, filename, pathname, options }) => {
|
||||||
|
const { adjustLineEndingOnSave, isUtf8BomEncoded, lineEnding } = options
|
||||||
commit('SET_FILENAME', filename)
|
commit('SET_FILENAME', filename)
|
||||||
commit('SET_PATHNAME', pathname)
|
commit('SET_PATHNAME', pathname)
|
||||||
commit('SET_MARKDOWN', file)
|
commit('SET_MARKDOWN', file)
|
||||||
commit('SET_SAVE_STATUS', true)
|
commit('SET_SAVE_STATUS', true)
|
||||||
commit('SET_IS_UTF8_BOM_ENCODED', isUtf8BomEncoded)
|
commit('SET_IS_UTF8_BOM_ENCODED', isUtf8BomEncoded)
|
||||||
|
commit('SET_LINE_ENDING', lineEnding)
|
||||||
|
commit('SET_ADJUST_LINE_ENDING_ON_SAVE', adjustLineEndingOnSave)
|
||||||
bus.$emit('file-loaded', file)
|
bus.$emit('file-loaded', file)
|
||||||
|
ipcRenderer.send('AGANI::update-line-ending-menu', lineEnding)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -229,7 +254,8 @@ const actions = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
LISTEN_FOR_CONTENT_CHANGE ({ commit, state }, { markdown, wordCount, cursor }) {
|
LISTEN_FOR_CONTENT_CHANGE ({ commit, state }, { markdown, wordCount, cursor }) {
|
||||||
const { pathname, autoSave, markdown: oldMarkdown, isUtf8BomEncoded } = state
|
const { pathname, autoSave, markdown: oldMarkdown } = state
|
||||||
|
const options = getOptionsFromState(state)
|
||||||
commit('SET_MARKDOWN', markdown)
|
commit('SET_MARKDOWN', markdown)
|
||||||
// set word count
|
// set word count
|
||||||
if (wordCount) commit('SET_WORD_COUNT', wordCount)
|
if (wordCount) commit('SET_WORD_COUNT', wordCount)
|
||||||
@ -238,7 +264,7 @@ const actions = {
|
|||||||
// change save status/save to file only when the markdown changed!
|
// change save status/save to file only when the markdown changed!
|
||||||
if (markdown !== oldMarkdown) {
|
if (markdown !== oldMarkdown) {
|
||||||
if (pathname && autoSave) {
|
if (pathname && autoSave) {
|
||||||
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, isUtf8BomEncoded })
|
ipcRenderer.send('AGANI::response-file-save', { pathname, markdown, options })
|
||||||
} else {
|
} else {
|
||||||
commit('SET_SAVE_STATUS', false)
|
commit('SET_SAVE_STATUS', false)
|
||||||
}
|
}
|
||||||
@ -323,13 +349,27 @@ const actions = {
|
|||||||
|
|
||||||
LISTEN_FOR_CLOSE ({ commit, state }) {
|
LISTEN_FOR_CLOSE ({ commit, state }) {
|
||||||
ipcRenderer.on('AGANI::ask-for-close', e => {
|
ipcRenderer.on('AGANI::ask-for-close', e => {
|
||||||
const { isSaved, markdown, pathname, filename, isUtf8BomEncoded } = state
|
const { isSaved, markdown, pathname, filename } = state
|
||||||
|
const options = getOptionsFromState(state)
|
||||||
if (!isSaved && /[^\n]/.test(markdown)) {
|
if (!isSaved && /[^\n]/.test(markdown)) {
|
||||||
ipcRenderer.send('AGANI::response-close-confirm', { filename, pathname, markdown, isUtf8BomEncoded })
|
ipcRenderer.send('AGANI::response-close-confirm', { filename, pathname, markdown, options })
|
||||||
} else {
|
} else {
|
||||||
ipcRenderer.send('AGANI::close-window')
|
ipcRenderer.send('AGANI::close-window')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
LINTEN_FOR_SET_LINE_ENDING ({ commit, state }) {
|
||||||
|
ipcRenderer.on('AGANI::set-line-ending', (e, { lineEnding, ignoreSaveStatus }) => {
|
||||||
|
const { lineEnding: oldLineEnding } = state
|
||||||
|
if (lineEnding !== oldLineEnding) {
|
||||||
|
commit('SET_LINE_ENDING', lineEnding)
|
||||||
|
commit('SET_ADJUST_LINE_ENDING_ON_SAVE', lineEnding !== 'lf')
|
||||||
|
if (!ignoreSaveStatus) {
|
||||||
|
commit('SET_SAVE_STATUS', false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,13 +4,15 @@ import Vuex from 'vuex'
|
|||||||
import editorStore from './editor'
|
import editorStore from './editor'
|
||||||
import aidouStore from './aidou'
|
import aidouStore from './aidou'
|
||||||
import autoUpdates from './autoUpdates'
|
import autoUpdates from './autoUpdates'
|
||||||
|
import notification from './notification'
|
||||||
|
|
||||||
Vue.use(Vuex)
|
Vue.use(Vuex)
|
||||||
|
|
||||||
const storeArray = [
|
const storeArray = [
|
||||||
editorStore,
|
editorStore,
|
||||||
aidouStore,
|
aidouStore,
|
||||||
autoUpdates
|
autoUpdates,
|
||||||
|
notification
|
||||||
]
|
]
|
||||||
|
|
||||||
const { actions, mutations, state } = storeArray.reduce((acc, s) => {
|
const { actions, mutations, state } = storeArray.reduce((acc, s) => {
|
||||||
|
21
src/renderer/store/notification.js
Normal file
21
src/renderer/store/notification.js
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { ipcRenderer } from 'electron'
|
||||||
|
import { error, message } from '../notice'
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
}
|
||||||
|
|
||||||
|
const mutations = {
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = {
|
||||||
|
LISTEN_FOR_NOTIFICATION ({ commit }) {
|
||||||
|
ipcRenderer.on('AGANI::show-error-notification', (e, msg) => {
|
||||||
|
error(msg)
|
||||||
|
})
|
||||||
|
ipcRenderer.on('AGANI::show-info-notification', (e, { msg, timeout }) => {
|
||||||
|
message(msg, timeout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { state, mutations, actions }
|
@ -6,6 +6,8 @@ Edit and save to update preferences. You can only change the JSON below!
|
|||||||
|
|
||||||
- **autoSave**: *Boolean* `true` or `false`
|
- **autoSave**: *Boolean* `true` or `false`
|
||||||
|
|
||||||
|
- **endOfLine**: *String* `lf`, `crlf` or `default`
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"fontSize": "16px",
|
"fontSize": "16px",
|
||||||
@ -19,7 +21,8 @@ Edit and save to update preferences. You can only change the JSON below!
|
|||||||
"preferLooseListItem": true,
|
"preferLooseListItem": true,
|
||||||
"autoPairBracket": true,
|
"autoPairBracket": true,
|
||||||
"autoPairMarkdownSyntax": true,
|
"autoPairMarkdownSyntax": true,
|
||||||
"autoPairQuote": true
|
"autoPairQuote": true,
|
||||||
|
"endOfLine": "default"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user