├── .browserslistrc
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .prettierrc.json
├── .stylelintignore
├── .stylelintrc.js
├── README.md
├── babel.config.js
├── build
└── icons
│ ├── 256x256.png
│ ├── 512x512.png
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── package.json
├── postcss.config.js
├── public
├── favicon.ico
└── index.html
├── screenshot.png
├── src
├── App.vue
├── assets
│ └── style
│ │ ├── Components
│ │ └── _index.scss
│ │ ├── Elements
│ │ ├── _code.scss
│ │ ├── _global.scss
│ │ └── _index.scss
│ │ ├── Generic
│ │ ├── _index.scss
│ │ └── _reset.scss
│ │ ├── Objects
│ │ └── _index.scss
│ │ ├── Settings
│ │ ├── _font.scss
│ │ ├── _index.scss
│ │ └── _variables.scss
│ │ ├── Tools
│ │ ├── _animation.scss
│ │ └── _index.scss
│ │ ├── Trumps
│ │ └── _index.scss
│ │ ├── Vendor
│ │ ├── _codemirror.scss
│ │ ├── _github-markdown.scss
│ │ ├── _index.scss
│ │ └── _prismjs.scss
│ │ └── main.scss
├── background.js
├── components
│ ├── DropField.vue
│ ├── Editor.vue
│ ├── KeyPrompt.vue
│ └── Toolbar.vue
├── lib
│ ├── event-bus.js
│ ├── markdown.js
│ └── utils.js
├── main.js
├── modules
│ ├── Encryptor.js
│ ├── Errors.js
│ ├── Filesystem.js
│ ├── dialog.js
│ └── editor.js
├── pages
│ └── Main.vue
├── plugins
│ ├── codemirror
│ │ └── index.js
│ ├── fontawesome
│ │ └── index.js
│ └── vue-meta
│ │ └── index.js
├── router.js
├── service
│ ├── app-menu-controller.js
│ └── app-menu.js
├── store.js
└── store
│ ├── index.js
│ └── modules
│ ├── App.js
│ ├── Editor.js
│ └── index.js
├── vue.config.js
└── yarn.lock
/.browserslistrc:
--------------------------------------------------------------------------------
1 | last 3 chrome version
2 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | trim_trailing_whitespace = true
7 | end_of_line = lf
8 | insert_final_newline = true
9 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | app/node_modules/**
2 | app/dist/**
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | node: true,
5 | },
6 | extends: "@hiro0218/eslint-config",
7 | };
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .report
3 | node_modules
4 | /dist
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 |
15 | # Editor directories and files
16 | .idea
17 | .vscode
18 | *.suo
19 | *.ntvs*
20 | *.njsproj
21 | *.sln
22 | *.sw*
23 |
24 | #Electron-builder output
25 | /dist_electron
26 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | "@hiro0218/prettier-config"
2 |
--------------------------------------------------------------------------------
/.stylelintignore:
--------------------------------------------------------------------------------
1 | *.styl
2 |
--------------------------------------------------------------------------------
/.stylelintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: [
3 | 'stylelint-config-recommended',
4 | 'stylelint-config-recommended-scss',
5 | 'stylelint-config-property-sort-order-smacss',
6 | ],
7 | plugins: [
8 | 'stylelint-declaration-block-no-ignored-properties',
9 | ],
10 | rules: {
11 | 'plugin/declaration-block-no-ignored-properties': true,
12 | 'no-empty-source': true,
13 | 'color-hex-length': 'short',
14 | 'color-no-invalid-hex': true,
15 | 'indentation': 2,
16 | 'length-zero-no-unit': true,
17 | "max-empty-lines": 2,
18 | 'string-quotes': 'single',
19 | 'declaration-block-no-duplicate-properties': [
20 | true,
21 | {
22 | ignore: [
23 | 'consecutive-duplicates'
24 | ]
25 | }
26 | ],
27 | 'block-opening-brace-space-before': 'always',
28 | 'block-opening-brace-newline-after': 'always',
29 | 'block-closing-brace-newline-after': 'always',
30 | 'block-closing-brace-newline-before': 'always',
31 | 'selector-list-comma-space-before': 'never',
32 | 'selector-list-comma-newline-after': 'always',
33 | 'selector-pseudo-element-colon-notation': 'double',
34 | 'value-list-comma-newline-after': 'always-multi-line',
35 | 'value-list-comma-space-after': 'always-single-line',
36 | 'value-list-comma-space-before': 'never',
37 | 'declaration-block-trailing-semicolon': 'always',
38 | 'declaration-block-semicolon-newline-after': 'always-multi-line',
39 | 'declaration-block-semicolon-newline-before': 'never-multi-line',
40 | 'declaration-block-semicolon-space-after': 'always-single-line',
41 | 'no-extra-semicolons': true,
42 | 'no-missing-end-of-source-newline': true,
43 | },
44 | };
45 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # miikun
2 |
3 | > A Simple Markdown Editor
4 |
5 |
6 |
7 | ## Build Setup
8 |
9 | ``` bash
10 | # install dependencies
11 | yarn install
12 |
13 | # serve
14 | yarn electron:serve
15 |
16 | # build electron app for production
17 | yarn electron:build
18 |
19 | # lint all JS/Vue component files in `app/src`
20 | yarn lint
21 | ```
22 | More information can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/).
23 |
24 | ---
25 |
26 | This project was generated from [electron-vue](https://github.com/SimulatedGREG/electron-vue) using [vue-cli](https://github.com/vuejs/vue-cli). Documentation about this project can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/index.html).
27 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | const presets = [
4 | '@vue/app',
5 | [
6 | '@babel/preset-env',
7 | {
8 | modules: false,
9 | useBuiltIns: 'usage',
10 | corejs: 3,
11 | },
12 | ],
13 | ];
14 | const plugins = ['@babel/plugin-transform-runtime'];
15 | const comments = false;
16 |
17 | return {
18 | comments,
19 | presets,
20 | plugins,
21 | };
22 | };
23 |
--------------------------------------------------------------------------------
/build/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiro0218/miikun/0ec53b11f9892c8e696a5aabb6fc325e4e755b51/build/icons/256x256.png
--------------------------------------------------------------------------------
/build/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiro0218/miikun/0ec53b11f9892c8e696a5aabb6fc325e4e755b51/build/icons/512x512.png
--------------------------------------------------------------------------------
/build/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiro0218/miikun/0ec53b11f9892c8e696a5aabb6fc325e4e755b51/build/icons/icon.icns
--------------------------------------------------------------------------------
/build/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiro0218/miikun/0ec53b11f9892c8e696a5aabb6fc325e4e755b51/build/icons/icon.ico
--------------------------------------------------------------------------------
/build/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiro0218/miikun/0ec53b11f9892c8e696a5aabb6fc325e4e755b51/build/icons/icon.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "miikun",
3 | "version": "3.2.1",
4 | "private": true,
5 | "description": "A Simple Markdown Editor",
6 | "author": "hiro ",
7 | "scripts": {
8 | "serve": "vue-cli-service serve",
9 | "build": "vue-cli-service build",
10 | "lint": "vue-cli-service lint",
11 | "electron:build": "vue-cli-service electron:build",
12 | "electron:serve": "vue-cli-service electron:serve",
13 | "format:scss": "stylelint --fix ./src",
14 | "lint:scss": "stylelint ./src",
15 | "postinstall": "electron-builder install-app-deps",
16 | "postuninstall": "electron-builder install-app-deps"
17 | },
18 | "main": "background.js",
19 | "dependencies": {
20 | "@fortawesome/fontawesome-svg-core": "^1.2.28",
21 | "@fortawesome/free-solid-svg-icons": "^5.13.0",
22 | "@fortawesome/vue-fontawesome": "^0.1.9",
23 | "codemirror": "^5.58.2",
24 | "core-js": "^3.6.4",
25 | "debounce": "^1.2.0",
26 | "markdown-it": "^11.0.0",
27 | "markdown-it-checkbox": "^1.1.0",
28 | "markdown-it-footnote": "^3.0.2",
29 | "normalize.css": "^8.0.1",
30 | "open-color": "^1.7.0",
31 | "prismjs": "^1.23.0",
32 | "vue": "^2.6.11",
33 | "vue-meta": "^2.3.4",
34 | "vue-router": "^3.3.2",
35 | "vuex": "^3.4.0",
36 | "vuex-persistedstate": "^3.0.1"
37 | },
38 | "devDependencies": {
39 | "@hiro0218/eslint-config": "^1.2.0",
40 | "@hiro0218/prettier-config": "^1.0.2",
41 | "@vue/cli-plugin-babel": "^4.4.1",
42 | "@vue/cli-plugin-eslint": "^4.4.1",
43 | "@vue/cli-service": "^4.4.1",
44 | "babel-eslint": "^10.0.3",
45 | "electron": "^9.4.0",
46 | "electron-devtools-installer": "^2.2.4",
47 | "eslint": "^6.1.0",
48 | "eslint-plugin-prettier": "^3.1.3",
49 | "fibers": "^5.0.0",
50 | "husky": "^4.2.5",
51 | "lint-staged": "^10.2.9",
52 | "markdown-it-anchor": "^5.3.0",
53 | "markdown-it-deflist": "^2.0.3",
54 | "markdown-it-multimd-table": "^4.0.2",
55 | "prettier": "^2.0.5",
56 | "sass": "^1.19.0",
57 | "sass-loader": "^7.3.1",
58 | "style-loader": "^1.2.1",
59 | "stylelint": "^13.6.0",
60 | "stylelint-config-property-sort-order-smacss": "^6.3.0",
61 | "stylelint-config-recommended": "^3.0.0",
62 | "stylelint-config-recommended-scss": "^4.2.0",
63 | "stylelint-declaration-block-no-ignored-properties": "^2.3.0",
64 | "stylelint-scss": "^3.17.2",
65 | "vue-cli-plugin-electron-builder": "^1.4.6",
66 | "vue-template-compiler": "^2.6.11"
67 | },
68 | "bugs": {
69 | "url": "https://github.com/hiro0218/Miikun/issues"
70 | },
71 | "homepage": "https://github.com/hiro0218/Miikun#readme",
72 | "husky": {
73 | "hooks": {
74 | "pre-commit": "lint-staged"
75 | }
76 | },
77 | "license": "MIT",
78 | "lint-staged": {
79 | "**/*.{js,vue}": [
80 | "vue-cli-service lint"
81 | ]
82 | },
83 | "build": {
84 | "appId": "jp.0218.miikun"
85 | },
86 | "productName": "miikun",
87 | "repository": {
88 | "type": "git",
89 | "url": "git+https://github.com/hiro0218/Miikun.git"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | autoprefixer: {},
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiro0218/miikun/0ec53b11f9892c8e696a5aabb6fc325e4e755b51/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | miikun
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/hiro0218/miikun/0ec53b11f9892c8e696a5aabb6fc325e4e755b51/screenshot.png
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
24 |
25 |
34 |
--------------------------------------------------------------------------------
/src/assets/style/Components/_index.scss:
--------------------------------------------------------------------------------
1 | //
2 |
--------------------------------------------------------------------------------
/src/assets/style/Elements/_code.scss:
--------------------------------------------------------------------------------
1 | code,
2 | pre {
3 | font-size: 1em;
4 | }
5 |
6 | code {
7 | margin: 0;
8 | padding: .2em .4em;
9 | border-radius: 3px;
10 | background-color: rgba(27, 31, 35, .05);
11 | &::before,
12 | &::after {
13 | content: '\00a0';
14 | letter-spacing: -.2em;
15 | }
16 | }
17 |
18 | pre {
19 | padding: 1rem;
20 | overflow: auto;
21 | border-radius: 3px;
22 | background-color: #f7f7f7;
23 | word-wrap: normal;
24 |
25 | > code {
26 | display: inline;
27 | padding: 0;
28 | border: 0;
29 | background: transparent;
30 | background-color: transparent;
31 | font-size: 100%;
32 | line-height: inherit;
33 | word-wrap: normal;
34 | word-break: normal;
35 | white-space: pre;
36 | &::before,
37 | &::after {
38 | content: normal;
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/assets/style/Elements/_global.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: $font-family-sansSerif;
3 | font-weight: $font-weight-base;
4 | line-height: $line-height-base;
5 | }
6 |
7 | h1,
8 | h2,
9 | h3,
10 | h4,
11 | h5,
12 | h6 {
13 | font-weight: bold;
14 | }
15 |
16 | h1 {
17 | font-size: $font-size-h1;
18 | }
19 | h2 {
20 | font-size: $font-size-h2;
21 | }
22 | h3 {
23 | font-size: $font-size-h3;
24 | }
25 | h4 {
26 | font-size: $font-size-h4;
27 | }
28 | h5 {
29 | font-size: $font-size-h5;
30 | }
31 | h6 {
32 | font-size: $font-size-h6;
33 | }
34 |
--------------------------------------------------------------------------------
/src/assets/style/Elements/_index.scss:
--------------------------------------------------------------------------------
1 | @import 'global';
2 | @import 'code';
3 |
--------------------------------------------------------------------------------
/src/assets/style/Generic/_index.scss:
--------------------------------------------------------------------------------
1 | @import 'reset';
2 |
--------------------------------------------------------------------------------
/src/assets/style/Generic/_reset.scss:
--------------------------------------------------------------------------------
1 | @import '~normalize.css/normalize.css';
2 |
3 | // stylelint-disable-next-line
4 | *,
5 | *::before,
6 | *::after {
7 | box-sizing: border-box;
8 | }
9 |
10 | html {
11 | font-family: sans-serif;
12 | font-size: 100%;
13 | -webkit-font-smoothing: antialiased;
14 | -webkit-overflow-scrolling: touch;
15 | -webkit-text-size-adjust: 100%;
16 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
17 | }
18 |
19 | img {
20 | max-width: 100%;
21 | border-style: none;
22 | background-color: #fff;
23 | vertical-align: middle;
24 | }
25 |
26 | svg:not(:root) {
27 | overflow: hidden;
28 | }
29 |
30 | hr {
31 | box-sizing: content-box;
32 | height: 0;
33 | overflow: visible;
34 | }
35 |
36 | button,
37 | [type='button'],
38 | [type='reset'],
39 | [type='submit']
40 | [role='button'] {
41 | cursor: pointer;
42 | }
43 |
44 | button,
45 | input,
46 | select,
47 | textarea {
48 | border-style: none;
49 | background-color: transparent;
50 | color: inherit;
51 | }
52 |
--------------------------------------------------------------------------------
/src/assets/style/Objects/_index.scss:
--------------------------------------------------------------------------------
1 | //
2 |
--------------------------------------------------------------------------------
/src/assets/style/Settings/_font.scss:
--------------------------------------------------------------------------------
1 | // Emoji
2 | @font-face {
3 | font-family: 'Emoji';
4 | src: local('Apple Color Emoji'),
5 | local('Gecko Emoji'),
6 | local('Noto Sans Emoji'),
7 | local('Segoe UI Emoji'),
8 | local('Segoe UI Symbol'),
9 | local('Noto Sans Symbols');
10 | }
11 |
--------------------------------------------------------------------------------
/src/assets/style/Settings/_index.scss:
--------------------------------------------------------------------------------
1 | @import 'variables';
2 | @import 'font';
3 |
--------------------------------------------------------------------------------
/src/assets/style/Settings/_variables.scss:
--------------------------------------------------------------------------------
1 | @import '~open-color/open-color.scss';
2 |
3 | //
4 | // Font
5 | // -------------------------
6 | $font-family-sansSerif: 'Noto Sans JP', Emoji, sans-serif;
7 | $font-family-monospace: 'Source Code Pro', #{$font-family-sansSerif}, Emoji, monospace !default;
8 | $font-weight-base: normal;
9 |
10 | $font-size-lg: 1rem * 4/3; // 1.33333rem
11 | $font-size-base: 1rem * 4/4; // 1rem
12 | $font-size-sm: 1rem * 4/5; // 0.8rem
13 | $font-size-xs: 1rem * 4/6; // 0.66667rem
14 |
15 | $font-size-h1: 1rem * 6/3; // 2rem
16 | $font-size-h2: 1rem * 6/4; // 1.5rem
17 | $font-size-h3: 1rem * 6/5; // 1.2rem
18 | $font-size-h4: 1rem * 6/6; // 1rem
19 | $font-size-h5: 1rem * 6/7; // 0.85714rem
20 | $font-size-h6: 1rem * 6/8; // 0.75rem
21 |
22 | $line-height-base: 16/8;
23 |
24 | //
25 | // Layout
26 | // -------------------------
27 |
28 | $screen-xs: 480px !default; // Extra small screen / phone
29 | $screen-sm: 768px !default; // Small screen / tablet
30 | $screen-md: 992px !default; // Medium screen / desktop
31 | $screen-lg: 1200px !default;
32 |
33 | $container-max-width: 1140px;
34 |
35 | $toolbar-height: 56px;
36 | $toolbar-width: 3.125rem;
37 | $statusbar-height: 20px;
38 |
39 |
40 | //
41 | // color const
42 | // -------------------------
43 | $color50: #fafafa;
44 | $color100: #f5f5f5;
45 | $color200: #eee;
46 | $color300: #e0e0e0;
47 | $color400: #bdbdbd;
48 | $color500: #9e9e9e;
49 | $color600: #757575;
50 | $color700: #616161;
51 | $color800: #424242;
52 | $color900: #212121;
53 |
54 | $white: #fff;
55 | $black: #000;
56 |
57 | $full-black: rgba(0, 0, 0, 1);
58 | $dark-black: rgba(0, 0, 0, .87);
59 | $light-black: rgba(0, 0, 0, .54);
60 | $min-black: rgba(0, 0, 0, .26);
61 | $faint-black: rgba(0, 0, 0, .12);
62 | $full-white: rgba(255, 255, 255, 1);
63 | $dark-white: rgba(255, 255, 255, .87);
64 | $light-white: rgba(255, 255, 255, .54);
65 |
66 | $text-color: $color800;
67 | $bg-color: $white;
68 |
69 |
70 | // color
71 | $selection-bgColor: $faint-black;
72 | $menu-bgColor: $color600;
73 | $editor-linenumber-color: $color500;
74 | $editor-linenumber-bgColor: transparent;
75 | $editor-activeline-bgColor: $color200;
76 |
--------------------------------------------------------------------------------
/src/assets/style/Tools/_animation.scss:
--------------------------------------------------------------------------------
1 | @keyframes blink {
2 | // stylelint-disable-next-line
3 | 0% {
4 |
5 | }
6 |
7 | 50% {
8 | background-color: transparent;
9 | }
10 |
11 | // stylelint-disable-next-line
12 | 100% {
13 |
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/assets/style/Tools/_index.scss:
--------------------------------------------------------------------------------
1 | @import 'animation';
2 |
--------------------------------------------------------------------------------
/src/assets/style/Trumps/_index.scss:
--------------------------------------------------------------------------------
1 | //
2 |
--------------------------------------------------------------------------------
/src/assets/style/Vendor/_codemirror.scss:
--------------------------------------------------------------------------------
1 | @import '~codemirror/addon/lint/lint.css';
2 |
3 | // BASICS
4 | .CodeMirror {
5 | position: relative;
6 | overflow: hidden;
7 | background: white;
8 | font-family: $font-family-sansSerif;
9 | font-size: 1rem;
10 | }
11 |
12 | // PADDING
13 | .CodeMirror-lines {
14 | min-height: 1px; /* prevents collapsing before first draw */
15 | padding: 0;
16 | cursor: text;
17 | }
18 | .CodeMirror pre {
19 | position: relative;
20 | z-index: 2;
21 | margin: 0;
22 | padding: 0 .25rem; // Horizontal padding of content
23 | overflow: visible;
24 | border-width: 0;
25 | /* Reset some styles that the rest of the page might have set */
26 | border-radius: 0;
27 | background: transparent;
28 | color: inherit;
29 | font-family: inherit;
30 | font-size: inherit;
31 | line-height: inherit;
32 | word-wrap: normal;
33 | white-space: pre;
34 | -webkit-tap-highlight-color: transparent;
35 | -webkit-font-variant-ligatures: none;
36 | font-variant-ligatures: none;
37 | }
38 |
39 | .CodeMirror-scrollbar-filler,
40 | .CodeMirror-gutter-filler {
41 | background-color: transparent; // The little square between H and V scrollbars
42 | }
43 |
44 | // GUTTER
45 | .CodeMirror-gutters {
46 | position: absolute;
47 | z-index: 3;
48 | top: 0;
49 | left: 0;
50 | min-height: 100%;
51 | border-right: 1px solid darken($editor-linenumber-bgColor, 2%);
52 | background: $editor-linenumber-bgColor;
53 | white-space: nowrap;
54 | }
55 |
56 | .CodeMirror-linenumber {
57 | min-width: 1rem;
58 | color: $editor-linenumber-color;
59 | font-family: $font-family-monospace;
60 | text-align: right;
61 | white-space: nowrap;
62 | }
63 |
64 | .CodeMirror-activeline {
65 | .CodeMirror-linenumber {
66 | color: darken($editor-linenumber-color, 16%);
67 | font-weight: bold;
68 | }
69 | }
70 |
71 | /* CURSOR */
72 |
73 | .CodeMirror-cursor {
74 | position: absolute;
75 | width: 0;
76 | border-right: none;
77 | border-left: 2px solid $color800;
78 | }
79 | /* Shown when moving in bi-directional text */
80 | .CodeMirror .CodeMirror-secondarycursor {
81 | border-left: 1px solid silver;
82 | }
83 | .cm-fat-cursor .CodeMirror-cursor {
84 | width: auto;
85 | border: 0;
86 | background: #7e7;
87 | }
88 | .cm-fat-cursor .CodeMirror-cursors {
89 | z-index: 1;
90 | }
91 |
92 | .cm-animate-fat-cursor {
93 | width: auto;
94 | animation: blink 1.06s steps(1) infinite;
95 | border: 0;
96 | background-color: #7e7;
97 | }
98 |
99 | /* Can style cursor different in overwrite (non-insert) mode */
100 | // .CodeMirror-overwrite .CodeMirror-cursor {}
101 |
102 | .cm-tab {
103 | display: inline-block;
104 | text-decoration: inherit;
105 | }
106 |
107 | .CodeMirror-ruler {
108 | position: absolute;
109 | border-left: 1px solid #ccc;
110 | }
111 |
112 | /* DEFAULT THEME */
113 | .CodeMirror-composing {
114 | border-bottom: 2px solid;
115 | }
116 |
117 | /* Default styles for common addons */
118 | /* stylelint-disable-next-line selector-type-no-unknown */
119 | .CodeMirror CodeMirror-matchingbracket {
120 | color: #0f0;
121 | }
122 | /* stylelint-disable-next-line selector-type-no-unknown */
123 | .CodeMirror CodeMirror-nonmatchingbracket {
124 | color: #f22;
125 | }
126 | .CodeMirror-matchingtag {
127 | background: rgba(255, 150, 0, .3);
128 | }
129 |
130 | // /* STOP */
131 | // /* The rest of this file contains styles related to the mechanics of
132 | // the editor. You probably shouldn't touch them. */
133 |
134 | .CodeMirror-scroll {
135 | position: relative;
136 | height: 100%;
137 | margin-right: -32px;
138 | /* 30px is the magic margin used to hide the element's real scrollbars */
139 | /* See overflow: hidden in .CodeMirror */
140 | margin-bottom: -32px;
141 | padding-bottom: 32px;
142 | overflow: scroll !important; /* Things will break if this is overridden */
143 | outline: none; /* Prevent dragging from highlighting the element */
144 | }
145 | .CodeMirror-sizer {
146 | position: relative;
147 | border-right: 32px solid transparent;
148 | }
149 |
150 | /* The fake, visible scrollbars. Used to force redraw during scrolling
151 | before actual scrolling happens, thus preventing shaking and
152 | flickering artifacts. */
153 | .CodeMirror-vscrollbar,
154 | .CodeMirror-hscrollbar,
155 | .CodeMirror-scrollbar-filler,
156 | .CodeMirror-gutter-filler {
157 | display: none;
158 | position: absolute;
159 | z-index: 6;
160 | }
161 | .CodeMirror-vscrollbar {
162 | top: 0;
163 | right: 0;
164 | overflow-x: hidden;
165 | overflow-y: scroll;
166 | }
167 | .CodeMirror-hscrollbar {
168 | bottom: 0;
169 | left: 0;
170 | overflow-x: scroll;
171 | overflow-y: hidden;
172 | }
173 | .CodeMirror-scrollbar-filler {
174 | right: 0;
175 | bottom: 0;
176 | }
177 | .CodeMirror-gutter-filler {
178 | bottom: 0;
179 | left: 0;
180 | }
181 |
182 | .CodeMirror-gutter {
183 | //height: 100%;
184 | display: inline-block;
185 | margin-bottom: -30px;
186 | vertical-align: top;
187 | white-space: normal;
188 | }
189 | .CodeMirror-gutter-wrapper {
190 | position: absolute;
191 | z-index: 4;
192 | border: none !important;
193 | background: none !important;
194 | user-select: none;
195 | }
196 | .CodeMirror-gutter-background {
197 | position: absolute;
198 | z-index: 4;
199 | top: 0;
200 | bottom: 0;
201 | }
202 | .CodeMirror-gutter-elt {
203 | position: absolute;
204 | z-index: 4;
205 | cursor: default;
206 | }
207 |
208 | .CodeMirror-wrap pre {
209 | word-wrap: break-word;
210 | word-break: normal;
211 | white-space: pre-wrap;
212 | }
213 |
214 | .CodeMirror-linebackground {
215 | position: absolute;
216 | z-index: 0;
217 | top: 0;
218 | right: 0;
219 | bottom: 0;
220 | left: 0;
221 | }
222 |
223 | .CodeMirror-linewidget {
224 | position: relative;
225 | z-index: 2;
226 | overflow: auto;
227 | }
228 |
229 | // .CodeMirror-widget {}
230 |
231 | .CodeMirror-code {
232 | outline: none;
233 | }
234 |
235 | .CodeMirror-measure {
236 | visibility: hidden;
237 | position: absolute;
238 | width: 100%;
239 | height: 0;
240 | overflow: hidden;
241 | }
242 |
243 | .CodeMirror-measure pre {
244 | position: static;
245 | }
246 |
247 | .CodeMirror-cursors {
248 | visibility: hidden;
249 | position: relative;
250 | z-index: 3;
251 | }
252 | .CodeMirror-dragcursors {
253 | visibility: visible;
254 | }
255 |
256 | .CodeMirror-focused .CodeMirror-cursors {
257 | visibility: visible;
258 | }
259 |
260 | .CodeMirror-crosshair {
261 | cursor: crosshair;
262 | }
263 |
264 | .CodeMirror-selected,
265 | .CodeMirror-line::selection,
266 | .CodeMirror-line > span::selection,
267 | .CodeMirror-line > span > span::selection {
268 | border-radius: 2px;
269 | background: $selection-bgColor;
270 | }
271 |
272 | .cm-searching {
273 | background: rgba(255, 255, 0, .4);
274 | }
275 |
276 | /* Used to force a border model for a node */
277 | .cm-force-border {
278 | padding-right: .1px;
279 | }
280 |
281 | /* See issue #2901 */
282 | .cm-tab-wrap-hack::after {
283 | content: '';
284 | }
285 |
286 | /* Help users use markselection to safely style text background */
287 | /* stylelint-disable-next-line selector-type-no-unknown */
288 | CodeMirror-selectedtext {
289 | background: none;
290 | }
291 |
292 |
293 | ////////////
294 |
295 | .CodeMirror-guttermarker {
296 | color: #ac4142;
297 | }
298 | .CodeMirror-guttermarker-subtle {
299 | color: #b0b0b0;//$color500
300 | }
301 | .cm-comment {
302 | color: $color600;
303 | font-family: $font-family-monospace;
304 | }
305 | .cm-atom {
306 | color: #aa759f;
307 | }
308 | .cm-number {
309 | color: #aa759f;
310 | }
311 | .cm-attribute,
312 | .cm-property {
313 | color: #90a959;
314 | }
315 | .cm-keyword {
316 | color: #ac4142;
317 | }
318 | .cm-string {
319 | color: #f4bf75;
320 | }
321 | .cm-variable {
322 | color: #90a959;
323 | }
324 | .cm-variable-2 {
325 | color: #6a9fb5;
326 | }
327 | .cm-def {
328 | color: #d28445;
329 | }
330 | .cm-bracket {
331 | color: #202020;
332 | }
333 | .cm-tag {
334 | color: #ac4142;
335 | }
336 | .cm-link {
337 | color: #aa759f;
338 | }
339 | .cm-error {
340 | background: #ac4142;
341 | color: #505050;
342 | }
343 | .CodeMirror-activeline-background {
344 | background: $editor-activeline-bgColor;
345 | }
346 |
347 | .CodeMirror-matchingbracket {
348 | color: white !important;
349 | text-decoration: underline;
350 | }
351 |
352 | // same: github-markdown style
353 | .cm-header-1 {
354 | @extend h1;
355 | }
356 | .cm-header-2 {
357 | @extend h2;
358 | }
359 | .cm-header-3 {
360 | @extend h3;
361 | }
362 | .cm-header-4 {
363 | @extend h4;
364 | }
365 | .cm-header-5 {
366 | @extend h5;
367 | }
368 | .cm-header-6 {
369 | @extend h6;
370 | }
371 |
372 | .cm-strong {
373 | font-weight: bold;
374 | }
375 | .cm-em {
376 | font-style: italic;
377 | }
378 |
379 | .cm-hr,
380 | .cm-formatting {
381 | color: $color500;
382 | font-family: $font-family-monospace;
383 | font-weight: bold;
384 | }
385 |
386 | .cm-quote {
387 | color: $color500;
388 | }
389 |
390 | // Lint
391 | .CodeMirror-lint-mark-error {
392 | border-radius: 2px;
393 | background: #ECACB5;
394 | }
395 | .CodeMirror-lint-tooltip {
396 | background: $dark-black;
397 | color: $color50;
398 | font-family: $font-family-monospace;
399 | font-size: $font-size-sm;
400 | }
401 | .CodeMirror-lint-message-error {
402 | background-image: none;
403 | &::before {
404 | content: '●';
405 | display: block;
406 | position: absolute;
407 | left: .5rem;
408 | color: #C7243A;
409 | font-size: 1rem;
410 | }
411 | }
412 |
413 | // fold
414 | .CodeMirror-foldmarker {
415 | margin: .25rem;
416 | color: $color400;
417 | line-height: .3;
418 | cursor: pointer;
419 | }
420 | .CodeMirror-foldgutter {
421 | width: 1rem;
422 | }
423 | .CodeMirror-foldgutter-open,
424 | .CodeMirror-foldgutter-folded {
425 | cursor: pointer;
426 | &::after {
427 | display: block;
428 | text-align: center;
429 | }
430 | }
431 | .CodeMirror-foldgutter-open::after {
432 | content: '\25BE';
433 | }
434 | .CodeMirror-foldgutter-folded::after {
435 | content: '\25B8';
436 | }
437 |
--------------------------------------------------------------------------------
/src/assets/style/Vendor/_github-markdown.scss:
--------------------------------------------------------------------------------
1 | .markdown-body {
2 | color: #333;
3 | font-size: 1rem;
4 | word-wrap: break-word;
5 |
6 | p,
7 | blockquote,
8 | ul,
9 | ol,
10 | dl,
11 | table,
12 | pre {
13 | margin-top: 0;
14 | margin-bottom: 1rem;
15 | }
16 |
17 | > * {
18 | &:first-child {
19 | margin-top: 0 !important;
20 | }
21 | &:last-child {
22 | margin-bottom: 0 !important;
23 | }
24 | }
25 |
26 | a {
27 | background-color: transparent;
28 | color: #4078c0;
29 | text-decoration: none;
30 | -webkit-text-decoration-skip: objects;
31 |
32 | &:hover,
33 | &:active {
34 | outline-width: 0;
35 | text-decoration: underline;
36 | }
37 |
38 | &:not([href]) {
39 | color: inherit;
40 | text-decoration: none;
41 | }
42 | }
43 |
44 | input {
45 | margin: 0;
46 | overflow: visible;
47 | font: inherit;
48 | pointer-events: none;
49 | user-select: none;
50 | -webkit-font-feature-settings: 'liga' 0;
51 | font-feature-settings: 'liga' 0;
52 | }
53 |
54 | hr {
55 | height: .25em;
56 | margin: 1.5rem 0;
57 | padding: 0;
58 | overflow: hidden;
59 | border: 0;
60 | background-color: #eee;
61 |
62 | &::before,
63 | &::after {
64 | content: '';
65 | display: table;
66 | }
67 | &::after {
68 | clear: both;
69 | }
70 | }
71 |
72 | table {
73 | display: block;
74 | width: 100%;
75 | overflow: auto;
76 | border-spacing: 0;
77 | border-collapse: collapse;
78 | word-break: normal;
79 | word-break: keep-all;
80 |
81 | th,
82 | td {
83 | padding: 0;
84 | padding: 6px 13px;
85 | border: 1px solid #ddd;
86 | }
87 |
88 | th {
89 | border-bottom-width: 2px;
90 | font-weight: bold;
91 | }
92 |
93 | tr {
94 | border-top: 1px solid #ccc;
95 | background-color: #fff;
96 | &:nth-child(2n) {
97 | background-color: #f8f8f8;
98 | }
99 | }
100 | }
101 |
102 | ul,
103 | ol {
104 | padding-left: 2em;
105 |
106 | ul,
107 | ol {
108 | margin-top: 0;
109 | margin-bottom: 0;
110 | }
111 |
112 | ol {
113 | list-style-type: lower-roman;
114 | }
115 |
116 | ul ol,
117 | ol ol {
118 | list-style-type: lower-alpha;
119 | }
120 |
121 | li {
122 | & + li {
123 | margin-top: .25em;
124 | }
125 | input {
126 | margin-right: 0.25em;
127 | }
128 | label {
129 | pointer-events: none;
130 | }
131 | }
132 | }
133 |
134 | dd {
135 | margin-left: 0;
136 | }
137 |
138 | blockquote {
139 | margin-left: 0.5em;
140 | padding: 0 1em;
141 | border-left: .25em solid #ddd;
142 | color: #777;
143 | > {
144 | :first-child {
145 | margin-top: 0;
146 | }
147 | :last-child {
148 | margin-bottom: 0;
149 | }
150 | }
151 | }
152 |
153 | h1 {
154 | border-bottom: 2px solid #eee;
155 | }
156 |
157 | h2 {
158 | border-bottom: 1px solid #eee;
159 | }
160 |
161 | dl {
162 | padding: 0;
163 | dt {
164 | margin-top: 1rem;
165 | padding: 0;
166 | font-size: 1em;
167 | font-style: italic;
168 | font-weight: bold;
169 | }
170 | dd {
171 | margin-bottom: 1rem;
172 | padding: 0 1rem;
173 | }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/assets/style/Vendor/_index.scss:
--------------------------------------------------------------------------------
1 | @import 'github-markdown';
2 | @import 'codemirror';
3 | @import 'prismjs';
4 |
--------------------------------------------------------------------------------
/src/assets/style/Vendor/_prismjs.scss:
--------------------------------------------------------------------------------
1 | /*
2 | Name: Base16 Tomorrow Light
3 | Author: Chris Kempson (http://chriskempson.com)
4 | Prism template by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/prism/)
5 | Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
6 | */
7 |
8 | $base00: #1d1f21;
9 | $base01: #282a2e;
10 | $base02: #373b41;
11 | $base03: #969896;
12 | $base04: #b4b7b4;
13 | $base05: #c5c8c6;
14 | $base06: #e0e0e0;
15 | $base07: #fff;
16 | $base08: #c66;
17 | $base09: #de935f;
18 | $base0a: #f0c674;
19 | $base0b: #b5bd68;
20 | $base0c: #8abeb7;
21 | $base0d: #81a2be;
22 | $base0e: #b294bb;
23 | $base0f: #a3685a;
24 |
25 | code[class*='language-'],
26 | pre[class*='language-'] {
27 | color: $base02 !important;
28 | font-size: 1rem;
29 | text-align: left;
30 | word-break: normal;
31 | white-space: pre;
32 | word-spacing: normal;
33 | hyphens: none;
34 | tab-size: 4;
35 | direction: ltr;
36 | }
37 |
38 | /* Code blocks */
39 | pre[class*='language-'] {
40 | overflow: auto;
41 | }
42 |
43 | /* Inline code */
44 | :not(pre) > code[class*='language-'] {
45 | padding: .1em;
46 | border-radius: .3em;
47 | }
48 |
49 |
50 | .token.comment,
51 | .token.prolog,
52 | .token.doctype,
53 | .token.cdata {
54 | color: $base04;
55 | }
56 |
57 | .token.punctuation {
58 | color: $base02;
59 | }
60 |
61 | .token.namespace {
62 | opacity: .7;
63 | }
64 |
65 | .token.operator,
66 | .token.boolean,
67 | .token.number {
68 | color: $base09;
69 | }
70 | .token.property {
71 | color: $base0a;
72 | }
73 | .token.tag {
74 | color: $base0d;
75 | }
76 | .token.string {
77 | color: $base0c;
78 | }
79 | .token.selector {
80 | color: $base0e;
81 | }
82 | .token.attr-name {
83 | color: $base09;
84 | }
85 | .token.entity,
86 | .token.url,
87 | .language-css .token.string,
88 | .style .token.string {
89 | color: $base0c;
90 | }
91 |
92 | .token.attr-value,
93 | .token.keyword,
94 | .token.control,
95 | .token.directive,
96 | .token.unit {
97 | color: $base0b;
98 | }
99 |
100 | .token.statement,
101 | .token.regex,
102 | .token.atrule {
103 | color: $base0c;
104 | }
105 |
106 | .token.placeholder,
107 | .token.variable {
108 | color: $base0d;
109 | }
110 |
111 | .token.deleted {
112 | text-decoration: line-through;
113 | }
114 |
115 | .token.inserted {
116 | border-bottom: 1px dotted $base00;
117 | text-decoration: none;
118 | }
119 |
120 | .token.italic {
121 | font-style: italic;
122 | }
123 |
124 | .token.important,
125 | .token.bold {
126 | font-weight: bold;
127 | }
128 |
129 | .token.important {
130 | color: $base08;
131 | }
132 |
133 | .token.entity {
134 | cursor: help;
135 | }
136 |
137 | pre > code.highlight {
138 | outline: .4em solid $base08;
139 | outline-offset: .4em;
140 | }
141 |
142 |
143 | // prism-show-language
144 | .prism-show-language {
145 | position: relative;
146 | }
147 |
148 | .prism-show-language > .prism-show-language-label {
149 | display: inline-block;
150 | position: absolute;
151 | z-index: 1;
152 | top: 0;
153 | right: 0;
154 | bottom: auto;
155 | left: auto;
156 | width: auto;
157 | height: auto;
158 | padding: 0 .5em;
159 | -webkit-transform: none;
160 | transform: none;
161 | border-radius: 0 0 0 5px;
162 | background-color: #CFCFCF;
163 | box-shadow: none;
164 | color: black;
165 | font-size: .9em;
166 | text-shadow: none;
167 | }
168 |
--------------------------------------------------------------------------------
/src/assets/style/main.scss:
--------------------------------------------------------------------------------
1 | @import 'Settings/index';
2 | @import 'Tools/index';
3 | @import 'Generic/index';
4 | @import 'Elements/index';
5 | @import 'Objects/index';
6 | @import 'Components/index';
7 | @import 'Trumps/index';
8 | @import 'Vendor/index';
9 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import { app, protocol, BrowserWindow } from 'electron';
4 | import { createProtocol, installVueDevtools } from 'vue-cli-plugin-electron-builder/lib';
5 | import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer';
6 | const isDevelopment = process.env.NODE_ENV !== 'production';
7 |
8 | app.allowRendererProcessReuse = true;
9 |
10 | // Note: Must match `build.appId` in package.json
11 | app.setAppUserModelId('jp.0218.miikun');
12 |
13 | // Keep a global reference of the window object, if you don't, the window will
14 | // be closed automatically when the JavaScript object is garbage collected.
15 | let win;
16 |
17 | // Scheme must be registered before the app is ready
18 | protocol.registerSchemesAsPrivileged([{ scheme: 'app', privileges: { secure: true, standard: true } }]);
19 |
20 | function createWindow() {
21 | // Create the browser window.
22 | win = new BrowserWindow({
23 | width: 800,
24 | height: 600,
25 | show: false,
26 | center: true,
27 | webPreferences: {
28 | nodeIntegration: true,
29 | webSecurity: false,
30 | },
31 | });
32 |
33 | if (process.env.WEBPACK_DEV_SERVER_URL) {
34 | // Load the url of the dev server if in development mode
35 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL);
36 |
37 | // install vue-devtools
38 | installExtension(VUEJS_DEVTOOLS)
39 | .then((name) => console.log(`Added Extension: ${name}`))
40 | .catch((err) => console.log('An error occurred: ', err));
41 |
42 | if (!process.env.IS_TEST) win.webContents.openDevTools();
43 | } else {
44 | createProtocol('app');
45 | // Load the index.html when not in development
46 | win.loadURL('app://./index.html');
47 | }
48 |
49 | win.once('ready-to-show', () => {
50 | win.show();
51 | });
52 |
53 | win.on('closed', () => {
54 | win = null;
55 | });
56 | }
57 |
58 | // Quit when all windows are closed.
59 | app.on('window-all-closed', () => {
60 | // On macOS it is common for applications and their menu bar
61 | // to stay active until the user quits explicitly with Cmd + Q
62 | if (process.platform !== 'darwin') {
63 | app.quit();
64 | }
65 | });
66 |
67 | app.on('activate', () => {
68 | // On macOS it's common to re-create a window in the app when the
69 | // dock icon is clicked and there are no other windows open.
70 | if (win === null) {
71 | createWindow();
72 | }
73 | });
74 |
75 | // This method will be called when Electron has finished
76 | // initialization and is ready to create browser windows.
77 | // Some APIs can only be used after this event occurs.
78 | app.on('ready', async () => {
79 | if (isDevelopment && !process.env.IS_TEST) {
80 | // Install Vue Devtools
81 | try {
82 | await installVueDevtools();
83 | } catch (e) {
84 | console.error('Vue Devtools failed to install:', e.toString());
85 | }
86 | }
87 | createWindow();
88 | });
89 |
90 | // Exit cleanly on request from parent process in development mode.
91 | if (isDevelopment) {
92 | if (process.platform === 'win32') {
93 | process.on('message', (data) => {
94 | if (data === 'graceful-exit') {
95 | app.quit();
96 | }
97 | });
98 | } else {
99 | process.on('SIGTERM', () => {
100 | app.quit();
101 | });
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/components/DropField.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
80 |
81 |
106 |
--------------------------------------------------------------------------------
/src/components/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
298 |
299 |
335 |
--------------------------------------------------------------------------------
/src/components/KeyPrompt.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ title }}
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
81 |
82 |
185 |
--------------------------------------------------------------------------------
/src/components/Toolbar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
15 |
20 |
21 |
22 |
23 |
57 |
58 |
104 |
--------------------------------------------------------------------------------
/src/lib/event-bus.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | export const EventBus = new Vue();
4 |
--------------------------------------------------------------------------------
/src/lib/markdown.js:
--------------------------------------------------------------------------------
1 | import MarkdownIt from 'markdown-it';
2 | import MarkdownItCheckbox from 'markdown-it-checkbox';
3 | import MarkdownItFootnote from 'markdown-it-footnote';
4 | import MarkdownItMultimdTable from 'markdown-it-multimd-table';
5 | import MarkdownItDeflist from 'markdown-it-deflist';
6 | import MarkdownItAnchor from 'markdown-it-anchor';
7 |
8 | import Prism from 'prismjs';
9 | import 'prismjs/plugins/remove-initial-line-feed/prism-remove-initial-line-feed.min.js';
10 |
11 | export default class Markdown {
12 | constructor() {
13 | this.markdownIt = new MarkdownIt({
14 | html: true,
15 | xhtmlOut: false,
16 | breaks: true,
17 | langPrefix: 'language-',
18 | linkify: true,
19 | typographer: false,
20 | highlight: function (str, lang) {
21 | const language = !lang || lang === 'html' ? 'markup' : lang;
22 | try {
23 | if (!Prism.languages[language]) {
24 | require(`prismjs/components/prism-${language}.min.js`);
25 | }
26 | if (Prism.languages[language]) {
27 | return Prism.highlight(str, Prism.languages[language]);
28 | }
29 | } catch (__) {
30 | // eslint-disable-next-line
31 | console.error(__);
32 | }
33 |
34 | return '';
35 | },
36 | });
37 |
38 | this.markdownIt
39 | .use(MarkdownItCheckbox, {
40 | idPrefix: 'checkbox_',
41 | })
42 | .use(MarkdownItFootnote)
43 | .use(MarkdownItAnchor)
44 | .use(MarkdownItMultimdTable, { enableRowspan: true })
45 | .use(MarkdownItDeflist);
46 | }
47 |
48 | render(markdown) {
49 | return this.markdownIt.render(markdown);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 | const { remote } = electron;
3 |
4 | export const isURL = (str) => {
5 | return /(?:^\w+:|^)\/\/(?:[^\s\.]+\.\S{2}|localhost[\:?\d]*)/.test(str);
6 | };
7 |
8 | export const openLinkExternal = () => {
9 | const currentWindow = remote.getCurrentWindow();
10 |
11 | document.addEventListener('click', (e) => {
12 | if (e.target.tagName !== 'A') return;
13 | const href = e.target.getAttribute('href');
14 |
15 | if (isURL(href)) {
16 | e.preventDefault();
17 | // get status
18 | const status = currentWindow.isAlwaysOnTop();
19 | // on top
20 | currentWindow.setAlwaysOnTop(true);
21 | // open link
22 | remote.shell.openExternal(href);
23 | // restore
24 | if (!status) {
25 | setTimeout(function () {
26 | currentWindow.setAlwaysOnTop(false);
27 | }, 1000);
28 | }
29 | }
30 | });
31 | };
32 |
33 | export const getLinkWithTitle = (event) => {
34 | const pastedString = event.clipboardData.getData('text/plain');
35 |
36 | // クリップボードの内容がURLの場合、`[title](url)`形式で返却する
37 | if (!isURL(pastedString)) return;
38 |
39 | event.preventDefault();
40 |
41 | return fetch(pastedString, {
42 | method: 'get',
43 | })
44 | .then((res) => res.text())
45 | .then((text) => new DOMParser().parseFromString(text, 'text/html'))
46 | .then((parsedBody) => `[${parsedBody.title}](${pastedString})`)
47 | .then((assembledString) => {
48 | // 組み立てた文字列を挿入
49 | return assembledString;
50 | })
51 | .catch((e) => {
52 | // 見つからない場合は貼り付けたテキストをそのまま挿入
53 | return pastedString;
54 | });
55 | };
56 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import App from './App.vue';
3 | import router from './router';
4 | import store from './store';
5 |
6 | Vue.config.productionTip = false;
7 |
8 | // vue-meta
9 | import './plugins/vue-meta/index';
10 |
11 | // codemirror
12 | import './plugins/codemirror/index';
13 |
14 | // fontawesome
15 | import './plugins/fontawesome/index';
16 |
17 | // Miikun Menu
18 | import appMenu from './service/app-menu';
19 |
20 | new Vue({
21 | router,
22 | store,
23 | mounted() {
24 | appMenu.setupAppMenu();
25 | appMenu.setupContextMenu();
26 | },
27 | render: (h) => h(App),
28 | }).$mount('#app');
29 |
--------------------------------------------------------------------------------
/src/modules/Encryptor.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import crypto from 'crypto';
4 | import { DecryptFailError } from './Errors';
5 |
6 | class Encryptor {
7 | constructor() {
8 | this.info = {
9 | cipher: {
10 | algorithm: 'aes-256-cbc',
11 | ivLength: 16,
12 | },
13 | hmac: {
14 | algorithm: 'sha256',
15 | key: 'sjIIhOQJWjlOl4J',
16 | },
17 | };
18 | }
19 |
20 | getInfo() {
21 | return this.info;
22 | }
23 |
24 | // return: Object { iv, enc content }
25 | encrypt(key, content) {
26 | const iv = crypto.randomBytes(this.info.cipher.ivLength);
27 | const cipher = crypto.createCipheriv(this.info.cipher.algorithm, this.hmac(key), iv);
28 | return {
29 | iv: iv,
30 | content: Buffer.concat([cipher.update(content), cipher.final()]),
31 | };
32 | }
33 |
34 | // return: Buffer
35 | decrypt(key, raw, iv) {
36 | const decipher = crypto.createDecipheriv(this.info.cipher.algorithm, this.hmac(key), Buffer.from(iv));
37 | try {
38 | return Buffer.concat([decipher.update(raw), decipher.final()]);
39 | } catch (err) {
40 | throw new DecryptFailError('Wrong Password');
41 | }
42 | }
43 |
44 | // return: Buffer
45 | hmac(data) {
46 | const hmac = crypto.createHmac(this.info.hmac.algorithm, this.info.hmac.key);
47 | hmac.update(data);
48 | return hmac.digest();
49 | }
50 |
51 | // return: Array[String]
52 | listCiphers() {
53 | return crypto.getCiphers();
54 | }
55 |
56 | listHashes() {
57 | return crypto.getHashes();
58 | }
59 | }
60 |
61 | export default new Encryptor();
62 |
--------------------------------------------------------------------------------
/src/modules/Errors.js:
--------------------------------------------------------------------------------
1 | const ERR_USER_CANCEL = -100;
2 | const ERR_ENCRYPT_FAIL = -101;
3 | const ERR_DECRYPT_FAIL = -102;
4 | const ERR_NULL_KEY = -103;
5 | const ERR_UNEXPECTED_STATE = -104;
6 |
7 | /* Workaround of ReferenceError: _construct is not defined */
8 | const _Error = Error;
9 |
10 | class UserCancelError extends _Error {
11 | constructor() {
12 | super('User Canceled.');
13 | this.name = 'UserCancelError';
14 | this.code = ERR_USER_CANCEL;
15 | }
16 | }
17 |
18 | class EncryptFailError extends _Error {
19 | constructor(reason) {
20 | super(reason);
21 | this.name = 'EncryptFailError';
22 | this.code = ERR_ENCRYPT_FAIL;
23 | }
24 | }
25 |
26 | class DecryptFailError extends _Error {
27 | constructor(reason) {
28 | super(reason);
29 | this.name = 'DecryptFailError';
30 | this.code = ERR_DECRYPT_FAIL;
31 | }
32 | }
33 |
34 | class NullKeyError extends _Error {
35 | constructor() {
36 | super('Cannot find key.');
37 | this.name = 'NullKeyError';
38 | this.code = ERR_NULL_KEY;
39 | }
40 | }
41 |
42 | class UnexpectedStateError extends _Error {
43 | constructor(name, value) {
44 | super('Unexpected state "' + name + '" with value "' + value + '"');
45 | this.name = 'UnexpectedStateError';
46 | this.code = ERR_UNEXPECTED_STATE;
47 | }
48 | }
49 |
50 | export {
51 | ERR_USER_CANCEL,
52 | UserCancelError,
53 | ERR_ENCRYPT_FAIL,
54 | EncryptFailError,
55 | ERR_DECRYPT_FAIL,
56 | DecryptFailError,
57 | ERR_NULL_KEY,
58 | NullKeyError,
59 | ERR_UNEXPECTED_STATE,
60 | UnexpectedStateError,
61 | };
62 |
--------------------------------------------------------------------------------
/src/modules/Filesystem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import fs from 'fs';
4 | import encryptor from './Encryptor';
5 | import { NullKeyError, DecryptFailError } from './Errors';
6 | import store from '../store';
7 |
8 | // This file looks looks
9 | // | Base info | HMAC | IV | Enc Content |
10 | // | 16 | 32 | 16 | .. |
11 |
12 | class Filesystem {
13 | constructor() {
14 | this.header = {
15 | length: 64,
16 | baseLength: 16,
17 | hmacLength: 32,
18 | ivLength: 16,
19 | };
20 | this.key = null;
21 | }
22 |
23 | // Return: Object contains header metadata
24 | getHeader() {
25 | return this.header;
26 | }
27 |
28 | // Return: Boolean
29 | shouldEncrypt(path) {
30 | return path.endsWith('.mii');
31 | }
32 |
33 | writeFile(path, content, cb, key = null) {
34 | if (this.shouldEncrypt(path)) {
35 | // Use the cached key instead of state, because when readFile fail, I
36 | // can keep the old key of current file.
37 | // It's hard and will make code looks ugly if implemnt in Editor.vue.
38 | key = key === null ? this.key : key;
39 | // Prevent null key, it should not happen
40 | // at this time.
41 | if (key === '' || key === null) {
42 | cb(new NullKeyError());
43 | return;
44 | }
45 | // Backup file at develop.
46 | if (process.env.NODE_ENV === 'development') {
47 | fs.writeFile(path + '.backup.md', content, 'utf8', cb);
48 | }
49 | content = this.encrypt(key, content);
50 | }
51 | fs.writeFile(path, content, 'utf8', cb);
52 | }
53 |
54 | readFile(path, cb) {
55 | let key = null;
56 | const isEncrypt = this.shouldEncrypt(path);
57 |
58 | if (isEncrypt) {
59 | key = store.state.Editor.crypt.key;
60 | // Prevent null key, it should not happen
61 | // at this time.
62 | if (key === '' || key === null) {
63 | cb(new NullKeyError());
64 | return;
65 | }
66 | }
67 | // Notice it's an async function, only return
68 | // the callback, not readFile
69 | fs.readFile(path, (err, content) => {
70 | if (err) {
71 | cb(err, null);
72 | return;
73 | }
74 | // Decrypt
75 | if (isEncrypt) {
76 | try {
77 | content = this.decrypt(key, content);
78 | } catch (err2) {
79 | cb(err2, null);
80 | return;
81 | }
82 | }
83 | this.updateKey(key);
84 | cb(null, content.toString('utf8'));
85 | });
86 | }
87 |
88 | encrypt(key, content) {
89 | // This return { iv, content }
90 | const encResult = encryptor.encrypt(key, content);
91 | const bufHmac = encryptor.hmac(content);
92 | // Pack base info, hmac of origin content, iv, enc content
93 | const bufFile = this.packHeader(encResult, bufHmac);
94 | return bufFile;
95 | }
96 |
97 | decrypt(key, content) {
98 | const fileStruct = this.unpackHeader(content);
99 | // eslint-disable-next-line no-useless-catch
100 | try {
101 | const decContent = encryptor.decrypt(key, fileStruct.encContent, fileStruct.iv);
102 | if (encryptor.hmac(decContent).equals(fileStruct.hmac)) {
103 | return decContent;
104 | } else {
105 | throw new DecryptFailError('Content mismatch.');
106 | }
107 | } catch (err) {
108 | throw err;
109 | }
110 | }
111 |
112 | // Return: Buffer
113 | packHeader(encResult, bufHmac) {
114 | const info = this.getHeader();
115 | const bufHeader = Buffer.alloc(info.length);
116 | // * Base info, add whatever you want :P
117 | bufHeader.write('Miikun@2018');
118 | // * Encrypt info
119 | bufHmac.copy(bufHeader, info.baseLength);
120 | // * IV
121 | encResult.iv.copy(bufHeader, info.baseLength + info.hmacLength);
122 | // this.dumpHeader(bufHeader)
123 | return Buffer.concat([bufHeader, encResult.content]);
124 | }
125 |
126 | unpackHeader(raw) {
127 | const info = this.getHeader();
128 | const base = raw.slice(0, info.baseLength);
129 | const hmac = raw.slice(info.baseLength, info.baseLength + info.hmacLength);
130 | const iv = raw.slice(info.baseLength + info.hmacLength, info.length);
131 | const encContent = raw.slice(info.length);
132 | return { base, hmac, iv, encContent };
133 | }
134 |
135 | dumpHeader(header) {
136 | const info = this.getHeader();
137 | const base = header.slice(0, info.baseLength);
138 | const hmac = header.slice(info.baseLength, info.baseLength + info.hmacLength);
139 | const iv = header.slice(info.baseLength + info.hmacLength, info.length);
140 | console.log('[Dump Header] Total bytes: ' + header.byteLength);
141 | console.log('[Dump Header] Base info: ' + base.toString('utf8'));
142 | console.log('[Dump Header] HMAC: ' + hmac.toString('utf8'));
143 | console.log('[Dump Header] IV: ' + iv.toString('utf8'));
144 | }
145 |
146 | updateKey(key) {
147 | this.key = key;
148 | }
149 | }
150 |
151 | export default new Filesystem();
152 |
--------------------------------------------------------------------------------
/src/modules/dialog.js:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 | const { remote } = electron;
3 | const { BrowserWindow, dialog } = remote;
4 |
5 | export const openDialog = (type, message) => {
6 | const focusedWindow = BrowserWindow.getFocusedWindow();
7 |
8 | return dialog.showMessageBoxSync(focusedWindow, {
9 | title: type,
10 | type: type,
11 | buttons: ['OK'],
12 | detail: message,
13 | });
14 | };
15 |
16 | export const showFileOpenDialog = () => {
17 | const focusedWindow = BrowserWindow.getFocusedWindow();
18 |
19 | return dialog.showOpenDialogSync(focusedWindow, {
20 | title: 'Open Dialog',
21 | filters: [
22 | {
23 | name: 'Documents',
24 | extensions: ['txt', 'md', 'mii'],
25 | },
26 | ],
27 | properties: ['openFile'],
28 | });
29 | };
30 |
31 | export const getSavePath = (extensions) => {
32 | const focusedWindow = BrowserWindow.getFocusedWindow();
33 |
34 | return dialog.showSaveDialogSync(focusedWindow, {
35 | title: 'Save Dialog',
36 | filters: extensions,
37 | });
38 | };
39 |
40 | export const getSelectedResult = ({ title, message, type, buttons, detail }) => {
41 | const focusedWindow = BrowserWindow.getFocusedWindow();
42 |
43 | return dialog.showMessageBoxSync(focusedWindow, {
44 | title,
45 | message,
46 | type,
47 | buttons,
48 | detail,
49 | });
50 | };
51 |
--------------------------------------------------------------------------------
/src/modules/editor.js:
--------------------------------------------------------------------------------
1 | import CodeMirror from 'codemirror';
2 | import store from '../store';
3 |
4 | export default class Editor {
5 | constructor(element) {
6 | const controlKey = process.platform === 'win32' ? 'Ctrl' : 'Cmd';
7 |
8 | this.option = {
9 | mode: {
10 | name: 'markdown',
11 | highlightFormatting: true,
12 | },
13 | theme: 'markdown',
14 | autofocus: true,
15 | autoCloseTags: true,
16 | showCursorWhenSelecting: true,
17 | inputStyle: 'textarea',
18 | lineNumbers: true,
19 | lineWrapping: true,
20 | foldGutter: true,
21 | tabSize: 2,
22 | indentUnit: 4,
23 | electricChars: true,
24 | styleActiveLine: true,
25 | matchBrackets: true,
26 | dragDrop: false,
27 | autoCloseBrackets: true,
28 | autoRefresh: true,
29 | extraKeys: {
30 | Enter: 'newlineAndIndentContinueMarkdownList',
31 | // **bold**
32 | [`${controlKey}-B`]: function (cm) {
33 | const s = cm.getSelection();
34 | const t = s.slice(0, 2) === '**' && s.slice(-2) === '**';
35 | cm.replaceSelection(t ? s.slice(2, -2) : '**' + s + '**', 'around');
36 | },
37 | // _italic_
38 | [`${controlKey}-I`]: function (cm) {
39 | const s = cm.getSelection();
40 | const t = s.slice(0, 1) === '_' && s.slice(-1) === '_';
41 | cm.replaceSelection(t ? s.slice(1, -1) : '_' + s + '_', 'around');
42 | },
43 | // `code`
44 | 'Shift-@': function (cm) {
45 | const s = cm.getSelection();
46 | const t = s.slice(0, 1) === '`' && s.slice(-1) === '`';
47 | cm.replaceSelection(t ? s.slice(1, -1) : '`' + s + '`', 'around');
48 | },
49 | },
50 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'],
51 | styleSelectedText: true,
52 | // highlightSelectionMatches: {
53 | // showToken: /\w/, annotateScrollbar: true
54 | // },
55 | };
56 |
57 | this.cm = CodeMirror.fromTextArea(element, this.option);
58 | }
59 |
60 | getCmInstance() {
61 | return this.cm;
62 | }
63 |
64 | initFilePath(path) {
65 | store.dispatch('initFilePath', path);
66 | }
67 |
68 | setValue(value) {
69 | this.cm.setValue(value);
70 | this.cm.save();
71 | }
72 |
73 | clean() {
74 | this.setValue('');
75 | this.initFilePath('');
76 | this.clearHistory();
77 | }
78 |
79 | isClean() {
80 | return this.cm.isClean();
81 | }
82 |
83 | isUnsaveFile() {
84 | return store.state.Editor.filePath;
85 | }
86 |
87 | updateHistory() {
88 | const { undo, redo } = this.cm.historySize();
89 | store.dispatch('setCanUndo', undo > 0);
90 | store.dispatch('setCanRedo', redo > 0);
91 | }
92 |
93 | clearHistory() {
94 | this.cm.markClean();
95 | this.cm.clearHistory();
96 | }
97 |
98 | insertTextToEditor(text, line, ch) {
99 | if (!text) return;
100 | this.cm.replaceRange(text, { line, ch }, { line, ch });
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/pages/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
15 |
--------------------------------------------------------------------------------
/src/plugins/codemirror/index.js:
--------------------------------------------------------------------------------
1 | import 'codemirror/mode/gfm/gfm.js';
2 | import 'codemirror/mode/markdown/markdown.js';
3 | import 'codemirror/mode/xml/xml.js';
4 | import 'codemirror/addon/edit/closetag.js';
5 | import 'codemirror/addon/edit/continuelist.js';
6 | import 'codemirror/addon/edit/closebrackets.js';
7 | import 'codemirror/addon/lint/lint.js';
8 | import 'codemirror/addon/mode/overlay.js';
9 | import 'codemirror/addon/fold/foldcode.js';
10 | import 'codemirror/addon/fold/foldgutter.js';
11 | import 'codemirror/addon/fold/brace-fold.js';
12 | import 'codemirror/addon/fold/xml-fold.js';
13 | import 'codemirror/addon/fold/markdown-fold.js';
14 | import 'codemirror/addon/fold/comment-fold.js';
15 | import 'codemirror/addon/selection/active-line.js';
16 |
--------------------------------------------------------------------------------
/src/plugins/fontawesome/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | import { library } from '@fortawesome/fontawesome-svg-core';
4 | import { faUndo, faRedo, faEye, faEyeSlash, faCog } from '@fortawesome/free-solid-svg-icons';
5 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
6 |
7 | library.add(faUndo, faRedo, faEye, faEyeSlash, faCog);
8 |
9 | Vue.component('font-awesome-icon', FontAwesomeIcon);
10 |
--------------------------------------------------------------------------------
/src/plugins/vue-meta/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import VueMeta from 'vue-meta';
3 |
4 | Vue.use(VueMeta, {
5 | refreshOnceOnNavigation: true,
6 | });
7 |
--------------------------------------------------------------------------------
/src/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Router from 'vue-router';
3 | import Main from '@/pages/Main.vue';
4 |
5 | Vue.use(Router);
6 |
7 | export default new Router({
8 | mode: 'history',
9 | base: process.env.BASE_URL,
10 | routes: [
11 | {
12 | path: '/',
13 | name: 'mii-main',
14 | component: Main,
15 | },
16 | {
17 | path: '*',
18 | redirect: '/',
19 | },
20 | ],
21 | });
22 |
--------------------------------------------------------------------------------
/src/service/app-menu-controller.js:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 | const { remote } = electron;
3 |
4 | import store from '../store';
5 | import AppMenu from '@/service/app-menu';
6 |
7 | import { EventBus } from '@/lib/event-bus';
8 |
9 | const AppMenuController = {
10 | toggleAlwaysOnTop() {
11 | const currentWindow = remote.getCurrentWindow();
12 | const isAlwaysOnTop = currentWindow.isAlwaysOnTop();
13 | currentWindow.setAlwaysOnTop(!isAlwaysOnTop);
14 | store.dispatch('updateAlwaysOnTop', !isAlwaysOnTop);
15 | },
16 | undo() {
17 | EventBus.$emit('undo');
18 | },
19 | redo() {
20 | EventBus.$emit('redo');
21 | },
22 | newFile() {
23 | EventBus.$emit('newFile');
24 | },
25 | openFile() {
26 | EventBus.$emit('openFile');
27 | },
28 | saveFile() {
29 | EventBus.$emit('saveFile');
30 | },
31 | saveAs() {
32 | EventBus.$emit('saveAs');
33 | },
34 | togglePreview() {
35 | AppMenu.checkedMenuItem('toggle_preview_panel', !this.isOpenPreview());
36 | store.dispatch('updateIsPreview', !this.isOpenPreview());
37 | },
38 | toggleToolbar() {
39 | AppMenu.checkedMenuItem('toggle_toolbar', !this.isOpenToolbar());
40 | store.dispatch('toggleToolbar');
41 | },
42 | isOpenPreview() {
43 | return store.state.Editor.isPreview;
44 | },
45 | isOpenToolbar() {
46 | return store.state.Editor.openToolbar;
47 | },
48 | };
49 |
50 | export default AppMenuController;
51 |
--------------------------------------------------------------------------------
/src/service/app-menu.js:
--------------------------------------------------------------------------------
1 | import electron from 'electron';
2 | const { remote } = electron;
3 | const { shell, Menu, MenuItem } = remote;
4 |
5 | import { name } from '../../package.json';
6 | const isDevelopment = process.env.NODE_ENV !== 'production';
7 |
8 | import store from '../store';
9 | import AppMenuController from '@/service/app-menu-controller';
10 |
11 | export default {
12 | menuInstance: null,
13 | appmMenuList: [
14 | {
15 | label: name,
16 | submenu: [{ role: 'quit' }],
17 | },
18 | {
19 | id: 'file',
20 | label: 'File',
21 | submenu: [
22 | {
23 | id: 'new',
24 | label: 'New',
25 | accelerator: 'CmdOrCtrl+N',
26 | click() {
27 | AppMenuController.newFile();
28 | },
29 | },
30 | {
31 | id: 'open',
32 | label: 'Open',
33 | accelerator: 'CmdOrCtrl+O',
34 | click() {
35 | AppMenuController.openFile();
36 | },
37 | },
38 | {
39 | id: 'save',
40 | label: 'Save',
41 | accelerator: 'CmdOrCtrl+S',
42 | click() {
43 | AppMenuController.saveFile();
44 | },
45 | },
46 | {
47 | id: 'save_as',
48 | label: 'Save as',
49 | accelerator: 'CmdOrCtrl+Shift+S',
50 | click() {
51 | AppMenuController.saveAs();
52 | },
53 | },
54 | ],
55 | },
56 | {
57 | id: 'edit',
58 | label: 'Edit',
59 | submenu: [
60 | { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
61 | { label: 'Redo', accelerator: 'CmdOrCtrl+Y', selector: 'redo:' },
62 | { type: 'separator' },
63 | { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' },
64 | { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' },
65 | { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' },
66 | { type: 'separator' },
67 | {
68 | label: 'Select All',
69 | accelerator: 'CmdOrCtrl+A',
70 | selector: 'selectAll:',
71 | },
72 | ],
73 | },
74 | {
75 | id: 'view',
76 | label: 'View',
77 | submenu: [
78 | {
79 | id: 'toggle_preview_panel',
80 | label: 'Toggle Preview Panel',
81 | type: 'checkbox',
82 | checked: store.state.Editor.isPreview,
83 | click() {
84 | AppMenuController.togglePreview();
85 | },
86 | },
87 | {
88 | id: 'toggle_toolbar',
89 | label: 'Toggle Toolbar',
90 | type: 'checkbox',
91 | checked: store.state.Editor.openToolbar,
92 | click() {
93 | AppMenuController.toggleToolbar();
94 | },
95 | },
96 | {
97 | label: 'Toggle Full Screen',
98 | role: 'togglefullscreen',
99 | },
100 | { type: 'separator' },
101 | {
102 | label: 'Zoom',
103 | submenu: [
104 | {
105 | label: 'Zoom In',
106 | role: 'zoomIn',
107 | },
108 | {
109 | label: 'Zoom Out',
110 | role: 'zoomOut',
111 | },
112 | { type: 'separator' },
113 | {
114 | label: 'Actual Size',
115 | role: 'resetZoom',
116 | },
117 | ],
118 | },
119 | { type: 'separator' },
120 | {
121 | label: 'Always on Top',
122 | accelerator: 'CmdOrCtrl+Shift+T',
123 | type: 'checkbox',
124 | checked: store.getters.isAlwaysOnTop,
125 | click: () => {
126 | AppMenuController.toggleAlwaysOnTop();
127 | },
128 | },
129 | ],
130 | },
131 | {
132 | id: 'help',
133 | label: 'Help',
134 | role: 'help',
135 | submenu: [
136 | {
137 | label: 'Website',
138 | click: () => {
139 | shell.openExternal('https://github.com/hiro0218/miikun/');
140 | },
141 | },
142 | ],
143 | },
144 | ...(isDevelopment
145 | ? [
146 | {
147 | id: 'develop',
148 | label: 'Development',
149 | submenu: [{ role: 'reload' }, { role: 'forcereload' }, { role: 'toggledevtools' }],
150 | },
151 | ]
152 | : []),
153 | ],
154 | setupAppMenu() {
155 | Menu.setApplicationMenu(null);
156 | this.menuInstance = Menu.buildFromTemplate(this.appmMenuList);
157 | Menu.setApplicationMenu(this.menuInstance);
158 |
159 | // Update based on store
160 | // set always on top
161 | if (store.getters.isAlwaysOnTop) {
162 | const currentWindow = remote.getCurrentWindow();
163 | currentWindow.setAlwaysOnTop(store.getters.isAlwaysOnTop);
164 | }
165 | },
166 | setupContextMenu() {
167 | window.addEventListener(
168 | 'contextmenu',
169 | (e) => {
170 | e.preventDefault();
171 |
172 | const menu = new Menu();
173 | const selectText = window.getSelection().toString().replace(/\n+/g, ' ');
174 |
175 | if (selectText) {
176 | menu.append(
177 | new MenuItem({
178 | label:
179 | 'Search Google for "' + (selectText.length > 20 ? selectText.substr(0, 17) + '...' : selectText) + '"',
180 | click: function () {
181 | shell.openExternal('https://www.google.com/search?q=' + encodeURIComponent(selectText));
182 | },
183 | }),
184 | );
185 | menu.append(
186 | new MenuItem({
187 | type: 'separator',
188 | }),
189 | );
190 | }
191 |
192 | menu.append(
193 | new MenuItem({
194 | label: 'Copy',
195 | accelerator: 'CmdOrCtrl+C',
196 | role: 'copy',
197 | }),
198 | );
199 |
200 | menu.popup({ window: remote.getCurrentWindow() });
201 | },
202 | false,
203 | );
204 | },
205 | getInstance() {
206 | return this.menuInstance;
207 | },
208 | getMenuItemById(menuId) {
209 | return this.menuInstance.getMenuItemById(menuId);
210 | },
211 | checkedMenuItem(menuId, state) {
212 | const appMenuItem = this.getMenuItemById(menuId);
213 | if (appMenuItem) appMenuItem.checked = state;
214 | },
215 | };
216 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 | import createPersistedState from 'vuex-persistedstate';
4 |
5 | import modules from './store/modules';
6 |
7 | Vue.use(Vuex);
8 |
9 | export default new Vuex.Store({
10 | modules,
11 | plugins: [
12 | createPersistedState({
13 | key: 'miikun',
14 | paths: ['App.isAlwaysOnTop', 'Editor.isPreview', 'Editor.openToolbar'],
15 | }),
16 | ],
17 | });
18 |
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Vuex from 'vuex';
3 |
4 | import modules from './modules';
5 |
6 | Vue.use(Vuex);
7 |
8 | export default new Vuex.Store({
9 | modules,
10 | strict: process.env.NODE_ENV !== 'production',
11 | });
12 |
--------------------------------------------------------------------------------
/src/store/modules/App.js:
--------------------------------------------------------------------------------
1 | const state = {
2 | isAlwaysOnTop: false,
3 | };
4 |
5 | const mutations = {
6 | SET_ALWAYS_ON_TOP(state, bool) {
7 | state.isAlwaysOnTop = bool;
8 | },
9 | };
10 |
11 | const actions = {
12 | updateAlwaysOnTop({ commit }, bool) {
13 | commit('SET_ALWAYS_ON_TOP', bool);
14 | },
15 | };
16 |
17 | const getters = {
18 | isAlwaysOnTop: (state) => state.isAlwaysOnTop,
19 | };
20 |
21 | export default {
22 | state,
23 | mutations,
24 | actions,
25 | getters,
26 | };
27 |
--------------------------------------------------------------------------------
/src/store/modules/Editor.js:
--------------------------------------------------------------------------------
1 | const state = {
2 | filePath: '',
3 | code: '',
4 | isPreview: false,
5 | openToolbar: true,
6 | canUndo: false,
7 | canRedo: false,
8 | crypt: {
9 | enable: false,
10 | key: null,
11 | op: {
12 | name: null,
13 | path: null,
14 | },
15 | },
16 | };
17 |
18 | const mutations = {
19 | RESET_FILEPATH(state) {
20 | state.filePath = '';
21 | },
22 | SET_FILEPATH(state, path) {
23 | state.filePath = path;
24 | },
25 | UPDATE_CODE(state, payload) {
26 | state.code = payload;
27 | },
28 | UPDATE_ISPREVIEW(state, bool) {
29 | state.isPreview = bool;
30 | },
31 | TOGGLE_TOOLBAR(state) {
32 | state.openToolbar = !state.openToolbar;
33 | },
34 | SET_CAN_UNDO(state, bool) {
35 | state.canUndo = bool;
36 | },
37 | SET_CAN_REDO(state, bool) {
38 | state.canRedo = bool;
39 | },
40 | SET_CRYPT_ENABLE(state, bool) {
41 | state.crypt.enable = bool;
42 | },
43 | SET_CRYPT_KEY(state, key) {
44 | state.crypt.key = key;
45 | },
46 | SET_CRYPT_OP(state, obj) {
47 | state.crypt.op.name = obj.name;
48 | state.crypt.op.path = obj.path;
49 | },
50 | };
51 |
52 | const actions = {
53 | updateFilePath({ commit }, path) {
54 | commit('SET_FILEPATH', path);
55 | },
56 | updateCode({ commit }, payload) {
57 | commit('UPDATE_CODE', payload);
58 | },
59 | updateIsPreview({ commit }, bool) {
60 | commit('UPDATE_ISPREVIEW', bool);
61 | },
62 | toggleToolbar({ commit }) {
63 | commit('TOGGLE_TOOLBAR');
64 | },
65 | setCanUndo({ commit }, bool) {
66 | commit('SET_CAN_UNDO', bool);
67 | },
68 | setCanRedo({ commit }, bool) {
69 | commit('SET_CAN_REDO', bool);
70 | },
71 | initFilePath({ commit }, path) {
72 | commit('SET_FILEPATH', path);
73 | commit('SET_CAN_UNDO', false);
74 | commit('SET_CAN_REDO', false);
75 | },
76 | setCryptEnable({ commit }, bool) {
77 | commit('SET_CRYPT_ENABLE', bool);
78 | },
79 | setCryptKey({ commit }, key) {
80 | commit('SET_CRYPT_KEY', key);
81 | },
82 | setCryptOP({ commit }, obj) {
83 | commit('SET_CRYPT_OP', obj);
84 | },
85 | };
86 |
87 | export default {
88 | state,
89 | mutations,
90 | actions,
91 | };
92 |
--------------------------------------------------------------------------------
/src/store/modules/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * The file enables `@/store/index.js` to import all vuex modules
3 | * in a one-shot manner. There should not be any reason to edit this file.
4 | */
5 |
6 | const files = require.context('.', false, /\.js$/);
7 | const modules = {};
8 |
9 | files.keys().forEach((key) => {
10 | if (key === './index.js') return;
11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default;
12 | });
13 |
14 | export default modules;
15 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | module.exports = {
4 | devServer: {
5 | port: 8888,
6 | disableHostCheck: true,
7 | },
8 | configureWebpack: {
9 | resolve: {
10 | alias: {
11 | '@': path.join(__dirname, './src'),
12 | },
13 | },
14 | },
15 | css: {
16 | loaderOptions: {
17 | scss: {
18 | data: '@import "./src/assets/style/Settings/index.scss";',
19 | options: {
20 | implementation: require('sass'),
21 | fiber: require('fibers'),
22 | },
23 | },
24 | },
25 | },
26 | pluginOptions: {
27 | electronBuilder: {
28 | externals: ['my-native-dep'],
29 | nodeModulesPath: ['../../node_modules', './node_modules'],
30 | },
31 | },
32 | productionSourceMap: false,
33 | transpileDependencies: ['vuetify'],
34 | };
35 |
--------------------------------------------------------------------------------