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:
Felix Häusler 2018-05-09 14:29:40 +02:00 committed by 冉四夕
parent 6cad091f6d
commit dfffc73e69
16 changed files with 305 additions and 100 deletions

View File

@ -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**

View File

@ -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)

View File

@ -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
} }

View File

@ -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/

View File

@ -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
View 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
})
}
})
}

View File

@ -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
}
}

View File

@ -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')
}
}]
}] }]
} }

View File

@ -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)
} }

View File

@ -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>

View File

@ -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>

View File

@ -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 => {

View File

@ -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)
}
}
})
} }
} }

View File

@ -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) => {

View 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 }

View File

@ -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"
} }
``` ```