diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 74d49a63..eaf73f4c 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,6 +1,15 @@ -### 0.7.18 +### 0.8.4 -**Bug fix** +**:cactus:Feature** + +- Add user preferences in `Mark Text menu`, the shoutcut is `CmdorCtrl + ,`, you can set the default `theme` and `autoSave`. +- Add `autoSave` to `file menu`, the default value is in `preferences.md` which you can open in `Mark Text menu`. #45 + +**:butterfly:Optimization** + +- Theme can be saved in user preferences now #16 + +**:beetle:Bug fix** - fix: prevent open image or file directly when drag and drop over Mark Text #42 - fix: set theme to all the open window not just the active one. diff --git a/package.json b/package.json index d00c3452..bfdb58bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "marktext", - "version": "0.7.17", + "version": "0.8.4", "author": "Jocs ", "description": "Next generation markdown editor", "license": "MIT", diff --git a/src/main/actions/file.js b/src/main/actions/file.js index 17078a6b..5f3bdcf1 100644 --- a/src/main/actions/file.js +++ b/src/main/actions/file.js @@ -6,6 +6,7 @@ import path from 'path' import { app, dialog, ipcMain, BrowserWindow } from 'electron' import createWindow, { windows } from '../createWindow' import { EXTENSIONS, EXTENSION_HASN } from '../config' +import { getUserPreference, setUserPreference } from '../utils' const watchAndReload = (pathname, win) => { // when i build, and failed. // const watcher = chokidar.watch(pathname, { @@ -114,6 +115,13 @@ ipcMain.on('AGANI::response-close-confirm', (e, { filename, pathname, markdown } }) ipcMain.on('AGANI::response-file-save', handleResponseForSave) + +ipcMain.on('AGANI::ask-for-auto-save', e => { + const win = BrowserWindow.fromWebContents(e.sender) + const { autoSave } = getUserPreference() + win.webContents.send('AGANI::auto-save', autoSave) +}) + ipcMain.on('AGANI::response-export', handleResponseForExport) ipcMain.on('AGANI::close-window', e => { @@ -155,6 +163,15 @@ export const saveAs = win => { win.webContents.send('AGANI::ask-file-save-as') } -export const autoSave = (menuItem, win) => { - // TODO +export const autoSave = (menuItem, browserWindow) => { + const { checked } = menuItem + setUserPreference('autoSave', checked) + .then(() => { + for (const win of windows.values()) { + win.webContents.send('AGANI::auto-save', checked) + } + }) + .catch(err => { + console.log(err) + }) } diff --git a/src/main/actions/theme.js b/src/main/actions/theme.js index d50362ca..93b3f8d0 100644 --- a/src/main/actions/theme.js +++ b/src/main/actions/theme.js @@ -1,7 +1,7 @@ import fs from 'fs' import path from 'path' import { ipcMain } from 'electron' -import { getMenuItem } from '../utils' +import { getMenuItem, setUserPreference } from '../utils' import { windows } from '../createWindow' /** * Set `__static` path to static files in production @@ -17,9 +17,15 @@ const THEME_PATH = path.join(__static, '/themes') const themeCSS = {} export const selectTheme = (theme, themeCSS) => { - for (const win of windows.values()) { - win.webContents.send('AGANI::theme', { theme, themeCSS }) - } + setUserPreference('theme', theme) + .then(() => { + for (const win of windows.values()) { + win.webContents.send('AGANI::theme', { theme, themeCSS }) + } + }) + .catch(err => { + console.log(err) + }) } const getSelectTheme = () => { diff --git a/src/main/config.js b/src/main/config.js index e2e18b00..80cfeaab 100644 --- a/src/main/config.js +++ b/src/main/config.js @@ -26,7 +26,7 @@ export const EXTENSION_HASN = { pdf: '.pdf' } -export const DEFAULT_THEME = 'dark' +// export const DEFAULT_THEME = 'dark' export const VIEW_MENU_ITEM = { 'Source Code Mode': false, diff --git a/src/main/menus/file.js b/src/main/menus/file.js index 1fdd517f..e17a8d1b 100755 --- a/src/main/menus/file.js +++ b/src/main/menus/file.js @@ -1,4 +1,7 @@ import * as actions from '../actions/file' +import { getUserPreference } from '../utils' + +const { autoSave } = getUserPreference() export default { label: 'File', @@ -37,9 +40,10 @@ export default { } }, { label: 'Auto Save', - type: 'radio', + type: 'checkbox', + checked: autoSave, click (menuItem, browserWindow) { - actions.autoSave(browserWindow) + actions.autoSave(menuItem, browserWindow) } }, { type: 'separator' diff --git a/src/main/menus/theme.js b/src/main/menus/theme.js index 70a372c3..7febd9bb 100644 --- a/src/main/menus/theme.js +++ b/src/main/menus/theme.js @@ -1,18 +1,21 @@ import * as actions from '../actions/theme' +import { getUserPreference } from '../utils' + +const { theme } = getUserPreference() export default { label: 'Theme', submenu: [{ label: 'Dark', type: 'radio', - checked: false, + checked: theme === 'dark', click (menuItem, browserWindow) { actions.selectTheme('dark') } }, { label: 'Light', type: 'radio', - checked: true, + checked: theme === 'light', click (menuItem, browserWindow) { actions.selectTheme('light') } diff --git a/src/main/utils.js b/src/main/utils.js index a83ee3bb..a9b98619 100644 --- a/src/main/utils.js +++ b/src/main/utils.js @@ -1,6 +1,47 @@ +import fs from 'fs' +import path from 'path' import { Menu } from 'electron' +const JSON_REG = /```json(.+)```/g +const preferencePath = path.join(__static, 'preference.md') +let PREFERENCE_CACHE = null + export const getMenuItem = menuName => { const menus = Menu.getApplicationMenu() return menus.items.find(menu => menu.label === menuName) } + +export const getUserPreference = () => { + if (PREFERENCE_CACHE) { + return PREFERENCE_CACHE + } else { + const content = fs.readFileSync(preferencePath, 'utf-8') + try { + const userSetting = JSON_REG.exec(content.replace(/\n/g, ''))[1] + PREFERENCE_CACHE = JSON.parse(userSetting) + return PREFERENCE_CACHE + } catch (err) { + // todo notice the user + console.log(err) + } + } +} + +export const setUserPreference = (key, value) => { + const preUserSetting = getUserPreference() + const newUserSetting = PREFERENCE_CACHE = Object.assign({}, preUserSetting, { [key]: value }) + + return new Promise((resolve, reject) => { + const content = fs.readFileSync(preferencePath, 'utf-8') + const tokens = content.split('```') + const newContent = tokens[0] + + '```json\n' + + JSON.stringify(newUserSetting, null, 2) + + '\n```' + + tokens[2] + fs.writeFile(preferencePath, newContent, 'utf-8', err => { + if (err) reject(err) + else resolve(newUserSetting) + }) + }) +} diff --git a/src/renderer/app.vue b/src/renderer/app.vue index ea2092df..ccf32cea 100644 --- a/src/renderer/app.vue +++ b/src/renderer/app.vue @@ -7,6 +7,7 @@ :word-count="wordCount" :theme="theme" :platform="platform" + :is-saved="isSaved" > {{ filename }} +
@@ -48,7 +49,8 @@ active: Boolean, wordCount: Object, theme: String, - platform: String + platform: String, + isSaved: Boolean }, computed: { paths () { @@ -81,7 +83,10 @@ }, handleMenuClick () { const win = remote.getCurrentWindow() - remote.Menu.getApplicationMenu().popup({window: win, x: 23, y: 20}) + remote + .Menu + .getApplicationMenu() + .popup({ window: win, x: 23, y: 20 }) } } } @@ -121,9 +126,25 @@ text-align: center; transition: all .25s ease-in-out; } + + .active .save-dot { + margin-left: 3px; + width: 7px; + height: 7px; + display: inline-block; + border-radius: 50%; + background: rgba(242, 134, 94, .7); + visibility: hidden; + } + .active .save-dot.show { + visibility: visible; + } .title:hover { color: #303133; } + .title:hover .save-dot { + background: rgb(242, 134, 94); + } .right-toolbar { padding: 0 10px; height: 100%; diff --git a/src/renderer/store/editor.js b/src/renderer/store/editor.js index 01ce2fad..2d555135 100644 --- a/src/renderer/store/editor.js +++ b/src/renderer/store/editor.js @@ -16,6 +16,7 @@ const state = { sourceCode: false, // source code mode pathname: '', isSaved: true, + autoSave: false, markdown: '', cursor: null, windowActive: true, @@ -51,7 +52,7 @@ const mutations = { window.__dirname = path.dirname(pathname) state.pathname = pathname }, - SET_STATUS (state, status) { + SET_SAVE_STATUS (state, status) { state.isSaved = status }, SET_MARKDOWN (state, markdown) { @@ -62,6 +63,9 @@ const mutations = { }, SET_CURSOR (state, cursor) { state.cursor = cursor + }, + SET_AUTO_SAVE (state, autoSave) { + state.autoSave = autoSave } } @@ -72,9 +76,24 @@ const actions = { commit('SET_THEME', themes) }) }, + + ASK_FOR_AUTO_SAVE ({ commit, state }) { + ipcRenderer.send('AGANI::ask-for-auto-save') + ipcRenderer.on('AGANI::auto-save', (e, autoSave) => { + const { pathname, markdown } = state + commit('SET_AUTO_SAVE', autoSave) + + if (autoSave && pathname) { + commit('SET_SAVE_STATUS', true) + ipcRenderer.send('AGANI::response-file-save', { pathname, markdown }) + } + }) + }, + SEARCH ({ commit }, value) { commit('SET_SEARCH', value) }, + ASK_FOR_MODE ({ commit }) { ipcRenderer.send('AGANI::ask-for-mode') ipcRenderer.on('AGANI::res-for-mode', (e, modes) => { @@ -86,70 +105,86 @@ const actions = { }) }) }, + LINTEN_WIN_STATUS ({ commit }) { ipcRenderer.on('AGANI::window-active-status', (e, { status }) => { commit('SET_WIN_STATUS', status) }) }, + LISTEN_FOR_SAVE ({ commit, state }) { ipcRenderer.on('AGANI::ask-file-save', () => { const { pathname, markdown } = state ipcRenderer.send('AGANI::response-file-save', { pathname, markdown }) + commit('SET_SAVE_STATUS', true) }) }, + LISTEN_FOR_SAVE_AS ({ commit, state }) { ipcRenderer.on('AGANI::ask-file-save-as', () => { const { pathname, markdown } = state ipcRenderer.send('AGANI::response-file-save-as', { pathname, markdown }) }) }, + GET_FILENAME ({ commit, state }) { ipcRenderer.on('AGANI::set-pathname', (e, { pathname, filename }) => { commit('SET_FILENAME', filename) commit('SET_PATHNAME', pathname) - commit('SET_STATUS', true) + commit('SET_SAVE_STATUS', true) }) }, + LISTEN_FOR_FILE_LOAD ({ commit, state }) { ipcRenderer.on('AGANI::file-loaded', (e, { file, filename, pathname }) => { commit('SET_FILENAME', filename) commit('SET_PATHNAME', pathname) commit('SET_MARKDOWN', file) - commit('SET_STATUS', true) + commit('SET_SAVE_STATUS', true) bus.$emit('file-loaded', file) }) }, + LISTEN_FOR_FILE_CHANGE ({ commit, state }) { ipcRenderer.on('AGANI::file-change', (e, { file, filename, pathname }) => { const { windowActive } = state commit('SET_FILENAME', filename) commit('SET_PATHNAME', pathname) commit('SET_MARKDOWN', file) - commit('SET_STATUS', true) + commit('SET_SAVE_STATUS', true) if (!windowActive) { bus.$emit('file-loaded', file) } }) }, + EDITE_FILE ({ commit }) { - commit('SET_STATUS', false) + commit('SET_SAVE_STATUS', false) }, + EXPORT ({ commit, state }, { type, content }) { const { filename, pathname } = state ipcRenderer.send('AGANI::response-export', { type, content, filename, pathname }) }, + SAVE_FILE ({ commit, state }, { markdown, wordCount, cursor }) { + const { pathname, autoSave, markdown: oldMarkdown } = state commit('SET_MARKDOWN', markdown) + // set word count if (wordCount) commit('SET_WORD_COUNT', wordCount) + // set cursor if (cursor) commit('SET_CURSOR', cursor) - const { pathname } = state - if (pathname) { - commit('SET_STATUS', true) - ipcRenderer.send('AGANI::response-file-save', { pathname, markdown }) - } else { - commit('SET_STATUS', false) + // save to file only when the markdown changed! + if (markdown !== oldMarkdown) { + if (pathname && autoSave) { + commit('SET_SAVE_STATUS', true) + ipcRenderer.send('AGANI::response-file-save', { pathname, markdown }) + } else { + commit('SET_SAVE_STATUS', false) + } } }, + SELECTION_CHANGE ({ commit }, changes) { const { start, end } = changes if (start.key === end.key && start.block.text) { @@ -163,15 +198,18 @@ const actions = { ipcRenderer.send('AGANI::selection-change', changes) }, + SELECTION_FORMATS ({ commit }, formats) { ipcRenderer.send('AGANI::selection-formats', formats) }, + // listen for export from main process LISTEN_FOR_EXPORT ({ commit, state }) { ipcRenderer.on('AGANI::export', (e, { type }) => { bus.$emit('export', type) }) }, + LISTEN_FOR_INSERT_IMAGE ({ commit, state }) { ipcRenderer.on('AGANI::INSERT_IMAGE', (e, { filename: imagePath, type }) => { if (type === 'absolute' || type === 'relative') { @@ -186,16 +224,19 @@ const actions = { } }) }, + LISTEN_FOR_EDIT ({ commit }) { ipcRenderer.on('AGANI::edit', (e, { type }) => { bus.$emit(type) }) }, + LISTEN_FOR_VIEW ({ commit }) { ipcRenderer.on('AGANI::view', (e, data) => { commit('SET_MODE', data) }) }, + LISTEN_FOR_PARAGRAPH_INLINE_STYLE ({ commit }) { ipcRenderer.on('AGANI::paragraph', (e, { type }) => { bus.$emit('paragraph', type) @@ -204,6 +245,7 @@ const actions = { bus.$emit('format', type) }) }, + LISTEN_FOR_CLOSE ({ commit, state }) { ipcRenderer.on('AGANI::ask-for-close', e => { const { isSaved, markdown, pathname, filename } = state diff --git a/static/preference.md b/static/preference.md index 07a91a94..e1d976ca 100644 --- a/static/preference.md +++ b/static/preference.md @@ -1,6 +1,10 @@ -#### User Preferences +### :bust_in_silhouette:User Preferences -Edit and save to update preferences: +Edit and save to update preferences, You can only change the json bellow! + +- **theme**: *String* `dark` or `light` + +- **autoSave**: *Boolean* `true` or `false` ```json { @@ -9,4 +13,10 @@ Edit and save to update preferences: } ``` -More user preferences comming. +More user preferences comming soon. + +**Please use `CmdOrCtrl + S` to save your preferences and reload Mark Text to use your setting!** + + + +> Your friends at Mark Text