mirror of
https://github.com/marktext/marktext.git
synced 2025-05-02 12:40:14 +08:00
refactor main source code (#1006)
* refactor main source code * fix invalid file cache entries during startup
This commit is contained in:
parent
b7d51e0d6c
commit
77ff23c2c8
@ -13,7 +13,6 @@ const Multispinner = require('multispinner')
|
||||
|
||||
const mainConfig = require('./webpack.main.config')
|
||||
const rendererConfig = require('./webpack.renderer.config')
|
||||
const webConfig = require('./webpack.web.config')
|
||||
|
||||
const doneLog = chalk.bgGreen.white(' DONE ') + ' '
|
||||
const errorLog = chalk.bgRed.white(' ERROR ') + ' '
|
||||
@ -103,20 +102,6 @@ function pack (config) {
|
||||
})
|
||||
}
|
||||
|
||||
function web () {
|
||||
del.sync(['dist/web/*', '!.gitkeep'])
|
||||
webpack(webConfig, (err, stats) => {
|
||||
if (err || stats.hasErrors()) console.log(err)
|
||||
|
||||
console.log(stats.toString({
|
||||
chunks: false,
|
||||
colors: true
|
||||
}))
|
||||
|
||||
process.exit()
|
||||
})
|
||||
}
|
||||
|
||||
function greeting () {
|
||||
const cols = process.stdout.columns
|
||||
let text = ''
|
||||
|
@ -43,9 +43,9 @@ function startRenderer () {
|
||||
rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)
|
||||
|
||||
const compiler = webpack(rendererConfig)
|
||||
hotMiddleware = webpackHotMiddleware(compiler, {
|
||||
log: false,
|
||||
heartbeat: 2500
|
||||
hotMiddleware = webpackHotMiddleware(compiler, {
|
||||
log: false,
|
||||
heartbeat: 2500
|
||||
})
|
||||
|
||||
compiler.plugin('compilation', compilation => {
|
||||
|
@ -57,6 +57,9 @@ const mainConfig = {
|
||||
new webpack.DefinePlugin(getEnvironmentDefinitions())
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'common': path.join(__dirname, '../src/common')
|
||||
},
|
||||
extensions: ['.js', '.json', '.node']
|
||||
},
|
||||
target: 'electron-main'
|
||||
|
@ -164,6 +164,7 @@ const rendererConfig = {
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.join(__dirname, '../src/renderer'),
|
||||
'common': path.join(__dirname, '../src/common'),
|
||||
'muya': path.join(__dirname, '../src/muya'),
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
},
|
||||
|
@ -1,175 +0,0 @@
|
||||
'use strict'
|
||||
|
||||
process.env.BABEL_ENV = 'web'
|
||||
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin')
|
||||
const MiniCssExtractPlugin = require("mini-css-extract-plugin")
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin')
|
||||
const VueLoaderPlugin = require('vue-loader/lib/plugin')
|
||||
const SpritePlugin = require('svg-sprite-loader/plugin')
|
||||
const postcssPresetEnv = require('postcss-preset-env')
|
||||
|
||||
const proMode = process.env.NODE_ENV === 'production'
|
||||
|
||||
const webConfig = {
|
||||
mode: 'development',
|
||||
devtool: '#cheap-module-eval-source-map',
|
||||
entry: {
|
||||
web: path.join(__dirname, '../src/renderer/main.js')
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
enforce: 'pre',
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'eslint-loader',
|
||||
options: {
|
||||
formatter: require('eslint-friendly-formatter')
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /(katex|github\-markdown|prism[\-a-z]*)\.css$/,
|
||||
use: [
|
||||
'to-string-loader',
|
||||
'css-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
exclude: /(katex|github\-markdown|prism[\-a-z]*)\.css$/,
|
||||
use: [
|
||||
proMode ? MiniCssExtractPlugin.loader : 'style-loader',
|
||||
{ loader: 'css-loader', options: { importLoaders: 1 } },
|
||||
{
|
||||
loader: 'postcss-loader', options: {
|
||||
ident: 'postcss',
|
||||
plugins: () => [
|
||||
postcssPresetEnv({
|
||||
stage: 0
|
||||
})
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.html$/,
|
||||
use: 'vue-html-loader'
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: 'babel-loader',
|
||||
include: [ path.resolve(__dirname, '../src/renderer') ],
|
||||
exclude: /node_modules/
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
use: {
|
||||
loader: 'vue-loader',
|
||||
options: {
|
||||
sourceMap: true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'svg-sprite-loader',
|
||||
options: {
|
||||
extract: true,
|
||||
publicPath: '/static/'
|
||||
}
|
||||
},
|
||||
'svgo-loader'
|
||||
]
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 10000,
|
||||
name: 'imgs/[name].[ext]'
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
query: {
|
||||
limit: 100000,
|
||||
name: 'fonts/[name].[ext]'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new SpritePlugin(),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: 'index.html',
|
||||
template: path.resolve(__dirname, '../src/index.ejs'),
|
||||
minify: {
|
||||
collapseWhitespace: true,
|
||||
removeAttributeQuotes: true,
|
||||
removeComments: true
|
||||
},
|
||||
nodeModules: false
|
||||
}),
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.IS_WEB': 'true'
|
||||
}),
|
||||
new webpack.HotModuleReplacementPlugin(),
|
||||
new webpack.NoEmitOnErrorsPlugin(),
|
||||
new VueLoaderPlugin()
|
||||
],
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.join(__dirname, '../dist/web')
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.join(__dirname, '../src/renderer'),
|
||||
'vue$': 'vue/dist/vue.esm.js'
|
||||
},
|
||||
extensions: ['.js', '.vue', '.json', '.css']
|
||||
},
|
||||
target: 'web'
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust webConfig for production settings
|
||||
*/
|
||||
if (proMode) {
|
||||
webConfig.devtool = '#nosources-source-map'
|
||||
webConfig.mode ='production'
|
||||
|
||||
webConfig.plugins.push(
|
||||
new MiniCssExtractPlugin({
|
||||
// Options similar to the same options in webpackOptions.output
|
||||
// both options are optional
|
||||
filename: '[name].[hash].css',
|
||||
chunkFilename: '[id].[hash].css'
|
||||
}),
|
||||
new CopyWebpackPlugin([
|
||||
{
|
||||
from: path.join(__dirname, '../static'),
|
||||
to: path.join(__dirname, '../dist/web/static'),
|
||||
ignore: ['.*']
|
||||
}
|
||||
]),
|
||||
new webpack.LoaderOptionsPlugin({
|
||||
minimize: true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
module.exports = webConfig
|
17
.eslintrc.js
17
.eslintrc.js
@ -15,7 +15,9 @@ module.exports = {
|
||||
extends: [
|
||||
'standard',
|
||||
'eslint:recommended',
|
||||
'plugin:vue/base' // 'plugin:vue/essential'
|
||||
'plugin:vue/base',
|
||||
'plugin:import/errors',
|
||||
'plugin:import/warnings'
|
||||
],
|
||||
globals: {
|
||||
__static: true
|
||||
@ -33,5 +35,18 @@ module.exports = {
|
||||
'no-console': 0,
|
||||
// allow debugger during development
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0
|
||||
},
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
alias: {
|
||||
map: [
|
||||
['common', './src/common'],
|
||||
// Normally only valid for renderer/
|
||||
['@', './src/renderer'],
|
||||
['muya', './src/muya']
|
||||
],
|
||||
extensions: ['.js', '.vue', '.json', '.css', '.node']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
41
doc/wip/ipc.md
Normal file
41
doc/wip/ipc.md
Normal file
@ -0,0 +1,41 @@
|
||||
# Inter-Process Communication (IPC)
|
||||
|
||||
[Electron](https://electronjs.org/docs/api/ipc-main) provides `ipcMain` and `ipcRenderer` to communicate asynchronously between the main process and renderer processes. The event name/channel must be prefixed with `mt::` (previously `AGANI::`) if used between main process and renderer processes. The default argument list will be `(event, ...args)`. The event name/channel is not prefixed when using `ipcMain` to emit events to the main process directly and emitted events don't have an `event` parameter. The parameter list will only be `(...args)`! When simulate a renderer event you must specify a [event](https://electronjs.org/docs/api/ipc-main#event-object) parameter (`null` or `undefined` may lead to unexpected exceptions).
|
||||
|
||||
## Examples
|
||||
|
||||
Listening to a renderer event in the main process:
|
||||
|
||||
```js
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
// Listen for renderer events
|
||||
ipcMain.on('mt::some-event-name', (event, arg1, arg2) => {
|
||||
// ...
|
||||
|
||||
// Send a direct response to the renderer process
|
||||
event.sender.send('mt::some-event-name-response', 'pong')
|
||||
})
|
||||
|
||||
// Listen for main events
|
||||
ipcMain.on('some-event-name', (arg1, arg2) => {
|
||||
// ...
|
||||
})
|
||||
|
||||
|
||||
ipcMain.emit('some-event-name', 'arg 1', 'arg 2')
|
||||
// ipcMain.emit('mt::some-event-name-response', undefined, 'arg 1', 'arg 2') // crash because event is used
|
||||
```
|
||||
|
||||
Listening to a main event in the renderer process:
|
||||
|
||||
```js
|
||||
import { ipcRenderer } from 'electron'
|
||||
|
||||
// Listen for main events
|
||||
ipcRenderer.on('mt::some-event-name-response', (event, arg1, arg2) => {
|
||||
// ...
|
||||
})
|
||||
|
||||
ipcRenderer.send('mt::some-event-name-response', 'arg 1', 'arg 2')
|
||||
```
|
@ -16,8 +16,8 @@ interface IMarkdownDocumentRaw
|
||||
// Full path (may be empty?)
|
||||
pathname: string,
|
||||
|
||||
// Indicates whether the document is UTF8 or UTF8-DOM encoded.
|
||||
isUtf8BomEncoded: boolean,
|
||||
// Document encoding
|
||||
encoding: string,
|
||||
// "lf" or "crlf"
|
||||
lineEnding: string,
|
||||
// Convert document ("lf") to `lineEnding` when saving
|
||||
@ -25,9 +25,6 @@ interface IMarkdownDocumentRaw
|
||||
|
||||
// Whether the document has mixed line endings (lf and crlf) and was converted to lf.
|
||||
isMixedLineEndings: boolean
|
||||
|
||||
// TODO(refactor:renderer/editor): Remove this entry! This should be loaded separately if needed.
|
||||
textDirection: boolean
|
||||
}
|
||||
```
|
||||
|
||||
@ -45,8 +42,20 @@ interface IMarkdownDocument
|
||||
// Full path (may be empty?)
|
||||
pathname: string,
|
||||
|
||||
// Indicates whether the document is UTF8 or UTF8-DOM encoded.
|
||||
isUtf8BomEncoded: boolean,
|
||||
// Document encoding
|
||||
encoding: string,
|
||||
// "lf" or "crlf"
|
||||
lineEnding: string,
|
||||
// Convert document ("lf") to `lineEnding` when saving
|
||||
adjustLineEndingOnSave: boolean
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
interface IMarkdownDocumentOptions
|
||||
{
|
||||
// Document encoding
|
||||
encoding: string,
|
||||
// "lf" or "crlf"
|
||||
lineEnding: string,
|
||||
// Convert document ("lf") to `lineEnding` when saving
|
||||
@ -65,10 +74,9 @@ interface IDocumentState
|
||||
pathname: string,
|
||||
filename: string,
|
||||
markdown: string,
|
||||
isUtf8BomEncoded: boolean,
|
||||
encoding: string,
|
||||
lineEnding: string,
|
||||
adjustLineEndingOnSave: boolean,
|
||||
textDirection: string,
|
||||
history: {
|
||||
stack: Array<any>,
|
||||
index: number
|
||||
|
@ -165,6 +165,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@hfelix/electron-localshortcut": "^3.1.1",
|
||||
"arg": "^4.1.0",
|
||||
"axios": "^0.18.0",
|
||||
"chokidar": "^2.1.5",
|
||||
"codemirror": "^5.46.0",
|
||||
@ -174,6 +175,7 @@
|
||||
"dompurify": "^1.0.10",
|
||||
"dragula": "^3.7.2",
|
||||
"electron-is-accelerator": "^0.1.2",
|
||||
"electron-log": "^3.0.5",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"element-resize-detector": "^1.2.0",
|
||||
"element-ui": "^2.8.2",
|
||||
@ -233,6 +235,7 @@
|
||||
"eslint": "^5.16.0",
|
||||
"eslint-config-standard": "^12.0.0",
|
||||
"eslint-friendly-formatter": "^4.0.1",
|
||||
"eslint-import-resolver-alias": "^1.1.2",
|
||||
"eslint-loader": "^2.1.2",
|
||||
"eslint-plugin-html": "^4.0.6",
|
||||
"eslint-plugin-import": "^2.16.0",
|
||||
|
50
src/common/envPaths.js
Normal file
50
src/common/envPaths.js
Normal file
@ -0,0 +1,50 @@
|
||||
import path from 'path'
|
||||
|
||||
class EnvPaths {
|
||||
|
||||
/**
|
||||
* @param {string} userDataPath The user data path.
|
||||
* @returns
|
||||
*/
|
||||
constructor (userDataPath) {
|
||||
const currentDate = new Date()
|
||||
if (!userDataPath) {
|
||||
throw new Error('"userDataPath" is not set.')
|
||||
}
|
||||
|
||||
this._electronUserDataPath = userDataPath // path.join(userDataPath, 'electronUserData')
|
||||
this._userDataPath = userDataPath
|
||||
this._logPath = path.join(this._userDataPath, 'logs', `${currentDate.getFullYear()}${currentDate.getMonth() + 1}`)
|
||||
this._preferencesPath = userDataPath // path.join(this._userDataPath, 'preferences')
|
||||
|
||||
this._preferencesFilePath = path.join(this._preferencesPath, 'preference.md')
|
||||
|
||||
// TODO(sessions): enable this...
|
||||
// this._globalStorage = path.join(this._userDataPath, 'globalStorage')
|
||||
// this._preferencesPath = path.join(this._userDataPath, 'preferences')
|
||||
// this._sessionsPath = path.join(this._userDataPath, 'sessions')
|
||||
}
|
||||
|
||||
get electronUserDataPath () {
|
||||
// This path is identical to app.getPath('userData') but userDataPath must not necessarily be the same path.
|
||||
return this._electronUserDataPath
|
||||
}
|
||||
|
||||
get userDataPath () {
|
||||
return this._userDataPath
|
||||
}
|
||||
|
||||
get logPath () {
|
||||
return this._logPath
|
||||
}
|
||||
|
||||
get preferencesPath () {
|
||||
return this._preferencesPath
|
||||
}
|
||||
|
||||
get preferencesFilePath () {
|
||||
return this._preferencesFilePath
|
||||
}
|
||||
}
|
||||
|
||||
export default EnvPaths
|
@ -1,13 +0,0 @@
|
||||
import { log } from '../utils'
|
||||
import userPreference from '../preference'
|
||||
import appWindow from '../window'
|
||||
|
||||
export const selectTheme = (theme, themeCSS) => {
|
||||
userPreference.setItem('theme', theme)
|
||||
.then(() => {
|
||||
for (const { win } of appWindow.windows.values()) {
|
||||
win.webContents.send('AGANI::user-preference', { theme })
|
||||
}
|
||||
})
|
||||
.catch(log)
|
||||
}
|
131
src/main/app.js
131
src/main/app.js
@ -1,131 +0,0 @@
|
||||
import { app, systemPreferences } from 'electron'
|
||||
import appWindow from './window'
|
||||
import { isOsx } from './config'
|
||||
import { dockMenu } from './menus'
|
||||
import { isDirectory, isMarkdownFileOrLink, getMenuItemById, normalizeAndResolvePath } from './utils'
|
||||
import { watchers } from './utils/imagePathAutoComplement'
|
||||
import { selectTheme } from './actions/theme'
|
||||
import preference from './preference'
|
||||
|
||||
class App {
|
||||
constructor () {
|
||||
this.openFilesCache = []
|
||||
}
|
||||
|
||||
init () {
|
||||
// Enable these features to use `backdrop-filter` css rules!
|
||||
if (isOsx) {
|
||||
app.commandLine.appendSwitch('enable-experimental-web-platform-features', 'true')
|
||||
}
|
||||
|
||||
app.on('open-file', this.openFile)
|
||||
|
||||
app.on('ready', this.ready)
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
app.removeListener('open-file', this.openFile)
|
||||
// close all the image path watcher
|
||||
for (const watcher of watchers.values()) {
|
||||
watcher.close()
|
||||
}
|
||||
if (!isOsx) {
|
||||
appWindow.clear()
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => {
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (appWindow.windows.size === 0) {
|
||||
this.ready()
|
||||
}
|
||||
})
|
||||
|
||||
// Prevent to load webview and opening links or new windows via HTML/JS.
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('will-attach-webview', (event, webPreferences, params) => {
|
||||
console.warn('Prevented webview creation.')
|
||||
event.preventDefault()
|
||||
})
|
||||
contents.on('will-navigate', event => {
|
||||
console.warn('Prevented opening a link.')
|
||||
event.preventDefault()
|
||||
})
|
||||
contents.on('new-window', (event, url) => {
|
||||
console.warn('Prevented opening a new window.')
|
||||
event.preventDefault()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ready = () => {
|
||||
if (!isOsx && process.argv.length >= 2) {
|
||||
for (const arg of process.argv) {
|
||||
if (arg.startsWith('--')) {
|
||||
continue
|
||||
} else if (isDirectory(arg) || isMarkdownFileOrLink(arg)) {
|
||||
// Normalize and resolve the path or link target.
|
||||
const resolved = normalizeAndResolvePath(arg)
|
||||
if (resolved) {
|
||||
// TODO: Allow to open multiple files.
|
||||
this.openFilesCache = [ resolved ]
|
||||
break
|
||||
} else {
|
||||
console.error(`[ERROR] Cannot resolve "${arg}".`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set dock on macOS
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.setMenu(dockMenu)
|
||||
|
||||
// Listen for system theme change and change Mark Text own `dark` and `light`.
|
||||
// In macOS 10.14 Mojave, Apple introduced a new system-wide dark mode for
|
||||
// all macOS computers.
|
||||
systemPreferences.subscribeNotification(
|
||||
'AppleInterfaceThemeChangedNotification',
|
||||
() => {
|
||||
const { theme } = preference.getAll()
|
||||
let setedTheme = null
|
||||
if (systemPreferences.isDarkMode() && theme !== 'dark') {
|
||||
selectTheme('dark')
|
||||
setedTheme = 'dark'
|
||||
}
|
||||
if (!systemPreferences.isDarkMode() && theme === 'dark') {
|
||||
selectTheme('light')
|
||||
setedTheme = 'light'
|
||||
}
|
||||
if (setedTheme) {
|
||||
const themeMenu = getMenuItemById('themeMenu')
|
||||
const menuItem = themeMenu.submenu.items.filter(item => (item.id === setedTheme))[0]
|
||||
if (menuItem) {
|
||||
menuItem.checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (this.openFilesCache.length) {
|
||||
this.openFilesCache.forEach(path => appWindow.createWindow(path))
|
||||
this.openFilesCache.length = 0 // empty the open file path cache
|
||||
} else {
|
||||
appWindow.createWindow()
|
||||
}
|
||||
}
|
||||
|
||||
openFile = (event, path) => {
|
||||
const { openFilesCache } = this
|
||||
event.preventDefault()
|
||||
if (app.isReady()) {
|
||||
appWindow.createWindow(path)
|
||||
} else {
|
||||
openFilesCache.push(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
23
src/main/app/accessor.js
Normal file
23
src/main/app/accessor.js
Normal file
@ -0,0 +1,23 @@
|
||||
import WindowManager from '../app/windowManager'
|
||||
import Preference from '../preferences'
|
||||
import Keybindings from '../keyboard/shortcutHandler'
|
||||
import AppMenu from '../menu'
|
||||
|
||||
class Accessor {
|
||||
|
||||
/**
|
||||
* @param {AppEnvironment} appEnvironment The application environment instance.
|
||||
*/
|
||||
constructor(appEnvironment) {
|
||||
const userDataPath = appEnvironment.paths.userDataPath
|
||||
|
||||
this.env = appEnvironment
|
||||
this.paths = appEnvironment.paths // export paths to make it better accessible
|
||||
this.preferences = new Preference(this.paths)
|
||||
this.keybindings = new Keybindings(userDataPath)
|
||||
this.menu = new AppMenu(this.preferences, this.keybindings, userDataPath)
|
||||
this.windowManager = new WindowManager(this.menu, this.preferences)
|
||||
}
|
||||
}
|
||||
|
||||
export default Accessor
|
91
src/main/app/env.js
Normal file
91
src/main/app/env.js
Normal file
@ -0,0 +1,91 @@
|
||||
import path from 'path'
|
||||
import AppPaths, { ensureAppDirectoriesSync } from './paths'
|
||||
|
||||
let envId = 0
|
||||
|
||||
const patchEnvPath = () => {
|
||||
if (process.platform === 'darwin') {
|
||||
process.env.PATH += (process.env.PATH.endsWith(path.delimiter) ? '' : path.delimiter) + '/Library/TeX/texbin'
|
||||
}
|
||||
}
|
||||
|
||||
export class AppEnvironment {
|
||||
|
||||
constructor (options) {
|
||||
this._id = envId++
|
||||
this._appPaths = new AppPaths(options.userDataPath)
|
||||
this._debug = !!options.debug
|
||||
this._verbose = !!options.verbose
|
||||
this._safeMode = !!options.safeMode
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unique identifier that can be used with IPC to identify messages from this environment.
|
||||
*
|
||||
* @returns {number} Returns an unique identifier.
|
||||
*/
|
||||
get id() {
|
||||
return this._id
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {AppPaths}
|
||||
*/
|
||||
get paths () {
|
||||
return this._appPaths
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get debug () {
|
||||
return this._debug
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get verbose () {
|
||||
return this._verbose
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get safeMode () {
|
||||
return this._safeMode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a (global) application environment instance and bootstraps the application.
|
||||
*
|
||||
* @param {arg.Result} args The parsed application arguments.
|
||||
* @returns {AppEnvironment} The current (global) environment.
|
||||
*/
|
||||
const setupEnvironment = args => {
|
||||
patchEnvPath()
|
||||
|
||||
const debug = args['--debug'] || !!process.env.MARKTEXT_DEBUG || process.env.NODE_ENV !== 'production'
|
||||
const verbose = args['--verbose'] || 0
|
||||
const safeMode = args['--safe']
|
||||
const userDataPath = args['--user-data-dir'] // or null (= default user data path)
|
||||
|
||||
const appEnvironment = new AppEnvironment({
|
||||
debug,
|
||||
verbose,
|
||||
safeMode,
|
||||
userDataPath
|
||||
})
|
||||
|
||||
ensureAppDirectoriesSync(appEnvironment.paths)
|
||||
|
||||
// Keep this for easier access.
|
||||
global.MARKTEXT_DEBUG = debug
|
||||
global.MARKTEXT_DEBUG_VERBOSE = verbose
|
||||
global.MARKTEXT_SAFE_MODE = safeMode
|
||||
|
||||
return appEnvironment
|
||||
}
|
||||
|
||||
export default setupEnvironment
|
248
src/main/app/index.js
Normal file
248
src/main/app/index.js
Normal file
@ -0,0 +1,248 @@
|
||||
import { app, ipcMain, systemPreferences } from 'electron'
|
||||
import { isOsx } from '../config'
|
||||
import { isDirectory, isMarkdownFileOrLink, normalizeAndResolvePath } from '../filesystem'
|
||||
import { getMenuItemById } from '../menu'
|
||||
import { selectTheme } from '../menu/actions/theme'
|
||||
import { dockMenu } from '../menu/templates'
|
||||
import { watchers } from '../utils/imagePathAutoComplement'
|
||||
import EditorWindow from '../windows/editor'
|
||||
|
||||
class App {
|
||||
|
||||
/**
|
||||
* @param {Accessor} accessor The application accessor for application instances.
|
||||
* @param {arg.Result} args Parsed application arguments.
|
||||
*/
|
||||
constructor (accessor, args) {
|
||||
this._accessor = accessor
|
||||
this._args = args || {_: []}
|
||||
this._openFilesCache = []
|
||||
this._openFilesTimer = null
|
||||
this._windowManager = this._accessor.windowManager
|
||||
|
||||
this._listenForIpcMain()
|
||||
}
|
||||
|
||||
/**
|
||||
* The entry point into the application.
|
||||
*/
|
||||
init () {
|
||||
// Enable these features to use `backdrop-filter` css rules!
|
||||
if (isOsx) {
|
||||
app.commandLine.appendSwitch('enable-experimental-web-platform-features', 'true')
|
||||
}
|
||||
|
||||
app.on('open-file', this.openFile) // macOS only
|
||||
|
||||
app.on('ready', this.ready)
|
||||
|
||||
app.on('window-all-closed', () => {
|
||||
// Close all the image path watcher
|
||||
for (const watcher of watchers.values()) {
|
||||
watcher.close()
|
||||
}
|
||||
this._windowManager.closeWatcher()
|
||||
if (!isOsx) {
|
||||
app.quit()
|
||||
}
|
||||
})
|
||||
|
||||
app.on('activate', () => { // macOS only
|
||||
// On OS X it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
if (this._windowManager.windowCount === 0) {
|
||||
this.ready()
|
||||
}
|
||||
})
|
||||
|
||||
// Prevent to load webview and opening links or new windows via HTML/JS.
|
||||
app.on('web-contents-created', (event, contents) => {
|
||||
contents.on('will-attach-webview', (event, webPreferences, params) => {
|
||||
console.warn('Prevented webview creation.')
|
||||
event.preventDefault()
|
||||
})
|
||||
contents.on('will-navigate', event => {
|
||||
console.warn('Prevented opening a link.')
|
||||
event.preventDefault()
|
||||
})
|
||||
contents.on('new-window', (event, url) => {
|
||||
console.warn('Prevented opening a new window.')
|
||||
event.preventDefault()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
ready = () => {
|
||||
const { _args: args } = this
|
||||
if (!isOsx && args._.length) {
|
||||
for (const pathname of args._) {
|
||||
// Ignore all unknown flags
|
||||
if (pathname.startsWith('--')) {
|
||||
continue
|
||||
}
|
||||
|
||||
const info = this.normalizePath(pathname)
|
||||
if (info) {
|
||||
this._openFilesCache.push(info)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.setMenu(dockMenu)
|
||||
|
||||
// Listen for system theme change and change Mark Text own `dark` and `light`.
|
||||
// In macOS 10.14 Mojave, Apple introduced a new system-wide dark mode for
|
||||
// all macOS computers.
|
||||
systemPreferences.subscribeNotification(
|
||||
'AppleInterfaceThemeChangedNotification',
|
||||
() => {
|
||||
const preferences = this._accessor.preferences
|
||||
const { theme } = preferences.getAll()
|
||||
let setedTheme = null
|
||||
if (systemPreferences.isDarkMode() && theme !== 'dark') {
|
||||
selectTheme('dark')
|
||||
setedTheme = 'dark'
|
||||
}
|
||||
if (!systemPreferences.isDarkMode() && theme === 'dark') {
|
||||
selectTheme('light')
|
||||
setedTheme = 'light'
|
||||
}
|
||||
if (setedTheme) {
|
||||
const themeMenu = getMenuItemById('themeMenu')
|
||||
const menuItem = themeMenu.submenu.items.filter(item => (item.id === setedTheme))[0]
|
||||
if (menuItem) {
|
||||
menuItem.checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (this._openFilesCache.length) {
|
||||
this.openFileCache()
|
||||
} else {
|
||||
this.createEditorWindow()
|
||||
}
|
||||
}
|
||||
|
||||
openFile = (event, pathname) => {
|
||||
event.preventDefault()
|
||||
const info = this.normalizePath(pathname)
|
||||
if (info) {
|
||||
this._openFilesCache.push(info)
|
||||
|
||||
if (app.isReady()) {
|
||||
// It might come more files
|
||||
if (this._openFilesTimer) {
|
||||
clearTimeout(this._openFilesTimer)
|
||||
}
|
||||
this._openFilesTimer = setTimeout(() => {
|
||||
this._openFilesTimer = null
|
||||
this.openFileCache()
|
||||
}, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openFileCache = () => {
|
||||
// TODO: Allow to open multiple files in the same window.
|
||||
this._openFilesCache.forEach(fileInfo => this.createEditorWindow(fileInfo.path))
|
||||
this._openFilesCache.length = 0 // empty the open file path cache
|
||||
}
|
||||
|
||||
normalizePath = pathname => {
|
||||
const isDir = isDirectory(pathname)
|
||||
if (isDir || isMarkdownFileOrLink(pathname)) {
|
||||
// Normalize and resolve the path or link target.
|
||||
const resolved = normalizeAndResolvePath(pathname)
|
||||
if (resolved) {
|
||||
return { isDir, path: resolved }
|
||||
} else {
|
||||
console.error(`[ERROR] Cannot resolve "${pathname}".`)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// --- private --------------------------------
|
||||
|
||||
/**
|
||||
* Creates a new editor window.
|
||||
*
|
||||
* @param {string} [pathname] Path to a file, directory or link.
|
||||
* @param {string} [markdown] Markdown content.
|
||||
* @param {*} [options] BrowserWindow options.
|
||||
*/
|
||||
createEditorWindow (pathname = null, markdown = '', options = {}) {
|
||||
const editor = new EditorWindow(this._accessor)
|
||||
editor.createWindow(pathname, markdown, options)
|
||||
this._windowManager.add(editor)
|
||||
if (this._windowManager.windowCount === 1) {
|
||||
this._accessor.menu.setActiveWindow(editor.id)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(sessions): ...
|
||||
// // Make Mark Text a single instance application.
|
||||
// _makeSingleInstance() {
|
||||
// if (process.mas) return
|
||||
//
|
||||
// app.requestSingleInstanceLock()
|
||||
//
|
||||
// app.on('second-instance', (event, argv, workingDirectory) => {
|
||||
// // // TODO: Get active/last active window and open process arvg etc
|
||||
// // if (currentWindow) {
|
||||
// // if (currentWindow.isMinimized()) currentWindow.restore()
|
||||
// // currentWindow.focus()
|
||||
// // }
|
||||
// })
|
||||
// }
|
||||
|
||||
_listenForIpcMain () {
|
||||
ipcMain.on('app-create-editor-window', () => {
|
||||
this.createEditorWindow()
|
||||
})
|
||||
|
||||
ipcMain.on('app-create-settings-window', () => {
|
||||
const { paths } = this._accessor
|
||||
this.createEditorWindow(paths.preferencesFilePath)
|
||||
})
|
||||
|
||||
// ipcMain.on('app-open-file', filePath => {
|
||||
// const windowId = this._windowManager.getActiveWindow()
|
||||
// ipcMain.emit('app-open-file-by-id', windowId, filePath)
|
||||
// })
|
||||
|
||||
ipcMain.on('app-open-file-by-id', (windowId, filePath) => {
|
||||
const { openFilesInNewWindow } = this._accessor.preferences.getAll()
|
||||
if (openFilesInNewWindow) {
|
||||
this.createEditorWindow(filePath)
|
||||
} else {
|
||||
const editor = this._windowManager.get(windowId)
|
||||
if (editor && !editor.quitting) {
|
||||
editor.openTab(filePath, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('app-open-markdown-by-id', (windowId, data) => {
|
||||
const { openFilesInNewWindow } = this._accessor.preferences.getAll()
|
||||
if (openFilesInNewWindow) {
|
||||
this.createEditorWindow(undefined, data)
|
||||
} else {
|
||||
const editor = this._windowManager.get(windowId)
|
||||
if (editor && !editor.quitting) {
|
||||
editor.openUntitledTab(true, data)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('app-open-directory-by-id', (windowId, pathname) => {
|
||||
// TODO: Open the directory in an existing window if prefered.
|
||||
this.createEditorWindow(pathname)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default App
|
37
src/main/app/paths.js
Normal file
37
src/main/app/paths.js
Normal file
@ -0,0 +1,37 @@
|
||||
import { app } from 'electron'
|
||||
import EnvPaths from 'common/envPaths'
|
||||
import { ensureDirSync } from '../filesystem'
|
||||
|
||||
class AppPaths extends EnvPaths {
|
||||
|
||||
/**
|
||||
* Configure and sets all application paths.
|
||||
*
|
||||
* @param {[string]} userDataPath The user data path or null.
|
||||
* @returns
|
||||
*/
|
||||
constructor (userDataPath='') {
|
||||
if (!userDataPath) {
|
||||
// Use default user data path.
|
||||
userDataPath = app.getPath('userData')
|
||||
}
|
||||
|
||||
// Initialize environment paths
|
||||
super(userDataPath)
|
||||
|
||||
// Changing the user data directory is only allowed during application bootstrap.
|
||||
app.setPath('userData', this._electronUserDataPath)
|
||||
}
|
||||
}
|
||||
|
||||
export const ensureAppDirectoriesSync = paths => {
|
||||
ensureDirSync(paths.userDataPath)
|
||||
ensureDirSync(paths.logPath)
|
||||
// TODO(sessions): enable this...
|
||||
// ensureDirSync(paths.electronUserDataPath)
|
||||
// ensureDirSync(paths.globalStorage)
|
||||
// ensureDirSync(paths.preferencesPath)
|
||||
// ensureDirSync(paths.sessionsPath)
|
||||
}
|
||||
|
||||
export default AppPaths
|
284
src/main/app/windowManager.js
Normal file
284
src/main/app/windowManager.js
Normal file
@ -0,0 +1,284 @@
|
||||
import { app, BrowserWindow, ipcMain } from 'electron'
|
||||
import EventEmitter from 'events'
|
||||
import log from 'electron-log'
|
||||
import Watcher from '../filesystem/watcher'
|
||||
|
||||
/**
|
||||
* A Mark Text window.
|
||||
* @typedef {EditorWindow} IApplicationWindow
|
||||
* @property {number | null} id Identifier (= browserWindow.id) or null during initialization.
|
||||
* @property {Electron.BrowserWindow} browserWindow The browse window.
|
||||
* @property {WindowType} type The window type.
|
||||
*/
|
||||
|
||||
// Currently it makes no sense because we have only one (editor) window but we
|
||||
// will add more windows like settings and worker windows.
|
||||
export const WindowType = {
|
||||
EDITOR: 0
|
||||
}
|
||||
|
||||
class WindowActivityList {
|
||||
constructor() {
|
||||
// Oldest Newest
|
||||
// <number>, ... , <number>
|
||||
this._buf = []
|
||||
}
|
||||
|
||||
getNewest () {
|
||||
const { _buf } = this
|
||||
if (_buf.length) {
|
||||
return _buf[_buf.length - 1]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
setNewest (id) {
|
||||
// I think we do not need a linked list for only a few windows.
|
||||
const { _buf } = this
|
||||
const index = _buf.indexOf(id)
|
||||
if (index !== -1) {
|
||||
const lastIndex = _buf.length - 1
|
||||
if (index === lastIndex) {
|
||||
return
|
||||
}
|
||||
_buf.splice(index, 1)
|
||||
}
|
||||
_buf.push(id)
|
||||
}
|
||||
|
||||
delete (id) {
|
||||
const { _buf } = this
|
||||
const index = _buf.indexOf(id);
|
||||
if (index !== -1) {
|
||||
_buf.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class WindowManager extends EventEmitter {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {AppMenu} appMenu The application menu instance.
|
||||
* @param {Preference} preferences The preference instance.
|
||||
*/
|
||||
constructor(appMenu, preferences) {
|
||||
super()
|
||||
|
||||
this._appMenu = appMenu
|
||||
|
||||
this._activeWindowId = null
|
||||
this._windows = new Map()
|
||||
this._windowActivity = new WindowActivityList()
|
||||
|
||||
// TODO(need::refactor): We should move watcher and search into another process/thread(?)
|
||||
this._watcher = new Watcher(preferences)
|
||||
|
||||
this._listenForIpcMain()
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given window to the window list.
|
||||
*
|
||||
* @param {IApplicationWindow} window The application window. We take ownership!
|
||||
*/
|
||||
add (window) {
|
||||
this._windows.set(window.id, window)
|
||||
if (this.windowCount === 1) {
|
||||
this.setActiveWindow(window.id)
|
||||
}
|
||||
|
||||
const { browserWindow } = window
|
||||
window.on('window-focus', () => {
|
||||
this.setActiveWindow(browserWindow.id)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the application window by id.
|
||||
*
|
||||
* @param {string} windowId The window id.
|
||||
* @returns {IApplicationWindow} The application window or undefined.
|
||||
*/
|
||||
get (windowId) {
|
||||
return this._windows.get(windowId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the BrowserWindow by id.
|
||||
*
|
||||
* @param {string} windowId The window id.
|
||||
* @returns {Electron.BrowserWindow} The window or undefined.
|
||||
*/
|
||||
getBrowserWindow (windowId) {
|
||||
const window = this.get(windowId)
|
||||
if (window) {
|
||||
return window.browserWindow
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given window by id.
|
||||
*
|
||||
* NOTE: All window event listeners are removed!
|
||||
*
|
||||
* @param {string} windowId The window id.
|
||||
* @returns {IApplicationWindow} Returns the application window. We no longer take ownership.
|
||||
*/
|
||||
remove (windowId) {
|
||||
const { _windows } = this
|
||||
const window = this.get(windowId)
|
||||
if (window) {
|
||||
window.removeAllListeners()
|
||||
|
||||
this._windowActivity.delete(windowId)
|
||||
let nextWindowId = this._windowActivity.getNewest()
|
||||
this.setActiveWindow(nextWindowId)
|
||||
|
||||
_windows.delete(windowId)
|
||||
}
|
||||
return window
|
||||
}
|
||||
|
||||
setActiveWindow (windowId) {
|
||||
if (this._activeWindowId !== windowId) {
|
||||
this._activeWindowId = windowId
|
||||
this._windowActivity.setNewest(windowId)
|
||||
if (windowId != null) {
|
||||
// windowId is null when all windows are closed (e.g. when gracefully closed).
|
||||
this._appMenu.setActiveWindow(windowId)
|
||||
}
|
||||
this.emit('activeWindowChanged', windowId)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the active window id or null if no window is registred.
|
||||
* @returns {number|null}
|
||||
*/
|
||||
getActiveWindow () {
|
||||
return this._activeWindowId
|
||||
}
|
||||
|
||||
get windows () {
|
||||
return this._windows
|
||||
}
|
||||
|
||||
get windowCount () {
|
||||
return this._windows.size
|
||||
}
|
||||
|
||||
// --- helper ---------------------------------
|
||||
|
||||
closeWatcher () {
|
||||
this._watcher.clear()
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the browser window and associated application window without asking to save documents.
|
||||
*
|
||||
* @param {Electron.BrowserWindow} browserWindow The browser window.
|
||||
*/
|
||||
forceClose (browserWindow) {
|
||||
if (!browserWindow) {
|
||||
return false
|
||||
}
|
||||
|
||||
const { id } = browserWindow
|
||||
const { _appMenu, _windows } = this
|
||||
|
||||
// Free watchers used by this window
|
||||
this._watcher.unWatchWin(browserWindow)
|
||||
|
||||
// Application clearup and remove listeners
|
||||
_appMenu.removeWindowMenu(id)
|
||||
const window = this.remove(id)
|
||||
|
||||
// Destroy window wrapper and browser window
|
||||
if (window) {
|
||||
window.destroy()
|
||||
} else {
|
||||
log.error('Something went wrong: Cannot find associated application window!')
|
||||
browserWindow.destroy()
|
||||
}
|
||||
|
||||
// Quit application on macOS if not windows are opened.
|
||||
if (_windows.size === 0) {
|
||||
app.quit()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes the application window and associated browser window without asking to save documents.
|
||||
*
|
||||
* @param {number} windowId The application window or browser window id.
|
||||
*/
|
||||
forceCloseById (windowId) {
|
||||
const browserWindow = this.getBrowserWindow(windowId)
|
||||
if (browserWindow) {
|
||||
return this.forceClose(browserWindow)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// --- events ---------------------------------
|
||||
|
||||
_listenForIpcMain () {
|
||||
// listen for file watch from renderer process eg
|
||||
// 1. click file in folder.
|
||||
// 2. new tab and save it.
|
||||
// 3. close tab(s) need unwatch.
|
||||
ipcMain.on('AGANI::file-watch', (e, { pathname, watch }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
if (watch) {
|
||||
// listen for file `change` and `unlink`
|
||||
this._watcher.watch(win, pathname, 'file')
|
||||
} else {
|
||||
// unlisten for file `change` and `unlink`
|
||||
this._watcher.unWatch(win, pathname, 'file')
|
||||
}
|
||||
})
|
||||
|
||||
// Force close a BrowserWindow
|
||||
ipcMain.on('AGANI::close-window', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
this.forceClose(win)
|
||||
})
|
||||
|
||||
// --- local events ---------------
|
||||
|
||||
ipcMain.on('watcher-watch-file', (win, filePath) => {
|
||||
this._watcher.watch(win, filePath, 'file')
|
||||
})
|
||||
ipcMain.on('watcher-watch-directory', (win, pathname) => {
|
||||
this._watcher.watch(win, pathname, 'dir')
|
||||
})
|
||||
ipcMain.on('watcher-unwatch-file', (win, filePath) => {
|
||||
this._watcher.unWatch(win, filePath, 'file')
|
||||
})
|
||||
ipcMain.on('watcher-unwatch-directory', (win, pathname) => {
|
||||
this._watcher.unWatch(win, pathname, 'dir')
|
||||
})
|
||||
|
||||
// Force close a window by id.
|
||||
ipcMain.on('window-close-by-id', id => {
|
||||
this.forceCloseById(id)
|
||||
})
|
||||
|
||||
ipcMain.on('window-toggle-always-on-top', win => {
|
||||
const flag = !win.isAlwaysOnTop()
|
||||
win.setAlwaysOnTop(flag)
|
||||
this._appMenu.updateAlwaysOnTopMenu(flag)
|
||||
})
|
||||
|
||||
ipcMain.on('broadcast-preferences-changed', prefs => {
|
||||
for (const { browserWindow } of this._windows.values()) {
|
||||
browserWindow.webContents.send('AGANI::user-preference', prefs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default WindowManager
|
@ -1,33 +0,0 @@
|
||||
import { dumpKeyboardInformation } from './keyboardUtils'
|
||||
|
||||
for (const arg of process.argv) {
|
||||
if (arg === '--dump-keyboard-layout') {
|
||||
console.log(dumpKeyboardInformation())
|
||||
process.exit(0)
|
||||
} else if (arg === '--debug') {
|
||||
global.MARKTEXT_DEBUG = true
|
||||
continue
|
||||
} else if (arg === '--safe') {
|
||||
global.MARKTEXT_SAFE_MODE = true
|
||||
continue
|
||||
} else if (arg === '--version') {
|
||||
console.log(`Mark Text: ${global.MARKTEXT_VERSION_STRING}
|
||||
Node.js: ${process.versions.node}
|
||||
Electron: ${process.versions.electron}
|
||||
Chromium: ${process.versions.chrome}
|
||||
`)
|
||||
process.exit(0)
|
||||
} else if (arg === '--help') {
|
||||
console.log(`Usage: marktext [commands] [path]
|
||||
|
||||
Available commands:
|
||||
|
||||
--debug Enable debug mode
|
||||
--safe Disable plugins and other user configuration
|
||||
--dump-keyboard-layout Dump keyboard information
|
||||
--version Print version information
|
||||
--help Print this help message
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
63
src/main/cli/index.js
Normal file
63
src/main/cli/index.js
Normal file
@ -0,0 +1,63 @@
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { isDirectory } from '../filesystem'
|
||||
import parseArgs from './parser'
|
||||
import { dumpKeyboardInformation } from '../keyboard'
|
||||
import { getPath } from '../utils'
|
||||
|
||||
const write = s => process.stdout.write(s)
|
||||
const writeLine = s => write(s + '\n')
|
||||
|
||||
const cli = () => {
|
||||
let argv = process.argv.slice(1)
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
// Don't pass electron development arguments to Mark Text and change user data path.
|
||||
argv = [ '--user-data-dir', path.join(getPath('appData'), 'marktext-dev') ]
|
||||
}
|
||||
|
||||
const args = parseArgs(argv, true)
|
||||
if (args['--help']) {
|
||||
write(`Usage: marktext [commands] [path ...]
|
||||
|
||||
Available commands:
|
||||
|
||||
--debug Enable debug mode
|
||||
--safe Disable plugins and other user configuration
|
||||
--dump-keyboard-layout Dump keyboard information
|
||||
--user-data-dir Change the user data directory
|
||||
-v, --verbose Be verbose
|
||||
--version Print version information
|
||||
-h, --help Print this help message
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (args['--version']) {
|
||||
writeLine(`Mark Text: ${global.MARKTEXT_VERSION_STRING}`)
|
||||
writeLine(`Node.js: ${process.versions.node}`)
|
||||
writeLine(`Electron: ${process.versions.electron}`)
|
||||
writeLine(`Chromium: ${process.versions.chrome}`)
|
||||
writeLine(`OS: ${os.type()} ${os.arch()} ${os.release()}`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (args['--dump-keyboard-layout']) {
|
||||
writeLine(dumpKeyboardInformation())
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
// Check for portable mode and ensure the user data path is absolute. We assume
|
||||
// that the path is writable if not this lead to an application crash.
|
||||
if (!args['--user-data-dir']) {
|
||||
const portablePath = path.resolve('marktext-user-data')
|
||||
if (isDirectory(portablePath)) {
|
||||
args['--user-data-dir'] = portablePath
|
||||
}
|
||||
} else {
|
||||
args['--user-data-dir'] = path.resolve(args['--user-data-dir'])
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
export default cli
|
31
src/main/cli/parser.js
Normal file
31
src/main/cli/parser.js
Normal file
@ -0,0 +1,31 @@
|
||||
import arg from 'arg'
|
||||
|
||||
/**
|
||||
* Parse the given arguments or the default program arguments.
|
||||
*
|
||||
* @param {string[]} argv Arguments if null the default program arguments are used.
|
||||
* @param {boolean} permissive If set to false an exception is throw about unknown flags.
|
||||
* @returns {arg.Result} Parsed arguments
|
||||
*/
|
||||
const parseArgs = (argv=null, permissive=true) => {
|
||||
if (argv == null) {
|
||||
argv = process.argv.slice(1)
|
||||
}
|
||||
const spec = {
|
||||
'--debug': Boolean,
|
||||
'--safe': Boolean,
|
||||
'--dump-keyboard-layout': Boolean,
|
||||
|
||||
'--user-data-dir': String,
|
||||
|
||||
// Misc
|
||||
'--help': Boolean,
|
||||
'-h': '--help',
|
||||
'--verbose': arg.COUNT,
|
||||
'-v': '--verbose',
|
||||
'--version': Boolean
|
||||
}
|
||||
return arg(spec, { argv, permissive })
|
||||
}
|
||||
|
||||
export default parseArgs
|
@ -1,11 +1,8 @@
|
||||
import path from 'path'
|
||||
|
||||
export const isOsx = process.platform === 'darwin'
|
||||
export const isWindows = process.platform === 'win32'
|
||||
export const isLinux = process.platform === 'linux'
|
||||
|
||||
export const defaultWinOptions = {
|
||||
icon: path.join(__static, 'logo-96px.png'),
|
||||
minWidth: 450,
|
||||
minHeight: 220,
|
||||
webPreferences: {
|
||||
|
@ -7,7 +7,8 @@
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
import { app, clipboard, crashReporter, dialog, ipcMain } from 'electron'
|
||||
import { log } from './utils'
|
||||
import os from 'os'
|
||||
import log from 'electron-log'
|
||||
import { createAndOpenGitHubIssueUrl } from './utils/createGitHubIssue'
|
||||
|
||||
const EXIT_ON_ERROR = !!process.env.MARKTEXT_EXIT_ON_ERROR
|
||||
@ -15,28 +16,17 @@ const SHOW_ERROR_DIALOG = !process.env.MARKTEXT_ERROR_INTERACTION
|
||||
const ERROR_MSG_MAIN = 'An unexpected error occurred in the main process'
|
||||
const ERROR_MSG_RENDERER = 'An unexpected error occurred in the renderer process'
|
||||
|
||||
// main process error handler
|
||||
process.on('uncaughtException', error => {
|
||||
handleError(ERROR_MSG_MAIN, error, 'main')
|
||||
})
|
||||
let logger = s => console.error(s)
|
||||
|
||||
// renderer process error handler
|
||||
ipcMain.on('AGANI::handle-renderer-error', (e, error) => {
|
||||
handleError(ERROR_MSG_RENDERER, error, 'renderer')
|
||||
})
|
||||
|
||||
// start crashReporter to save core dumps to temporary folder
|
||||
crashReporter.start({
|
||||
companyName: 'marktext',
|
||||
productName: 'marktext',
|
||||
submitURL: 'http://0.0.0.0/',
|
||||
uploadToServer: false
|
||||
})
|
||||
const getOSInformation = () => {
|
||||
return `${os.type()} ${os.arch()} ${os.release()} (${os.platform()})`
|
||||
}
|
||||
|
||||
const bundleException = (error, type) => {
|
||||
const { message, stack } = error
|
||||
return {
|
||||
version: app.getVersion(),
|
||||
version: global.MARKTEXT_VERSION_STRING || app.getVersion(),
|
||||
os: getOSInformation(),
|
||||
type,
|
||||
date: new Date().toGMTString(),
|
||||
message,
|
||||
@ -47,10 +37,11 @@ const bundleException = (error, type) => {
|
||||
const handleError = (title, error, type) => {
|
||||
const { message, stack } = error
|
||||
|
||||
// log error
|
||||
const info = bundleException(error, type)
|
||||
console.error(info)
|
||||
log(JSON.stringify(info, null, 2))
|
||||
// Write error into file
|
||||
if (type === 'main') {
|
||||
const info = bundleException(error, type)
|
||||
logger(JSON.stringify(info, null, 2))
|
||||
}
|
||||
|
||||
if (EXIT_ON_ERROR) {
|
||||
console.log('Mark Text was terminated due to an unexpected error (MARKTEXT_EXIT_ON_ERROR variable was set)!')
|
||||
@ -98,7 +89,7 @@ ${title}.
|
||||
### Version
|
||||
|
||||
Mark Text: ${global.MARKTEXT_VERSION_STRING}
|
||||
Operating system: ${process.platform}`)
|
||||
Operating system: ${getOSInformation()}`)
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -108,3 +99,30 @@ Operating system: ${process.platform}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
const setupExceptionHandler = () => {
|
||||
// main process error handler
|
||||
process.on('uncaughtException', error => {
|
||||
handleError(ERROR_MSG_MAIN, error, 'main')
|
||||
})
|
||||
|
||||
// renderer process error handler
|
||||
ipcMain.on('AGANI::handle-renderer-error', (e, error) => {
|
||||
handleError(ERROR_MSG_RENDERER, error, 'renderer')
|
||||
})
|
||||
|
||||
// start crashReporter to save core dumps to temporary folder
|
||||
crashReporter.start({
|
||||
companyName: 'marktext',
|
||||
productName: 'marktext',
|
||||
submitURL: 'http://0.0.0.0/',
|
||||
uploadToServer: false
|
||||
})
|
||||
}
|
||||
|
||||
export const initExceptionLogger = () => {
|
||||
// replace placeholder logger
|
||||
logger = log.error
|
||||
}
|
||||
|
||||
export default setupExceptionHandler
|
||||
|
114
src/main/filesystem/index.js
Normal file
114
src/main/filesystem/index.js
Normal file
@ -0,0 +1,114 @@
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { hasMarkdownExtension } from '../utils'
|
||||
|
||||
/**
|
||||
* Ensure that a directory exist.
|
||||
*
|
||||
* @param {string} dirPath The directory path.
|
||||
*/
|
||||
export const ensureDirSync = dirPath => {
|
||||
try {
|
||||
fs.ensureDirSync(dirPath)
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a directory with read access.
|
||||
*
|
||||
* @param {string} dirPath The directory path.
|
||||
*/
|
||||
export const isDirectory = dirPath => {
|
||||
try {
|
||||
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a file with read access.
|
||||
*
|
||||
* @param {string} filepath The file path.
|
||||
*/
|
||||
export const isFile = filepath => {
|
||||
try {
|
||||
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile()
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a symbolic link with read access.
|
||||
*
|
||||
* @param {string} filepath The link path.
|
||||
*/
|
||||
export const isSymbolicLink = filepath => {
|
||||
try {
|
||||
return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink()
|
||||
} catch (_) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a markdown file.
|
||||
*
|
||||
* @param {string} filepath The path or link path.
|
||||
*/
|
||||
export const isMarkdownFile = filepath => {
|
||||
return isFile(filepath) && hasMarkdownExtension(filepath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a markdown file or symbolic link to a markdown file.
|
||||
*
|
||||
* @param {string} filepath The path or link path.
|
||||
*/
|
||||
export const isMarkdownFileOrLink = filepath => {
|
||||
if (!isFile(filepath)) return false
|
||||
if (hasMarkdownExtension(filepath)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Symbolic link to a markdown file
|
||||
if (isSymbolicLink(filepath)) {
|
||||
const targetPath = fs.readlinkSync(filepath)
|
||||
return isFile(targetPath) && hasMarkdownExtension(targetPath)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the path into an absolute path and resolves the link target if needed.
|
||||
*
|
||||
* @param {string} pathname The path or link path.
|
||||
* @returns {string} Returns the absolute path and resolved link. If the link target
|
||||
* cannot be resolved, an empty string is returned.
|
||||
*/
|
||||
export const normalizeAndResolvePath = pathname => {
|
||||
if (isSymbolicLink(pathname)) {
|
||||
const absPath = path.dirname(pathname)
|
||||
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname))
|
||||
if (isFile(targetPath) || isDirectory(targetPath)) {
|
||||
return path.resolve(targetPath)
|
||||
}
|
||||
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`)
|
||||
return ''
|
||||
}
|
||||
return path.resolve(pathname)
|
||||
}
|
||||
|
||||
export const writeFile = (pathname, content, extension) => {
|
||||
if (!pathname) {
|
||||
return Promise.reject('[ERROR] Cannot save file without path.')
|
||||
}
|
||||
pathname = !extension || pathname.endsWith(extension) ? pathname : `${pathname}${extension}`
|
||||
|
||||
return fs.outputFile(pathname, content, 'utf-8')
|
||||
}
|
@ -1,20 +1,8 @@
|
||||
import fse from 'fs-extra'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG, isWindows } from '../config'
|
||||
import userPreference from '../preference'
|
||||
|
||||
export const getOsLineEndingName = () => {
|
||||
const { endOfLine } = userPreference.getAll()
|
||||
if (endOfLine === 'lf') {
|
||||
return 'lf'
|
||||
}
|
||||
return endOfLine === 'crlf' || isWindows ? 'crlf' : 'lf'
|
||||
}
|
||||
|
||||
export const getDefaultTextDirection = () => {
|
||||
const { textDirection } = userPreference.getAll()
|
||||
return textDirection
|
||||
}
|
||||
import log from 'electron-log'
|
||||
import { LINE_ENDING_REG, LF_LINE_ENDING_REG, CRLF_LINE_ENDING_REG } from '../config'
|
||||
import { writeFile } from '../filesystem'
|
||||
|
||||
const getLineEnding = lineEnding => {
|
||||
if (lineEnding === 'lf') {
|
||||
@ -22,27 +10,28 @@ const getLineEnding = lineEnding => {
|
||||
} else if (lineEnding === 'crlf') {
|
||||
return '\r\n'
|
||||
}
|
||||
return getOsLineEndingName() === 'crlf' ? '\r\n' : '\n'
|
||||
|
||||
// This should not happend but use fallback value.
|
||||
log.error(`Invalid end of line character: expected "lf" or "crlf" but got "${lineEnding}".`)
|
||||
return '\n'
|
||||
}
|
||||
|
||||
const convertLineEndings = (text, lineEnding) => {
|
||||
return text.replace(LINE_ENDING_REG, getLineEnding(lineEnding))
|
||||
}
|
||||
|
||||
export const writeFile = (pathname, content, extension) => {
|
||||
if (!pathname) {
|
||||
return Promise.reject('[ERROR] Cannot save file without path.')
|
||||
}
|
||||
pathname = !extension || pathname.endsWith(extension) ? pathname : `${pathname}${extension}`
|
||||
|
||||
return fse.outputFile(pathname, content, 'utf-8')
|
||||
}
|
||||
|
||||
export const writeMarkdownFile = (pathname, content, options, win) => {
|
||||
const { adjustLineEndingOnSave, isUtf8BomEncoded, lineEnding } = options
|
||||
/**
|
||||
* Write the content into a file.
|
||||
*
|
||||
* @param {string} pathname The path to the file.
|
||||
* @param {string} content The buffer to save.
|
||||
* @param {IMarkdownDocumentOptions} options The markdown document options
|
||||
*/
|
||||
export const writeMarkdownFile = (pathname, content, options) => {
|
||||
const { adjustLineEndingOnSave, encoding, lineEnding } = options
|
||||
const extension = path.extname(pathname) || '.md'
|
||||
|
||||
if (isUtf8BomEncoded) {
|
||||
if (encoding === 'utf8bom') {
|
||||
content = '\uFEFF' + content
|
||||
}
|
||||
|
||||
@ -50,29 +39,35 @@ export const writeMarkdownFile = (pathname, content, options, win) => {
|
||||
content = convertLineEndings(content, lineEnding)
|
||||
}
|
||||
|
||||
// TODO(@fxha): "safeSaveDocuments" using temporary file and rename syscall.
|
||||
return writeFile(pathname, content, extension)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the contents of a markdown file.
|
||||
*
|
||||
* @param {String} The path to the markdown file.
|
||||
* @param {string} pathname The path to the markdown file.
|
||||
* @param {string} preferedEOL The prefered EOL.
|
||||
* @returns {IMarkdownDocumentRaw} Returns a raw markdown document.
|
||||
*/
|
||||
export const loadMarkdownFile = async pathname => {
|
||||
let markdown = await fse.readFile(path.resolve(pathname), 'utf-8')
|
||||
export const loadMarkdownFile = async (pathname, preferedEOL) => {
|
||||
let markdown = await fs.readFile(path.resolve(pathname), 'utf-8')
|
||||
|
||||
// Check UTF-8 BOM (EF BB BF) encoding
|
||||
const isUtf8BomEncoded = markdown.length >= 1 && markdown.charCodeAt(0) === 0xFEFF
|
||||
if (isUtf8BomEncoded) {
|
||||
markdown = markdown.slice(1)
|
||||
markdown.splice(0, 1)
|
||||
}
|
||||
|
||||
// TODO(@fxha): Check for more file encodings and whether the file is binary but for now expect UTF-8.
|
||||
const encoding = isUtf8BomEncoded ? 'utf8bom' : 'utf8'
|
||||
|
||||
// Detect line ending
|
||||
const isLf = LF_LINE_ENDING_REG.test(markdown)
|
||||
const isCrlf = CRLF_LINE_ENDING_REG.test(markdown)
|
||||
const isMixedLineEndings = isLf && isCrlf
|
||||
const isUnknownEnding = !isLf && !isCrlf
|
||||
let lineEnding = getOsLineEndingName()
|
||||
let lineEnding = preferedEOL
|
||||
if (isLf && !isCrlf) {
|
||||
lineEnding = 'lf'
|
||||
} else if (isCrlf && !isLf) {
|
||||
@ -87,10 +82,6 @@ export const loadMarkdownFile = async pathname => {
|
||||
}
|
||||
|
||||
const filename = path.basename(pathname)
|
||||
|
||||
// TODO(refactor:renderer/editor): Remove this entry! This should be loaded separately if needed.
|
||||
const textDirection = getDefaultTextDirection()
|
||||
|
||||
return {
|
||||
// document information
|
||||
markdown,
|
||||
@ -98,14 +89,11 @@ export const loadMarkdownFile = async pathname => {
|
||||
pathname,
|
||||
|
||||
// options
|
||||
isUtf8BomEncoded,
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave,
|
||||
|
||||
// raw file information
|
||||
isMixedLineEndings,
|
||||
|
||||
// TODO(refactor:renderer/editor): see above
|
||||
textDirection
|
||||
isMixedLineEndings
|
||||
}
|
||||
}
|
@ -1,17 +1,22 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import log from 'electron-log'
|
||||
import { promisify } from 'util'
|
||||
import chokidar from 'chokidar'
|
||||
import { getUniqueId, log, hasMarkdownExtension } from './utils'
|
||||
import { loadMarkdownFile } from './utils/filesystem'
|
||||
import { isLinux } from './config'
|
||||
import { getUniqueId, hasMarkdownExtension } from '../utils'
|
||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||
import { isLinux } from '../config'
|
||||
|
||||
// TODO(need::refactor):
|
||||
// - Refactor this file
|
||||
// - Outsource watcher/search features into worker (per window) and use something like "file-matcher" for searching on disk.
|
||||
|
||||
const EVENT_NAME = {
|
||||
dir: 'AGANI::update-object-tree',
|
||||
file: 'AGANI::update-file'
|
||||
}
|
||||
|
||||
const add = async (win, pathname) => {
|
||||
const add = async (win, pathname, endOfLine) => {
|
||||
const stats = await promisify(fs.stat)(pathname)
|
||||
const birthTime = stats.birthtime
|
||||
const isMarkdown = hasMarkdownExtension(pathname)
|
||||
@ -24,7 +29,7 @@ const add = async (win, pathname) => {
|
||||
isMarkdown
|
||||
}
|
||||
if (isMarkdown) {
|
||||
const data = await loadMarkdownFile(pathname)
|
||||
const data = await loadMarkdownFile(pathname, endOfLine)
|
||||
file.data = data
|
||||
}
|
||||
|
||||
@ -42,11 +47,11 @@ const unlink = (win, pathname, type) => {
|
||||
})
|
||||
}
|
||||
|
||||
const change = async (win, pathname, type) => {
|
||||
const change = async (win, pathname, type, endOfLine) => {
|
||||
const isMarkdown = hasMarkdownExtension(pathname)
|
||||
|
||||
if (isMarkdown) {
|
||||
const data = await loadMarkdownFile(pathname)
|
||||
const data = await loadMarkdownFile(pathname, endOfLine)
|
||||
const file = {
|
||||
pathname,
|
||||
data
|
||||
@ -85,9 +90,15 @@ const unlinkDir = (win, pathname) => {
|
||||
}
|
||||
|
||||
class Watcher {
|
||||
constructor () {
|
||||
|
||||
/**
|
||||
* @param {Preference} preferences The preference instance.
|
||||
*/
|
||||
constructor (preferences) {
|
||||
this._preferences = preferences
|
||||
this.watchers = {}
|
||||
}
|
||||
|
||||
// return a unwatch function
|
||||
watch (win, watchPath, type = 'dir'/* file or dir */) {
|
||||
const id = getUniqueId()
|
||||
@ -98,13 +109,13 @@ class Watcher {
|
||||
})
|
||||
|
||||
watcher
|
||||
.on('add', pathname => add(win, pathname))
|
||||
.on('change', pathname => change(win, pathname, type))
|
||||
.on('add', pathname => add(win, pathname, this._preferences.getPreferedEOL()))
|
||||
.on('change', pathname => change(win, pathname, type, this._preferences.getPreferedEOL()))
|
||||
.on('unlink', pathname => unlink(win, pathname, type))
|
||||
.on('addDir', pathname => addDir(win, pathname))
|
||||
.on('unlinkDir', pathname => unlinkDir(win, pathname))
|
||||
.on('raw', (event, path, details) => {
|
||||
if (global.MARKTEXT_DEBUG_VERBOSE) {
|
||||
if (global.MARKTEXT_DEBUG_VERBOSE >= 3) {
|
||||
console.log(event, path, details)
|
||||
}
|
||||
|
||||
@ -119,7 +130,7 @@ class Watcher {
|
||||
.on('error', error => {
|
||||
const msg = `Watcher error: ${error}`
|
||||
console.log(msg)
|
||||
log(msg)
|
||||
log.error(msg)
|
||||
})
|
||||
|
||||
this.watchers[id] = {
|
||||
@ -173,6 +184,7 @@ class Watcher {
|
||||
|
||||
clear () {
|
||||
Object.keys(this.watchers).forEach(id => this.watchers[id].watcher.close())
|
||||
this.watchers = {}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Set `__static` path to static files in production
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
|
||||
}
|
||||
import path from 'path'
|
||||
|
||||
global.MARKTEXT_DEBUG = process.env.MARKTEXT_DEBUG || process.env.NODE_ENV !== 'production'
|
||||
global.MARKTEXT_DEBUG_VERBOSE = global.MARKTEXT_DEBUG && process.env.MARKTEXT_DEBUG_VERBOSE
|
||||
global.MARKTEXT_SAFE_MODE = false
|
||||
// Set `__static` path to static files in production.
|
||||
if (process.env.NODE_ENV !== 'development') {
|
||||
global.__static = path.join(__dirname, '/static').replace(/\\/g, '\\\\')
|
||||
}
|
||||
|
@ -22,5 +22,7 @@ require('electron').app.on('ready', () => {
|
||||
})
|
||||
})
|
||||
|
||||
/* eslint-enable */
|
||||
|
||||
// Require `main` process to boot app
|
||||
require('./index')
|
||||
|
@ -1,17 +1,44 @@
|
||||
import './globalSetting'
|
||||
import './cli'
|
||||
import './exceptionHandler'
|
||||
import { checkSystem } from './utils/checkSystem'
|
||||
import path from 'path'
|
||||
import cli from './cli'
|
||||
import setupExceptionHandler, { initExceptionLogger } from './exceptionHandler'
|
||||
import log from 'electron-log'
|
||||
import App from './app'
|
||||
import Accessor from './app/accessor'
|
||||
import setupEnvironment from './app/env'
|
||||
import { getLogLevel } from './utils'
|
||||
|
||||
const initializeLogger = appEnvironment => {
|
||||
log.transports.console.level = process.env.NODE_ENV === 'development'
|
||||
log.transports.rendererConsole = null
|
||||
log.transports.file.file = path.join(appEnvironment.paths.logPath, 'main.log')
|
||||
log.transports.file.level = getLogLevel()
|
||||
log.transports.file.sync = false
|
||||
log.transports.file.init()
|
||||
initExceptionLogger()
|
||||
}
|
||||
|
||||
// -----------------------------------------------
|
||||
|
||||
// NOTE: We only support Linux, macOS and Windows but not BSD nor SunOS.
|
||||
if (!/^(darwin|win32|linux)$/i.test(process.platform)) {
|
||||
console.error(`Operating system "${process.platform}" is not supported! Please open an issue at "https://github.com/marktext/marktext".`)
|
||||
process.stdout.write(`Operating system "${process.platform}" is not supported! Please open an issue at "https://github.com/marktext/marktext".\n`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
checkSystem()
|
||||
setupExceptionHandler()
|
||||
|
||||
const app = new App()
|
||||
const args = cli()
|
||||
const appEnvironment = setupEnvironment(args)
|
||||
initializeLogger(appEnvironment)
|
||||
|
||||
// Mark Text environment is configured successfully. You can now access paths, use the logger etc.
|
||||
// Create other instances that need access to the modules from above.
|
||||
const accessor = new Accessor(appEnvironment)
|
||||
|
||||
// -----------------------------------------------
|
||||
// Be careful when changing code before this line!
|
||||
// NOTE: Do not create classes or other code before this line!
|
||||
|
||||
const app = new App(accessor, args)
|
||||
app.init()
|
||||
|
@ -1,3 +1,4 @@
|
||||
// import EventEmitter from 'events'
|
||||
import { getCurrentKeyboardLayout, getCurrentKeyboardLanguage, getCurrentKeymap } from 'keyboard-layout'
|
||||
|
||||
export const getKeyboardLanguage = () => {
|
||||
@ -32,3 +33,24 @@ export const getVirtualLetters = () => {
|
||||
}
|
||||
return vkeys
|
||||
}
|
||||
|
||||
// class KeyboardLayoutMonitor {
|
||||
//
|
||||
// constructor() {
|
||||
// this._eventEmitter = new EventEmitter()
|
||||
// this._subscription = null
|
||||
// }
|
||||
//
|
||||
// onDidChangeCurrentKeyboardLayout (callback) {
|
||||
// if (!this._subscription) {
|
||||
// this._subscription = onDidChangeCurrentKeyboardLayout(layout => {
|
||||
// this._eventEmitter.emit('onDidChangeCurrentKeyboardLayout', layout)
|
||||
// })
|
||||
// }
|
||||
// this._eventEmitter.on('onDidChangeCurrentKeyboardLayout', callback)
|
||||
// }
|
||||
//
|
||||
// }
|
||||
//
|
||||
// // TODO(@fxha): Reload ShortcutHandler on change
|
||||
// export const keyboardLayoutMonitor = new KeyboardLayoutMonitor()
|
@ -1,10 +1,12 @@
|
||||
import { Menu } from 'electron'
|
||||
import fs from 'fs-extra'
|
||||
import path from 'path'
|
||||
import log from 'electron-log'
|
||||
import isAccelerator from 'electron-is-accelerator'
|
||||
import electronLocalshortcut from '@hfelix/electron-localshortcut'
|
||||
import { isOsx } from './config'
|
||||
import { getKeyboardLanguage, getVirtualLetters } from './keyboardUtils'
|
||||
import { isFile, getPath, log, readJson } from './utils'
|
||||
import { isOsx } from '../config'
|
||||
import { getKeyboardLanguage, getVirtualLetters } from '../keyboard'
|
||||
import { isFile } from '../filesystem'
|
||||
|
||||
// Problematic key bindings:
|
||||
// Aidou: Ctrl+/ -> dead key
|
||||
@ -12,8 +14,12 @@ import { isFile, getPath, log, readJson } from './utils'
|
||||
// Upgrade Heading: Ctrl+= -> points to Ctrl+Plus which is ok; Ctrl+Plus is broken
|
||||
|
||||
class Keybindings {
|
||||
constructor () {
|
||||
this.configPath = path.join(getPath('userData'), 'keybindings.json')
|
||||
|
||||
/**
|
||||
* @param {string} userDataPath The user data path.
|
||||
*/
|
||||
constructor (userDataPath) {
|
||||
this.configPath = path.join(userDataPath, 'keybindings.json')
|
||||
|
||||
this.keys = new Map([
|
||||
// marktext - macOS only
|
||||
@ -101,13 +107,50 @@ class Keybindings {
|
||||
|
||||
// fix non-US keyboards
|
||||
this.mnemonics = new Map()
|
||||
this.fixLayout()
|
||||
this._fixLayout()
|
||||
|
||||
// load user-defined keybindings
|
||||
this.loadLocalKeybindings()
|
||||
this._loadLocalKeybindings()
|
||||
}
|
||||
|
||||
fixLayout () {
|
||||
getAccelerator (id) {
|
||||
const name = this.keys.get(id)
|
||||
if (!name) {
|
||||
return ''
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
registerKeyHandlers (win, acceleratorMap) {
|
||||
for (const item of acceleratorMap) {
|
||||
let { accelerator } = item
|
||||
|
||||
// Fix broken shortcuts because of dead keys or non-US keyboard problems. We bind the
|
||||
// shortcut to another accelerator because of key mapping issues. E.g: 'Alt+/' is not
|
||||
// available on a German keyboard, because you have to press 'Shift+7' to produce '/'.
|
||||
// In this case we can remap the accelerator to 'Alt+7' or 'Ctrl+Shift+7'.
|
||||
const acceleratorFix = this.mnemonics.get(accelerator)
|
||||
if (acceleratorFix) {
|
||||
accelerator = acceleratorFix
|
||||
}
|
||||
|
||||
// Regisiter shortcuts on the BrowserWindow instead of using Chromium's native menu.
|
||||
// This makes it possible to receive key down events before Chromium/Electron and we
|
||||
// can handle reserved Chromium shortcuts. Afterwards prevent the default action of
|
||||
// the event so the native menu is not triggered.
|
||||
electronLocalshortcut.register(win, accelerator, () => {
|
||||
if (global.MARKTEXT_DEBUG && process.env.MARKTEXT_DEBUG_KEYBOARD) {
|
||||
console.log(`You pressed ${accelerator}`)
|
||||
}
|
||||
callMenuCallback(item, win)
|
||||
return true // prevent default action
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// --- private --------------------------------
|
||||
|
||||
_fixLayout () {
|
||||
// fix wrong virtual key mapping on non-QWERTY layouts
|
||||
electronLocalshortcut.updateVirtualKeys(getVirtualLetters())
|
||||
|
||||
@ -121,10 +164,10 @@ class Keybindings {
|
||||
case 'fi':
|
||||
case 'no':
|
||||
case 'se':
|
||||
this.fixInlineCode()
|
||||
this._fixInlineCode()
|
||||
|
||||
if (!isOsx) {
|
||||
this.fixAidou()
|
||||
this._fixAidou()
|
||||
}
|
||||
break
|
||||
|
||||
@ -136,7 +179,7 @@ class Keybindings {
|
||||
case 'pl':
|
||||
case 'pt':
|
||||
if (!isOsx) {
|
||||
this.fixAidou()
|
||||
this._fixAidou()
|
||||
}
|
||||
break
|
||||
|
||||
@ -144,29 +187,29 @@ class Keybindings {
|
||||
case 'bg':
|
||||
if (!isOsx) {
|
||||
this.mnemonics.set('CmdOrCtrl+/', 'CmdOrCtrl+8')
|
||||
this.fixInlineCode()
|
||||
this._fixInlineCode()
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fixAidou () {
|
||||
_fixAidou () {
|
||||
this.mnemonics.set('CmdOrCtrl+/', 'CmdOrCtrl+7')
|
||||
}
|
||||
|
||||
// fix dead backquote key on layouts like German
|
||||
fixInlineCode () {
|
||||
_fixInlineCode () {
|
||||
this.keys.set('formatInlineCode', 'CmdOrCtrl+Shift+B')
|
||||
}
|
||||
|
||||
loadLocalKeybindings () {
|
||||
_loadLocalKeybindings () {
|
||||
if (global.MARKTEXT_SAFE_MODE || !isFile(this.configPath)) {
|
||||
return
|
||||
}
|
||||
|
||||
const json = readJson(this.configPath, true)
|
||||
const json = fs.readJsonSync(this.configPath, { throws: false })
|
||||
if (!json || typeof json !== 'object') {
|
||||
log('Invalid keybindings.json configuration.')
|
||||
log.warn('Invalid keybindings.json configuration.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -205,10 +248,10 @@ class Keybindings {
|
||||
// check for duplicate shortcuts
|
||||
for (const [userKey, userValue] of userAccelerators) {
|
||||
for (const [key, value] of accelerators) {
|
||||
if (this.isEqualAccelerator(value, userValue)) {
|
||||
if (this._isEqualAccelerator(value, userValue)) {
|
||||
const err = `Invalid keybindings.json configuration: Duplicate key ${userKey} - ${key}`
|
||||
console.log(err)
|
||||
log(err)
|
||||
log.error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -219,15 +262,7 @@ class Keybindings {
|
||||
this.keys = accelerators
|
||||
}
|
||||
|
||||
getAccelerator (id) {
|
||||
const name = this.keys.get(id)
|
||||
if (!name) {
|
||||
return ''
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
isEqualAccelerator (a, b) {
|
||||
_isEqualAccelerator (a, b) {
|
||||
a = a.toLowerCase().replace('cmdorctrl', 'ctrl').replace('command', 'ctrl')
|
||||
b = b.toLowerCase().replace('cmdorctrl', 'ctrl').replace('command', 'ctrl')
|
||||
const i1 = a.indexOf('+')
|
||||
@ -245,8 +280,6 @@ class Keybindings {
|
||||
}
|
||||
}
|
||||
|
||||
const keybindings = new Keybindings()
|
||||
|
||||
export const parseMenu = menuTemplate => {
|
||||
const { submenu, accelerator, click, id, visible } = menuTemplate
|
||||
let items = []
|
||||
@ -268,33 +301,6 @@ export const parseMenu = menuTemplate => {
|
||||
return items.length === 0 ? null : items
|
||||
}
|
||||
|
||||
export const registerKeyHandler = (win, acceleratorMap) => {
|
||||
for (const item of acceleratorMap) {
|
||||
let { accelerator } = item
|
||||
|
||||
// Fix broken shortcuts because of dead keys or non-US keyboard problems. We bind the
|
||||
// shortcut to another accelerator because of key mapping issues. E.g: 'Alt+/' is not
|
||||
// available on a German keyboard, because you have to press 'Shift+7' to produce '/'.
|
||||
// In this case we can remap the accelerator to 'Alt+7' or 'Ctrl+Shift+7'.
|
||||
const acceleratorFix = keybindings.mnemonics.get(accelerator)
|
||||
if (acceleratorFix) {
|
||||
accelerator = acceleratorFix
|
||||
}
|
||||
|
||||
// Regisiter shortcuts on the BrowserWindow instead of using Chromium's native menu.
|
||||
// This makes it possible to receive key down events before Chromium/Electron and we
|
||||
// can handle reserved Chromium shortcuts. Afterwards prevent the default action of
|
||||
// the event so the native menu is not triggered.
|
||||
electronLocalshortcut.register(win, accelerator, () => {
|
||||
if (global.MARKTEXT_DEBUG && process.env.MARKTEXT_DEBUG_KEYBOARD) {
|
||||
console.log(`You pressed ${accelerator}`)
|
||||
}
|
||||
callMenuCallback(item, win)
|
||||
return true // prevent default action
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const callMenuCallback = (menuInfo, win) => {
|
||||
const { click, id } = menuInfo
|
||||
if (click) {
|
||||
@ -312,4 +318,4 @@ const callMenuCallback = (menuInfo, win) => {
|
||||
}
|
||||
}
|
||||
|
||||
export default keybindings
|
||||
export default Keybindings
|
@ -1,11 +1,12 @@
|
||||
import path from 'path'
|
||||
import { dialog, ipcMain, BrowserWindow } from 'electron'
|
||||
import { IMAGE_EXTENSIONS } from '../config'
|
||||
import { searchFilesAndDir } from '../utils/imagePathAutoComplement'
|
||||
import appMenu from '../menu'
|
||||
import { log } from '../utils'
|
||||
import log from 'electron-log'
|
||||
import { IMAGE_EXTENSIONS } from '../../config'
|
||||
import { updateLineEndingMenu } from '../../menu'
|
||||
import { searchFilesAndDir } from '../../utils/imagePathAutoComplement'
|
||||
|
||||
const getAndSendImagePath = (win, type) => {
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filename = dialog.showOpenDialog(win, {
|
||||
properties: [ 'openFile' ],
|
||||
filters: [{
|
||||
@ -25,7 +26,7 @@ ipcMain.on('AGANI::ask-for-insert-image', (e, type) => {
|
||||
|
||||
ipcMain.on('AGANI::ask-for-image-auto-path', (e, { pathname, src }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
if (src.endsWith('/') || src.endsWith('.')) {
|
||||
if (src.endsWith('/') || src.endsWith('\\') || src.endsWith('.')) {
|
||||
return win.webContents.send('AGANI::image-auto-path', [])
|
||||
}
|
||||
const fullPath = path.isAbsolute(src) ? src : path.join(path.dirname(pathname), src)
|
||||
@ -35,15 +36,11 @@ ipcMain.on('AGANI::ask-for-image-auto-path', (e, { pathname, src }) => {
|
||||
.then(files => {
|
||||
win.webContents.send('AGANI::image-auto-path', files)
|
||||
})
|
||||
.catch(log)
|
||||
.catch(log.error)
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::update-line-ending-menu', (e, lineEnding) => {
|
||||
appMenu.updateLineEndingnMenu(lineEnding)
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::update-text-direction-menu', (e, textDirection) => {
|
||||
appMenu.updateTextDirectionMenu(textDirection)
|
||||
updateLineEndingMenu(lineEnding)
|
||||
})
|
||||
|
||||
export const edit = (win, type) => {
|
||||
@ -61,7 +58,3 @@ export const insertImage = (win, type) => {
|
||||
win.webContents.send('AGANI::INSERT_IMAGE', { type })
|
||||
}
|
||||
}
|
||||
|
||||
export const textDirection = (win, textDirection) => {
|
||||
win.webContents.send('AGANI::set-text-direction', { textDirection })
|
||||
}
|
@ -1,26 +1,30 @@
|
||||
import fs from 'fs'
|
||||
// import chokidar from 'chokidar'
|
||||
import path from 'path'
|
||||
import { promisify } from 'util'
|
||||
import { BrowserWindow, dialog, ipcMain, shell } from 'electron'
|
||||
import appWindow from '../window'
|
||||
import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../config'
|
||||
import { writeFile, writeMarkdownFile } from '../utils/filesystem'
|
||||
import appMenu from '../menu'
|
||||
import { getPath, isMarkdownFile, isMarkdownFileOrLink, normalizeAndResolvePath, log, isFile, isDirectory, getRecommendTitle } from '../utils'
|
||||
import userPreference from '../preference'
|
||||
import pandoc from '../utils/pandoc'
|
||||
import log from 'electron-log'
|
||||
import { EXTENSION_HASN, EXTENSIONS, PANDOC_EXTENSIONS, URL_REG } from '../../config'
|
||||
import { isDirectory, isFile, isMarkdownFile, isMarkdownFileOrLink, normalizeAndResolvePath, writeFile } from '../../filesystem'
|
||||
import { writeMarkdownFile } from '../../filesystem/markdown'
|
||||
import { getPath, getRecommendTitleFromMarkdownString } from '../../utils'
|
||||
import pandoc from '../../utils/pandoc'
|
||||
|
||||
// handle the response from render process.
|
||||
// TODO:
|
||||
// - use async dialog version to not block the main process.
|
||||
// - catch "fs." exceptions. Otherwise the main process crashes...
|
||||
|
||||
// Handle the export response from renderer process.
|
||||
const handleResponseForExport = async (e, { type, content, pathname, markdown }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const extension = EXTENSION_HASN[type]
|
||||
const dirname = pathname ? path.dirname(pathname) : getPath('documents')
|
||||
let nakedFilename = getRecommendTitle(markdown)
|
||||
let nakedFilename = getRecommendTitleFromMarkdownString(markdown)
|
||||
if (!nakedFilename) {
|
||||
nakedFilename = pathname ? path.basename(pathname, '.md') : 'Untitled'
|
||||
}
|
||||
const defaultPath = `${dirname}/${nakedFilename}${extension}`
|
||||
const defaultPath = path.join(dirname, `${nakedFilename}${extension}`)
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filePath = dialog.showSaveDialog(win, {
|
||||
defaultPath
|
||||
})
|
||||
@ -37,7 +41,7 @@ const handleResponseForExport = async (e, { type, content, pathname, markdown })
|
||||
win.webContents.send('AGANI::export-success', { type, filePath })
|
||||
}
|
||||
} catch (err) {
|
||||
log(err)
|
||||
log.error(err)
|
||||
const ERROR_MSG = err.message || `Error happened when export ${filePath}`
|
||||
win.webContents.send('AGANI::show-notification', {
|
||||
title: 'Export File Error',
|
||||
@ -71,27 +75,29 @@ const handleResponseForPrint = e => {
|
||||
|
||||
const handleResponseForSave = (e, { id, markdown, pathname, options }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
let recommendFilename = getRecommendTitle(markdown)
|
||||
let recommendFilename = getRecommendTitleFromMarkdownString(markdown)
|
||||
if (!recommendFilename) {
|
||||
recommendFilename = 'Untitled'
|
||||
}
|
||||
|
||||
// If the file doesn't exist on disk add it to the recently used documents later.
|
||||
const alreadyExistOnDisk = !!pathname
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
pathname = pathname || dialog.showSaveDialog(win, {
|
||||
defaultPath: getPath('documents') + `/${recommendFilename}.md`
|
||||
defaultPath: path.join(getPath('documents'), `${recommendFilename}.md`)
|
||||
})
|
||||
|
||||
if (pathname && typeof pathname === 'string') {
|
||||
if (!alreadyExistOnDisk) {
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
ipcMain.emit('menu-clear-recently-used')
|
||||
}
|
||||
|
||||
return writeMarkdownFile(pathname, markdown, options, win)
|
||||
.then(() => {
|
||||
if (!alreadyExistOnDisk) {
|
||||
// it's a new created file, need watch
|
||||
appWindow.watcher.watch(win, pathname, 'file')
|
||||
ipcMain.emit('watcher-watch-file', win, pathname)
|
||||
}
|
||||
const filename = path.basename(pathname)
|
||||
win.webContents.send('AGANI::set-pathname', { id, pathname, filename })
|
||||
@ -126,6 +132,7 @@ const showUnsavedFilesMessage = (win, files) => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const noticePandocNotFound = win => {
|
||||
return win.webContents.send('AGANI::pandoc-not-exists', {
|
||||
title: 'Import Warning',
|
||||
@ -135,14 +142,13 @@ const noticePandocNotFound = win => {
|
||||
})
|
||||
}
|
||||
|
||||
const pandocFile = async pathname => {
|
||||
const openPandocFile = async (windowId, pathname) => {
|
||||
try {
|
||||
const converter = pandoc(pathname, 'markdown')
|
||||
const data = await converter()
|
||||
// TODO: allow to open data also in a new tab instead window.
|
||||
appWindow.createWindow(undefined, data)
|
||||
ipcMain.emit('app-open-markdown-by-id', windowId, data)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
log.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,70 +157,75 @@ const removePrintServiceFromWindow = win => {
|
||||
win.webContents.send('AGANI::print-service-clearup')
|
||||
}
|
||||
|
||||
ipcMain.on('AGANI::save-all', (e, unSavedFiles) => {
|
||||
Promise.all(unSavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
.catch(log)
|
||||
// --- events -----------------------------------
|
||||
|
||||
ipcMain.on('AGANI::save-all', (e, unsavedFiles) => {
|
||||
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
.catch(log.error)
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::save-close', async (e, unSavedFiles, isSingle) => {
|
||||
ipcMain.on('AGANI::save-close', async (e, unsavedFiles, isSingle) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const { needSave } = await showUnsavedFilesMessage(win, unSavedFiles)
|
||||
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
||||
const EVENT = isSingle ? 'AGANI::save-single-response' : 'AGANI::save-all-response'
|
||||
if (needSave) {
|
||||
Promise.all(unSavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
.then(arr => {
|
||||
const data = arr.filter(id => id)
|
||||
win.send(EVENT, { err: null, data })
|
||||
})
|
||||
.catch(err => {
|
||||
win.send(EVENT, { err, data: null })
|
||||
log(err)
|
||||
log.error(err.error)
|
||||
})
|
||||
} else {
|
||||
const data = unSavedFiles.map(f => f.id)
|
||||
const data = unsavedFiles.map(f => f.id)
|
||||
win.send(EVENT, { err: null, data })
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::response-file-save-as', (e, { id, markdown, pathname, options }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
let recommendFilename = getRecommendTitle(markdown)
|
||||
let recommendFilename = getRecommendTitleFromMarkdownString(markdown)
|
||||
if (!recommendFilename) {
|
||||
recommendFilename = 'Untitled'
|
||||
}
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filePath = dialog.showSaveDialog(win, {
|
||||
defaultPath: pathname || getPath('documents') + `/${recommendFilename}.md`
|
||||
})
|
||||
|
||||
if (filePath) {
|
||||
writeMarkdownFile(filePath, markdown, options, win)
|
||||
.then(() => {
|
||||
// need watch file after `save as`
|
||||
if (pathname !== filePath) {
|
||||
appWindow.watcher.watch(win, filePath, 'file')
|
||||
// unWatch the old file.
|
||||
appWindow.watcher.unWatch(win, pathname, 'file')
|
||||
// unwatch the old file
|
||||
ipcMain.emit('watcher-unwatch-file', win, pathname)
|
||||
ipcMain.emit('watcher-watch-file', win, filePath)
|
||||
}
|
||||
const filename = path.basename(filePath)
|
||||
win.webContents.send('AGANI::set-pathname', { id, pathname: filePath, filename })
|
||||
})
|
||||
.catch(log)
|
||||
.catch(log.error)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::response-close-confirm', async (e, unSavedFiles) => {
|
||||
ipcMain.on('AGANI::response-close-confirm', async (e, unsavedFiles) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
const { needSave } = await showUnsavedFilesMessage(win, unSavedFiles)
|
||||
const { needSave } = await showUnsavedFilesMessage(win, unsavedFiles)
|
||||
if (needSave) {
|
||||
Promise.all(unSavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
Promise.all(unsavedFiles.map(file => handleResponseForSave(e, file)))
|
||||
.then(() => {
|
||||
appWindow.forceClose(win)
|
||||
ipcMain.emit('window-close-by-id', win.id)
|
||||
})
|
||||
.catch(err => {
|
||||
console.log(err)
|
||||
log(err)
|
||||
log.error(err)
|
||||
})
|
||||
} else {
|
||||
appWindow.forceClose(win)
|
||||
ipcMain.emit('window-close-by-id', win.id)
|
||||
}
|
||||
})
|
||||
|
||||
@ -224,11 +235,6 @@ ipcMain.on('AGANI::response-export', handleResponseForExport)
|
||||
|
||||
ipcMain.on('AGANI::response-print', handleResponseForPrint)
|
||||
|
||||
ipcMain.on('AGANI::close-window', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
appWindow.forceClose(win)
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::window::drop', async (e, fileList) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
for (const file of fileList) {
|
||||
@ -242,7 +248,7 @@ ipcMain.on('AGANI::window::drop', async (e, fileList) => {
|
||||
if (!existsPandoc) {
|
||||
noticePandocNotFound(win)
|
||||
} else {
|
||||
pandocFile(file)
|
||||
openPandocFile(win.id, file)
|
||||
}
|
||||
break
|
||||
}
|
||||
@ -263,7 +269,7 @@ ipcMain.on('AGANI::rename', (e, { id, pathname, newPathname }) => {
|
||||
type: 'warning',
|
||||
buttons: ['Replace', 'Cancel'],
|
||||
defaultId: 1,
|
||||
message: `The file ${path.basename(newPathname)} is already exists. Do you want to replace it?`,
|
||||
message: `The file "${path.basename(newPathname)}" already exists. Do you want to replace it?`,
|
||||
cancelId: 1,
|
||||
noLink: true
|
||||
}, index => {
|
||||
@ -281,6 +287,8 @@ ipcMain.on('AGANI::rename', (e, { id, pathname, newPathname }) => {
|
||||
|
||||
ipcMain.on('AGANI::response-file-move-to', (e, { id, pathname }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
let newPath = dialog.showSaveDialog(win, {
|
||||
buttonLabel: 'Move to',
|
||||
nameFieldLabel: 'Filename:',
|
||||
@ -293,11 +301,13 @@ ipcMain.on('AGANI::response-file-move-to', (e, { id, pathname }) => {
|
||||
|
||||
ipcMain.on('AGANI::ask-for-open-project-in-sidebar', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const pathname = dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
})
|
||||
if (pathname && pathname[0]) {
|
||||
appWindow.openFolder(win, pathname[0])
|
||||
ipcMain.emit('app-open-directory-by-id', win.id, pathname[0])
|
||||
}
|
||||
})
|
||||
|
||||
@ -318,6 +328,8 @@ ipcMain.on('AGANI::format-link-click', (e, { data, dirname }) => {
|
||||
}
|
||||
})
|
||||
|
||||
// --- menu -------------------------------------
|
||||
|
||||
export const exportFile = (win, type) => {
|
||||
win.webContents.send('AGANI::export', { type })
|
||||
}
|
||||
@ -328,6 +340,8 @@ export const importFile = async win => {
|
||||
if (!existsPandoc) {
|
||||
return noticePandocNotFound(win)
|
||||
}
|
||||
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const filename = dialog.showOpenDialog(win, {
|
||||
properties: [ 'openFile' ],
|
||||
filters: [{
|
||||
@ -337,7 +351,7 @@ export const importFile = async win => {
|
||||
})
|
||||
|
||||
if (filename && filename[0]) {
|
||||
pandocFile(filename[0])
|
||||
openPandocFile(win.id, filename[0])
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,32 +359,8 @@ export const print = win => {
|
||||
win.webContents.send('AGANI::print')
|
||||
}
|
||||
|
||||
export const openFileOrFolder = (win, pathname) => {
|
||||
const resolvedPath = normalizeAndResolvePath(pathname)
|
||||
if (isFile(resolvedPath)) {
|
||||
const { openFilesInNewWindow } = userPreference.getAll()
|
||||
if (openFilesInNewWindow) {
|
||||
appWindow.createWindow(resolvedPath)
|
||||
} else {
|
||||
appWindow.newTab(win, pathname)
|
||||
}
|
||||
} else if (isDirectory(resolvedPath)) {
|
||||
appWindow.createWindow(resolvedPath)
|
||||
} else {
|
||||
console.error(`[ERROR] Cannot open unknown file: "${resolvedPath}"`)
|
||||
}
|
||||
}
|
||||
|
||||
export const openFolder = win => {
|
||||
const dirList = dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
})
|
||||
if (dirList && dirList[0]) {
|
||||
openFileOrFolder(win, dirList[0])
|
||||
}
|
||||
}
|
||||
|
||||
export const openFile = win => {
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const fileList = dialog.showOpenDialog(win, {
|
||||
properties: ['openFile'],
|
||||
filters: [{
|
||||
@ -383,18 +373,33 @@ export const openFile = win => {
|
||||
}
|
||||
}
|
||||
|
||||
export const newFile = () => {
|
||||
appWindow.createWindow()
|
||||
export const openFolder = win => {
|
||||
// TODO(need::refactor): use async dialog version
|
||||
const dirList = dialog.showOpenDialog(win, {
|
||||
properties: ['openDirectory', 'createDirectory']
|
||||
})
|
||||
if (dirList && dirList[0]) {
|
||||
openFileOrFolder(win, dirList[0])
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new tab.
|
||||
*
|
||||
* @param {BrowserWindow} win Browser window
|
||||
* @param {IMarkdownDocumentRaw} [rawDocument] Optional markdown document. If null a blank tab is created.
|
||||
*/
|
||||
export const newTab = (win, rawDocument = null) => {
|
||||
win.webContents.send('AGANI::new-tab', rawDocument)
|
||||
export const openFileOrFolder = (win, pathname) => {
|
||||
const resolvedPath = normalizeAndResolvePath(pathname)
|
||||
if (isFile(resolvedPath)) {
|
||||
ipcMain.emit('app-open-file-by-id', win.id, resolvedPath)
|
||||
} else if (isDirectory(resolvedPath)) {
|
||||
ipcMain.emit('app-open-directory-by-id', win.id, resolvedPath)
|
||||
} else {
|
||||
console.error(`[ERROR] Cannot open unknown file: "${resolvedPath}"`)
|
||||
}
|
||||
}
|
||||
|
||||
export const newBlankTab = win => {
|
||||
win.webContents.send('mt::new-untitled-tab')
|
||||
}
|
||||
|
||||
export const newEditorWindow = () => {
|
||||
ipcMain.emit('app-create-editor-window')
|
||||
}
|
||||
|
||||
export const closeTab = win => {
|
||||
@ -411,13 +416,7 @@ export const saveAs = win => {
|
||||
|
||||
export const autoSave = (menuItem, browserWindow) => {
|
||||
const { checked } = menuItem
|
||||
userPreference.setItem('autoSave', checked)
|
||||
.then(() => {
|
||||
for (const { win } of appWindow.windows.values()) {
|
||||
win.webContents.send('AGANI::user-preference', { autoSave: checked })
|
||||
}
|
||||
})
|
||||
.catch(log)
|
||||
ipcMain.emit('mt::set-user-preference', { autoSave: checked })
|
||||
}
|
||||
|
||||
export const moveTo = win => {
|
||||
@ -429,5 +428,5 @@ export const rename = win => {
|
||||
}
|
||||
|
||||
export const clearRecentlyUsed = () => {
|
||||
appMenu.clearRecentlyUsedDocuments()
|
||||
ipcMain.emit('menu-clear-recently-used')
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { getMenuItemById } from '../utils'
|
||||
import { getMenuItemById } from '../../menu'
|
||||
|
||||
const MENU_ID_FORMAT_MAP = {
|
||||
'strongMenuItem': 'strong',
|
@ -1,7 +1,5 @@
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
import { ipcMain } from 'electron'
|
||||
import appWindow from '../window'
|
||||
import userPreference from '../preference'
|
||||
|
||||
let updater
|
||||
let win
|
||||
@ -47,7 +45,7 @@ autoUpdater.on('update-downloaded', () => {
|
||||
})
|
||||
|
||||
export const userSetting = (menuItem, browserWindow) => {
|
||||
appWindow.createWindow(userPreference.userDataPath)
|
||||
ipcMain.emit('app-create-settings-window')
|
||||
}
|
||||
|
||||
export const checkUpdates = (menuItem, browserWindow) => {
|
@ -1,5 +1,5 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { getMenuItemById } from '../utils'
|
||||
import { getMenuItemById } from '../../menu'
|
||||
|
||||
const DISABLE_LABELS = [
|
||||
// paragraph menu items
|
5
src/main/menu/actions/theme.js
Normal file
5
src/main/menu/actions/theme.js
Normal file
@ -0,0 +1,5 @@
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
export const selectTheme = theme => {
|
||||
ipcMain.emit('mt::set-user-preference', undefined, { theme })
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { ipcMain, BrowserWindow } from 'electron'
|
||||
import { getMenuItemById } from '../utils'
|
||||
import { getMenuItemById } from '../../menu'
|
||||
|
||||
const sourceCodeModeMenuItemId = 'sourceCodeModeMenuItem'
|
||||
const typewriterModeMenuItemId = 'typewriterModeMenuItem'
|
7
src/main/menu/actions/window.js
Normal file
7
src/main/menu/actions/window.js
Normal file
@ -0,0 +1,7 @@
|
||||
import { ipcMain } from 'electron'
|
||||
|
||||
export const toggleAlwaysOnTop = win => {
|
||||
if (win) {
|
||||
ipcMain.emit('window-toggle-always-on-top', win)
|
||||
}
|
||||
}
|
@ -1,20 +1,33 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { app, ipcMain, Menu } from 'electron'
|
||||
import configureMenu from './menus'
|
||||
import { isDirectory, isFile, ensureDir, getPath, log } from './utils'
|
||||
import { parseMenu, registerKeyHandler } from './shortcutHandler'
|
||||
import log from 'electron-log'
|
||||
import { ensureDirSync, isDirectory, isFile } from '../filesystem'
|
||||
import { parseMenu } from '../keyboard/shortcutHandler'
|
||||
import configureMenu from '../menu/templates'
|
||||
|
||||
class AppMenu {
|
||||
constructor () {
|
||||
const FILE_NAME = 'recently-used-documents.json'
|
||||
|
||||
/**
|
||||
* @param {Preference} preferences The preferences instances.
|
||||
* @param {Keybindings} keybindings The keybindings instances.
|
||||
* @param {string} userDataPath The user data path.
|
||||
*/
|
||||
constructor (preferences, keybindings, userDataPath) {
|
||||
const FILE_NAME = 'recently-used-documents.json'
|
||||
this.MAX_RECENTLY_USED_DOCUMENTS = 12
|
||||
this.RECENTS_PATH = path.join(getPath('userData'), FILE_NAME)
|
||||
|
||||
this._preferences = preferences
|
||||
this._keybindings = keybindings
|
||||
this._userDataPath = userDataPath
|
||||
|
||||
this.RECENTS_PATH = path.join(userDataPath, FILE_NAME)
|
||||
this.isOsxOrWindows = /darwin|win32/.test(process.platform)
|
||||
this.isOsx = process.platform === 'darwin'
|
||||
this.activeWindowId = -1
|
||||
this.windowMenus = new Map()
|
||||
|
||||
this._listenForIpcMain()
|
||||
}
|
||||
|
||||
addRecentlyUsedDocument (filePath) {
|
||||
@ -41,7 +54,7 @@ class AppMenu {
|
||||
this.updateAppMenu(recentDocuments)
|
||||
|
||||
if (needSave) {
|
||||
ensureDir(getPath('userData'))
|
||||
ensureDirSync(this._userDataPath)
|
||||
const json = JSON.stringify(recentDocuments, null, 2)
|
||||
fs.writeFileSync(RECENTS_PATH, json, 'utf-8')
|
||||
}
|
||||
@ -62,7 +75,7 @@ class AppMenu {
|
||||
}
|
||||
return recentDocuments
|
||||
} catch (err) {
|
||||
log(err)
|
||||
log.error(err)
|
||||
return []
|
||||
}
|
||||
}
|
||||
@ -75,11 +88,11 @@ class AppMenu {
|
||||
const recentDocuments = []
|
||||
this.updateAppMenu(recentDocuments)
|
||||
const json = JSON.stringify(recentDocuments, null, 2)
|
||||
ensureDir(getPath('userData'))
|
||||
ensureDirSync(this._userDataPath)
|
||||
fs.writeFileSync(RECENTS_PATH, json, 'utf-8')
|
||||
}
|
||||
|
||||
addWindowMenuWithListener (window) {
|
||||
addEditorMenu (window) {
|
||||
const { windowMenus } = this
|
||||
windowMenus.set(window.id, this.buildDefaultMenu(true))
|
||||
|
||||
@ -100,11 +113,11 @@ class AppMenu {
|
||||
typewriterModeMenuItem.enabled = false
|
||||
focusModeMenuItem.enabled = false
|
||||
}
|
||||
registerKeyHandler(window, shortcutMap)
|
||||
this._keybindings.registerKeyHandlers(window, shortcutMap)
|
||||
}
|
||||
|
||||
removeWindowMenu (windowId) {
|
||||
// NOTE: Shortcut handler is automatically unregistered
|
||||
// NOTE: Shortcut handler is automatically unregistered when window is closed.
|
||||
const { activeWindowId } = this
|
||||
this.windowMenus.delete(windowId)
|
||||
if (activeWindowId === windowId) {
|
||||
@ -115,16 +128,18 @@ class AppMenu {
|
||||
getWindowMenuById (windowId) {
|
||||
const { menu } = this.windowMenus.get(windowId)
|
||||
if (!menu) {
|
||||
log(`getWindowMenuById: Cannot find window menu for id ${windowId}.`)
|
||||
log.error(`getWindowMenuById: Cannot find window menu for id ${windowId}.`)
|
||||
throw new Error(`Cannot find window menu for id ${windowId}.`)
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
setActiveWindow (windowId) {
|
||||
// change application menu to the current window menu
|
||||
Menu.setApplicationMenu(this.getWindowMenuById(windowId))
|
||||
this.activeWindowId = windowId
|
||||
if (this.activeWindowId !== windowId) {
|
||||
// Change application menu to the current window menu.
|
||||
Menu.setApplicationMenu(this.getWindowMenuById(windowId))
|
||||
this.activeWindowId = windowId
|
||||
}
|
||||
}
|
||||
|
||||
buildDefaultMenu (createShortcutMap, recentUsedDocuments) {
|
||||
@ -132,7 +147,7 @@ class AppMenu {
|
||||
recentUsedDocuments = this.getRecentlyUsedDocuments()
|
||||
}
|
||||
|
||||
const menuTemplate = configureMenu(recentUsedDocuments)
|
||||
const menuTemplate = configureMenu(this._keybindings, this._preferences, recentUsedDocuments)
|
||||
const menu = Menu.buildFromTemplate(menuTemplate)
|
||||
|
||||
let shortcutMap = null
|
||||
@ -178,26 +193,24 @@ class AppMenu {
|
||||
})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
updateLineEndingMenu (lineEnding) {
|
||||
updateLineEndingMenu(lineEnding)
|
||||
}
|
||||
|
||||
updateTextDirectionMenu (textDirection) {
|
||||
updateAlwaysOnTopMenu (flag) {
|
||||
const menus = Menu.getApplicationMenu()
|
||||
const ltrMenu = menus.getMenuItemById('textDirectionLTRMenuEntry')
|
||||
const rtlMenu = menus.getMenuItemById('textDirectionRTLMenuEntry')
|
||||
if (textDirection === 'ltr') {
|
||||
ltrMenu.checked = true
|
||||
} else {
|
||||
rtlMenu.checked = true
|
||||
}
|
||||
const menu = menus.getMenuItemById('alwaysOnTopMenuItem')
|
||||
menu.checked = flag
|
||||
}
|
||||
|
||||
_listenForIpcMain () {
|
||||
ipcMain.on('mt::add-recently-used-document', (e, pathname) => {
|
||||
this.addRecentlyUsedDocument(pathname)
|
||||
})
|
||||
|
||||
ipcMain.on('menu-clear-recently-used', () => {
|
||||
this.clearRecentlyUsedDocuments()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -219,10 +232,31 @@ const updateMenuItemSafe = (oldMenus, newMenus, id, defaultValue) => {
|
||||
newItem.checked = checked
|
||||
}
|
||||
|
||||
const appMenu = new AppMenu()
|
||||
// ----------------------------------------------
|
||||
|
||||
ipcMain.on('AGANI::add-recently-used-document', (e, pathname) => {
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
})
|
||||
// HACKY: We have one application menu per window and switch the menu when
|
||||
// switching windows, so we can access and change the menu items via Electron.
|
||||
|
||||
export default appMenu
|
||||
/**
|
||||
* Return the menu from the application menu.
|
||||
*
|
||||
* @param {string} menuId Menu ID
|
||||
* @returns {Electron.Menu} Returns the menu or null.
|
||||
*/
|
||||
export const getMenuItemById = menuId => {
|
||||
const menus = Menu.getApplicationMenu()
|
||||
return menus.getMenuItemById(menuId)
|
||||
}
|
||||
|
||||
export const updateLineEndingMenu = 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
|
||||
}
|
||||
}
|
||||
|
||||
export default AppMenu
|
156
src/main/menu/templates/edit.js
Executable file
156
src/main/menu/templates/edit.js
Executable file
@ -0,0 +1,156 @@
|
||||
import * as actions from '../actions/edit'
|
||||
|
||||
export default function (keybindings, userPreference) {
|
||||
const { aidou } = userPreference.getAll()
|
||||
return {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
label: 'Undo',
|
||||
accelerator: keybindings.getAccelerator('editUndo'),
|
||||
click: (menuItem, browserWindow) => {
|
||||
actions.edit(browserWindow, 'undo')
|
||||
}
|
||||
}, {
|
||||
label: 'Redo',
|
||||
accelerator: keybindings.getAccelerator('editRedo'),
|
||||
click: (menuItem, browserWindow) => {
|
||||
actions.edit(browserWindow, 'redo')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Cut',
|
||||
accelerator: keybindings.getAccelerator('editCut'),
|
||||
role: 'cut'
|
||||
}, {
|
||||
label: 'Copy',
|
||||
accelerator: keybindings.getAccelerator('editCopy'),
|
||||
role: 'copy'
|
||||
}, {
|
||||
label: 'Paste',
|
||||
accelerator: keybindings.getAccelerator('editPaste'),
|
||||
role: 'paste'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Copy As Markdown',
|
||||
accelerator: keybindings.getAccelerator('editCopyAsMarkdown'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'copyAsMarkdown')
|
||||
}
|
||||
}, {
|
||||
label: 'Copy As HTML',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'copyAsHtml')
|
||||
}
|
||||
}, {
|
||||
label: 'Paste As Plain Text',
|
||||
accelerator: keybindings.getAccelerator('editCopyAsPlaintext'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'pasteAsPlainText')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Select All',
|
||||
accelerator: keybindings.getAccelerator('editSelectAll'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'selectAll')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Duplicate',
|
||||
accelerator: keybindings.getAccelerator('editDuplicate'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'duplicate')
|
||||
}
|
||||
}, {
|
||||
label: 'Create Paragraph',
|
||||
accelerator: keybindings.getAccelerator('editCreateParagraph'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'createParagraph')
|
||||
}
|
||||
}, {
|
||||
label: 'Delete Paragraph',
|
||||
accelerator: keybindings.getAccelerator('editDeleteParagraph'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'deleteParagraph')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Find',
|
||||
accelerator: keybindings.getAccelerator('editFind'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'find')
|
||||
}
|
||||
}, {
|
||||
label: 'Find Next',
|
||||
accelerator: keybindings.getAccelerator('editFindNext'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'fineNext')
|
||||
}
|
||||
}, {
|
||||
label: 'Find Previous',
|
||||
accelerator: keybindings.getAccelerator('editFindPrevious'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'findPrev')
|
||||
}
|
||||
}, {
|
||||
label: 'Replace',
|
||||
accelerator: keybindings.getAccelerator('editReplace'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'replace')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Aidou',
|
||||
visible: aidou,
|
||||
accelerator: keybindings.getAccelerator('editAidou'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'aidou')
|
||||
}
|
||||
}, {
|
||||
label: 'Insert Image',
|
||||
submenu: [{
|
||||
label: 'Absolute Path',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.insertImage(browserWindow, 'absolute')
|
||||
}
|
||||
}, {
|
||||
label: 'Relative Path',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.insertImage(browserWindow, 'relative')
|
||||
}
|
||||
}, {
|
||||
label: 'Upload to Cloud (EXP)',
|
||||
click (menuItem, browserWindow) {
|
||||
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')
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
type: 'separator'
|
||||
}]
|
||||
}
|
||||
}
|
@ -2,10 +2,8 @@ import { app } from 'electron'
|
||||
import * as actions from '../actions/file'
|
||||
import { userSetting } from '../actions/marktext'
|
||||
import { showTabBar } from '../actions/view'
|
||||
import userPreference from '../preference'
|
||||
import keybindings from '../shortcutHandler'
|
||||
|
||||
export default function (recentlyUsedFiles) {
|
||||
export default function (keybindings, userPreference, recentlyUsedFiles) {
|
||||
const { autoSave } = userPreference.getAll()
|
||||
const notOsx = process.platform !== 'darwin'
|
||||
let fileMenu = {
|
||||
@ -14,14 +12,14 @@ export default function (recentlyUsedFiles) {
|
||||
label: 'New Tab',
|
||||
accelerator: keybindings.getAccelerator('fileNewFile'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.newTab(browserWindow)
|
||||
actions.newBlankTab(browserWindow)
|
||||
showTabBar(browserWindow)
|
||||
}
|
||||
}, {
|
||||
label: 'New Window',
|
||||
accelerator: keybindings.getAccelerator('fileNewTab'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.newFile()
|
||||
actions.newEditorWindow()
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
101
src/main/menu/templates/format.js
Normal file
101
src/main/menu/templates/format.js
Normal file
@ -0,0 +1,101 @@
|
||||
import * as actions from '../actions/format'
|
||||
|
||||
export default function (keybindings) {
|
||||
return {
|
||||
id: 'formatMenuItem',
|
||||
label: 'Format',
|
||||
submenu: [{
|
||||
id: 'strongMenuItem',
|
||||
label: 'Strong',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatStrong'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'strong')
|
||||
}
|
||||
}, {
|
||||
id: 'emphasisMenuItem',
|
||||
label: 'Emphasis',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatEmphasis'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'em')
|
||||
}
|
||||
}, {
|
||||
id: 'underlineMenuItem',
|
||||
label: 'Underline',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatUnderline'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'u')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'superscriptMenuItem',
|
||||
label: 'Superscript',
|
||||
type: 'checkbox',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'sup')
|
||||
}
|
||||
}, {
|
||||
id: 'subscriptMenuItem',
|
||||
label: 'Subscript',
|
||||
type: 'checkbox',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'sub')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'inlineCodeMenuItem',
|
||||
label: 'Inline Code',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatInlineCode'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'inline_code')
|
||||
}
|
||||
}, {
|
||||
id: 'inlineMathMenuItem',
|
||||
label: 'Inline Math',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatInlineMath'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'inline_math')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'strikeMenuItem',
|
||||
label: 'Strike',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatStrike'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'del')
|
||||
}
|
||||
}, {
|
||||
id: 'hyperlinkMenuItem',
|
||||
label: 'Hyperlink',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatHyperlink'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'link')
|
||||
}
|
||||
}, {
|
||||
id: 'imageMenuItem',
|
||||
label: 'Image',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatImage'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'image')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Clear Format',
|
||||
accelerator: keybindings.getAccelerator('formatClearFormat'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'clear')
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
76
src/main/menu/templates/help.js
Executable file
76
src/main/menu/templates/help.js
Executable file
@ -0,0 +1,76 @@
|
||||
import path from 'path'
|
||||
import { shell } from 'electron'
|
||||
import * as actions from '../actions/help'
|
||||
import { checkUpdates } from '../actions/marktext'
|
||||
import { isFile } from '../../filesystem'
|
||||
|
||||
export default function () {
|
||||
const helpMenu = {
|
||||
label: 'Help',
|
||||
role: 'help',
|
||||
submenu: [{
|
||||
label: 'Learn More',
|
||||
click () {
|
||||
shell.openExternal('https://marktext.app')
|
||||
}
|
||||
}, {
|
||||
label: 'Source Code on GitHub',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/marktext/marktext')
|
||||
}
|
||||
}, {
|
||||
label: 'Changelog',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/marktext/marktext/blob/master/.github/CHANGELOG.md')
|
||||
}
|
||||
}, {
|
||||
label: 'Markdown syntax',
|
||||
click () {
|
||||
shell.openExternal('https://spec.commonmark.org/0.29/')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Feedback via Twitter',
|
||||
click (item, win) {
|
||||
actions.showTweetDialog(win, 'twitter')
|
||||
}
|
||||
}, {
|
||||
label: 'Report Issue or Feature request',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/marktext/marktext/issues')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Follow @Jocs on Github',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/Jocs')
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
if (isFile(path.join(process.resourcesPath, 'app-update.yml')) &&
|
||||
(process.platform === 'win32' || !!process.env.APPIMAGE)) {
|
||||
helpMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Check for updates...',
|
||||
click (menuItem, browserWindow) {
|
||||
checkUpdates(menuItem, browserWindow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
helpMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'About Mark Text',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.showAboutDialog(browserWindow)
|
||||
}
|
||||
})
|
||||
}
|
||||
return helpMenu
|
||||
}
|
32
src/main/menu/templates/index.js
Normal file
32
src/main/menu/templates/index.js
Normal file
@ -0,0 +1,32 @@
|
||||
import edit from './edit'
|
||||
import file from './file'
|
||||
import help from './help'
|
||||
import marktext from './marktext'
|
||||
import view from './view'
|
||||
import window from './window'
|
||||
import paragraph from './paragraph'
|
||||
import format from './format'
|
||||
import theme from './theme'
|
||||
|
||||
export dockMenu from './dock'
|
||||
|
||||
/**
|
||||
* Create the application menu for the editor window.
|
||||
*
|
||||
* @param {Keybindings} keybindings The keybindings instance.
|
||||
* @param {Preference} preferences The preference instance.
|
||||
* @param {string[]} recentlyUsedFiles The recently used files.
|
||||
*/
|
||||
export default function (keybindings, preferences, recentlyUsedFiles) {
|
||||
return [
|
||||
...(process.platform === 'darwin' ? [ marktext(keybindings) ] : []),
|
||||
file(keybindings, preferences, recentlyUsedFiles),
|
||||
edit(keybindings, preferences),
|
||||
paragraph(keybindings),
|
||||
format(keybindings),
|
||||
window(keybindings),
|
||||
theme(preferences),
|
||||
view(keybindings),
|
||||
help()
|
||||
]
|
||||
}
|
51
src/main/menu/templates/marktext.js
Executable file
51
src/main/menu/templates/marktext.js
Executable file
@ -0,0 +1,51 @@
|
||||
import { app } from 'electron'
|
||||
import { showAboutDialog } from '../actions/help'
|
||||
import * as actions from '../actions/marktext'
|
||||
|
||||
export default function (keybindings) {
|
||||
return {
|
||||
label: 'Mark Text',
|
||||
submenu: [{
|
||||
label: 'About Mark Text',
|
||||
click (menuItem, browserWindow) {
|
||||
showAboutDialog(browserWindow)
|
||||
}
|
||||
}, {
|
||||
label: 'Check for updates...',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.checkUpdates(menuItem, browserWindow)
|
||||
}
|
||||
}, {
|
||||
label: 'Preferences',
|
||||
accelerator: keybindings.getAccelerator('filePreferences'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.userSetting(menuItem, browserWindow)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Services',
|
||||
role: 'services',
|
||||
submenu: []
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Hide Mark Text',
|
||||
accelerator: keybindings.getAccelerator('mtHide'),
|
||||
role: 'hide'
|
||||
}, {
|
||||
label: 'Hide Others',
|
||||
accelerator: keybindings.getAccelerator('mtHideOthers'),
|
||||
role: 'hideothers'
|
||||
}, {
|
||||
label: 'Show All',
|
||||
role: 'unhide'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Quit Mark Text',
|
||||
accelerator: keybindings.getAccelerator('fileQuit'),
|
||||
click: app.quit
|
||||
}]
|
||||
}
|
||||
}
|
177
src/main/menu/templates/paragraph.js
Normal file
177
src/main/menu/templates/paragraph.js
Normal file
@ -0,0 +1,177 @@
|
||||
import * as actions from '../actions/paragraph'
|
||||
|
||||
export default function (keybindings) {
|
||||
return {
|
||||
id: 'paragraphMenuEntry',
|
||||
label: 'Paragraph',
|
||||
submenu: [{
|
||||
id: 'heading1MenuItem',
|
||||
label: 'Heading 1',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading1'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 1')
|
||||
}
|
||||
}, {
|
||||
id: 'heading2MenuItem',
|
||||
label: 'Heading 2',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading2'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 2')
|
||||
}
|
||||
}, {
|
||||
id: 'heading3MenuItem',
|
||||
label: 'Heading 3',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading3'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 3')
|
||||
}
|
||||
}, {
|
||||
id: 'heading4MenuItem',
|
||||
label: 'Heading 4',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading4'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 4')
|
||||
}
|
||||
}, {
|
||||
id: 'heading5MenuItem',
|
||||
label: 'Heading 5',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading5'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 5')
|
||||
}
|
||||
}, {
|
||||
id: 'heading6MenuItem',
|
||||
label: 'Heading 6',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading6'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 6')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'upgradeHeadingMenuItem',
|
||||
label: 'Upgrade Heading',
|
||||
accelerator: keybindings.getAccelerator('paragraphUpgradeHeading'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'upgrade heading')
|
||||
}
|
||||
}, {
|
||||
id: 'degradeHeadingMenuItem',
|
||||
label: 'Degrade Heading',
|
||||
accelerator: keybindings.getAccelerator('paragraphDegradeHeading'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'degrade heading')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'tableMenuItem',
|
||||
label: 'Table',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphTable'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'table')
|
||||
}
|
||||
}, {
|
||||
id: 'codeFencesMenuItem',
|
||||
label: 'Code Fences',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphCodeFence'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'pre')
|
||||
}
|
||||
}, {
|
||||
id: 'quoteBlockMenuItem',
|
||||
label: 'Quote Block',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphQuoteBlock'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'blockquote')
|
||||
}
|
||||
}, {
|
||||
id: 'mathBlockMenuItem',
|
||||
label: 'Math Block',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphMathBlock'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'mathblock')
|
||||
}
|
||||
}, {
|
||||
id: 'htmlBlockMenuItem',
|
||||
label: 'Html Block',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHtmlBlock'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'html')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'orderListMenuItem',
|
||||
label: 'Order List',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphOrderList'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'ol-order')
|
||||
}
|
||||
}, {
|
||||
id: 'bulletListMenuItem',
|
||||
label: 'Bullet List',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphBulletList'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'ul-bullet')
|
||||
}
|
||||
}, {
|
||||
id: 'taskListMenuItem',
|
||||
label: 'Task List',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphTaskList'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'ul-task')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'looseListItemMenuItem',
|
||||
label: 'Loose List Item',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphLooseListItem'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'loose-list-item')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'paragraphMenuItem',
|
||||
label: 'Paragraph',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphParagraph'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'paragraph')
|
||||
}
|
||||
}, {
|
||||
id: 'horizontalLineMenuItem',
|
||||
label: 'Horizontal Line',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHorizontalLine'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'hr')
|
||||
}
|
||||
}, {
|
||||
id: 'frontMatterMenuItem',
|
||||
label: 'YAML Front Matter',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphYAMLFrontMatter'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'front-matter')
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
58
src/main/menu/templates/theme.js
Normal file
58
src/main/menu/templates/theme.js
Normal file
@ -0,0 +1,58 @@
|
||||
import * as actions from '../actions/theme'
|
||||
|
||||
export default function (userPreference) {
|
||||
const { theme } = userPreference.getAll()
|
||||
return {
|
||||
label: 'Theme',
|
||||
id: 'themeMenu',
|
||||
submenu: [{
|
||||
label: 'Cadmium Light',
|
||||
type: 'radio',
|
||||
id: 'light',
|
||||
checked: theme === 'light',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('light')
|
||||
}
|
||||
}, {
|
||||
label: 'Dark',
|
||||
type: 'radio',
|
||||
id: 'dark',
|
||||
checked: theme === 'dark',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('dark')
|
||||
}
|
||||
}, {
|
||||
label: 'Graphite Light',
|
||||
type: 'radio',
|
||||
id: 'graphite',
|
||||
checked: theme === 'graphite',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('graphite')
|
||||
}
|
||||
}, {
|
||||
label: 'Material Dark',
|
||||
type: 'radio',
|
||||
id: 'material-dark',
|
||||
checked: theme === 'material-dark',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('material-dark')
|
||||
}
|
||||
}, {
|
||||
label: 'One Dark',
|
||||
type: 'radio',
|
||||
id: 'one-dark',
|
||||
checked: theme === 'one-dark',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('one-dark')
|
||||
}
|
||||
}, {
|
||||
label: 'Ulysses Light',
|
||||
type: 'radio',
|
||||
id: 'ulysses',
|
||||
checked: theme === 'ulysses',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('ulysses')
|
||||
}
|
||||
}]
|
||||
}
|
||||
}
|
131
src/main/menu/templates/view.js
Executable file
131
src/main/menu/templates/view.js
Executable file
@ -0,0 +1,131 @@
|
||||
import * as actions from '../actions/view'
|
||||
import { isOsx } from '../../config'
|
||||
|
||||
export default function (keybindings) {
|
||||
let viewMenu = {
|
||||
label: 'View',
|
||||
submenu: [{
|
||||
label: 'Toggle Full Screen',
|
||||
accelerator: keybindings.getAccelerator('viewToggleFullScreen'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Font...',
|
||||
accelerator: keybindings.getAccelerator('viewChangeFont'),
|
||||
click (item, browserWindow) {
|
||||
actions.changeFont(browserWindow)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'sourceCodeModeMenuItem',
|
||||
label: 'Source Code Mode',
|
||||
accelerator: keybindings.getAccelerator('viewSourceCodeMode'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
actions.typeMode(browserWindow, 'sourceCode', item)
|
||||
}
|
||||
}, {
|
||||
id: 'typewriterModeMenuItem',
|
||||
label: 'Typewriter Mode',
|
||||
accelerator: keybindings.getAccelerator('viewTypewriterMode'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
actions.typeMode(browserWindow, 'typewriter', item)
|
||||
}
|
||||
}, {
|
||||
id: 'focusModeMenuItem',
|
||||
label: 'Focus Mode',
|
||||
accelerator: keybindings.getAccelerator('viewFocusMode'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
actions.typeMode(browserWindow, 'focus', item)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Toggle Side Bar',
|
||||
id: 'sideBarMenuItem',
|
||||
accelerator: keybindings.getAccelerator('viewToggleSideBar'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
|
||||
actions.layout(item, browserWindow, 'showSideBar')
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle Tab Bar',
|
||||
id: 'tabBarMenuItem',
|
||||
accelerator: keybindings.getAccelerator('viewToggleTabBar'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
|
||||
actions.layout(item, browserWindow, 'showTabBar')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}]
|
||||
}
|
||||
|
||||
if (global.MARKTEXT_DEBUG) {
|
||||
// add devtool when development
|
||||
viewMenu.submenu.push({
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: keybindings.getAccelerator('viewDevToggleDeveloperTools'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools()
|
||||
}
|
||||
}
|
||||
})
|
||||
// add reload when development
|
||||
viewMenu.submenu.push({
|
||||
label: 'Reload',
|
||||
accelerator: keybindings.getAccelerator('viewDevReload'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isOsx) {
|
||||
viewMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Bring All to Front',
|
||||
role: 'front'
|
||||
})
|
||||
}
|
||||
return viewMenu
|
||||
}
|
26
src/main/menu/templates/window.js
Executable file
26
src/main/menu/templates/window.js
Executable file
@ -0,0 +1,26 @@
|
||||
import { toggleAlwaysOnTop } from '../actions/window'
|
||||
|
||||
export default function (keybindings) {
|
||||
return {
|
||||
label: 'Window',
|
||||
role: 'window',
|
||||
submenu: [{
|
||||
label: 'Minimize',
|
||||
accelerator: keybindings.getAccelerator('windowMinimize'),
|
||||
role: 'minimize'
|
||||
}, {
|
||||
id: 'alwaysOnTopMenuItem',
|
||||
label: 'Always on Top',
|
||||
type: 'checkbox',
|
||||
click (menuItem, browserWindow) {
|
||||
toggleAlwaysOnTop(browserWindow)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Close Window',
|
||||
accelerator: keybindings.getAccelerator('windowCloseWindow'),
|
||||
role: 'close'
|
||||
}]
|
||||
}
|
||||
}
|
@ -1,174 +0,0 @@
|
||||
import * as actions from '../actions/edit'
|
||||
import userPreference from '../preference'
|
||||
import keybindings from '../shortcutHandler'
|
||||
|
||||
const { aidou } = userPreference.getAll()
|
||||
|
||||
export default {
|
||||
label: 'Edit',
|
||||
submenu: [{
|
||||
label: 'Undo',
|
||||
accelerator: keybindings.getAccelerator('editUndo'),
|
||||
click: (menuItem, browserWindow) => {
|
||||
actions.edit(browserWindow, 'undo')
|
||||
}
|
||||
}, {
|
||||
label: 'Redo',
|
||||
accelerator: keybindings.getAccelerator('editRedo'),
|
||||
click: (menuItem, browserWindow) => {
|
||||
actions.edit(browserWindow, 'redo')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Cut',
|
||||
accelerator: keybindings.getAccelerator('editCut'),
|
||||
role: 'cut'
|
||||
}, {
|
||||
label: 'Copy',
|
||||
accelerator: keybindings.getAccelerator('editCopy'),
|
||||
role: 'copy'
|
||||
}, {
|
||||
label: 'Paste',
|
||||
accelerator: keybindings.getAccelerator('editPaste'),
|
||||
role: 'paste'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Copy As Markdown',
|
||||
accelerator: keybindings.getAccelerator('editCopyAsMarkdown'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'copyAsMarkdown')
|
||||
}
|
||||
}, {
|
||||
label: 'Copy As HTML',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'copyAsHtml')
|
||||
}
|
||||
}, {
|
||||
label: 'Paste As Plain Text',
|
||||
accelerator: keybindings.getAccelerator('editCopyAsPlaintext'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'pasteAsPlainText')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Select All',
|
||||
accelerator: keybindings.getAccelerator('editSelectAll'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'selectAll')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Duplicate',
|
||||
accelerator: keybindings.getAccelerator('editDuplicate'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'duplicate')
|
||||
}
|
||||
}, {
|
||||
label: 'Create Paragraph',
|
||||
accelerator: keybindings.getAccelerator('editCreateParagraph'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'createParagraph')
|
||||
}
|
||||
}, {
|
||||
label: 'Delete Paragraph',
|
||||
accelerator: keybindings.getAccelerator('editDeleteParagraph'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'deleteParagraph')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Find',
|
||||
accelerator: keybindings.getAccelerator('editFind'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'find')
|
||||
}
|
||||
}, {
|
||||
label: 'Find Next',
|
||||
accelerator: keybindings.getAccelerator('editFindNext'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'fineNext')
|
||||
}
|
||||
}, {
|
||||
label: 'Find Previous',
|
||||
accelerator: keybindings.getAccelerator('editFindPrevious'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'findPrev')
|
||||
}
|
||||
}, {
|
||||
label: 'Replace',
|
||||
accelerator: keybindings.getAccelerator('editReplace'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'replace')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Aidou',
|
||||
visible: aidou,
|
||||
accelerator: keybindings.getAccelerator('editAidou'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.edit(browserWindow, 'aidou')
|
||||
}
|
||||
}, {
|
||||
label: 'Insert Image',
|
||||
submenu: [{
|
||||
label: 'Absolute Path',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.insertImage(browserWindow, 'absolute')
|
||||
}
|
||||
}, {
|
||||
label: 'Relative Path',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.insertImage(browserWindow, 'relative')
|
||||
}
|
||||
}, {
|
||||
label: 'Upload to Cloud (EXP)',
|
||||
click (menuItem, browserWindow) {
|
||||
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')
|
||||
}
|
||||
}]
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Text Direction',
|
||||
submenu: [{
|
||||
id: 'textDirectionLTRMenuEntry',
|
||||
label: 'Left To Right',
|
||||
type: 'radio',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.textDirection(browserWindow, 'ltr')
|
||||
}
|
||||
}, {
|
||||
id: 'textDirectionRTLMenuEntry',
|
||||
label: 'Right To Left',
|
||||
type: 'radio',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.textDirection(browserWindow, 'rtl')
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import * as actions from '../actions/format'
|
||||
import keybindings from '../shortcutHandler'
|
||||
|
||||
export default {
|
||||
id: 'formatMenuItem',
|
||||
label: 'Format',
|
||||
submenu: [{
|
||||
id: 'strongMenuItem',
|
||||
label: 'Strong',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatStrong'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'strong')
|
||||
}
|
||||
}, {
|
||||
id: 'emphasisMenuItem',
|
||||
label: 'Emphasis',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatEmphasis'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'em')
|
||||
}
|
||||
}, {
|
||||
id: 'underlineMenuItem',
|
||||
label: 'Underline',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatUnderline'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'u')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'superscriptMenuItem',
|
||||
label: 'Superscript',
|
||||
type: 'checkbox',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'sup')
|
||||
}
|
||||
}, {
|
||||
id: 'subscriptMenuItem',
|
||||
label: 'Subscript',
|
||||
type: 'checkbox',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'sub')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'inlineCodeMenuItem',
|
||||
label: 'Inline Code',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatInlineCode'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'inline_code')
|
||||
}
|
||||
}, {
|
||||
id: 'inlineMathMenuItem',
|
||||
label: 'Inline Math',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatInlineMath'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'inline_math')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'strikeMenuItem',
|
||||
label: 'Strike',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatStrike'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'del')
|
||||
}
|
||||
}, {
|
||||
id: 'hyperlinkMenuItem',
|
||||
label: 'Hyperlink',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatHyperlink'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'link')
|
||||
}
|
||||
}, {
|
||||
id: 'imageMenuItem',
|
||||
label: 'Image',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('formatImage'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'image')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Clear Format',
|
||||
accelerator: keybindings.getAccelerator('formatClearFormat'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.format(browserWindow, 'clear')
|
||||
}
|
||||
}]
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
import path from 'path'
|
||||
import { shell } from 'electron'
|
||||
import * as actions from '../actions/help'
|
||||
import { checkUpdates } from '../actions/marktext'
|
||||
import { isFile } from '../utils'
|
||||
|
||||
const helpMenu = {
|
||||
label: 'Help',
|
||||
role: 'help',
|
||||
submenu: [{
|
||||
label: 'Learn More',
|
||||
click () {
|
||||
shell.openExternal('https://marktext.app')
|
||||
}
|
||||
}, {
|
||||
label: 'Source Code on GitHub',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/marktext/marktext')
|
||||
}
|
||||
}, {
|
||||
label: 'Changelog',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/marktext/marktext/blob/master/.github/CHANGELOG.md')
|
||||
}
|
||||
}, {
|
||||
label: 'Markdown syntax',
|
||||
click () {
|
||||
shell.openExternal('https://spec.commonmark.org/0.29/')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Feedback via Twitter',
|
||||
click (item, win) {
|
||||
actions.showTweetDialog(win, 'twitter')
|
||||
}
|
||||
}, {
|
||||
label: 'Report Issue or Feature request',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/marktext/marktext/issues')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Follow @Jocs on Github',
|
||||
click () {
|
||||
shell.openExternal('https://github.com/Jocs')
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
if (isFile(path.join(process.resourcesPath, 'app-update.yml')) &&
|
||||
(process.platform === 'win32' || !!process.env.APPIMAGE)) {
|
||||
helpMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Check for updates...',
|
||||
click (menuItem, browserWindow) {
|
||||
checkUpdates(menuItem, browserWindow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
helpMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'About Mark Text',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.showAboutDialog(browserWindow)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default helpMenu
|
@ -1,25 +0,0 @@
|
||||
import edit from './edit'
|
||||
import file from './file'
|
||||
import help from './help'
|
||||
import marktext from './marktext'
|
||||
import view from './view'
|
||||
import windowMenu from './windowMenu'
|
||||
import paragraph from './paragraph'
|
||||
import format from './format'
|
||||
import theme from './theme'
|
||||
|
||||
export dockMenu from './dock'
|
||||
|
||||
export default function (recentlyUsedFiles) {
|
||||
return [
|
||||
...(process.platform === 'darwin' ? [marktext] : []),
|
||||
file(recentlyUsedFiles),
|
||||
edit,
|
||||
paragraph,
|
||||
format,
|
||||
windowMenu,
|
||||
theme,
|
||||
view,
|
||||
help
|
||||
]
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import { app } from 'electron'
|
||||
import { showAboutDialog } from '../actions/help'
|
||||
import * as actions from '../actions/marktext'
|
||||
import keybindings from '../shortcutHandler'
|
||||
|
||||
export default {
|
||||
label: 'Mark Text',
|
||||
submenu: [{
|
||||
label: 'About Mark Text',
|
||||
click (menuItem, browserWindow) {
|
||||
showAboutDialog(browserWindow)
|
||||
}
|
||||
}, {
|
||||
label: 'Check for updates...',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.checkUpdates(menuItem, browserWindow)
|
||||
}
|
||||
}, {
|
||||
label: 'Preferences',
|
||||
accelerator: keybindings.getAccelerator('filePreferences'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.userSetting(menuItem, browserWindow)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Services',
|
||||
role: 'services',
|
||||
submenu: []
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Hide Mark Text',
|
||||
accelerator: keybindings.getAccelerator('mtHide'),
|
||||
role: 'hide'
|
||||
}, {
|
||||
label: 'Hide Others',
|
||||
accelerator: keybindings.getAccelerator('mtHideOthers'),
|
||||
role: 'hideothers'
|
||||
}, {
|
||||
label: 'Show All',
|
||||
role: 'unhide'
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Quit Mark Text',
|
||||
accelerator: keybindings.getAccelerator('fileQuit'),
|
||||
click: app.quit
|
||||
}]
|
||||
}
|
@ -1,176 +0,0 @@
|
||||
import * as actions from '../actions/paragraph'
|
||||
import keybindings from '../shortcutHandler'
|
||||
|
||||
export default {
|
||||
id: 'paragraphMenuEntry',
|
||||
label: 'Paragraph',
|
||||
submenu: [{
|
||||
id: 'heading1MenuItem',
|
||||
label: 'Heading 1',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading1'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 1')
|
||||
}
|
||||
}, {
|
||||
id: 'heading2MenuItem',
|
||||
label: 'Heading 2',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading2'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 2')
|
||||
}
|
||||
}, {
|
||||
id: 'heading3MenuItem',
|
||||
label: 'Heading 3',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading3'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 3')
|
||||
}
|
||||
}, {
|
||||
id: 'heading4MenuItem',
|
||||
label: 'Heading 4',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading4'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 4')
|
||||
}
|
||||
}, {
|
||||
id: 'heading5MenuItem',
|
||||
label: 'Heading 5',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading5'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 5')
|
||||
}
|
||||
}, {
|
||||
id: 'heading6MenuItem',
|
||||
label: 'Heading 6',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHeading6'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'heading 6')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'upgradeHeadingMenuItem',
|
||||
label: 'Upgrade Heading',
|
||||
accelerator: keybindings.getAccelerator('paragraphUpgradeHeading'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'upgrade heading')
|
||||
}
|
||||
}, {
|
||||
id: 'degradeHeadingMenuItem',
|
||||
label: 'Degrade Heading',
|
||||
accelerator: keybindings.getAccelerator('paragraphDegradeHeading'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'degrade heading')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'tableMenuItem',
|
||||
label: 'Table',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphTable'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'table')
|
||||
}
|
||||
}, {
|
||||
id: 'codeFencesMenuItem',
|
||||
label: 'Code Fences',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphCodeFence'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'pre')
|
||||
}
|
||||
}, {
|
||||
id: 'quoteBlockMenuItem',
|
||||
label: 'Quote Block',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphQuoteBlock'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'blockquote')
|
||||
}
|
||||
}, {
|
||||
id: 'mathBlockMenuItem',
|
||||
label: 'Math Block',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphMathBlock'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'mathblock')
|
||||
}
|
||||
}, {
|
||||
id: 'htmlBlockMenuItem',
|
||||
label: 'Html Block',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHtmlBlock'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'html')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'orderListMenuItem',
|
||||
label: 'Order List',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphOrderList'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'ol-order')
|
||||
}
|
||||
}, {
|
||||
id: 'bulletListMenuItem',
|
||||
label: 'Bullet List',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphBulletList'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'ul-bullet')
|
||||
}
|
||||
}, {
|
||||
id: 'taskListMenuItem',
|
||||
label: 'Task List',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphTaskList'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'ul-task')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'looseListItemMenuItem',
|
||||
label: 'Loose List Item',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphLooseListItem'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'loose-list-item')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'paragraphMenuItem',
|
||||
label: 'Paragraph',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphParagraph'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'paragraph')
|
||||
}
|
||||
}, {
|
||||
id: 'horizontalLineMenuItem',
|
||||
label: 'Horizontal Line',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphHorizontalLine'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'hr')
|
||||
}
|
||||
}, {
|
||||
id: 'frontMatterMenuItem',
|
||||
label: 'YAML Front Matter',
|
||||
type: 'checkbox',
|
||||
accelerator: keybindings.getAccelerator('paragraphYAMLFrontMatter'),
|
||||
click (menuItem, browserWindow) {
|
||||
actions.paragraph(browserWindow, 'front-matter')
|
||||
}
|
||||
}]
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
import * as actions from '../actions/theme'
|
||||
import userPreference from '../preference'
|
||||
|
||||
const { theme } = userPreference.getAll()
|
||||
|
||||
export default {
|
||||
label: 'Theme',
|
||||
id: 'themeMenu',
|
||||
submenu: [{
|
||||
label: 'Cadmium Light',
|
||||
type: 'radio',
|
||||
id: 'light',
|
||||
checked: theme === 'light',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('light')
|
||||
}
|
||||
}, {
|
||||
label: 'Dark',
|
||||
type: 'radio',
|
||||
id: 'dark',
|
||||
checked: theme === 'dark',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('dark')
|
||||
}
|
||||
}, {
|
||||
label: 'Graphite Light',
|
||||
type: 'radio',
|
||||
id: 'graphite',
|
||||
checked: theme === 'graphite',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('graphite')
|
||||
}
|
||||
}, {
|
||||
label: 'Material Dark',
|
||||
type: 'radio',
|
||||
id: 'material-dark',
|
||||
checked: theme === 'material-dark',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('material-dark')
|
||||
}
|
||||
}, {
|
||||
label: 'One Dark',
|
||||
type: 'radio',
|
||||
id: 'one-dark',
|
||||
checked: theme === 'one-dark',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('one-dark')
|
||||
}
|
||||
}, {
|
||||
label: 'Ulysses Light',
|
||||
type: 'radio',
|
||||
id: 'ulysses',
|
||||
checked: theme === 'ulysses',
|
||||
click (menuItem, browserWindow) {
|
||||
actions.selectTheme('ulysses')
|
||||
}
|
||||
}]
|
||||
}
|
@ -1,131 +0,0 @@
|
||||
import * as actions from '../actions/view'
|
||||
import { isOsx } from '../config'
|
||||
import keybindings from '../shortcutHandler'
|
||||
|
||||
let viewMenu = {
|
||||
label: 'View',
|
||||
submenu: [{
|
||||
label: 'Toggle Full Screen',
|
||||
accelerator: keybindings.getAccelerator('viewToggleFullScreen'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
|
||||
}
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Font...',
|
||||
accelerator: keybindings.getAccelerator('viewChangeFont'),
|
||||
click (item, browserWindow) {
|
||||
actions.changeFont(browserWindow)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
id: 'sourceCodeModeMenuItem',
|
||||
label: 'Source Code Mode',
|
||||
accelerator: keybindings.getAccelerator('viewSourceCodeMode'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
actions.typeMode(browserWindow, 'sourceCode', item)
|
||||
}
|
||||
}, {
|
||||
id: 'typewriterModeMenuItem',
|
||||
label: 'Typewriter Mode',
|
||||
accelerator: keybindings.getAccelerator('viewTypewriterMode'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
actions.typeMode(browserWindow, 'typewriter', item)
|
||||
}
|
||||
}, {
|
||||
id: 'focusModeMenuItem',
|
||||
label: 'Focus Mode',
|
||||
accelerator: keybindings.getAccelerator('viewFocusMode'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
actions.typeMode(browserWindow, 'focus', item)
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Toggle Side Bar',
|
||||
id: 'sideBarMenuItem',
|
||||
accelerator: keybindings.getAccelerator('viewToggleSideBar'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
|
||||
actions.layout(item, browserWindow, 'showSideBar')
|
||||
}
|
||||
}, {
|
||||
label: 'Toggle Tab Bar',
|
||||
id: 'tabBarMenuItem',
|
||||
accelerator: keybindings.getAccelerator('viewToggleTabBar'),
|
||||
type: 'checkbox',
|
||||
checked: false,
|
||||
click (item, browserWindow, event) {
|
||||
// if we call this function, the checked state is not set
|
||||
if (!event) {
|
||||
item.checked = !item.checked
|
||||
}
|
||||
|
||||
actions.layout(item, browserWindow, 'showTabBar')
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}]
|
||||
}
|
||||
|
||||
if (global.MARKTEXT_DEBUG) {
|
||||
// add devtool when development
|
||||
viewMenu.submenu.push({
|
||||
label: 'Toggle Developer Tools',
|
||||
accelerator: keybindings.getAccelerator('viewDevToggleDeveloperTools'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.webContents.toggleDevTools()
|
||||
}
|
||||
}
|
||||
})
|
||||
// add reload when development
|
||||
viewMenu.submenu.push({
|
||||
label: 'Reload',
|
||||
accelerator: keybindings.getAccelerator('viewDevReload'),
|
||||
click (item, focusedWindow) {
|
||||
if (focusedWindow) {
|
||||
focusedWindow.reload()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (isOsx) {
|
||||
viewMenu.submenu.push({
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'Bring All to Front',
|
||||
role: 'front'
|
||||
})
|
||||
}
|
||||
|
||||
export default viewMenu
|
@ -1,15 +0,0 @@
|
||||
import keybindings from '../shortcutHandler'
|
||||
|
||||
export default {
|
||||
label: 'Window',
|
||||
role: 'window',
|
||||
submenu: [{
|
||||
label: 'Minimize',
|
||||
accelerator: keybindings.getAccelerator('windowMinimize'),
|
||||
role: 'minimize'
|
||||
}, {
|
||||
label: 'Close Window',
|
||||
accelerator: keybindings.getAccelerator('windowCloseWindow'),
|
||||
role: 'close'
|
||||
}]
|
||||
}
|
@ -1,10 +1,12 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { ipcMain, BrowserWindow, systemPreferences } from 'electron'
|
||||
import { isOsx, isWindows } from './config'
|
||||
import appWindow from './window'
|
||||
import { getPath, hasSameKeys, log, ensureDir } from './utils'
|
||||
import { getStringRegKey, winHKEY } from './platform/win32/registry.js'
|
||||
import EventEmitter from 'events'
|
||||
import { BrowserWindow, ipcMain, systemPreferences } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import { isOsx, isWindows } from '../config'
|
||||
import { ensureDirSync } from '../filesystem'
|
||||
import { hasSameKeys } from '../utils'
|
||||
import { getStringRegKey, winHKEY } from '../platform/win32/registry.js'
|
||||
|
||||
const isDarkSystemMode = () => {
|
||||
if (isOsx) {
|
||||
@ -17,37 +19,41 @@ const isDarkSystemMode = () => {
|
||||
return false
|
||||
}
|
||||
|
||||
class Preference {
|
||||
constructor () {
|
||||
const FILE_NAME = 'preference.md'
|
||||
const staticPath = path.join(__static, FILE_NAME)
|
||||
const userDataPath = path.join(getPath('userData'), FILE_NAME)
|
||||
class Preference extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @param {AppPaths} userDataPath The path instance.
|
||||
*/
|
||||
constructor (paths) {
|
||||
super()
|
||||
|
||||
const { userDataPath, preferencesFilePath } = paths
|
||||
this._userDataPath = userDataPath
|
||||
|
||||
this.cache = null
|
||||
this.staticPath = staticPath
|
||||
this.userDataPath = userDataPath
|
||||
|
||||
this.staticPath = path.join(__static, 'preference.md')
|
||||
this.settingsPath = preferencesFilePath
|
||||
this.init()
|
||||
}
|
||||
|
||||
init () {
|
||||
const { userDataPath, staticPath } = this
|
||||
const { settingsPath, staticPath } = this
|
||||
const defaultSettings = this.loadJson(staticPath)
|
||||
let userSetting = null
|
||||
|
||||
// Try to load settings or write default settings if file doesn't exists.
|
||||
if (!fs.existsSync(userDataPath) || !this.loadJson(userDataPath)) {
|
||||
ensureDir(getPath('userData'))
|
||||
if (!fs.existsSync(settingsPath) || !this.loadJson(settingsPath)) {
|
||||
ensureDirSync(this._userDataPath)
|
||||
const content = fs.readFileSync(staticPath, 'utf-8')
|
||||
fs.writeFileSync(userDataPath, content, 'utf-8')
|
||||
fs.writeFileSync(settingsPath, content, 'utf-8')
|
||||
|
||||
userSetting = this.loadJson(userDataPath)
|
||||
userSetting = this.loadJson(settingsPath)
|
||||
if (isDarkSystemMode()) {
|
||||
userSetting.theme = 'dark'
|
||||
}
|
||||
this.validateSettings(userSetting)
|
||||
} else {
|
||||
userSetting = this.loadJson(userDataPath)
|
||||
userSetting = this.loadJson(settingsPath)
|
||||
|
||||
// Update outdated settings
|
||||
const requiresUpdate = !hasSameKeys(defaultSettings, userSetting)
|
||||
@ -66,7 +72,7 @@ class Preference {
|
||||
}
|
||||
this.validateSettings(userSetting)
|
||||
this.writeJson(userSetting, false)
|
||||
.catch(log)
|
||||
.catch(log.error)
|
||||
} else {
|
||||
this.validateSettings(userSetting)
|
||||
}
|
||||
@ -77,7 +83,11 @@ class Preference {
|
||||
userSetting = defaultSettings
|
||||
this.validateSettings(userSetting)
|
||||
}
|
||||
|
||||
this.cache = userSetting
|
||||
this.emit('loaded', userSetting)
|
||||
|
||||
this._listenForIpcMain()
|
||||
}
|
||||
|
||||
getAll () {
|
||||
@ -87,6 +97,28 @@ class Preference {
|
||||
setItem (key, value) {
|
||||
const preUserSetting = this.getAll()
|
||||
const newUserSetting = this.cache = Object.assign({}, preUserSetting, { [key]: value })
|
||||
this.emit('entry-changed', key, value)
|
||||
return this.writeJson(newUserSetting)
|
||||
}
|
||||
|
||||
/**
|
||||
* Change multiple setting entries.
|
||||
*
|
||||
* @param {Object.<string, *>} settings A settings object or subset object with key/value entries.
|
||||
*/
|
||||
setItems (settings) {
|
||||
if (!settings) {
|
||||
log.error('Cannot change settings without entires: object is undefined or null.')
|
||||
return
|
||||
}
|
||||
|
||||
const preUserSetting = this.getAll()
|
||||
const newUserSetting = this.cache = Object.assign({}, preUserSetting, settings)
|
||||
|
||||
Object.keys(settings).map(key => {
|
||||
this.emit('entry-changed', key, settings[key])
|
||||
})
|
||||
|
||||
return this.writeJson(newUserSetting)
|
||||
}
|
||||
|
||||
@ -97,13 +129,13 @@ class Preference {
|
||||
const userSetting = JSON_REG.exec(content.replace(/(?:\r\n|\n)/g, ''))[1]
|
||||
return JSON.parse(userSetting)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
log.error(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
writeJson (json, async = true) {
|
||||
const { userDataPath } = this
|
||||
const { settingsPath } = this
|
||||
return new Promise((resolve, reject) => {
|
||||
const content = fs.readFileSync(this.staticPath, 'utf-8')
|
||||
const tokens = content.split('```')
|
||||
@ -113,17 +145,25 @@ class Preference {
|
||||
'\n```' +
|
||||
tokens[2]
|
||||
if (async) {
|
||||
fs.writeFile(userDataPath, newContent, 'utf-8', err => {
|
||||
fs.writeFile(settingsPath, newContent, 'utf-8', err => {
|
||||
if (err) reject(err)
|
||||
else resolve(json)
|
||||
})
|
||||
} else {
|
||||
fs.writeFileSync(userDataPath, newContent, 'utf-8')
|
||||
fs.writeFileSync(settingsPath, newContent, 'utf-8')
|
||||
resolve(json)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getPreferedEOL () {
|
||||
const { endOfLine } = this.getAll()
|
||||
if (endOfLine === 'lf') {
|
||||
return 'lf'
|
||||
}
|
||||
return endOfLine === 'crlf' || isWindows ? 'crlf' : 'lf'
|
||||
}
|
||||
|
||||
/**
|
||||
* workaround for issue #265
|
||||
* expects: settings != null
|
||||
@ -131,7 +171,7 @@ class Preference {
|
||||
*/
|
||||
validateSettings (settings) {
|
||||
if (!settings) {
|
||||
log('Broken settings detected: invalid settings object.')
|
||||
log.warn('Broken settings detected: invalid settings object.')
|
||||
return
|
||||
}
|
||||
|
||||
@ -141,6 +181,17 @@ class Preference {
|
||||
settings.theme = 'light'
|
||||
}
|
||||
|
||||
if (!settings.codeFontFamily || typeof settings.codeFontFamily !== 'string' || settings.codeFontFamily.length > 60) {
|
||||
settings.codeFontFamily = 'DejaVu Sans Mono'
|
||||
}
|
||||
if (!settings.codeFontSize || typeof settings.codeFontSize !== 'string' || settings.codeFontFamily.length > 10) {
|
||||
settings.codeFontSize = '14px'
|
||||
}
|
||||
|
||||
if (!settings.endOfLine || !/^(?:lf|crlf)$/.test(settings.endOfLine)) {
|
||||
settings.endOfLine = isWindows ? 'crlf' : 'lf'
|
||||
}
|
||||
|
||||
if (!settings.bulletListMarker ||
|
||||
(settings.bulletListMarker && !/^(?:\+|-|\*)$/.test(settings.bulletListMarker))) {
|
||||
brokenSettings = true
|
||||
@ -170,7 +221,7 @@ class Preference {
|
||||
}
|
||||
|
||||
if (brokenSettings) {
|
||||
log('Broken settings detected; fallback to default value(s).')
|
||||
log.warn('Broken settings detected; fallback to default value(s).')
|
||||
}
|
||||
|
||||
// Currently no CSD is available on Linux and Windows (GH#690)
|
||||
@ -179,25 +230,19 @@ class Preference {
|
||||
settings.titleBarStyle = 'custom'
|
||||
}
|
||||
}
|
||||
|
||||
_listenForIpcMain () {
|
||||
ipcMain.on('mt::ask-for-user-preference', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
win.webContents.send('AGANI::user-preference', this.getAll())
|
||||
})
|
||||
|
||||
ipcMain.on('mt::set-user-preference', (e, settings) => {
|
||||
this.setItems(settings).then(() => {
|
||||
ipcMain.emit('broadcast-preferences-changed', settings)
|
||||
}).catch(log.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const preference = new Preference()
|
||||
|
||||
ipcMain.on('AGANI::ask-for-user-preference', e => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
win.webContents.send('AGANI::user-preference', preference.getAll())
|
||||
})
|
||||
|
||||
ipcMain.on('AGANI::set-user-preference', (e, pre) => {
|
||||
Object.keys(pre).map(key => {
|
||||
preference.setItem(key, pre[key])
|
||||
.then(() => {
|
||||
for (const { win } of appWindow.windows.values()) {
|
||||
win.webContents.send('AGANI::user-preference', { [key]: pre[key] })
|
||||
}
|
||||
})
|
||||
.catch(log)
|
||||
})
|
||||
})
|
||||
|
||||
export default preference
|
||||
export default Preference
|
@ -1,36 +0,0 @@
|
||||
import path from 'path'
|
||||
|
||||
const additionalPaths = ({
|
||||
'win32': [],
|
||||
'linux': [
|
||||
'/usr/bin'
|
||||
],
|
||||
'darwin': [
|
||||
'/usr/local/bin',
|
||||
'/Library/TeX/texbin'
|
||||
]
|
||||
})[process.platform] || []
|
||||
|
||||
export const checkSystem = () => {
|
||||
if (additionalPaths.length > 0) {
|
||||
// First integrate the additional paths that we need.
|
||||
const nPATH = process.env.PATH.split(path.delimiter)
|
||||
|
||||
for (const x of additionalPaths) {
|
||||
// Check for both trailing and non-trailing slashes (to not add any
|
||||
// directory more than once)
|
||||
const y = (x[x.length - 1] === '/') ? x.substr(0, x.length - 1) : x + '/'
|
||||
if (!nPATH.includes(x) && !nPATH.includes(y)) {
|
||||
nPATH.push(x)
|
||||
}
|
||||
}
|
||||
|
||||
process.env.PATH = nPATH.join(path.delimiter)
|
||||
}
|
||||
|
||||
if (path.dirname('pandoc').length > 0) {
|
||||
if (process.env.PATH.indexOf(path.dirname('pandoc')) === -1) {
|
||||
process.env.PATH += path.delimiter + path.dirname('pandoc')
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,11 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { filter } from 'fuzzaldrin'
|
||||
import { isDirectory, isFile, log } from './index'
|
||||
import log from 'electron-log'
|
||||
import { IMAGE_EXTENSIONS, BLACK_LIST } from '../config'
|
||||
import { isDirectory, isFile } from '../filesystem'
|
||||
|
||||
// TODO(need::refactor): Refactor this file. Just return an array of directories and files without caching and watching?
|
||||
|
||||
// TODO: rebuild cache @jocs
|
||||
const IMAGE_PATH = new Map()
|
||||
@ -42,7 +45,7 @@ const filesHandler = (files, directory, key) => {
|
||||
|
||||
const rebuild = (directory) => {
|
||||
fs.readdir(directory, (err, files) => {
|
||||
if (err) log(err)
|
||||
if (err) log.error(err)
|
||||
else {
|
||||
filesHandler(files, directory)
|
||||
}
|
||||
@ -67,8 +70,9 @@ export const searchFilesAndDir = (directory, key) => {
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readdir(directory, (err, files) => {
|
||||
if (err) reject(err)
|
||||
else {
|
||||
if (err) {
|
||||
reject(err)
|
||||
} else {
|
||||
result = filesHandler(files, directory, key)
|
||||
watchDirectory(directory)
|
||||
resolve(result)
|
||||
|
@ -1,7 +1,4 @@
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import fse from 'fs-extra'
|
||||
import { app, Menu } from 'electron'
|
||||
import { app } from 'electron'
|
||||
import { EXTENSIONS } from '../config'
|
||||
|
||||
const ID_PREFIX = 'mt-'
|
||||
@ -11,18 +8,8 @@ export const getUniqueId = () => {
|
||||
return `${ID_PREFIX}${id++}`
|
||||
}
|
||||
|
||||
// creates a directory if it doesn't already exist.
|
||||
export const ensureDir = dirPath => {
|
||||
try {
|
||||
fse.ensureDirSync(dirPath)
|
||||
} catch (e) {
|
||||
if (e.code !== 'EEXIST') {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getRecommendTitle = markdown => {
|
||||
// TODO: We should map all heading into the MarkdownDocument.
|
||||
export const getRecommendTitleFromMarkdownString = markdown => {
|
||||
const tokens = markdown.match(/#{1,6} {1,}(.+)(?:\n|$)/g)
|
||||
if (!tokens) return ''
|
||||
let headers = tokens.map(t => {
|
||||
@ -35,24 +22,26 @@ export const getRecommendTitle = markdown => {
|
||||
return headers.sort((a, b) => a.level - b.level)[0].content
|
||||
}
|
||||
|
||||
export const getPath = directory => {
|
||||
return app.getPath(directory)
|
||||
/**
|
||||
* Returns a special directory path for the requested name.
|
||||
*
|
||||
* NOTE: Do not use "userData" to get the user data path, instead use AppPaths!
|
||||
*
|
||||
* @param {string} name The special directory name.
|
||||
* @returns {string} The resolved special directory path.
|
||||
*/
|
||||
export const getPath = name => {
|
||||
if (name === 'userData') {
|
||||
throw new Error('Do not use "getPath" for user data path!')
|
||||
}
|
||||
return app.getPath(name)
|
||||
}
|
||||
|
||||
export const getMenuItemById = menuId => {
|
||||
const menus = Menu.getApplicationMenu()
|
||||
return menus.getMenuItemById(menuId)
|
||||
}
|
||||
|
||||
export const log = data => {
|
||||
if (typeof data !== 'string') data = (data.stack || data).toString()
|
||||
const LOG_DATA_PATH = path.join(getPath('userData'), 'error.log')
|
||||
const LOG_TIME = new Date().toLocaleString()
|
||||
ensureDir(getPath('userData'))
|
||||
fs.appendFileSync(LOG_DATA_PATH, `\n${LOG_TIME}\n${data}\n`)
|
||||
}
|
||||
|
||||
// returns true if the filename matches one of the markdown extensions
|
||||
/**
|
||||
* Returns true if the filename matches one of the markdown extensions.
|
||||
*
|
||||
* @param {string} filename Path or filename
|
||||
*/
|
||||
export const hasMarkdownExtension = filename => {
|
||||
if (!filename || typeof filename !== 'string') return false
|
||||
return EXTENSIONS.some(ext => filename.endsWith(`.${ext}`))
|
||||
@ -64,99 +53,14 @@ export const hasSameKeys = (a, b) => {
|
||||
return JSON.stringify(aKeys) === JSON.stringify(bKeys)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a directory with read access.
|
||||
*
|
||||
* @param {string} dirPath The directory path.
|
||||
*/
|
||||
export const isDirectory = dirPath => {
|
||||
try {
|
||||
return fs.existsSync(dirPath) && fs.lstatSync(dirPath).isDirectory()
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a file with read access.
|
||||
*
|
||||
* @param {string} filepath The file path.
|
||||
*/
|
||||
export const isFile = filepath => {
|
||||
try {
|
||||
return fs.existsSync(filepath) && fs.lstatSync(filepath).isFile()
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a symbolic link with read access.
|
||||
*
|
||||
* @param {string} filepath The link path.
|
||||
*/
|
||||
export const isSymbolicLink = filepath => {
|
||||
try {
|
||||
return fs.existsSync(filepath) && fs.lstatSync(filepath).isSymbolicLink()
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a markdown file.
|
||||
*
|
||||
* @param {string} filepath The path or link path.
|
||||
*/
|
||||
export const isMarkdownFile = filepath => {
|
||||
return isFile(filepath) && hasMarkdownExtension(filepath)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the path is a markdown file or symbolic link to a markdown file.
|
||||
*
|
||||
* @param {string} filepath The path or link path.
|
||||
*/
|
||||
export const isMarkdownFileOrLink = filepath => {
|
||||
if (!isFile(filepath)) return false
|
||||
if (hasMarkdownExtension(filepath)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Symbolic link to a markdown file
|
||||
if (isSymbolicLink(filepath)) {
|
||||
const targetPath = fs.readlinkSync(filepath)
|
||||
return isFile(targetPath) && hasMarkdownExtension(targetPath)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize the path into an absolute path and resolves the link target if needed.
|
||||
*
|
||||
* @param {string} pathname The path or link path.
|
||||
* @returns {string} Returns the absolute path and resolved link. If the link target
|
||||
* cannot be resolved, an empty string is returned.
|
||||
*/
|
||||
export const normalizeAndResolvePath = pathname => {
|
||||
if (isSymbolicLink(pathname)) {
|
||||
const absPath = path.dirname(pathname)
|
||||
const targetPath = path.resolve(absPath, fs.readlinkSync(pathname))
|
||||
if (isFile(targetPath) || isDirectory(targetPath)) {
|
||||
return path.resolve(targetPath)
|
||||
}
|
||||
console.error(`Cannot resolve link target "${pathname}" (${targetPath}).`)
|
||||
return ''
|
||||
}
|
||||
return path.resolve(pathname)
|
||||
}
|
||||
|
||||
export const readJson = (filePath, printError) => {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
return JSON.parse(content)
|
||||
} catch (e) {
|
||||
if (printError) console.log(e)
|
||||
return null
|
||||
export const getLogLevel = () => {
|
||||
if (!global.MARKTEXT_DEBUG_VERBOSE || typeof global.MARKTEXT_DEBUG_VERBOSE !== 'number' ||
|
||||
global.MARKTEXT_DEBUG_VERBOSE <= 0) {
|
||||
return process.env.NODE_ENV === 'development' ? 'debug' : 'info'
|
||||
} else if (global.MARKTEXT_DEBUG_VERBOSE === 1) {
|
||||
return 'verbose'
|
||||
} else if (global.MARKTEXT_DEBUG_VERBOSE === 2) {
|
||||
return 'debug'
|
||||
}
|
||||
return 'silly' // >= 3
|
||||
}
|
||||
|
@ -1,258 +0,0 @@
|
||||
import { app, BrowserWindow, screen, ipcMain } from 'electron'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { getOsLineEndingName, loadMarkdownFile, getDefaultTextDirection } from './utils/filesystem'
|
||||
import appMenu from './menu'
|
||||
import Watcher from './watcher'
|
||||
import { isMarkdownFile, isDirectory, normalizeAndResolvePath, log } from './utils'
|
||||
import { TITLE_BAR_HEIGHT, defaultWinOptions, isLinux } from './config'
|
||||
import userPreference from './preference'
|
||||
import { newTab } from './actions/file'
|
||||
|
||||
class AppWindow {
|
||||
constructor () {
|
||||
this.focusedWindowId = -1
|
||||
this.windows = new Map()
|
||||
this.watcher = new Watcher()
|
||||
this.listen()
|
||||
}
|
||||
|
||||
listen () {
|
||||
// listen for file watch from renderer process eg
|
||||
// 1. click file in folder.
|
||||
// 2. new tab and save it.
|
||||
// 3. close tab(s) need unwatch.
|
||||
ipcMain.on('AGANI::file-watch', (e, { pathname, watch }) => {
|
||||
const win = BrowserWindow.fromWebContents(e.sender)
|
||||
if (watch) {
|
||||
// listen for file `change` and `unlink`
|
||||
this.watcher.watch(win, pathname, 'file')
|
||||
} else {
|
||||
// unlisten for file `change` and `unlink`
|
||||
this.watcher.unWatch(win, pathname, 'file')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
ensureWindowPosition (mainWindowState) {
|
||||
// "workArea" doesn't work on Linux
|
||||
const { bounds, workArea } = screen.getPrimaryDisplay()
|
||||
const screenArea = isLinux ? bounds : workArea
|
||||
|
||||
let { x, y, width, height } = mainWindowState
|
||||
let center = false
|
||||
if (x === undefined || y === undefined) {
|
||||
center = true
|
||||
|
||||
// First app start; check whether window size is larger than screen size
|
||||
if (screenArea.width < width) width = screenArea.width
|
||||
if (screenArea.height < height) height = screenArea.height
|
||||
} else {
|
||||
center = !screen.getAllDisplays().map(display =>
|
||||
x >= display.bounds.x && x <= display.bounds.x + display.bounds.width &&
|
||||
y >= display.bounds.y && y <= display.bounds.y + display.bounds.height)
|
||||
.some(display => display)
|
||||
}
|
||||
if (center) {
|
||||
// win.center() doesn't work on Linux
|
||||
x = Math.max(0, Math.ceil(screenArea.x + (screenArea.width - width) / 2))
|
||||
y = Math.max(0, Math.ceil(screenArea.y + (screenArea.height - height) / 2))
|
||||
}
|
||||
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new editor window.
|
||||
*
|
||||
* @param {string} [pathname] Path to a file, directory or link.
|
||||
* @param {string} [markdown] Markdown content.
|
||||
* @param {*} [options] BrowserWindow options.
|
||||
*/
|
||||
createWindow (pathname = null, markdown = '', options = {}) {
|
||||
// Ensure path is normalized
|
||||
if (pathname) {
|
||||
pathname = normalizeAndResolvePath(pathname)
|
||||
}
|
||||
|
||||
const { windows } = this
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1200,
|
||||
defaultHeight: 800
|
||||
})
|
||||
|
||||
const { x, y, width, height } = this.ensureWindowPosition(mainWindowState)
|
||||
const winOpt = Object.assign({ x, y, width, height }, defaultWinOptions, options)
|
||||
|
||||
// Enable native or custom window
|
||||
const { titleBarStyle } = userPreference.getAll()
|
||||
if (titleBarStyle === 'custom') {
|
||||
winOpt.titleBarStyle = ''
|
||||
} else if (titleBarStyle === 'native') {
|
||||
winOpt.frame = true
|
||||
winOpt.titleBarStyle = ''
|
||||
}
|
||||
|
||||
const win = new BrowserWindow(winOpt)
|
||||
windows.set(win.id, { win })
|
||||
|
||||
// create a menu for the current window
|
||||
appMenu.addWindowMenuWithListener(win)
|
||||
if (windows.size === 1) {
|
||||
appMenu.setActiveWindow(win.id)
|
||||
}
|
||||
|
||||
win.once('ready-to-show', async () => {
|
||||
mainWindowState.manage(win)
|
||||
win.show()
|
||||
|
||||
// open single markdown file
|
||||
if (pathname && isMarkdownFile(pathname)) {
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
try {
|
||||
this.openFile(win, pathname)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
}
|
||||
// open directory / folder
|
||||
} else if (pathname && isDirectory(pathname)) {
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
this.openFolder(win, pathname)
|
||||
// open a window but do not open a file or directory
|
||||
} else {
|
||||
const lineEnding = getOsLineEndingName()
|
||||
const textDirection = getDefaultTextDirection()
|
||||
win.webContents.send('AGANI::open-blank-window', {
|
||||
lineEnding,
|
||||
markdown
|
||||
})
|
||||
appMenu.updateLineEndingnMenu(lineEnding)
|
||||
appMenu.updateTextDirectionMenu(textDirection)
|
||||
}
|
||||
})
|
||||
|
||||
win.on('focus', () => {
|
||||
win.webContents.send('AGANI::window-active-status', { status: true })
|
||||
|
||||
if (win.id !== this.focusedWindowId) {
|
||||
this.focusedWindowId = win.id
|
||||
win.webContents.send('AGANI::req-update-line-ending-menu')
|
||||
win.webContents.send('AGANI::request-for-view-layout')
|
||||
win.webContents.send('AGANI::req-update-text-direction-menu')
|
||||
|
||||
// update application menu
|
||||
appMenu.setActiveWindow(win.id)
|
||||
}
|
||||
})
|
||||
|
||||
win.on('blur', () => {
|
||||
win.webContents.send('AGANI::window-active-status', { status: false })
|
||||
})
|
||||
|
||||
win.on('close', event => { // before closed
|
||||
event.preventDefault()
|
||||
win.webContents.send('AGANI::ask-for-close')
|
||||
})
|
||||
|
||||
// set renderer arguments
|
||||
const { codeFontFamily, codeFontSize, theme } = userPreference.getAll()
|
||||
// wow, this can be accessesed in renderer process.
|
||||
win.stylePrefs = {
|
||||
codeFontFamily,
|
||||
codeFontSize,
|
||||
theme
|
||||
}
|
||||
|
||||
const winURL = process.env.NODE_ENV === 'development'
|
||||
? `http://localhost:9091`
|
||||
: `file://${__dirname}/index.html`
|
||||
|
||||
win.loadURL(winURL)
|
||||
win.setSheetOffset(TITLE_BAR_HEIGHT)
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
openFile = async (win, filePath) => {
|
||||
const data = await loadMarkdownFile(filePath)
|
||||
const {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
isUtf8BomEncoded,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave,
|
||||
isMixedLineEndings,
|
||||
textDirection
|
||||
} = data
|
||||
|
||||
appMenu.updateLineEndingnMenu(lineEnding)
|
||||
appMenu.updateTextDirectionMenu(textDirection)
|
||||
win.webContents.send('AGANI::open-single-file', {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
options: {
|
||||
isUtf8BomEncoded,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave
|
||||
}
|
||||
})
|
||||
// listen for file `change` and `unlink`
|
||||
this.watcher.watch(win, filePath, 'file')
|
||||
// Notify user about mixed endings
|
||||
if (isMixedLineEndings) {
|
||||
win.webContents.send('AGANI::show-notification', {
|
||||
title: 'Mixed Line Endings',
|
||||
type: 'error',
|
||||
message: `The document has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
||||
time: 20000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
newTab (win, filePath) {
|
||||
this.watcher.watch(win, filePath, 'file')
|
||||
loadMarkdownFile(filePath).then(rawDocument => {
|
||||
appMenu.addRecentlyUsedDocument(filePath)
|
||||
newTab(win, rawDocument)
|
||||
}).catch(err => {
|
||||
// TODO: Handle error --> create a end-user error handler.
|
||||
console.error('[ERROR] Cannot open file or directory.')
|
||||
log(err)
|
||||
})
|
||||
}
|
||||
|
||||
openFolder (win, pathname) {
|
||||
this.watcher.watch(win, pathname, 'dir')
|
||||
try {
|
||||
win.webContents.send('AGANI::open-project', pathname)
|
||||
} catch (err) {
|
||||
log(err)
|
||||
}
|
||||
}
|
||||
|
||||
forceClose (win) {
|
||||
if (!win) return
|
||||
const { windows } = this
|
||||
if (windows.has(win.id)) {
|
||||
this.watcher.unWatchWin(win)
|
||||
windows.delete(win.id)
|
||||
}
|
||||
appMenu.removeWindowMenu(win.id)
|
||||
win.destroy() // if use win.close(), it will cause a endless loop.
|
||||
if (windows.size === 0) {
|
||||
app.quit()
|
||||
}
|
||||
}
|
||||
|
||||
clear () {
|
||||
this.watcher.clear()
|
||||
}
|
||||
}
|
||||
|
||||
export default new AppWindow()
|
242
src/main/windows/editor.js
Normal file
242
src/main/windows/editor.js
Normal file
@ -0,0 +1,242 @@
|
||||
import path from 'path'
|
||||
import EventEmitter from 'events'
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { WindowType } from '../app/windowManager'
|
||||
import { TITLE_BAR_HEIGHT, defaultWinOptions, isLinux } from '../config'
|
||||
import { isDirectory, isMarkdownFile, normalizeAndResolvePath } from '../filesystem'
|
||||
import { loadMarkdownFile } from '../filesystem/markdown'
|
||||
import { ensureWindowPosition } from './utils'
|
||||
|
||||
class EditorWindow extends EventEmitter {
|
||||
|
||||
/**
|
||||
* @param {Accessor} accessor The application accessor for application instances.
|
||||
*/
|
||||
constructor (accessor) {
|
||||
super()
|
||||
|
||||
this._accessor = accessor
|
||||
|
||||
this.id = null
|
||||
this.browserWindow = null
|
||||
this.type = WindowType.EDITOR
|
||||
this.quitting = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new editor window.
|
||||
*
|
||||
* @param {string} [pathname] Path to a file, directory or link.
|
||||
* @param {string} [markdown] Markdown content.
|
||||
* @param {*} [options] BrowserWindow options.
|
||||
*/
|
||||
createWindow (pathname = null, markdown = '', options = {}) {
|
||||
const { menu: appMenu, env, preferences } = this._accessor
|
||||
|
||||
// Ensure path is normalized
|
||||
if (pathname) {
|
||||
pathname = normalizeAndResolvePath(pathname)
|
||||
}
|
||||
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 1200,
|
||||
defaultHeight: 800
|
||||
})
|
||||
|
||||
const { x, y, width, height } = ensureWindowPosition(mainWindowState)
|
||||
const winOptions = Object.assign({ x, y, width, height }, defaultWinOptions, options)
|
||||
if (isLinux) {
|
||||
winOptions.icon = path.join(__static, 'logo-96px.png')
|
||||
}
|
||||
|
||||
// Enable native or custom/frameless window and titlebar
|
||||
const { titleBarStyle } = preferences.getAll()
|
||||
if (titleBarStyle === 'custom') {
|
||||
winOptions.titleBarStyle = ''
|
||||
} else if (titleBarStyle === 'native') {
|
||||
winOptions.frame = true
|
||||
winOptions.titleBarStyle = ''
|
||||
}
|
||||
|
||||
let win = this.browserWindow = new BrowserWindow(winOptions)
|
||||
this.id = win.id
|
||||
|
||||
// Create a menu for the current window
|
||||
appMenu.addEditorMenu(win)
|
||||
|
||||
win.once('ready-to-show', async () => {
|
||||
mainWindowState.manage(win)
|
||||
win.show()
|
||||
|
||||
this.emit('window-ready-to-show')
|
||||
|
||||
if (pathname && isMarkdownFile(pathname)) {
|
||||
// Open single markdown file
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
this._openFile(pathname)
|
||||
} else if (pathname && isDirectory(pathname)) {
|
||||
// Open directory / folder
|
||||
appMenu.addRecentlyUsedDocument(pathname)
|
||||
this.openFolder(pathname)
|
||||
} else {
|
||||
// Open a blank window
|
||||
const lineEnding = preferences.getPreferedEOL()
|
||||
win.webContents.send('mt::bootstrap-blank-window', {
|
||||
lineEnding,
|
||||
markdown
|
||||
})
|
||||
appMenu.updateLineEndingMenu(lineEnding)
|
||||
}
|
||||
})
|
||||
|
||||
win.on('focus', () => {
|
||||
this.emit('window-focus')
|
||||
win.webContents.send('AGANI::window-active-status', { status: true })
|
||||
})
|
||||
|
||||
// Lost focus
|
||||
win.on('blur', () => {
|
||||
this.emit('window-blur')
|
||||
win.webContents.send('AGANI::window-active-status', { status: false })
|
||||
})
|
||||
|
||||
// Before closed. We cancel the action and ask the editor further instructions.
|
||||
win.on('close', event => {
|
||||
this.emit('window-close')
|
||||
|
||||
event.preventDefault()
|
||||
win.webContents.send('AGANI::ask-for-close')
|
||||
|
||||
// TODO: Close all watchers etc. Should we do this manually or listen to 'quit' event?
|
||||
})
|
||||
|
||||
// The window is now destroyed.
|
||||
win.on('closed', () => {
|
||||
this.emit('window-closed')
|
||||
|
||||
// Free window reference
|
||||
win = null
|
||||
})
|
||||
|
||||
win.loadURL(this._buildUrlWithSettings(this.id, env, preferences))
|
||||
win.setSheetOffset(TITLE_BAR_HEIGHT)
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
openTab (filePath, selectTab=true) {
|
||||
if (this.quitting) return
|
||||
|
||||
const { browserWindow } = this
|
||||
const { menu: appMenu, preferences } = this._accessor
|
||||
|
||||
// Listen for file changed.
|
||||
ipcMain.emit('watcher-watch-file', browserWindow, filePath)
|
||||
|
||||
loadMarkdownFile(filePath, preferences.getPreferedEOL()).then(rawDocument => {
|
||||
appMenu.addRecentlyUsedDocument(filePath)
|
||||
browserWindow.webContents.send('AGANI::new-tab', rawDocument, selectTab)
|
||||
}).catch(err => {
|
||||
// TODO: Handle error --> create a end-user error handler.
|
||||
console.error('[ERROR] Cannot open file or directory.')
|
||||
log.error(err)
|
||||
})
|
||||
}
|
||||
|
||||
openUntitledTab (selectTab=true, markdownString='') {
|
||||
if (this.quitting) return
|
||||
|
||||
const { browserWindow } = this
|
||||
browserWindow.webContents.send('mt::new-untitled-tab', selectTab, markdownString)
|
||||
}
|
||||
|
||||
openFolder (pathname) {
|
||||
if (this.quitting) return
|
||||
|
||||
const { browserWindow } = this
|
||||
ipcMain.emit('watcher-watch-directory', browserWindow, pathname)
|
||||
browserWindow.webContents.send('AGANI::open-project', pathname)
|
||||
}
|
||||
|
||||
destroy () {
|
||||
this.quitting = true
|
||||
this.emit('bye')
|
||||
|
||||
this.removeAllListeners()
|
||||
this.browserWindow.destroy()
|
||||
this.browserWindow = null
|
||||
this.id = null
|
||||
}
|
||||
|
||||
// --- private ---------------------------------
|
||||
|
||||
// Only called once during window bootstrapping.
|
||||
_openFile = async filePath => {
|
||||
const { browserWindow } = this
|
||||
const { menu: appMenu, preferences } = this._accessor
|
||||
|
||||
const data = await loadMarkdownFile(filePath, preferences.getPreferedEOL())
|
||||
const {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave,
|
||||
isMixedLineEndings
|
||||
} = data
|
||||
|
||||
appMenu.updateLineEndingMenu(lineEnding)
|
||||
browserWindow.webContents.send('mt::bootstrap-window', {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
options: {
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave
|
||||
}
|
||||
})
|
||||
|
||||
// Listen for file changed.
|
||||
ipcMain.emit('watcher-watch-file', browserWindow, filePath)
|
||||
|
||||
// Notify user about mixed endings
|
||||
if (isMixedLineEndings) {
|
||||
browserWindow.webContents.send('AGANI::show-notification', {
|
||||
title: 'Mixed Line Endings',
|
||||
type: 'error',
|
||||
message: `The document has mixed line endings which are automatically normalized to ${lineEnding.toUpperCase()}.`,
|
||||
time: 20000
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
_buildUrlWithSettings (windowId, env, userPreference) {
|
||||
// NOTE: Only send absolutely necessary values. Theme and titlebar settings
|
||||
// are sended because we delay load the preferences.
|
||||
const { debug, paths } = env
|
||||
const { codeFontFamily, codeFontSize, theme, titleBarStyle } = userPreference.getAll()
|
||||
|
||||
const baseUrl = process.env.NODE_ENV === 'development'
|
||||
? `http://localhost:9091`
|
||||
: `file://${__dirname}/index.html`
|
||||
|
||||
const url = new URL(baseUrl)
|
||||
url.searchParams.set('udp', paths.userDataPath)
|
||||
url.searchParams.set('debug', debug ? '1' : '0')
|
||||
url.searchParams.set('wid', windowId)
|
||||
|
||||
// Settings
|
||||
url.searchParams.set('cff', codeFontFamily)
|
||||
url.searchParams.set('cfs', codeFontSize)
|
||||
url.searchParams.set('theme', theme)
|
||||
url.searchParams.set('tbs', titleBarStyle)
|
||||
|
||||
return url.toString()
|
||||
}
|
||||
}
|
||||
|
||||
export default EditorWindow
|
34
src/main/windows/utils.js
Normal file
34
src/main/windows/utils.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { screen } from 'electron'
|
||||
import { isLinux } from '../config'
|
||||
|
||||
export const ensureWindowPosition = windowState => {
|
||||
// "workArea" doesn't work on Linux
|
||||
const { bounds, workArea } = screen.getPrimaryDisplay()
|
||||
const screenArea = isLinux ? bounds : workArea
|
||||
|
||||
let { x, y, width, height } = windowState
|
||||
let center = false
|
||||
if (x === undefined || y === undefined) {
|
||||
center = true
|
||||
|
||||
// First app start; check whether window size is larger than screen size
|
||||
if (screenArea.width < width) width = screenArea.width
|
||||
if (screenArea.height < height) height = screenArea.height
|
||||
} else {
|
||||
center = !screen.getAllDisplays().map(display =>
|
||||
x >= display.bounds.x && x <= display.bounds.x + display.bounds.width &&
|
||||
y >= display.bounds.y && y <= display.bounds.y + display.bounds.height)
|
||||
.some(display => display)
|
||||
}
|
||||
if (center) {
|
||||
// win.center() doesn't work on Linux
|
||||
x = Math.max(0, Math.ceil(screenArea.x + (screenArea.width - width) / 2))
|
||||
y = Math.max(0, Math.ceil(screenArea.y + (screenArea.height - height) / 2))
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { deepCopy } from '../utils'
|
||||
import { UNDO_DEPTH } from '../config'
|
||||
|
||||
export class History {
|
||||
class History {
|
||||
constructor (contentState) {
|
||||
this.stack = []
|
||||
this.index = -1
|
||||
|
@ -38,7 +38,6 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { remote } from 'electron'
|
||||
import { addStyles, addThemeStyle } from '@/util/theme'
|
||||
import Recent from '@/components/recent'
|
||||
import EditorWithTabs from '@/components/editorWithTabs'
|
||||
@ -78,7 +77,8 @@
|
||||
...mapState({
|
||||
'showTabBar': state => state.layout.showTabBar,
|
||||
'sourceCode': state => state.preferences.sourceCode,
|
||||
'theme': state => state.preferences.theme
|
||||
'theme': state => state.preferences.theme,
|
||||
'textDirection': state => state.preferences.textDirection
|
||||
}),
|
||||
...mapState({
|
||||
'projectTree': state => state.project.projectTree,
|
||||
@ -87,8 +87,7 @@
|
||||
'isSaved': state => state.editor.currentFile.isSaved,
|
||||
'markdown': state => state.editor.currentFile.markdown,
|
||||
'cursor': state => state.editor.currentFile.cursor,
|
||||
'wordCount': state => state.editor.currentFile.wordCount,
|
||||
'textDirection': state => state.editor.currentFile.textDirection
|
||||
'wordCount': state => state.editor.currentFile.wordCount
|
||||
}),
|
||||
...mapState([
|
||||
'windowActive', 'platform', 'init'
|
||||
@ -105,7 +104,13 @@
|
||||
}
|
||||
},
|
||||
created () {
|
||||
const { dispatch } = this.$store
|
||||
const { commit, dispatch } = this.$store
|
||||
|
||||
// Apply initial state (theme and titleBarStyle) and delay load other values.
|
||||
if (global.marktext.initialState) {
|
||||
commit('SET_USER_PREFERENCE', global.marktext.initialState)
|
||||
}
|
||||
|
||||
// store/index.js
|
||||
dispatch('LINTEN_WIN_STATUS')
|
||||
// module: tweet
|
||||
@ -133,8 +138,7 @@
|
||||
dispatch('LISTEN_FOR_MOVE_TO')
|
||||
dispatch('LISTEN_FOR_SAVE')
|
||||
dispatch('LISTEN_FOR_SET_PATHNAME')
|
||||
dispatch('LISTEN_FOR_OPEN_SINGLE_FILE')
|
||||
dispatch('LISTEN_FOR_OPEN_BLANK_WINDOW')
|
||||
dispatch('LISTEN_FOR_BOOTSTRAP_WINDOW')
|
||||
dispatch('LISTEN_FOR_SAVE_CLOSE')
|
||||
dispatch('LISTEN_FOR_EXPORT_PRINT')
|
||||
dispatch('LISTEN_FOR_INSERT_IMAGE')
|
||||
@ -144,9 +148,7 @@
|
||||
dispatch('LISTEN_FOR_CLOSE_TAB')
|
||||
dispatch('LINTEN_FOR_PRINT_SERVICE_CLEARUP')
|
||||
dispatch('LINTEN_FOR_EXPORT_SUCCESS')
|
||||
dispatch('LISTEN_FOR_SET_TEXT_DIRECTION')
|
||||
dispatch('LISTEN_FOR_FILE_CHANGE')
|
||||
dispatch('LISTEN_FOR_TEXT_DIRECTION_MENU')
|
||||
// module: notification
|
||||
dispatch('LISTEN_FOR_NOTIFICATION')
|
||||
|
||||
@ -176,8 +178,7 @@
|
||||
}, false)
|
||||
|
||||
this.$nextTick(() => {
|
||||
const win = remote.getCurrentWindow()
|
||||
const style = win.stylePrefs || DEFAULT_STYLE
|
||||
const style = global.marktext.initialState || DEFAULT_STYLE
|
||||
addStyles(style)
|
||||
})
|
||||
}
|
||||
|
79
src/renderer/bootstrap.js
vendored
Normal file
79
src/renderer/bootstrap.js
vendored
Normal file
@ -0,0 +1,79 @@
|
||||
import path from 'path'
|
||||
import { crashReporter, ipcRenderer } from 'electron'
|
||||
import log from 'electron-log'
|
||||
import EnvPaths from "common/envPaths";
|
||||
|
||||
let exceptionLogger = s => console.error(s)
|
||||
|
||||
const configureLogger = () => {
|
||||
const { debug, paths, windowId } = global.marktext.env
|
||||
log.transports.console.level = process.env.NODE_ENV === 'development' // mirror to window console
|
||||
log.transports.mainConsole = null
|
||||
log.transports.file.file = path.join(paths.logPath, `editor-${windowId}.log`)
|
||||
log.transports.file.level = debug ? 'debug' : 'info'
|
||||
log.transports.file.sync = false
|
||||
log.transports.file.init()
|
||||
exceptionLogger = log.error
|
||||
}
|
||||
|
||||
const parseUrlArgs = () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const codeFontFamily = params.get('cff')
|
||||
const codeFontSize = params.get('cfs')
|
||||
const debug = params.get('debug') === '1'
|
||||
const theme = params.get('theme')
|
||||
const titleBarStyle = params.get('tbs')
|
||||
const userDataPath = params.get('udp')
|
||||
const windowId = params.get('wid')
|
||||
return {
|
||||
debug,
|
||||
userDataPath,
|
||||
windowId,
|
||||
initialState: {
|
||||
codeFontFamily,
|
||||
codeFontSize,
|
||||
theme,
|
||||
titleBarStyle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapRenderer = () => {
|
||||
// Start crash reporter to save core dumps for the renderer process
|
||||
crashReporter.start({
|
||||
companyName: 'marktext',
|
||||
productName: 'marktext',
|
||||
submitURL: 'http://0.0.0.0/',
|
||||
uploadToServer: false
|
||||
})
|
||||
|
||||
// Register renderer exception handler
|
||||
window.addEventListener('error', event => {
|
||||
const { message, name, stack } = event.error
|
||||
const copy = {
|
||||
message,
|
||||
name,
|
||||
stack
|
||||
}
|
||||
|
||||
exceptionLogger(event.error)
|
||||
|
||||
// Pass exception to main process exception handler to show a error dialog.
|
||||
ipcRenderer.send('AGANI::handle-renderer-error', copy)
|
||||
})
|
||||
|
||||
const { debug, initialState, userDataPath, windowId } = parseUrlArgs()
|
||||
const marktext = {
|
||||
initialState,
|
||||
env: {
|
||||
debug,
|
||||
paths: new EnvPaths(userDataPath),
|
||||
windowId
|
||||
}
|
||||
}
|
||||
global.marktext = marktext
|
||||
|
||||
configureLogger()
|
||||
}
|
||||
|
||||
export default bootstrapRenderer
|
@ -64,7 +64,7 @@
|
||||
},
|
||||
methods: {
|
||||
newFile () {
|
||||
this.$store.dispatch('NEW_BLANK_FILE')
|
||||
this.$store.dispatch('NEW_UNTITLED_TAB')
|
||||
},
|
||||
handleTabScroll (event) {
|
||||
// Use mouse wheel value first but prioritize X value more (e.g. touchpad input).
|
||||
|
@ -23,7 +23,7 @@
|
||||
},
|
||||
methods: {
|
||||
newFile () {
|
||||
this.$store.dispatch('NEW_BLANK_FILE')
|
||||
this.$store.dispatch('NEW_UNTITLED_TAB')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,9 @@
|
||||
this.searchResult = []
|
||||
return
|
||||
}
|
||||
|
||||
// TODO(need::refactor): See TODO in src/main/filesystem/watcher.js.
|
||||
|
||||
this.searchResult = this.fileList.filter(f => f.data.markdown.indexOf(keyword) >= 0)
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import Vue from 'vue'
|
||||
import VueElectron from 'vue-electron'
|
||||
import axios from 'axios'
|
||||
import { crashReporter, ipcRenderer } from 'electron'
|
||||
import sourceMapSupport from 'source-map-support'
|
||||
import lang from 'element-ui/lib/locale/lang/en'
|
||||
import locale from 'element-ui/lib/locale'
|
||||
import bootstrapRenderer from './bootstrap'
|
||||
import App from './app'
|
||||
import store from './store'
|
||||
import './assets/symbolIcon'
|
||||
@ -25,33 +27,20 @@ import services from './services'
|
||||
import './assets/styles/index.css'
|
||||
import './assets/styles/printService.css'
|
||||
|
||||
// -----------------------------------------------
|
||||
|
||||
// Decode source map in production - must be registered first
|
||||
import sourceMapSupport from 'source-map-support'
|
||||
sourceMapSupport.install({
|
||||
environment: 'node',
|
||||
handleUncaughtExceptions: false,
|
||||
hookRequire: false
|
||||
})
|
||||
|
||||
// Start crash reporter to save core dumps for the renderer process
|
||||
crashReporter.start({
|
||||
companyName: 'marktext',
|
||||
productName: 'marktext',
|
||||
submitURL: 'http://0.0.0.0/',
|
||||
uploadToServer: false
|
||||
})
|
||||
global.marktext = {}
|
||||
bootstrapRenderer()
|
||||
|
||||
// Register renderer error handler
|
||||
window.addEventListener('error', event => {
|
||||
const { message, name, stack } = event.error
|
||||
const copy = {
|
||||
message,
|
||||
name,
|
||||
stack
|
||||
}
|
||||
// pass error to error handler
|
||||
ipcRenderer.send('AGANI::handle-renderer-error', copy)
|
||||
})
|
||||
// -----------------------------------------------
|
||||
// Be careful when changing code before this line!
|
||||
|
||||
// Configure Vue
|
||||
locale.use(lang)
|
||||
@ -69,7 +58,7 @@ Vue.use(Col)
|
||||
Vue.use(Row)
|
||||
Vue.use(Tree)
|
||||
|
||||
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
|
||||
Vue.use(VueElectron)
|
||||
Vue.http = Vue.prototype.$http = axios
|
||||
Vue.config.productionTip = false
|
||||
|
||||
|
@ -35,7 +35,7 @@ export const fileMixins = {
|
||||
watch: true
|
||||
})
|
||||
|
||||
ipcRenderer.send("AGANI::add-recently-used-document", pathname)
|
||||
ipcRenderer.send('mt::add-recently-used-document', pathname)
|
||||
|
||||
if (isMixedLineEndings && !isOpened) {
|
||||
this.$notify({
|
||||
|
@ -11,7 +11,6 @@ const state = {
|
||||
lineEnding: 'lf',
|
||||
currentFile: {},
|
||||
tabs: [],
|
||||
textDirection: 'ltr',
|
||||
toc: []
|
||||
}
|
||||
|
||||
@ -79,8 +78,8 @@ const mutations = {
|
||||
LOAD_CHANGE (state, change) {
|
||||
const { tabs, currentFile } = state
|
||||
const { data, pathname } = change
|
||||
const { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, isUtf8BomEncoded, markdown, textDirection, filename } = data
|
||||
const options = { isUtf8BomEncoded, lineEnding, adjustLineEndingOnSave, textDirection }
|
||||
const { isMixedLineEndings, lineEnding, adjustLineEndingOnSave, encoding, markdown, filename } = data
|
||||
const options = { encoding, lineEnding, adjustLineEndingOnSave }
|
||||
const newFileState = getSingleFileState({ markdown, filename, pathname, options })
|
||||
if (isMixedLineEndings) {
|
||||
notice.notify({
|
||||
@ -142,9 +141,9 @@ const mutations = {
|
||||
state.currentFile.markdown = markdown
|
||||
}
|
||||
},
|
||||
SET_IS_UTF8_BOM_ENCODED (state, isUtf8BomEncoded) {
|
||||
SET_DOCUMENT_ENCODING (state, encoding) {
|
||||
if (hasKeys(state.currentFile)) {
|
||||
state.currentFile.isUtf8BomEncoded = isUtf8BomEncoded
|
||||
state.currentFile.encoding = encoding
|
||||
}
|
||||
},
|
||||
SET_LINE_ENDING (state, lineEnding) {
|
||||
@ -209,13 +208,10 @@ const mutations = {
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// TODO: Remove "SET_GLOBAL_LINE_ENDING" because nowhere used.
|
||||
SET_GLOBAL_LINE_ENDING (state, ending) {
|
||||
state.lineEnding = ending
|
||||
},
|
||||
SET_TEXT_DIRECTION (state, textDirection) {
|
||||
if (hasKeys(state.currentFile)) {
|
||||
state.currentFile.textDirection = textDirection
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -277,19 +273,6 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
LISTEN_FOR_TEXT_DIRECTION_MENU ({ commit, state, dispatch }) {
|
||||
ipcRenderer.on('AGANI::req-update-text-direction-menu', e => {
|
||||
dispatch('UPDATE_TEXT_DIRECTION_MENU')
|
||||
})
|
||||
},
|
||||
|
||||
UPDATE_TEXT_DIRECTION_MENU ({ commit, state }) {
|
||||
const { textDirection } = state.currentFile
|
||||
if (textDirection) {
|
||||
ipcRenderer.send('AGANI::update-text-direction-menu', textDirection)
|
||||
}
|
||||
},
|
||||
|
||||
CLOSE_SINGLE_FILE ({ commit, state }, file) {
|
||||
const { id, pathname, filename, markdown } = file
|
||||
const options = getOptionsFromState(file)
|
||||
@ -415,16 +398,15 @@ const actions = {
|
||||
|
||||
UPDATE_CURRENT_FILE ({ commit, state, dispatch }, currentFile) {
|
||||
commit('SET_CURRENT_FILE', currentFile)
|
||||
dispatch('UPDATE_TEXT_DIRECTION_MENU', state)
|
||||
const { tabs } = state
|
||||
if (!tabs.some(file => file.id === currentFile.id)) {
|
||||
commit('ADD_FILE_TO_TABS', currentFile)
|
||||
}
|
||||
},
|
||||
|
||||
// This event is only used when loading a file during window creation.
|
||||
LISTEN_FOR_OPEN_SINGLE_FILE ({ commit, state, dispatch }) {
|
||||
ipcRenderer.on('AGANI::open-single-file', (e, { markdown, filename, pathname, options }) => {
|
||||
// This events are only used during window creation.
|
||||
LISTEN_FOR_BOOTSTRAP_WINDOW ({ commit, state, dispatch }) {
|
||||
ipcRenderer.on('mt::bootstrap-window', (e, { markdown, filename, pathname, options }) => {
|
||||
const fileState = getSingleFileState({ markdown, filename, pathname, options })
|
||||
const { id } = fileState
|
||||
const { lineEnding } = options
|
||||
@ -439,19 +421,42 @@ const actions = {
|
||||
})
|
||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||
})
|
||||
|
||||
ipcRenderer.on('mt::bootstrap-blank-window', (e, { lineEnding, markdown: source }) => {
|
||||
const { tabs } = state
|
||||
const fileState = getBlankFileState(tabs, lineEnding, source)
|
||||
const { id, markdown } = fileState
|
||||
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
|
||||
dispatch('INIT_STATUS', true)
|
||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
commit('SET_LAYOUT', {
|
||||
rightColumn: 'files',
|
||||
showSideBar: false,
|
||||
showTabBar: false
|
||||
})
|
||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||
})
|
||||
},
|
||||
|
||||
// Open a new tab, optionally with content.
|
||||
LISTEN_FOR_NEW_TAB ({ dispatch }) {
|
||||
ipcRenderer.on('AGANI::new-tab', (e, markdownDocument) => {
|
||||
ipcRenderer.on('AGANI::new-tab', (e, markdownDocument, selectTab=true) => {
|
||||
// TODO: allow to add a tab without selecting it
|
||||
if (markdownDocument) {
|
||||
// Create tab with content.
|
||||
dispatch('NEW_TAB_WITH_CONTENT', markdownDocument)
|
||||
} else {
|
||||
// Create an empty tab
|
||||
dispatch('NEW_BLANK_FILE')
|
||||
// Fallback: create a blank tab
|
||||
dispatch('NEW_UNTITLED_TAB')
|
||||
}
|
||||
})
|
||||
|
||||
ipcRenderer.on('mt::new-untitled-tab', (e, selectTab=true, markdownString='', ) => {
|
||||
// TODO: allow to add a tab without selecting it
|
||||
// Create a blank tab
|
||||
dispatch('NEW_UNTITLED_TAB', markdownString)
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_CLOSE_TAB ({ commit, state, dispatch }) {
|
||||
@ -467,10 +472,11 @@ const actions = {
|
||||
})
|
||||
},
|
||||
|
||||
NEW_BLANK_FILE ({ commit, state, dispatch }) {
|
||||
// Create a new untitled tab optional with markdown string.
|
||||
NEW_UNTITLED_TAB ({ commit, state, dispatch }, markdownString) {
|
||||
dispatch('SHOW_TAB_VIEW', false)
|
||||
const { tabs, lineEnding } = state
|
||||
const fileState = getBlankFileState(tabs, lineEnding)
|
||||
const fileState = getBlankFileState(tabs, lineEnding, markdownString)
|
||||
const { id, markdown } = fileState
|
||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
@ -485,7 +491,7 @@ const actions = {
|
||||
NEW_TAB_WITH_CONTENT ({ commit, state, dispatch }, markdownDocument) {
|
||||
if (!markdownDocument) {
|
||||
console.warn('Cannot create a file tab without a markdown document!')
|
||||
dispatch('NEW_BLANK_FILE')
|
||||
dispatch('NEW_UNTITLED_TAB')
|
||||
return
|
||||
}
|
||||
|
||||
@ -526,24 +532,6 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
LISTEN_FOR_OPEN_BLANK_WINDOW ({ commit, state, dispatch }) {
|
||||
ipcRenderer.on('AGANI::open-blank-window', (e, { lineEnding, markdown: source }) => {
|
||||
const { tabs } = state
|
||||
const fileState = getBlankFileState(tabs, lineEnding, source)
|
||||
const { id, markdown } = fileState
|
||||
commit('SET_GLOBAL_LINE_ENDING', lineEnding)
|
||||
dispatch('INIT_STATUS', true)
|
||||
dispatch('UPDATE_CURRENT_FILE', fileState)
|
||||
bus.$emit('file-loaded', { id, markdown })
|
||||
commit('SET_LAYOUT', {
|
||||
rightColumn: 'files',
|
||||
showSideBar: false,
|
||||
showTabBar: false
|
||||
})
|
||||
dispatch('SET_LAYOUT_MENU_ITEM')
|
||||
})
|
||||
},
|
||||
|
||||
// Content change from realtime preview editor and source code editor
|
||||
// WORKAROUND: id is "muya" if changes come from muya and not source code editor! So we don't have to apply the workaround.
|
||||
LISTEN_FOR_CONTENT_CHANGE ({ commit, state, rootState }, { id, markdown, wordCount, cursor, history, toc }) {
|
||||
@ -688,15 +676,6 @@ const actions = {
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_SET_TEXT_DIRECTION ({ commit, state }) {
|
||||
ipcRenderer.on('AGANI::set-text-direction', (e, { textDirection }) => {
|
||||
const { textDirection: oldTextDirection } = state.currentFile
|
||||
if (textDirection !== oldTextDirection) {
|
||||
commit('SET_TEXT_DIRECTION', textDirection)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
LISTEN_FOR_FILE_CHANGE ({ commit, state, rootState }) {
|
||||
ipcRenderer.on('AGANI::update-file', (e, { type, change }) => {
|
||||
// TODO: Set `isSaved` to false.
|
||||
|
@ -12,10 +12,9 @@ export const defaultFileState = {
|
||||
pathname: '',
|
||||
filename: 'Untitled-1',
|
||||
markdown: '',
|
||||
isUtf8BomEncoded: false,
|
||||
encoding: 'utf8', // Currently just "utf8" or "utf8bom"
|
||||
lineEnding: 'lf', // lf or crlf
|
||||
adjustLineEndingOnSave: false, // convert editor buffer (LF) to CRLF when saving
|
||||
textDirection: 'ltr',
|
||||
history: {
|
||||
stack: [],
|
||||
index: -1
|
||||
@ -35,8 +34,8 @@ export const defaultFileState = {
|
||||
}
|
||||
|
||||
export const getOptionsFromState = file => {
|
||||
const { isUtf8BomEncoded, lineEnding, adjustLineEndingOnSave } = file
|
||||
return { isUtf8BomEncoded, lineEnding, adjustLineEndingOnSave }
|
||||
const { encoding, lineEnding, adjustLineEndingOnSave } = file
|
||||
return { encoding, lineEnding, adjustLineEndingOnSave }
|
||||
}
|
||||
|
||||
export const getFileStateFromData = data => {
|
||||
@ -45,10 +44,9 @@ export const getFileStateFromData = data => {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
isUtf8BomEncoded,
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave,
|
||||
textDirection
|
||||
adjustLineEndingOnSave
|
||||
} = data
|
||||
const id = getUniqueId()
|
||||
|
||||
@ -59,10 +57,9 @@ export const getFileStateFromData = data => {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
isUtf8BomEncoded,
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave,
|
||||
textDirection
|
||||
adjustLineEndingOnSave
|
||||
})
|
||||
}
|
||||
|
||||
@ -78,6 +75,11 @@ export const getBlankFileState = (tabs, lineEnding = 'lf', markdown = '') => {
|
||||
|
||||
const id = getUniqueId()
|
||||
|
||||
// We may pass muarkdown=null as parameter.
|
||||
if (markdown == null) {
|
||||
markdown = ''
|
||||
}
|
||||
|
||||
return Object.assign(fileState, {
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave: lineEnding.toLowerCase() === 'crlf',
|
||||
@ -91,7 +93,7 @@ export const getSingleFileState = ({ id = getUniqueId(), markdown, filename, pat
|
||||
// TODO(refactor:renderer/editor): Replace this function with `createDocumentState`.
|
||||
|
||||
const fileState = JSON.parse(JSON.stringify(defaultFileState))
|
||||
const { isUtf8BomEncoded, lineEnding, adjustLineEndingOnSave, textDirection = 'ltr' } = options
|
||||
const { encoding, lineEnding, adjustLineEndingOnSave = 'ltr' } = options
|
||||
|
||||
assertLineEnding(adjustLineEndingOnSave, lineEnding)
|
||||
|
||||
@ -100,9 +102,8 @@ export const getSingleFileState = ({ id = getUniqueId(), markdown, filename, pat
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
isUtf8BomEncoded,
|
||||
encoding,
|
||||
lineEnding,
|
||||
textDirection,
|
||||
adjustLineEndingOnSave
|
||||
})
|
||||
}
|
||||
@ -120,7 +121,7 @@ export const createDocumentState = (markdownDocument, id = getUniqueId()) => {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
isUtf8BomEncoded,
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave,
|
||||
} = markdownDocument
|
||||
@ -132,7 +133,7 @@ export const createDocumentState = (markdownDocument, id = getUniqueId()) => {
|
||||
markdown,
|
||||
filename,
|
||||
pathname,
|
||||
isUtf8BomEncoded,
|
||||
encoding,
|
||||
lineEnding,
|
||||
adjustLineEndingOnSave
|
||||
})
|
||||
|
@ -19,7 +19,8 @@ const state = {
|
||||
platform: process.platform, // platform of system `darwin` | `win32` | `linux`
|
||||
appVersion: process.versions.MARKTEXT_VERSION_STRING, // Mark Text version string
|
||||
windowActive: true, // weather current window is active or focused
|
||||
init: process.env.NODE_ENV === 'development' // weather Mark Text is inited
|
||||
// TODO: "init" is nowhere used
|
||||
init: process.env.NODE_ENV === 'development' // whether Mark Text is inited
|
||||
}
|
||||
|
||||
const getters = {}
|
||||
|
@ -9,6 +9,7 @@ const state = {
|
||||
codeFontFamily: 'DejaVu Sans Mono',
|
||||
codeFontSize: '14px',
|
||||
lineHeight: 1.6,
|
||||
textDirection: 'ltr',
|
||||
lightColor: '#303133', // color in light theme
|
||||
darkColor: 'rgb(217, 217, 217)', // color in dark theme
|
||||
autoSave: false,
|
||||
@ -45,7 +46,8 @@ const mutations = {
|
||||
|
||||
const actions = {
|
||||
ASK_FOR_USER_PREFERENCE ({ commit, state, rootState }) {
|
||||
ipcRenderer.send('AGANI::ask-for-user-preference')
|
||||
ipcRenderer.send('mt::ask-for-user-preference')
|
||||
|
||||
ipcRenderer.on('AGANI::user-preference', (e, preference) => {
|
||||
const { autoSave } = preference
|
||||
commit('SET_USER_PREFERENCE', preference)
|
||||
@ -77,7 +79,7 @@ const actions = {
|
||||
CHANGE_FONT ({ commit }, { type, value }) {
|
||||
commit('SET_USER_PREFERENCE', { [type]: value })
|
||||
// save to preference.md
|
||||
ipcRenderer.send('AGANI::set-user-preference', { [type]: value })
|
||||
ipcRenderer.send('mt::set-user-preference', { [type]: value })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -108,7 +108,7 @@ const actions = {
|
||||
LISTEN_FOR_LOAD_PROJECT ({ commit, dispatch }) {
|
||||
ipcRenderer.on('AGANI::open-project', (e, pathname) => {
|
||||
// Initialize editor and show empty/new tab
|
||||
dispatch('NEW_BLANK_FILE')
|
||||
dispatch('NEW_UNTITLED_TAB')
|
||||
|
||||
dispatch('INIT_STATUS', true)
|
||||
commit('SET_PROJECT_TREE', pathname)
|
||||
|
15
yarn.lock
15
yarn.lock
@ -597,6 +597,11 @@ are-we-there-yet@~1.1.2:
|
||||
delegates "^1.0.0"
|
||||
readable-stream "^2.0.6"
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0"
|
||||
integrity sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg==
|
||||
|
||||
argparse@^1.0.7:
|
||||
version "1.0.10"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911"
|
||||
@ -3895,6 +3900,11 @@ electron-localshortcut@^3.1.0:
|
||||
keyboardevent-from-electron-accelerator "^1.1.0"
|
||||
keyboardevents-areequal "^0.2.1"
|
||||
|
||||
electron-log@^3.0.5:
|
||||
version "3.0.5"
|
||||
resolved "https://registry.yarnpkg.com/electron-log/-/electron-log-3.0.5.tgz#9bdd307f1f1aec85c0873babd6bdfffb1a661436"
|
||||
integrity sha512-wWOPNBVGh7NJx/OLXdgtuYrqxoG9ZWO+kZYloIHi66B5W9EgXZw71jdZ1ddeWNg4CkABypa8tkrRGqCKF+9tYg==
|
||||
|
||||
electron-osx-sign@0.4.11:
|
||||
version "0.4.11"
|
||||
resolved "https://registry.yarnpkg.com/electron-osx-sign/-/electron-osx-sign-0.4.11.tgz#8377732fe7b207969f264b67582ee47029ce092f"
|
||||
@ -4215,6 +4225,11 @@ eslint-friendly-formatter@^4.0.1:
|
||||
strip-ansi "^4.0.0"
|
||||
text-table "^0.2.0"
|
||||
|
||||
eslint-import-resolver-alias@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz#297062890e31e4d6651eb5eba9534e1f6e68fc97"
|
||||
integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==
|
||||
|
||||
eslint-import-resolver-node@^0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a"
|
||||
|
Loading…
Reference in New Issue
Block a user