├── .eslintignore
├── dist
├── web
│ └── .gitkeep
└── electron
│ └── .gitkeep
├── static
├── .gitkeep
├── trayTemplate.png
├── trayTemplate@2x.png
└── css
│ └── hljs
│ ├── atom-one-dark.css
│ └── atom-one-light.css
├── .prettierrc
├── demo.gif
├── logo.png
├── preview.png
├── src
├── renderer
│ ├── event-bus.js
│ ├── assets
│ │ └── scss
│ │ │ ├── main.scss
│ │ │ ├── mixins.scss
│ │ │ ├── themes.scss
│ │ │ ├── vendor.scss
│ │ │ ├── base.scss
│ │ │ ├── variables.scss
│ │ │ └── reset.scss
│ ├── components
│ │ ├── uikit
│ │ │ ├── AppForm.vue
│ │ │ ├── AppIcon.vue
│ │ │ ├── AppMenuItem.vue
│ │ │ ├── AppCheckboxGroup.vue
│ │ │ ├── AppFormItem.vue
│ │ │ ├── index.js
│ │ │ ├── AppButton.vue
│ │ │ ├── AppSelect.vue
│ │ │ ├── AppAlert.vue
│ │ │ ├── AppMenu.vue
│ │ │ ├── AppCheckbox.vue
│ │ │ ├── AppInput.vue
│ │ │ ├── AppPopper.vue
│ │ │ └── AppInputTags.vue
│ │ ├── tray
│ │ │ ├── TraySnippetList.vue
│ │ │ └── TraySnippetListItem.vue
│ │ ├── preferences
│ │ │ ├── Interface.vue
│ │ │ ├── Assistant.vue
│ │ │ ├── Editor.vue
│ │ │ └── Storage.vue
│ │ ├── snippets
│ │ │ ├── SnippetTabsPane.vue
│ │ │ ├── SnippetTabs.vue
│ │ │ ├── ActionBar.vue
│ │ │ ├── SnippetTabsItem.vue
│ │ │ └── SnippetList.vue
│ │ ├── sidebar
│ │ │ ├── TheSidebar.vue
│ │ │ ├── TheTags.vue
│ │ │ ├── SidebarList.vue
│ │ │ ├── TheLibrary.vue
│ │ │ └── TheFolders.vue
│ │ └── editor
│ │ │ ├── languages.js
│ │ │ └── MarkdownPreview.vue
│ ├── util
│ │ └── helpers.js
│ ├── store
│ │ ├── index.js
│ │ └── modules
│ │ │ ├── preferences.js
│ │ │ ├── tags.js
│ │ │ ├── app.js
│ │ │ └── folders.js
│ ├── main.js
│ ├── router.js
│ ├── directives.js
│ ├── lib
│ │ └── ipcRenderer.js
│ ├── views
│ │ ├── Main.vue
│ │ ├── Preferences.vue
│ │ └── Tray.vue
│ ├── App.vue
│ └── datastore.js
├── main
│ ├── lib
│ │ ├── index.js
│ │ ├── dialog.js
│ │ ├── menu.js
│ │ ├── touch-bar.js
│ │ ├── update-check.js
│ │ └── analytics.js
│ ├── store
│ │ ├── index.js
│ │ └── module
│ │ │ ├── app.js
│ │ │ └── preferences.js
│ ├── util
│ │ └── platform.js
│ ├── index.dev.js
│ ├── main.js
│ ├── tray.js
│ └── index.js
└── index.ejs
├── .gitignore
├── .editorconfig
├── jsconfig.json
├── scripts
├── release.sh
└── verify-commit.js
├── appveyor.yml
├── .github
├── FUNDING.yml
├── ISSUE_TEMPLATE.md
├── PULL_REQUEST_TEMPLATE.md
├── COMMIT_CONVENTION.md
└── workflows
│ └── main.yml
├── .babelrc
├── .eslintrc.js
├── .vscode
└── tasks.json
├── .electron-vue
├── terserOptions.js
├── dev-client.js
├── webpack.main.config.js
├── build.js
├── webpack.web.config.js
├── dev-runner.js
└── webpack.renderer.config.js
├── README.md
└── package.json
/.eslintignore:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dist/web/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/static/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/dist/electron/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/massCode/HEAD/demo.gif
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/massCode/HEAD/logo.png
--------------------------------------------------------------------------------
/preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/massCode/HEAD/preview.png
--------------------------------------------------------------------------------
/src/renderer/event-bus.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 |
3 | export default new Vue()
4 |
--------------------------------------------------------------------------------
/static/trayTemplate.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/massCode/HEAD/static/trayTemplate.png
--------------------------------------------------------------------------------
/static/trayTemplate@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/antonreshetov/massCode/HEAD/static/trayTemplate@2x.png
--------------------------------------------------------------------------------
/src/main/lib/index.js:
--------------------------------------------------------------------------------
1 | import menu from './menu'
2 | import dialog from './dialog'
3 |
4 | export { menu, dialog }
5 |
--------------------------------------------------------------------------------
/src/main/lib/dialog.js:
--------------------------------------------------------------------------------
1 | import { remote } from 'electron'
2 |
3 | const { dialog } = remote
4 |
5 | export default dialog
6 |
--------------------------------------------------------------------------------
/src/renderer/assets/scss/main.scss:
--------------------------------------------------------------------------------
1 | @import './reset';
2 | @import './variables';
3 | @import './markdown';
4 | @import './base';
5 | @import './vendor';
6 | @import './themes';
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/electron/*
3 | dist/web/*
4 | build/*
5 | !build/icons
6 | node_modules/
7 | npm-debug.log
8 | npm-debug.log.*
9 | thumbs.db
10 | .env
11 | !.gitkeep
12 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/src/main/store/index.js:
--------------------------------------------------------------------------------
1 | import app from './module/app'
2 | import preferences from './module/preferences'
3 |
4 | const store = {
5 | app,
6 | preferences
7 | }
8 |
9 | export default store
10 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./src/renderer/*"],
6 | "@@/*": ["./src/main/*"]
7 | }
8 | },
9 | "exclude": ["node_modules", ".nuxt", "dist"]
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/util/platform.js:
--------------------------------------------------------------------------------
1 | export function convertName (platform) {
2 | if (platform === 'darwin') {
3 | return 'macOS'
4 | }
5 | if (platform === 'win32') {
6 | return 'Windows'
7 | }
8 | if (platform === 'linux') {
9 | return 'Linux'
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppForm.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/renderer/components/tray/TraySnippetList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main/lib/menu.js:
--------------------------------------------------------------------------------
1 | import { remote } from 'electron'
2 | const { Menu, MenuItem } = remote
3 |
4 | function popup (templates) {
5 | const menu = new Menu()
6 | templates.forEach(item => {
7 | menu.append(new MenuItem(item))
8 | })
9 | menu.popup(remote.getCurrentWindow())
10 |
11 | return menu
12 | }
13 |
14 | const menu = {
15 | popup
16 | }
17 |
18 | export default menu
19 |
--------------------------------------------------------------------------------
/src/renderer/util/helpers.js:
--------------------------------------------------------------------------------
1 | export function defaultLibraryQuery (query = {}, id) {
2 | if (id === 'trash') {
3 | query = { isDeleted: true }
4 | }
5 | if (id === 'favorites') {
6 | query = { isFavorites: true }
7 | }
8 | if (id === 'allSnippets') {
9 | query = {}
10 | }
11 | if (id === 'inBox') {
12 | query = { folderId: null }
13 | }
14 | return query
15 | }
16 |
--------------------------------------------------------------------------------
/scripts/release.sh:
--------------------------------------------------------------------------------
1 | # !/usr/bin/env sh
2 | set -e
3 | CURRENT=$(node -p "require('./package.json').version")
4 | echo "Current $CURRENT"
5 | echo "Enter release version: "
6 | read VERSION
7 |
8 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r
9 | echo
10 | if [[ $REPLY =~ ^[Yy]$ ]]
11 | then
12 | git add -A
13 | npm version $VERSION -m "build: release $VERSION"
14 | git push origin master
15 | git push origin --tags
16 | fi
--------------------------------------------------------------------------------
/src/renderer/store/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Vuex from 'vuex'
3 |
4 | import app from './modules/app'
5 | import folders from './modules/folders'
6 | import preferences from './modules/preferences'
7 | import snippets from './modules/snippets'
8 | import tags from './modules/tags'
9 |
10 | Vue.use(Vuex)
11 |
12 | export default new Vuex.Store({
13 | strict: true,
14 | modules: {
15 | app,
16 | folders,
17 | preferences,
18 | snippets,
19 | tags
20 | }
21 | })
22 |
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | version: 0.1.{build}
2 |
3 | branches:
4 | only:
5 | - master
6 |
7 | image: Visual Studio 2017
8 | platform:
9 | - x64
10 |
11 | cache:
12 | - node_modules
13 | - '%APPDATA%\npm-cache'
14 | - '%USERPROFILE%\.electron'
15 | - '%USERPROFILE%\AppData\Local\Yarn\cache'
16 |
17 | init:
18 | - git config --global core.autocrlf input
19 |
20 | install:
21 | - ps: Install-Product node 8 x64
22 | - git reset --hard HEAD
23 | - yarn
24 | - node --version
25 |
26 | build_script:
27 | - yarn build
28 |
29 | test: off
30 |
--------------------------------------------------------------------------------
/src/renderer/assets/scss/mixins.scss:
--------------------------------------------------------------------------------
1 | @mixin form-input-default() {
2 | color: var(--color-text);
3 | box-sizing: border-box;
4 | -webkit-appearance: none;
5 | outline: none;
6 | border: none;
7 | line-height: 32px;
8 | height: 32px;
9 | background-color: transparent;
10 | border: 1px solid var(--color-contrast-low);
11 | font-size: var(--text-md);
12 | transition: border-color 0.3s;
13 | &:focus {
14 | transition: border-color 0.3s;
15 | }
16 | &::-webkit-input-placeholder {
17 | color: var(--color-contrast-low);
18 | }
19 | }
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: #['antonreshetov']
4 | patreon: antonreshetov
5 | open_collective: masscode
6 | ko_fi: antonreshetov
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | otechie: # Replace with a single Otechie username
12 | custom: ['https://paypal.me/antonreshetov']
13 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "comments": false,
3 | "env": {
4 | "main": {
5 | "presets": [
6 | [
7 | "@babel/preset-env",
8 | {
9 | "targets": { "node": "12" }
10 | }
11 | ]
12 | ]
13 | },
14 | "renderer": {
15 | "presets": [["@babel/preset-env"]]
16 | },
17 | "web": {
18 | "presets": [["@babel/preset-env"]]
19 | }
20 | },
21 | "plugins": [
22 | ["@babel/plugin-transform-runtime",
23 | {
24 | "regenerator": true
25 | }
26 | ],
27 | "@babel/plugin-proposal-optional-chaining"
28 | ]
29 | }
30 |
--------------------------------------------------------------------------------
/src/renderer/assets/scss/themes.scss:
--------------------------------------------------------------------------------
1 | [data-theme='light'] {
2 | .snippet-list-item--selected {
3 | background-color: var(--color-contrast-lower);
4 | &.focus {
5 | background-color: var(--color-primary);
6 | .title,
7 | .meta {
8 | color: #fff;
9 | }
10 | }
11 | }
12 | }
13 | [data-theme='dark'] {
14 | .snippet-list-item--selected {
15 | background-color: var(--color-contrast-lower);
16 | &.focus {
17 | background-color: var(--color-primary);
18 | .title,
19 | .meta {
20 | color: #fff;
21 | }
22 | }
23 | .title,
24 | .meta {
25 | color: #fff;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/renderer/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import App from './App'
3 | import store from './store'
4 | import router from './router'
5 | import db from '@/datastore'
6 | import eventBus from '@/event-bus'
7 | import { clickOutside } from './directives'
8 | import UiKit from '@/components/uikit'
9 |
10 | if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
11 |
12 | Vue.config.productionTip = false
13 | Vue.prototype.$db = db
14 | Vue.prototype.$bus = eventBus
15 |
16 | Vue.directive('click-outside', clickOutside)
17 | Vue.use(UiKit)
18 |
19 | /* eslint-disable no-new */
20 | new Vue({
21 | store,
22 | router,
23 | render: h => h(App)
24 | }).$mount('#app')
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | ## Describe the bug
2 |
3 | A clear and concise description of what the bug is.
4 |
5 | ## To Reproduce
6 |
7 | Steps to reproduce the behavior:
8 |
9 | **Example:**
10 |
11 | - Go to '...'
12 | - Click on '....'
13 | - Scroll down to '....'
14 | - See error
15 |
16 | ## Expected behavior
17 |
18 | A clear and concise description of what you expected to happen.
19 |
20 | ## Screenshots
21 |
22 | If applicable, add screenshots to help explain your problem.
23 |
24 | ## Environment
25 |
26 | Version: [e.g. 1.0.0]
27 | OS version and name: [e.g. macOS High Sierra 10.13.6]
28 |
29 | ## Additional context
30 | Add any other context about the problem here.
31 |
--------------------------------------------------------------------------------
/src/renderer/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import Router from 'vue-router'
3 | import Preferences from './views/Preferences.vue'
4 | import Main from './views/Main.vue'
5 | import Tray from './views/Tray.vue'
6 |
7 | Vue.use(Router)
8 |
9 | export default new Router({
10 | routes: [
11 | {
12 | path: '/',
13 | name: 'main',
14 | component: Main
15 | },
16 | {
17 | path: '/preferences',
18 | name: 'preferences',
19 | component: Preferences
20 | },
21 | {
22 | path: '/tray',
23 | name: 'tray',
24 | component: Tray
25 | },
26 | {
27 | path: '*',
28 | redirect: '/'
29 | }
30 | ]
31 | })
32 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppMenuItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
38 |
39 |
43 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | node: true
6 | },
7 | parserOptions: {
8 | parser: 'babel-eslint'
9 | },
10 | extends: ['plugin:vue/recommended', '@vue/standard'],
11 | globals: {
12 | __static: true
13 | },
14 | rules: {
15 | 'vue/component-name-in-template-casing': [
16 | 'error',
17 | 'PascalCase',
18 | { registeredComponentsOnly: false }
19 | ],
20 | 'vue/max-attributes-per-line': [
21 | 'error',
22 | {
23 | singleline: 1,
24 | multiline: {
25 | max: 1,
26 | allowFirstLine: false
27 | }
28 | }
29 | ],
30 | 'arrow-parens': 0,
31 | 'no-console': 0,
32 | 'import/order': 0
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/renderer/components/preferences/Interface.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/src/main/store/module/app.js:
--------------------------------------------------------------------------------
1 | import Store from 'electron-store'
2 |
3 | const app = new Store({
4 | name: 'app',
5 | cwd: 'massCode',
6 |
7 | schema: {
8 | bounds: {
9 | default: null
10 | },
11 | sidebarWidth: {
12 | default: 180
13 | },
14 | snippetListWidth: {
15 | default: 220
16 | },
17 | selectedFolderId: {
18 | default: 'inBox'
19 | },
20 | selectedFolderIds: {
21 | default: null
22 | },
23 | selectedSnippetId: {
24 | default: null
25 | },
26 | snippetsSort: {
27 | default: 'updateAt'
28 | },
29 | install: {
30 | default: null
31 | },
32 | updateAvailable: {
33 | default: false
34 | },
35 | nextVersionNotify: {
36 | default: false
37 | }
38 | }
39 | })
40 |
41 | export default app
42 |
--------------------------------------------------------------------------------
/src/main/index.dev.js:
--------------------------------------------------------------------------------
1 | /**
2 | * This file is used specifically and only for development. It installs
3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to
4 | * modify this file, but it can be used to extend your development
5 | * environment.
6 | */
7 |
8 | /* eslint-disable */
9 |
10 | // Install `electron-debug` with `devtron`
11 | require('electron-debug')({ showDevTools: true })
12 |
13 | // Install `vue-devtools`
14 | require('electron').app.on('ready', () => {
15 | let installExtension = require('electron-devtools-installer')
16 | installExtension
17 | .default(installExtension.VUEJS_DEVTOOLS)
18 | .then(() => {})
19 | .catch(err => {
20 | console.log('Unable to install `vue-devtools`: \n', err)
21 | })
22 | })
23 |
24 | // Require `main` process to boot app
25 | require('./index')
26 |
--------------------------------------------------------------------------------
/src/main/lib/touch-bar.js:
--------------------------------------------------------------------------------
1 | import { TouchBar } from 'electron'
2 | import { mainWindow } from '../main'
3 |
4 | const { TouchBarButton, TouchBarSpacer } = TouchBar
5 |
6 | const newSnippet = new TouchBarButton({
7 | label: 'New Snippet',
8 | click: () => {
9 | mainWindow.webContents.send('menu:new-snippet')
10 | }
11 | })
12 |
13 | const newFolder = new TouchBarButton({
14 | label: 'New Folder',
15 | click: () => {
16 | mainWindow.webContents.send('menu:new-folder')
17 | }
18 | })
19 |
20 | const favorites = new TouchBarButton({
21 | label: 'Favorites',
22 | click: () => {
23 | mainWindow.webContents.send('menu:favorites')
24 | }
25 | })
26 |
27 | export default new TouchBar({
28 | items: [
29 | newSnippet,
30 | newFolder,
31 | new TouchBarSpacer({ size: 'small' }),
32 | favorites
33 | ]
34 | })
35 |
--------------------------------------------------------------------------------
/src/main/lib/update-check.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import { mainWindow } from '../main'
3 |
4 | const pkg = require('../../../package.json')
5 | const isDev = process.env.NODE_ENV === 'development'
6 |
7 | function checkForUpdatesAndNotify () {
8 | if (isDev) return
9 |
10 | async function check () {
11 | const currentVersion = pkg.version
12 | const res = await axios.get(
13 | 'https://github.com/antonreshetov/masscode/releases/latest'
14 | )
15 |
16 | if (res) {
17 | const latest = res.request.socket._httpMessage.path
18 | .split('/')
19 | .pop()
20 | .substring(1)
21 | if (latest !== currentVersion) {
22 | mainWindow.webContents.send('update-available')
23 | }
24 | }
25 | }
26 |
27 | check()
28 | setInterval(check, 1000 * 60 * 15)
29 | }
30 |
31 | export { checkForUpdatesAndNotify }
32 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppCheckboxGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
50 |
--------------------------------------------------------------------------------
/src/renderer/components/snippets/SnippetTabsPane.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
12 |
13 |
41 |
42 |
51 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppFormItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
37 |
38 |
53 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/index.js:
--------------------------------------------------------------------------------
1 | import AppButton from './AppButton.vue'
2 | import AppCheckbox from './AppCheckbox.vue'
3 | import AppCheckboxGroup from './AppCheckboxGroup.vue'
4 | import AppForm from './AppForm.vue'
5 | import AppFormItem from './AppFormItem.vue'
6 | import AppIcon from './AppIcon.vue'
7 | import AppInput from './AppInput.vue'
8 | import AppMenu from './AppMenu.vue'
9 | import AppMenuItem from './AppMenuItem.vue'
10 | import AppSelect from './AppSelect.vue'
11 | import AppInputTags from './AppInputTags.vue'
12 | import AppPopper from './AppPopper.vue'
13 |
14 | const components = [
15 | AppButton,
16 | AppCheckbox,
17 | AppCheckboxGroup,
18 | AppForm,
19 | AppFormItem,
20 | AppIcon,
21 | AppInput,
22 | AppMenu,
23 | AppMenuItem,
24 | AppSelect,
25 | AppInputTags,
26 | AppPopper
27 | ]
28 |
29 | export default {
30 | install (Vue, options = {}) {
31 | components.forEach(component => {
32 | Vue.component(component.name, component)
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/renderer/directives.js:
--------------------------------------------------------------------------------
1 | export const clickOutside = {
2 | bind (el, binding, vNode) {
3 | if (typeof binding.value !== 'function') {
4 | const compName = vNode.context.name
5 | let warn = `[Vue-click-outside:] provided expression '${binding.expression}' is not a function, but has to be`
6 | if (compName) {
7 | warn += `Found in component '${compName}'`
8 | }
9 |
10 | console.warn(warn)
11 | }
12 | // Define Handler and cache it on the element
13 | const bubble = binding.modifiers.bubble
14 | const handler = e => {
15 | if (bubble || (!el.contains(e.target) && el !== e.target)) {
16 | binding.value(e)
17 | }
18 | }
19 | el.__vueClickOutside__ = handler
20 |
21 | // add Event Listeners
22 | document.addEventListener('click', handler)
23 | },
24 |
25 | unbind (el, binding) {
26 | // Remove Event Listeners
27 | document.removeEventListener('click', el.__vueClickOutside__)
28 | el.__vueClickOutside__ = null
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // See https://go.microsoft.com/fwlink/?LinkId=733558
3 | // for the documentation about the tasks.json format
4 | "version": "2.0.0",
5 | "tasks": [
6 | {
7 | "label": "Eslint current file",
8 | "type": "shell",
9 | "command": "npx eslint --fix ${file}",
10 | "problemMatcher": [
11 | "$eslint-stylish"
12 | ],
13 | "presentation": {
14 | "reveal": "never"
15 | }
16 | },
17 | {
18 | "label": "Prettier current file",
19 | "type": "shell",
20 | "command": "node ./node_modules/prettier/bin-prettier.js --write ${file}",
21 | "problemMatcher": [
22 | "$eslint-stylish"
23 | ]
24 | "presentation": {
25 | "reveal": "never"
26 | }
27 | },
28 | {
29 | "label": "Lint",
30 | "dependsOn": [
31 | "Prettier current file",
32 | "Eslint current file"
33 | ],
34 | "problemMatcher": [
35 | "$eslint-stylish"
36 | ],
37 | "presentation": {
38 | "reveal": "never"
39 | }
40 | }
41 | ]
42 | }
--------------------------------------------------------------------------------
/scripts/verify-commit.js:
--------------------------------------------------------------------------------
1 | /* eslint import/no-extraneous-dependencies: ['error', {'devDependencies': true}] */
2 | const chalk = require('chalk')
3 |
4 | const msgPath = process.env.HUSKY_GIT_PARAMS
5 | const msg = require('fs')
6 | .readFileSync(msgPath, 'utf-8')
7 | .trim()
8 |
9 | const commitRE = /^(revert: )?(feat|fix|polish|docs|style|refactor|perf|test|workflow|ci|chore|types|build)(\(.+\))?: .{1,50}/
10 |
11 | if (!commitRE.test(msg)) {
12 | console.log()
13 | /* eslint prefer-template: ['off'] */
14 | console.error(
15 | ` ${chalk.bgRed.white(' ERROR ')} ${chalk.red(
16 | 'invalid commit message format.'
17 | )}\n\n` +
18 | chalk.red(' Proper commit message format is required. Examples:\n\n') +
19 | ` ${chalk.green("feat: add 'comments' option")}\n` +
20 | ` ${chalk.green(
21 | 'fix(input): handle events on blur (close #28)'
22 | )}\n\n` +
23 | chalk.red(' See ./.github/COMMIT_CONVENTION.md for more details.\n')
24 | )
25 | process.exit(1)
26 | } else {
27 | console.log(chalk.green('verify commit message success'))
28 | }
29 |
--------------------------------------------------------------------------------
/.github/PULL_REQUEST_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | Thank you for your contribution to the massCode repo.
2 | Before submitting this PR, please make sure:
3 |
4 | - [ ] Your code builds clean without any errors or warnings
5 | - [ ] Your commits follow the [сonvention](https://github.com/antonreshetov/massCode/blob/master/.github/COMMIT_CONVENTION.md)
6 |
7 | **What kind of change does this PR introduce?** (check at least one)
8 |
9 | - [ ] Bugfix
10 | - [ ] Feature
11 | - [ ] Refactor
12 | - [ ] Build-related changes
13 | - [ ] Other, please describe:
14 |
15 | **Does this PR introduce a breaking change?** (check one)
16 |
17 | - [ ] Yes
18 | - [ ] No
19 |
20 | **The PR fulfills these requirements:**
21 |
22 | - [ ] It's submitted to the `master` branch for v1.x
23 | - [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
24 |
25 | If adding a **new feature**, the PR's description includes:
26 |
27 | - [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
27 |
28 |
54 |
--------------------------------------------------------------------------------
/.electron-vue/terserOptions.js:
--------------------------------------------------------------------------------
1 | module.exports = options => ({
2 | terserOptions: {
3 | compress: {
4 | // turn off flags with small gains to speed up minification
5 | arrows: false,
6 | collapse_vars: false, // 0.3kb
7 | comparisons: false,
8 | computed_props: false,
9 | hoist_funs: false,
10 | hoist_props: false,
11 | hoist_vars: false,
12 | inline: false,
13 | loops: false,
14 | negate_iife: false,
15 | properties: false,
16 | reduce_funcs: false,
17 | reduce_vars: false,
18 | switches: false,
19 | toplevel: false,
20 | typeofs: false,
21 |
22 | // a few flags with noticable gains/speed ratio
23 | // numbers based on out of the box vendor bundle
24 | booleans: true, // 0.7kb
25 | if_return: true, // 0.4kb
26 | sequences: true, // 0.7kb
27 | unused: true, // 2.3kb
28 |
29 | // required features to drop conditional branches
30 | conditionals: true,
31 | dead_code: true,
32 | evaluate: true
33 | },
34 | mangle: {
35 | safari10: true
36 | }
37 | },
38 | sourceMap: options ? options.sourceMap : false,
39 | cache: true,
40 | parallel: options ? options.parallel : true
41 | })
42 |
--------------------------------------------------------------------------------
/src/renderer/assets/scss/vendor.scss:
--------------------------------------------------------------------------------
1 | @import '~perfect-scrollbar/css/perfect-scrollbar.css';
2 |
3 | .ps {
4 | $r: &;
5 | &__rail-y {
6 | background-color: rgba(121, 121, 121, .8);
7 | width: 5px;
8 | &:hover {
9 | #{$r}__thumb-y {
10 | width: 5px;
11 | right: 0;
12 | }
13 | }
14 | }
15 | &__rail-x {
16 | background-color: rgba(121, 121, 121, .5);
17 | width: 4px;
18 | &:hover {
19 | height: 4px;
20 | background-color: transparent !important;
21 | opacity: 0.4 !important;
22 | #{$r}__thumb-x {
23 | height: 4px;
24 | right: 0;
25 | }
26 | }
27 | }
28 | &__thumb-y {
29 | border-radius: 0;
30 | background-color: rgba(121, 121, 121, .8);
31 | width: 5px;
32 | right: 0;
33 | }
34 | &__thumb-x {
35 | border-radius: 0;
36 | background-color: rgba(121, 121, 121, .8);
37 | height: 4px;
38 | right: 0;
39 | }
40 | }
41 |
42 |
43 | .draggable-placeholder-inner{
44 | border: 1px dashed var(--color-contrast-medium);
45 | box-sizing: border-box;
46 | border-radius: 4px;
47 | text-align: center;
48 | padding: 0;
49 | display: flex;
50 | align-items: center;
51 | position: relative;
52 | margin-right: 1px;
53 |
54 | }
55 |
56 | .draggable-placeholder{
57 | }
--------------------------------------------------------------------------------
/src/main/store/module/preferences.js:
--------------------------------------------------------------------------------
1 | import Store from 'electron-store'
2 | import { homedir, platform } from 'os'
3 |
4 | const isWin = platform() === 'win32'
5 |
6 | const defaultPath = isWin ? homedir() + '\\massCode' : homedir() + '/massCode'
7 | const backupPath = isWin ? `${defaultPath}\\backups` : `${defaultPath}/backups`
8 |
9 | const preferences = new Store({
10 | name: 'preferences',
11 | cwd: 'massCode',
12 |
13 | schema: {
14 | storagePath: {
15 | default: defaultPath
16 | },
17 | backupPath: {
18 | default: backupPath
19 | },
20 | theme: {
21 | default: 'dark'
22 | },
23 | assistant: {
24 | default: true
25 | },
26 | assistantShortcut: {
27 | default: 'Option+S'
28 | },
29 | renderWhitespace: {
30 | default: 'none'
31 | },
32 | wordWrap: {
33 | default: 'off'
34 | },
35 | tabSize: {
36 | default: 2
37 | },
38 | insertSpaces: {
39 | default: true
40 | },
41 | prettierSemi: {
42 | default: false
43 | },
44 | prettierQuotes: {
45 | default: true
46 | },
47 | allowAnalytics: {
48 | default: true
49 | }
50 | }
51 | })
52 |
53 | if (process.platform !== 'darwin') {
54 | preferences.set('assistant', false)
55 | }
56 |
57 | export default preferences
58 |
--------------------------------------------------------------------------------
/src/renderer/components/sidebar/TheSidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
11 |
45 |
46 |
62 |
--------------------------------------------------------------------------------
/.electron-vue/dev-client.js:
--------------------------------------------------------------------------------
1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
2 |
3 | hotClient.subscribe(event => {
4 | /**
5 | * Reload browser when HTMLWebpackPlugin emits a new index.html
6 | *
7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved.
8 | * https://github.com/SimulatedGREG/electron-vue/issues/437
9 | * https://github.com/jantimon/html-webpack-plugin/issues/680
10 | */
11 | // if (event.action === 'reload') {
12 | // window.location.reload()
13 | // }
14 |
15 | /**
16 | * Notify `mainWindow` when `main` process is compiling,
17 | * giving notice for an expected reload of the `electron` process
18 | */
19 | if (event.action === 'compiling') {
20 | document.body.innerHTML += `
21 |
34 |
35 |
36 | Compiling Main Process...
37 |
38 | `
39 | }
40 | })
41 |
--------------------------------------------------------------------------------
/src/main/lib/analytics.js:
--------------------------------------------------------------------------------
1 | import ua from 'universal-analytics'
2 | import store from '../store'
3 | import { convertName } from '../util/platform'
4 |
5 | const pkg = require('../../../package.json')
6 |
7 | const analytics = ua('UA-56182454-13')
8 | const isDev = process.env.NODE_ENV === 'development'
9 |
10 | function track (path) {
11 | if (isDev) return
12 |
13 | const isAllowAnalytics = store.preferences.get('allowAnalytics')
14 |
15 | if (isAllowAnalytics) {
16 | const version = pkg.version
17 | analytics.pageview(`${version}/${path}`).send()
18 | }
19 | }
20 |
21 | function trackEvent (category, action, label, value) {
22 | if (isDev) return
23 |
24 | const isAllowAnalytics = store.preferences.get('allowAnalytics')
25 | const version = pkg.version
26 |
27 | if (isAllowAnalytics) {
28 | analytics.event(`${version}-${category}`, action, label, value).send()
29 | }
30 | }
31 |
32 | function initAnalytics () {
33 | if (isDev) return
34 |
35 | if (store.preferences.get('allowAnalytics') === undefined) {
36 | store.preferences.set('allowAnalytics', true)
37 | }
38 |
39 | const version = pkg.version
40 | const installedVersion = store.preferences.get('install')
41 |
42 | if (installedVersion !== version) {
43 | store.preferences.set('install', version)
44 | const os = convertName(process.platform)
45 | track(`${os}/install`)
46 | }
47 | }
48 |
49 | export { track, trackEvent, initAnalytics }
50 |
--------------------------------------------------------------------------------
/src/renderer/assets/scss/base.scss:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: var(--font-primary);
3 | background-color: var(--color-bg);
4 | }
5 |
6 | #app {
7 | color: var(--color-text);
8 | font-size: var(--text-md);
9 | }
10 |
11 | h1,
12 | h2,
13 | h3,
14 | h4,
15 | h5,
16 | h6 {
17 | font-weight: 600;
18 | }
19 | h1 {
20 | font-size: var(--text-xxxl);
21 | margin: var(--spacing-xl) 0;
22 | }
23 | h2 {
24 | font-size: var(--text-xxl);
25 | margin: var(--spacing-xl) 0;
26 | }
27 | h3 {
28 | font-size: var(--text-xl);
29 | margin: var(--spacing-xl) 0;
30 | }
31 | h3 {
32 | font-size: var(--text-lg);
33 | margin: var(--spacing-lg) 0;
34 | }
35 | h4 {
36 | font-size: var(--text-md);
37 | margin: var(--spacing-md) 0;
38 | }
39 | h5 {
40 | font-size: var(--text-sm);
41 | margin: var(--spacing-xs) 0;
42 | }
43 | h6 {
44 | font-size: var(--text-xs);
45 | text-transform: uppercase;
46 | margin: var(--spacing-xs) 0;
47 | font-weight: 700;
48 | color: var(--color-contrast-medium);
49 | }
50 |
51 | .gutter-line {
52 | position: absolute;
53 | top: 0;
54 | right: 0;
55 | width: 1px;
56 | height: 100vh;
57 | background-color: var(--color-border);
58 | cursor: col-resize;
59 | &::after {
60 | content: '';
61 | display: block;
62 | height: 100%;
63 | width: 8px;
64 | position: absolute;
65 | left: -3px;
66 | z-index: 10;
67 | }
68 | }
69 |
70 | .desc {
71 | font-size: var(--text-sm);
72 | color: var(--color-contrast-medium);
73 | margin-top: var(--spacing-xs);
74 | }
--------------------------------------------------------------------------------
/src/renderer/lib/ipcRenderer.js:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron'
2 | import store from '@/store'
3 | import router from '@/router'
4 | import eventBus from '@/event-bus'
5 | import { track } from '@@/lib/analytics'
6 |
7 | ipcRenderer.on('menu:new-snippet', () => {
8 | const folderId = store.getters['folders/selectedId']
9 | store.dispatch('snippets/addSnippet', { folderId })
10 | track('snippets/new')
11 | })
12 |
13 | ipcRenderer.on('menu:new-fragment', () => {
14 | const snippetId = store.getters['snippets/selectedId']
15 | if (snippetId) {
16 | eventBus.$emit('snippet:new-fragment')
17 | }
18 | track('snippets/new-fragment')
19 | })
20 |
21 | ipcRenderer.on('menu:new-folder', () => {
22 | store.dispatch('folders/addFolder')
23 | track('folders/new')
24 | })
25 |
26 | ipcRenderer.on('menu:preferences', () => {
27 | router.push('/preferences')
28 | track('view/preferences')
29 | })
30 |
31 | ipcRenderer.on('menu:find-snippets', () => {
32 | eventBus.$emit('menu:find-snippets')
33 | track('snippets/search')
34 | })
35 |
36 | ipcRenderer.on('menu:copy-snippet', () => {
37 | eventBus.$emit('menu:copy-snippet')
38 | track('snippets/copied')
39 | })
40 |
41 | ipcRenderer.on('menu:format-snippet', () => {
42 | eventBus.$emit('menu:format-snippet')
43 | })
44 |
45 | ipcRenderer.on('menu:markdown-preview', (e, value) => {
46 | store.commit('app/SET_MARKDOWN_PREVIEW', value)
47 | })
48 |
49 | ipcRenderer.on('menu:favorites', () => {
50 | store.dispatch('folders/setSelectedFolder', 'favorites')
51 | })
52 |
53 | ipcRenderer.on('update-available', () => {
54 | store.commit('app/SET_UPDATE_AVAILABLE', true)
55 | })
56 |
--------------------------------------------------------------------------------
/src/main/main.js:
--------------------------------------------------------------------------------
1 | import { BrowserWindow } from 'electron'
2 | import store from './store'
3 |
4 | const isDev = process.env.NODE_ENV === 'development'
5 |
6 | let mainWindow
7 | const winURL =
8 | process.env.NODE_ENV === 'development'
9 | ? 'http://localhost:9080'
10 | : `file://${__dirname}/index.html`
11 |
12 | function createMainWindow () {
13 | const bounds = {
14 | height: 563,
15 | width: 1000,
16 | ...store.app.get('bounds')
17 | }
18 |
19 | const backgroundColor =
20 | store.preferences.get('theme') === 'dark' ? '#333' : '#fff'
21 |
22 | mainWindow = new BrowserWindow({
23 | title: 'massCode',
24 | useContentSize: true,
25 | titleBarStyle: 'hidden',
26 | // Убираем хайлайт вокруг приложения на Mac
27 | transparent: process.platform === 'darwin',
28 | backgroundColor,
29 | webPreferences: {
30 | nodeIntegration: true
31 | }
32 | })
33 |
34 | mainWindow.setBounds(bounds)
35 | mainWindow.loadURL(winURL)
36 |
37 | if (isDev) {
38 | mainWindow.webContents.openDevTools({ mode: 'detach' })
39 | }
40 |
41 | if (process.platform === 'darwin') {
42 | mainWindow.on('close', e => {
43 | e.preventDefault()
44 |
45 | if (mainWindow.isFullScreen()) {
46 | mainWindow.once('leave-full-screen', () => {
47 | mainWindow.hide()
48 | })
49 | mainWindow.setFullScreen(false)
50 | } else {
51 | mainWindow.hide()
52 | }
53 | store.app.set('bounds', mainWindow.getBounds())
54 | })
55 | }
56 |
57 | mainWindow.on('closed', e => {
58 | mainWindow = null
59 | })
60 | }
61 |
62 | export { createMainWindow, mainWindow }
63 |
--------------------------------------------------------------------------------
/static/css/hljs/atom-one-dark.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Atom One Dark by Daniel Gamage
4 | Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
5 |
6 | base: #282c34
7 | mono-1: #abb2bf
8 | mono-2: #818896
9 | mono-3: #5c6370
10 | hue-1: #56b6c2
11 | hue-2: #61aeee
12 | hue-3: #c678dd
13 | hue-4: #98c379
14 | hue-5: #e06c75
15 | hue-5-2: #be5046
16 | hue-6: #d19a66
17 | hue-6-2: #e6c07b
18 |
19 | */
20 |
21 | .hljs {
22 | display: block;
23 | overflow-x: auto;
24 | padding: 0.5em;
25 | color: #abb2bf;
26 | background: #282c34;
27 | }
28 |
29 | .hljs-comment,
30 | .hljs-quote {
31 | color: #5c6370;
32 | font-style: italic;
33 | }
34 |
35 | .hljs-doctag,
36 | .hljs-keyword,
37 | .hljs-formula {
38 | color: #c678dd;
39 | }
40 |
41 | .hljs-section,
42 | .hljs-name,
43 | .hljs-selector-tag,
44 | .hljs-deletion,
45 | .hljs-subst {
46 | color: #e06c75;
47 | }
48 |
49 | .hljs-literal {
50 | color: #56b6c2;
51 | }
52 |
53 | .hljs-string,
54 | .hljs-regexp,
55 | .hljs-addition,
56 | .hljs-attribute,
57 | .hljs-meta-string {
58 | color: #98c379;
59 | }
60 |
61 | .hljs-built_in,
62 | .hljs-class .hljs-title {
63 | color: #e6c07b;
64 | }
65 |
66 | .hljs-attr,
67 | .hljs-variable,
68 | .hljs-template-variable,
69 | .hljs-type,
70 | .hljs-selector-class,
71 | .hljs-selector-attr,
72 | .hljs-selector-pseudo,
73 | .hljs-number {
74 | color: #d19a66;
75 | }
76 |
77 | .hljs-symbol,
78 | .hljs-bullet,
79 | .hljs-link,
80 | .hljs-meta,
81 | .hljs-selector-id,
82 | .hljs-title {
83 | color: #61aeee;
84 | }
85 |
86 | .hljs-emphasis {
87 | font-style: italic;
88 | }
89 |
90 | .hljs-strong {
91 | font-weight: bold;
92 | }
93 |
94 | .hljs-link {
95 | text-decoration: underline;
96 | }
97 |
--------------------------------------------------------------------------------
/static/css/hljs/atom-one-light.css:
--------------------------------------------------------------------------------
1 | /*
2 |
3 | Atom One Light by Daniel Gamage
4 | Original One Light Syntax theme from https://github.com/atom/one-light-syntax
5 |
6 | base: #fafafa
7 | mono-1: #383a42
8 | mono-2: #686b77
9 | mono-3: #a0a1a7
10 | hue-1: #0184bb
11 | hue-2: #4078f2
12 | hue-3: #a626a4
13 | hue-4: #50a14f
14 | hue-5: #e45649
15 | hue-5-2: #c91243
16 | hue-6: #986801
17 | hue-6-2: #c18401
18 |
19 | */
20 |
21 | .hljs {
22 | display: block;
23 | overflow-x: auto;
24 | padding: 0.5em;
25 | color: #383a42;
26 | background: #fafafa;
27 | }
28 |
29 | .hljs-comment,
30 | .hljs-quote {
31 | color: #a0a1a7;
32 | font-style: italic;
33 | }
34 |
35 | .hljs-doctag,
36 | .hljs-keyword,
37 | .hljs-formula {
38 | color: #a626a4;
39 | }
40 |
41 | .hljs-section,
42 | .hljs-name,
43 | .hljs-selector-tag,
44 | .hljs-deletion,
45 | .hljs-subst {
46 | color: #e45649;
47 | }
48 |
49 | .hljs-literal {
50 | color: #0184bb;
51 | }
52 |
53 | .hljs-string,
54 | .hljs-regexp,
55 | .hljs-addition,
56 | .hljs-attribute,
57 | .hljs-meta-string {
58 | color: #50a14f;
59 | }
60 |
61 | .hljs-built_in,
62 | .hljs-class .hljs-title {
63 | color: #c18401;
64 | }
65 |
66 | .hljs-attr,
67 | .hljs-variable,
68 | .hljs-template-variable,
69 | .hljs-type,
70 | .hljs-selector-class,
71 | .hljs-selector-attr,
72 | .hljs-selector-pseudo,
73 | .hljs-number {
74 | color: #986801;
75 | }
76 |
77 | .hljs-symbol,
78 | .hljs-bullet,
79 | .hljs-link,
80 | .hljs-meta,
81 | .hljs-selector-id,
82 | .hljs-title {
83 | color: #4078f2;
84 | }
85 |
86 | .hljs-emphasis {
87 | font-style: italic;
88 | }
89 |
90 | .hljs-strong {
91 | font-weight: bold;
92 | }
93 |
94 | .hljs-link {
95 | text-decoration: underline;
96 | }
97 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
15 | {{ i.label }}
16 |
17 |
18 |
22 |
23 |
24 |
25 |
52 |
53 |
81 |
--------------------------------------------------------------------------------
/src/renderer/components/sidebar/TheTags.vue:
--------------------------------------------------------------------------------
1 |
2 |
19 |
20 |
21 |
56 |
57 |
86 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/preferences.js:
--------------------------------------------------------------------------------
1 | import electronStore from '@@/store'
2 |
3 | export default {
4 | namespaced: true,
5 | state: {
6 | renderWhitespace: electronStore.preferences.get('renderWhitespace'),
7 | wordWrap: electronStore.preferences.get('wordWrap'),
8 | tabSize: electronStore.preferences.get('tabSize'),
9 | insertSpaces: electronStore.preferences.get('insertSpaces'),
10 | prettierSemi: electronStore.preferences.get('prettierSemi'),
11 | prettierQuotes: electronStore.preferences.get('prettierQuotes')
12 | },
13 | getters: {},
14 | mutations: {
15 | SET_RENDER_WHITESPACE (state, type) {
16 | state.renderWhitespace = type
17 | },
18 | SET_WORD_WRAP (state, type) {
19 | state.wordWrap = type
20 | },
21 | SET_TAB_SIZE (state, size) {
22 | state.tabSize = size
23 | },
24 | SET_INSERT_SPACES (state, bool) {
25 | state.insertSpaces = bool
26 | },
27 | SET_PRETTIER_SEMI (state, bool) {
28 | state.prettierSemi = bool
29 | },
30 | SET_PRETTIER_QUOTES (state, bool) {
31 | state.prettierQuotes = bool
32 | }
33 | },
34 | actions: {
35 | setWhitespaceType ({ commit }, type) {
36 | commit('SET_RENDER_WHITESPACE', type)
37 | electronStore.preferences.set('renderWhitespace', type)
38 | },
39 | setWordWrap ({ commit }, type) {
40 | commit('SET_WORD_WRAP', type)
41 | electronStore.preferences.set('wordWrap', type)
42 | },
43 | setTabSize ({ commit }, size) {
44 | commit('SET_TAB_SIZE', Number(size))
45 | electronStore.preferences.set('tabSize', Number(size))
46 | },
47 | setInsertSpaces ({ commit }, bool) {
48 | commit('SET_INSERT_SPACES', bool)
49 | electronStore.preferences.set('insertSpaces', bool)
50 | },
51 | setPrettierSemi ({ commit }, bool) {
52 | commit('SET_PRETTIER_SEMI', bool)
53 | electronStore.preferences.set('prettierSemi', bool)
54 | },
55 | setPrettierQuotes ({ commit }, bool) {
56 | commit('SET_PRETTIER_QUOTES', bool)
57 | electronStore.preferences.set('prettierQuotes', bool)
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppAlert.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Hi 👋, Anton here.
5 |
6 | I am excited to announce that a new version of
7 | massCode
11 | is in active development. The masCode v1 code base is obsolete and I
12 | decided to start from scratch. Please support the new repository with a
13 | star. 🙏
14 |
15 |
16 | Stay tuned on
17 | Twitter .
21 |
22 |
23 |
29 |
30 |
31 |
32 |
59 |
60 |
91 |
--------------------------------------------------------------------------------
/src/renderer/components/preferences/Assistant.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 | Enable Assistant
9 |
10 |
11 | App Assistant lets you access snippets right from the Tray. (macOS only)
12 |
13 |
14 |
15 |
20 |
24 | Apply
25 |
26 |
27 |
28 |
29 |
30 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.main.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'main'
4 |
5 | const path = require('path')
6 | const { dependencies } = require('../package.json')
7 | const webpack = require('webpack')
8 |
9 | const TerserPlugin = require('terser-webpack-plugin')
10 |
11 | let mainConfig = {
12 | entry: {
13 | main: path.join(__dirname, '../src/main/index.js')
14 | },
15 | externals: [...Object.keys(dependencies || {})],
16 | module: {
17 | rules: [
18 | // {
19 | // test: /\.(js)$/,
20 | // enforce: 'pre',
21 | // exclude: /node_modules/,
22 | // use: {
23 | // loader: 'eslint-loader',
24 | // options: {
25 | // formatter: require('eslint-friendly-formatter')
26 | // }
27 | // }
28 | // },
29 | {
30 | test: /\.js$/,
31 | use: 'babel-loader',
32 | exclude: /node_modules/
33 | },
34 | {
35 | test: /\.node$/,
36 | use: 'node-loader'
37 | }
38 | ]
39 | },
40 | node: {
41 | __dirname: process.env.NODE_ENV !== 'production',
42 | __filename: process.env.NODE_ENV !== 'production'
43 | },
44 | output: {
45 | filename: '[name].js',
46 | libraryTarget: 'commonjs2',
47 | path: path.join(__dirname, '../dist/electron')
48 | },
49 | plugins: [new webpack.NoEmitOnErrorsPlugin()],
50 | optimization: {
51 | minimizer: []
52 | },
53 | resolve: {
54 | extensions: ['.js', '.json', '.node']
55 | },
56 | target: 'electron-main'
57 | }
58 |
59 | /**
60 | * Adjust mainConfig for development settings
61 | */
62 | if (process.env.NODE_ENV !== 'production') {
63 | mainConfig.plugins.push(
64 | new webpack.DefinePlugin({
65 | __static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
66 | })
67 | )
68 | }
69 |
70 | /**
71 | * Adjust mainConfig for production settings
72 | */
73 | if (process.env.NODE_ENV === 'production') {
74 | mainConfig.plugins.push(
75 | new webpack.DefinePlugin({
76 | 'process.env.NODE_ENV': '"production"'
77 | })
78 | )
79 | const terserOptions = require('./terserOptions')
80 | mainConfig.optimization.minimizer.push(new TerserPlugin(terserOptions()))
81 | }
82 |
83 | module.exports = mainConfig
84 |
--------------------------------------------------------------------------------
/src/main/tray.js:
--------------------------------------------------------------------------------
1 | import { BrowserWindow, Tray, ipcMain, globalShortcut } from 'electron'
2 | import store from './store'
3 |
4 | import path from 'path'
5 |
6 | let tray
7 | let trayWindow
8 |
9 | const winURL =
10 | process.env.NODE_ENV === 'development'
11 | ? 'http://localhost:9080/#tray'
12 | : `file://${__dirname}/index.html#tray`
13 |
14 | function createTray () {
15 | tray = new Tray(path.join(__static, '/trayTemplate.png'))
16 |
17 | tray.on('click', () => {
18 | showTray()
19 | })
20 | createTrayWindow()
21 |
22 | const shortcut = store.preferences.get('assistantShortcut')
23 |
24 | globalShortcut.register(shortcut, () => {
25 | showTray()
26 | })
27 |
28 | ipcMain.on('preferences:assistant:shortcut', (e, shortcuts) => {
29 | globalShortcut.unregister(shortcuts.old)
30 | globalShortcut.register(shortcuts.new, () => {
31 | showTray()
32 | })
33 | })
34 | }
35 |
36 | function createTrayWindow () {
37 | trayWindow = new BrowserWindow({
38 | width: 300,
39 | height: 400,
40 | frame: false,
41 | fullscreenable: false,
42 | resizable: false,
43 | transparent: true,
44 | alwaysOnTop: true,
45 | show: false,
46 | movable: false,
47 | webPreferences: {
48 | nodeIntegration: true,
49 | devTools: false
50 | }
51 | })
52 |
53 | trayWindow.loadURL(winURL)
54 |
55 | trayWindow.on('blur', () => {
56 | trayWindow.hide()
57 | trayWindow.webContents.send('tray:hide')
58 | })
59 |
60 | trayWindow.on('show', () => {
61 | trayWindow.focus()
62 | trayWindow.webContents.send('tray:show')
63 | })
64 |
65 | ipcMain.on('tray:hide', () => {
66 | trayWindow.hide()
67 | })
68 | }
69 |
70 | function showTray () {
71 | const trayWindowBounds = trayWindow.getBounds()
72 |
73 | let { x, y, width, height } = tray.getBounds()
74 | x = x - (trayWindowBounds.width / 2 - width / 2)
75 | y = y + height + 3
76 |
77 | trayWindow.setBounds({ x, y })
78 | trayWindow.show()
79 | }
80 |
81 | function destroyTray () {
82 | const shortcut = store.preferences.get('assistantShortcut')
83 |
84 | trayWindow.destroy()
85 | tray.destroy()
86 | globalShortcut.unregister(shortcut)
87 | }
88 |
89 | export { createTray, createTrayWindow, showTray, destroyTray }
90 |
--------------------------------------------------------------------------------
/src/renderer/views/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
13 |
18 | Update available
19 |
20 |
21 |
22 |
23 |
24 |
71 |
72 |
93 |
--------------------------------------------------------------------------------
/src/renderer/assets/scss/variables.scss:
--------------------------------------------------------------------------------
1 | :root {
2 | --sidebar-width: 180px;
3 | --snippets-list-width: 220px;
4 | --snippets-view-header-height: 60px;
5 | --snippets-view-header-full-height: calc(var(--snippets-view-header-height) + var(--snippets-tags-height));
6 | --snippets-view-footer-height: 30px;
7 | --snippet-tab-header-height: 40px;
8 | --snippet-tab-height: 30px;
9 | --title-bar-height: 20px;
10 | --menu-header: 80px;
11 |
12 | --spacing-unit: 4px;
13 | --spacing-xs: calc(var(--spacing-unit) * 2);
14 | --spacing-sm: calc(var(--spacing-unit) * 4);
15 | --spacing-md: calc(var(--spacing-unit) * 6);
16 | --spacing-lg: calc(var(--spacing-unit) * 8);
17 | --spacing-xl: calc(var(--spacing-unit) * 10);
18 |
19 | --font-primary: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
20 | Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
21 | --font-code: Menlo, Monaco, "Courier New", monospace;
22 |
23 | --text-base-size: 10px;
24 | --text-xs: calc(var(--text-base-size) * 1);
25 | --text-sm: calc(var(--text-base-size) * 1.2);
26 | --text-md: calc(var(--text-base-size) * 1.4);
27 | --text-lg: calc(var(--text-base-size) * 1.8);
28 | --text-xl: calc(var(--text-base-size) * 2.4);
29 | --text-xxl: calc(var(--text-base-size) * 3.2);
30 | --text-xxxl: calc(var(--text-base-size) * 3.6);
31 | }
32 |
33 | [data-theme='light'] {
34 | --color-primary: hsl(213, 81%, 67%);
35 |
36 | --color-bg: hsl(0, 0%, 100%);
37 | --color-contrast-lower: hsl(0, 0%, 97%);
38 | --color-contrast-low: hsl(0, 0%, 85%);
39 | --color-contrast-low-alt: hsl(0, 0%, 80%);
40 | --color-contrast-medium: hsl(0, 0%, 50%);
41 | --color-contrast-high: hsl(0, 0%, 15%);
42 | --color-contrast-higher: hsl(0, 0%, 0%);
43 |
44 | --color-text: var(--color-contrast-high);
45 | --color-border: var(--color-contrast-low);
46 | }
47 |
48 | [data-theme='dark'] {
49 | --color-primary: hsl(213, 81%, 67%);
50 |
51 | --color-bg: hsl(0, 0%, 20%);
52 | --color-contrast-lower: hsl(0, 0%, 25%);
53 | --color-contrast-low: hsl(0, 0%, 32%);
54 | --color-contrast-low-alt: hsl(0, 0%, 37%);
55 | --color-contrast-medium: hsl(0, 0%, 50%);
56 | --color-contrast-high: hsl(0, 0%, 85%);
57 | --color-contrast-higher: hsl(0, 0%, 100%);
58 |
59 | --color-text: var(--color-contrast-high);
60 | --color-border: var(--color-contrast-low);
61 | }
62 |
--------------------------------------------------------------------------------
/src/renderer/components/sidebar/SidebarList.vue:
--------------------------------------------------------------------------------
1 |
2 |
27 |
28 |
29 |
59 |
60 |
104 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppMenu.vue:
--------------------------------------------------------------------------------
1 |
2 |
23 |
24 |
25 |
78 |
79 |
104 |
--------------------------------------------------------------------------------
/src/main/index.js:
--------------------------------------------------------------------------------
1 | import { app, Menu, ipcMain, dialog, BrowserWindow } from 'electron'
2 | import { createMainWindow, mainWindow } from './main'
3 | import { createTray, destroyTray } from './tray'
4 | import store from './store'
5 | import mainMenu from './lib/main-menu'
6 | import touchBar from './lib/touch-bar'
7 | import { initAnalytics } from './lib/analytics'
8 | import { checkForUpdatesAndNotify } from './lib/update-check'
9 |
10 | const isDev = process.env.NODE_ENV === 'development'
11 | let menu
12 |
13 | /**
14 | * Set `__static` path to static files in production
15 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
16 | */
17 | if (!isDev) {
18 | global.__static = require('path')
19 | .join(__dirname, '/static')
20 | .replace(/\\/g, '\\\\')
21 | }
22 |
23 | function init () {
24 | createMainWindow()
25 |
26 | menu = Menu.buildFromTemplate(mainMenu(mainWindow))
27 | Menu.setApplicationMenu(menu)
28 | }
29 |
30 | function initTray () {
31 | if (process.platform !== 'darwin') return
32 |
33 | const isAssistant = store.preferences.get('assistant')
34 | if (isAssistant) createTray()
35 | }
36 |
37 | app.on('ready', () => {
38 | init()
39 | initTray()
40 | initAnalytics()
41 | checkForUpdatesAndNotify()
42 | mainWindow.setTouchBar(touchBar)
43 | })
44 |
45 | app.on('window-all-closed', () => {
46 | if (process.platform !== 'darwin') {
47 | app.quit()
48 | }
49 | })
50 |
51 | app.on('before-quit', () => {
52 | mainWindow.removeAllListeners()
53 | })
54 |
55 | app.on('activate', () => {
56 | if (mainWindow === null) {
57 | init()
58 | } else {
59 | mainWindow.show()
60 | }
61 | })
62 |
63 | ipcMain.on('preferences:assistant', (e, enable) => {
64 | enable ? initTray() : destroyTray()
65 | })
66 |
67 | ipcMain.on('message', (e, options) => {
68 | dialog.showMessageBox(BrowserWindow.getFocusedWindow(), options)
69 | })
70 |
71 | // Переключение чекбокса у Editor/Preview Markdown
72 | ipcMain.on('menu:markdown-preview', (e, value) => {
73 | menu.items[3].submenu.items[2].checked = value
74 | })
75 |
76 | /**
77 | * Auto Updater
78 | *
79 | * Uncomment the following code below and install `electron-updater` to
80 | * support auto updating. Code Signing with a valid certificate is required.
81 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
82 | */
83 |
84 | /*
85 | import { autoUpdater } from 'electron-updater'
86 |
87 | autoUpdater.on('update-downloaded', () => {
88 | autoUpdater.quitAndInstall()
89 | })
90 |
91 | app.on('ready', () => {
92 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
93 | })
94 | */
95 |
--------------------------------------------------------------------------------
/src/renderer/components/snippets/SnippetTabs.vue:
--------------------------------------------------------------------------------
1 |
2 |
25 |
26 |
27 |
83 |
84 |
122 |
--------------------------------------------------------------------------------
/src/renderer/components/snippets/ActionBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
16 |
17 |
18 |
24 |
30 |
31 |
32 |
33 |
34 |
81 |
82 |
110 |
--------------------------------------------------------------------------------
/src/renderer/components/sidebar/TheLibrary.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
14 |
15 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
95 |
96 |
112 |
--------------------------------------------------------------------------------
/src/renderer/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
99 |
100 |
113 |
--------------------------------------------------------------------------------
/src/renderer/components/tray/TraySnippetListItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 | {{ model.name }}
11 |
12 |
13 |
21 |
22 |
23 |
24 |
83 |
84 |
131 |
--------------------------------------------------------------------------------
/src/renderer/assets/scss/reset.scss:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | html {
6 | line-height: 1.15;
7 | -webkit-text-size-adjust: 100%;
8 | }
9 |
10 | body {
11 | margin: 0;
12 | }
13 |
14 | h1 {
15 | font-size: 2em;
16 | margin: 0.67em 0;
17 | }
18 |
19 | hr {
20 | box-sizing: content-box;
21 | height: 0;
22 | overflow: visible;
23 | }
24 |
25 | a {
26 | background-color: transparent;
27 | }
28 |
29 | abbr[title] {
30 | border-bottom: none;
31 | text-decoration: underline dotted;
32 | }
33 |
34 | b,
35 | strong {
36 | font-weight: bolder;
37 | }
38 |
39 | small {
40 | font-size: 80%;
41 | }
42 |
43 | sub,
44 | sup {
45 | font-size: 75%;
46 | line-height: 0;
47 | position: relative;
48 | vertical-align: baseline;
49 | }
50 |
51 | sub {
52 | bottom: -0.25em;
53 | }
54 |
55 | sup {
56 | top: -0.5em;
57 | }
58 |
59 | img {
60 | border-style: none;
61 | }
62 |
63 | button,
64 | input,
65 | optgroup,
66 | select,
67 | textarea {
68 | font-family: inherit;
69 | font-size: 100%;
70 | line-height: 1.15;
71 | margin: 0;
72 | }
73 |
74 | button,
75 | input {
76 | overflow: visible;
77 | }
78 |
79 | button,
80 | select {
81 | text-transform: none;
82 | }
83 |
84 | button,
85 | [type='button'],
86 | [type='reset'],
87 | [type='submit'] {
88 | -webkit-appearance: button;
89 | }
90 |
91 | button::-moz-focus-inner,
92 | [type='button']::-moz-focus-inner,
93 | [type='reset']::-moz-focus-inner,
94 | [type='submit']::-moz-focus-inner {
95 | border-style: none;
96 | padding: 0;
97 | }
98 |
99 | button:-moz-focusring,
100 | [type='button']:-moz-focusring,
101 | [type='reset']:-moz-focusring,
102 | [type='submit']:-moz-focusring {
103 | outline: 1px dotted ButtonText;
104 | }
105 |
106 | fieldset {
107 | padding: 0.35em 0.75em 0.625em;
108 | }
109 |
110 | legend {
111 | box-sizing: border-box;
112 | color: inherit;
113 | display: table;
114 | max-width: 100%;
115 | white-space: normal;
116 | padding: 0;
117 | }
118 |
119 | progress {
120 | vertical-align: baseline;
121 | }
122 |
123 | textarea {
124 | overflow: auto;
125 | }
126 |
127 | [type='checkbox'],
128 | [type='radio'] {
129 | box-sizing: border-box;
130 | padding: 0;
131 | }
132 |
133 | [type='number']::-webkit-inner-spin-button,
134 | [type='number']::-webkit-outer-spin-button {
135 | height: auto;
136 | }
137 |
138 | [type='search'] {
139 | -webkit-appearance: textfield;
140 | outline-offset: -2px;
141 | }
142 |
143 | [type='search']::-webkit-search-decoration {
144 | -webkit-appearance: none;
145 | }
146 |
147 | ::-webkit-file-upload-button {
148 | -webkit-appearance: button;
149 | font: inherit;
150 | }
151 |
152 | summary {
153 | display: list-item;
154 | }
155 |
156 | main,
157 | details {
158 | display: block;
159 | }
160 |
161 | pre,
162 | code,
163 | kbd,
164 | samp {
165 | font-family: monospace, monospace;
166 | font-size: 1em;
167 | }
168 |
169 | template,
170 | [hidden] {
171 | display: none;
172 | }
173 |
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <% if (htmlWebpackPlugin.options.nodeModules) { %>
7 |
8 |
11 | <% } %>
12 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
massCode is loading
86 |
90 |
91 |
92 |
93 |
94 | <% if (!htmlWebpackPlugin.options.isBrowser && !htmlWebpackPlugin.options.isDevelopment) { %>
95 |
98 | <% } %>
99 |
100 |
101 |
102 |
103 |
104 |
--------------------------------------------------------------------------------
/.github/COMMIT_CONVENTION.md:
--------------------------------------------------------------------------------
1 | ## Git Commit Message Convention
2 |
3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular).
4 |
5 | #### TL;DR:
6 |
7 | Messages must be matched by the following regex:
8 |
9 | ```js
10 | /^(revert: )?(feat|fix|polish|docs|style|refactor|perf|test|workflow|ci|chore|types|build)(\(.+\))?: .{1,50}/
11 | ```
12 |
13 | #### Examples
14 |
15 | Appears under "Features" header, `compiler` subheader:
16 |
17 | ```
18 | feat(compiler): add 'comments' option
19 | ```
20 |
21 | Appears under "Bug Fixes" header, `v-model` subheader, with a link to issue #28:
22 |
23 | ```
24 | fix(v-model): handle events on blur
25 |
26 | close #28
27 | ```
28 |
29 | Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
30 |
31 | ```
32 | perf(core): improve vdom diffing by removing 'foo' option
33 |
34 | BREAKING CHANGE: The 'foo' option has been removed.
35 | ```
36 |
37 | The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
38 |
39 | ```
40 | revert: feat(compiler): add 'comments' option
41 |
42 | This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
43 | ```
44 |
45 | ### Full Message Format
46 |
47 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**:
48 |
49 | ```
50 | ():
51 |
52 |
53 |
54 |
55 | ```
56 |
57 | The **header** is mandatory and the **scope** of the header is optional.
58 |
59 | ### Revert
60 |
61 | If the commit reverts a previous commit, it should begin with `revert:`, followed by the header of the reverted commit. In the body it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted.
62 |
63 | ### Type
64 |
65 | If the prefix is `feat`, `fix` or `perf`, it will appear in the changelog. However if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
66 |
67 | Other prefixes are up to your discretion. Suggested prefixes are `docs`, `chore`, `style`, `refactor`, and `test` for non-changelog related tasks.
68 |
69 | ### Scope
70 |
71 | The scope could be anything specifying place of the commit change. For example `core`, `compiler`, `ssr`, `v-model`, `transition` etc...
72 |
73 | ### Subject
74 |
75 | The subject contains succinct description of the change:
76 |
77 | - use the imperative, present tense: "change" not "changed" nor "changes"
78 | - don't capitalize first letter
79 | - no dot (.) at the end
80 |
81 | ### Body
82 |
83 | Just as in the **subject**, use the imperative, present tense: "change" not "changed" nor "changes".
84 | The body should include the motivation for the change and contrast this with previous behavior.
85 |
86 | ### Footer
87 |
88 | The footer should contain any information about **Breaking Changes** and is also the place to
89 | reference GitHub issues that this commit **Closes**.
90 |
91 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
92 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/tags.js:
--------------------------------------------------------------------------------
1 | import db from '@/datastore'
2 |
3 | export default {
4 | namespaced: true,
5 | state: {
6 | list: [],
7 | selectedId: null
8 | },
9 | getters: {
10 | tags (state) {
11 | return state.list.map(i => {
12 | i.text = i.name
13 | return i
14 | })
15 | },
16 | selectedId (state) {
17 | return state.selectedId
18 | }
19 | },
20 | mutations: {
21 | SET_TAGS (state, tags) {
22 | state.list = tags
23 | },
24 | SET_SELECTED_ID (state, id) {
25 | state.selectedId = id
26 | }
27 | },
28 | actions: {
29 | getTags ({ commit }) {
30 | return new Promise((resolve, reject) => {
31 | db.tags
32 | .find({})
33 | .sort({ name: 1 })
34 | .exec((err, doc) => {
35 | if (err) reject(err)
36 |
37 | commit('SET_TAGS', doc)
38 | resolve(doc)
39 | })
40 | })
41 | },
42 | async addTag ({ dispatch }, tag) {
43 | return new Promise((resolve, reject) => {
44 | db.tags.findOne({ name: tag.name }, (err, doc) => {
45 | if (err) return
46 |
47 | if (!doc) {
48 | db.tags.insert(tag, (err, doc) => {
49 | if (err) reject(err)
50 | resolve(doc)
51 | })
52 | dispatch('getTags')
53 | } else {
54 | resolve(null)
55 | }
56 | })
57 | })
58 | },
59 | async addTagToSnippet ({ commit, rootGetters }, { tagId, snippetId }) {
60 | db.snippets.update({ _id: snippetId }, { $addToSet: { tags: tagId } })
61 | },
62 | removeTag ({ state, commit, dispatch, rootGetters }, id) {
63 | db.snippets.find({ tags: { $elemMatch: id } }, (err, doc) => {
64 | if (err) return
65 | // Собираем ids
66 | if (doc) {
67 | const ids = doc.reduce((acc, item) => {
68 | acc.push(item._id)
69 | return acc
70 | }, [])
71 | // Удаляем тег у всех найденных сниппетов с этим тегом
72 | db.snippets.update(
73 | { _id: { $in: ids } },
74 | { $pull: { tags: id } },
75 | { multi: true },
76 | (err, doc) => {
77 | if (err) return
78 | // Удаляем сам тег
79 | db.tags.remove({ _id: id }, async (err, doc) => {
80 | if (err) return
81 | // Получаем обновленный список тегов
82 | const tags = await dispatch('getTags')
83 | const firstTag = tags[0]
84 | // Если есть первый тег, то устанавливаем его как выбранный,
85 | // затем получаем список снипеттов по тегу
86 | if (firstTag) {
87 | commit('SET_SELECTED_ID', firstTag._id)
88 | await dispatch(
89 | 'snippets/getSnippets',
90 | { tags: { $elemMatch: firstTag._id } },
91 | { root: true }
92 | )
93 | } else {
94 | // Если нет, то переключаем на библиотеку
95 | dispatch('app/setShowTags', false, { root: true })
96 | }
97 | })
98 | }
99 | )
100 | }
101 | })
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/src/renderer/views/Preferences.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
91 |
92 |
140 |
--------------------------------------------------------------------------------
/src/renderer/components/snippets/SnippetTabsItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
23 | {{ computedLabel }}
27 |
28 |
29 |
30 |
112 |
113 |
146 |
--------------------------------------------------------------------------------
/.electron-vue/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.NODE_ENV = 'production'
4 |
5 | const { say } = require('cfonts')
6 | const chalk = require('chalk')
7 | const del = require('del')
8 | const { spawn } = require('child_process')
9 | const webpack = require('webpack')
10 | const Multispinner = require('multispinner')
11 |
12 | const mainConfig = require('./webpack.main.config')
13 | const rendererConfig = require('./webpack.renderer.config')
14 | const webConfig = require('./webpack.web.config')
15 |
16 | const doneLog = chalk.bgGreen.white(' DONE ') + ' '
17 | const errorLog = chalk.bgRed.white(' ERROR ') + ' '
18 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
19 | const isCI = process.env.CI || false
20 |
21 | if (process.env.BUILD_TARGET === 'clean') clean()
22 | else if (process.env.BUILD_TARGET === 'web') web()
23 | else build()
24 |
25 | function clean() {
26 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
27 | console.log(`\n${doneLog}\n`)
28 | process.exit()
29 | }
30 |
31 | function build() {
32 | greeting()
33 |
34 | del.sync(['dist/electron/*', '!.gitkeep'])
35 |
36 | const tasks = ['main', 'renderer']
37 | const m = new Multispinner(tasks, {
38 | preText: 'building',
39 | postText: 'process'
40 | })
41 |
42 | let results = ''
43 |
44 | m.on('success', () => {
45 | process.stdout.write('\x1B[2J\x1B[0f')
46 | console.log(`\n\n${results}`)
47 | console.log(
48 | `${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`
49 | )
50 | process.exit()
51 | })
52 |
53 | pack(mainConfig)
54 | .then(result => {
55 | results += result + '\n\n'
56 | m.success('main')
57 | })
58 | .catch(err => {
59 | m.error('main')
60 | console.log(`\n ${errorLog}failed to build main process`)
61 | console.error(`\n${err}\n`)
62 | process.exit(1)
63 | })
64 |
65 | pack(rendererConfig)
66 | .then(result => {
67 | results += result + '\n\n'
68 | m.success('renderer')
69 | })
70 | .catch(err => {
71 | m.error('renderer')
72 | console.log(`\n ${errorLog}failed to build renderer process`)
73 | console.error(`\n${err}\n`)
74 | process.exit(1)
75 | })
76 | }
77 |
78 | function pack(config) {
79 | return new Promise((resolve, reject) => {
80 | config.mode = 'production'
81 | webpack(config, (err, stats) => {
82 | if (err) reject(err.stack || err)
83 | else if (stats.hasErrors()) {
84 | let err = ''
85 |
86 | stats
87 | .toString({
88 | chunks: false,
89 | colors: true
90 | })
91 | .split(/\r?\n/)
92 | .forEach(line => {
93 | err += ` ${line}\n`
94 | })
95 |
96 | reject(err)
97 | } else {
98 | resolve(
99 | stats.toString({
100 | chunks: false,
101 | colors: true
102 | })
103 | )
104 | }
105 | })
106 | })
107 | }
108 |
109 | function web() {
110 | del.sync(['dist/web/*', '!.gitkeep'])
111 | webConfig.mode = 'production'
112 | webpack(webConfig, (err, stats) => {
113 | if (err || stats.hasErrors()) console.log(err)
114 |
115 | console.log(
116 | stats.toString({
117 | chunks: false,
118 | colors: true
119 | })
120 | )
121 |
122 | process.exit()
123 | })
124 | }
125 |
126 | function greeting() {
127 | const cols = process.stdout.columns
128 | let text = ''
129 |
130 | if (cols > 85) text = 'lets-build'
131 | else if (cols > 60) text = 'lets-|build'
132 | else text = false
133 |
134 | if (text && !isCI) {
135 | say(text, {
136 | colors: ['yellow'],
137 | font: 'simple3d',
138 | space: false
139 | })
140 | } else console.log(chalk.yellow.bold('\n lets-build'))
141 | console.log()
142 | }
143 |
--------------------------------------------------------------------------------
/src/renderer/components/preferences/Editor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | Editor
4 |
5 |
6 |
7 | The number of spaces a tab is equal to.
8 |
9 |
10 |
11 |
15 |
16 | Select type of indentation.
17 |
18 |
19 |
20 |
24 |
25 | Controls how the editor should render whitespace characters.
26 |
27 |
28 |
29 |
33 |
34 | Controls how lines should wrap.
35 |
36 |
37 | Format
38 |
39 |
40 | Enable
41 |
42 |
43 | Print semicolons at the ends of statements.
44 |
45 |
46 |
47 |
48 | Enable
49 |
50 |
51 | Use single quotes instead of double quotes.
52 |
53 |
54 |
55 |
56 |
57 |
135 |
136 |
143 |
--------------------------------------------------------------------------------
/src/renderer/components/sidebar/TheFolders.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
18 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
129 |
130 |
144 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/app.js:
--------------------------------------------------------------------------------
1 | import electronStore from '@@/store'
2 | import { format } from 'date-fns'
3 | import db from '@/datastore'
4 |
5 | export default {
6 | namespaced: true,
7 | state: {
8 | os: process.platform,
9 | init: false,
10 | theme: electronStore.preferences.get('theme'),
11 | sidebarWidth: electronStore.app.get('sidebarWidth') || 180, // Принудительное значение если пришел null
12 | snippetListWidth: electronStore.app.get('snippetListWidth') || 220, // Принудительное значение если пришел null
13 | storagePath: electronStore.preferences.get('storagePath'),
14 | editorWidth: null,
15 | backupPath: electronStore.preferences.get('backupPath'),
16 | backups: [], // Временные метки
17 | markdownPreview: false,
18 | updateAvailable: false,
19 | nextVersionNotify: electronStore.app.get('nextVersionNotify'),
20 | showTags: false
21 | },
22 | getters: {
23 | isTagsShow (state) {
24 | return state.showTags
25 | },
26 | backups (state) {
27 | return state.backups
28 | .map(i => {
29 | return {
30 | label: format(i, 'dd MMM yyyy, HH:mm:ss'),
31 | date: i
32 | }
33 | })
34 | .sort((a, b) => (a > b ? 1 : -1))
35 | }
36 | },
37 | mutations: {
38 | SET_INIT (state, bool) {
39 | state.init = bool
40 | },
41 | SET_SIDEBAR_WIDTH (state, width) {
42 | state.sidebarWidth = width
43 | },
44 | SET_SNIPPET_LIST_WIDTH (state, width) {
45 | state.snippetListWidth = width
46 | },
47 | SET_STORAGE_PATH (state, path) {
48 | state.storagePath = path
49 | },
50 | SET_THEME (state, theme) {
51 | state.theme = theme
52 | },
53 | SET_MARKDOWN_PREVIEW (state, bool) {
54 | state.markdownPreview = bool
55 | },
56 | SET_UPDATE_AVAILABLE (state, bool) {
57 | state.updateAvailable = bool
58 | },
59 | SET_NEXT_VERSION_NOTIFY (state, bool) {
60 | state.nextVersionNotify = bool
61 | },
62 | SET_SHOW_TAGS (state, bool) {
63 | state.showTags = bool
64 | },
65 | SET_BACKUP_PATH (state, path) {
66 | state.backupPath = path
67 | },
68 | SET_BACKUPS (state, backups) {
69 | state.backups = backups
70 | }
71 | },
72 | actions: {
73 | setSidebarWidth ({ commit }, width) {
74 | width = Math.ceil(width)
75 | commit('SET_SIDEBAR_WIDTH', width)
76 | electronStore.app.set('sidebarWidth', width)
77 | },
78 | setSnippetListWidth ({ commit }, width) {
79 | width = Math.ceil(width)
80 | commit('SET_SNIPPET_LIST_WIDTH', width)
81 | electronStore.app.set('snippetListWidth', width)
82 | },
83 | setTheme ({ commit }, theme) {
84 | commit('SET_THEME', theme)
85 | electronStore.preferences.set('theme', theme)
86 | },
87 | async setShowTags ({ commit, dispatch, rootGetters }, bool) {
88 | if (bool) {
89 | commit('SET_SHOW_TAGS', true)
90 | const selectedTagId = rootGetters['tags/selectedId']
91 | await dispatch(
92 | 'snippets/getSnippets',
93 | { tags: { $elemMatch: selectedTagId } },
94 | { root: true }
95 | )
96 | } else {
97 | commit('SET_SHOW_TAGS', false)
98 | const selectedFolderIds = rootGetters['folders/selectedIds']
99 | const defaultQueryBySystemFolder =
100 | rootGetters['folders/defaultQueryBySystemFolder']
101 | const isSystemFolder = rootGetters['folders/isSystemFolder']
102 |
103 | let query = {}
104 |
105 | if (isSystemFolder) {
106 | query = defaultQueryBySystemFolder
107 | }
108 |
109 | if (this.selectedIds) {
110 | query = { folderId: { $in: selectedFolderIds } }
111 | }
112 |
113 | await dispatch('snippets/getSnippets', query, { root: true })
114 | }
115 |
116 | const firstSnippet = rootGetters['snippets/snippetsBySort'][0]
117 |
118 | if (firstSnippet) {
119 | dispatch('snippets/setSelected', firstSnippet, { root: true })
120 | } else {
121 | dispatch('snippets/setSelected', null, { root: true })
122 | }
123 | },
124 | async getBackups ({ commit }) {
125 | const backups = await db.getBackupsDirsAsDate()
126 | commit('SET_BACKUPS', backups)
127 | }
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/renderer/components/editor/languages.js:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | label: 'ABAP',
4 | value: 'abap'
5 | },
6 | {
7 | label: 'Apex',
8 | value: 'apex'
9 | },
10 | {
11 | label: 'Azure CLI',
12 | value: 'azcli'
13 | },
14 | {
15 | label: 'Batch',
16 | value: 'bat'
17 | },
18 | {
19 | label: 'Cameligo',
20 | value: 'cameligo'
21 | },
22 | {
23 | label: 'Clojure',
24 | value: 'clojure'
25 | },
26 | {
27 | label: 'CoffeeScript',
28 | value: 'coffeescript'
29 | },
30 | {
31 | label: 'C++',
32 | value: 'c'
33 | },
34 | {
35 | label: 'C#',
36 | value: 'csharp'
37 | },
38 | {
39 | label: 'CSP',
40 | value: 'csp'
41 | },
42 | {
43 | label: 'CSS',
44 | value: 'css'
45 | },
46 | {
47 | label: 'Dart',
48 | value: 'dart'
49 | },
50 | {
51 | label: 'Dockerfile',
52 | value: 'dockerfile'
53 | },
54 | {
55 | label: 'F#',
56 | value: 'fsharp'
57 | },
58 | {
59 | label: 'Go',
60 | value: 'go'
61 | },
62 | {
63 | label: 'GraphQL',
64 | value: 'graphql'
65 | },
66 | {
67 | label: 'Handlebars',
68 | value: 'handlebars'
69 | },
70 | {
71 | label: 'HTML',
72 | value: 'html'
73 | },
74 | {
75 | label: 'Ini',
76 | value: 'ini'
77 | },
78 | {
79 | label: 'Java',
80 | value: 'java'
81 | },
82 | {
83 | label: 'JavaScript',
84 | value: 'javascript'
85 | },
86 | {
87 | label: 'JSON',
88 | value: 'json'
89 | },
90 | {
91 | label: 'Kotlin',
92 | value: 'kotlin'
93 | },
94 | {
95 | label: 'Less',
96 | value: 'less'
97 | },
98 | {
99 | label: 'Lua',
100 | value: 'lua'
101 | },
102 | {
103 | label: 'Markdown',
104 | value: 'markdown'
105 | },
106 | {
107 | label: 'MIPS',
108 | value: 'mips'
109 | },
110 | {
111 | label: 'MSDAX',
112 | value: 'msdax'
113 | },
114 | {
115 | label: 'MySQL',
116 | value: 'mysql'
117 | },
118 | {
119 | label: 'Objective-C',
120 | value: 'objective-c'
121 | },
122 | {
123 | label: 'Pascal',
124 | value: 'pascal'
125 | },
126 | {
127 | label: 'Pascaligo',
128 | value: 'pascaligo'
129 | },
130 | {
131 | label: 'Perl',
132 | value: 'perl'
133 | },
134 | {
135 | label: 'PostgreSQL',
136 | value: 'pgsql'
137 | },
138 | {
139 | label: 'PHP',
140 | value: 'php'
141 | },
142 | {
143 | label: 'ATS/Postiats',
144 | value: 'postiats'
145 | },
146 | {
147 | label: 'Plain text',
148 | value: 'text'
149 | },
150 | {
151 | label: 'Power Query',
152 | value: 'powerquery'
153 | },
154 | {
155 | label: 'PowerShell',
156 | value: 'powershell'
157 | },
158 | {
159 | label: 'Pug',
160 | value: 'pug'
161 | },
162 | {
163 | label: 'Python',
164 | value: 'python'
165 | },
166 | {
167 | label: 'R',
168 | value: 'r'
169 | },
170 | {
171 | label: 'Razor',
172 | value: 'razor'
173 | },
174 | {
175 | label: 'Redis',
176 | value: 'redis'
177 | },
178 | {
179 | label: 'Redshift',
180 | value: 'redshift'
181 | },
182 | {
183 | label: 'Ruby',
184 | value: 'ruby'
185 | },
186 | {
187 | label: 'Rust',
188 | value: 'rust'
189 | },
190 | {
191 | label: 'Small Basic',
192 | value: 'sb'
193 | },
194 | {
195 | label: 'Scheme',
196 | value: 'scheme'
197 | },
198 | {
199 | label: 'Sass',
200 | value: 'scss'
201 | },
202 | {
203 | label: 'Shell',
204 | value: 'shell'
205 | },
206 | {
207 | label: 'Solidity',
208 | value: 'sol'
209 | },
210 | {
211 | label: 'Sophia',
212 | value: 'aes'
213 | },
214 | {
215 | label: 'SQL',
216 | value: 'sql'
217 | },
218 | {
219 | label: 'StructuredText',
220 | value: 'st'
221 | },
222 | {
223 | label: 'Swift',
224 | value: 'swift'
225 | },
226 | {
227 | label: 'Tcl',
228 | value: 'tcl'
229 | },
230 | {
231 | label: 'Twig',
232 | value: 'twig'
233 | },
234 | {
235 | label: 'TypeScript',
236 | value: 'typescript'
237 | },
238 | {
239 | label: 'Visual Basic',
240 | value: 'vb'
241 | },
242 | {
243 | label: 'XML',
244 | value: 'xml'
245 | },
246 | {
247 | label: 'YAML',
248 | value: 'yaml'
249 | }
250 | ]
251 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppCheckbox.vue:
--------------------------------------------------------------------------------
1 |
2 |
31 |
32 |
33 |
110 |
111 |
173 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.web.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'web'
4 |
5 | const path = require('path')
6 | const webpack = require('webpack')
7 |
8 | const TerserPlugin = require('terser-webpack-plugin')
9 | const CopyWebpackPlugin = require('copy-webpack-plugin')
10 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
11 | const HtmlWebpackPlugin = require('html-webpack-plugin')
12 | const { VueLoaderPlugin } = require('vue-loader')
13 |
14 | let webConfig = {
15 | devtool: '#cheap-module-eval-source-map',
16 | entry: {
17 | web: path.join(__dirname, '../src/renderer/main.js')
18 | },
19 | module: {
20 | rules: [
21 | // {
22 | // test: /\.(js|vue)$/,
23 | // enforce: 'pre',
24 | // exclude: /node_modules/,
25 | // use: {
26 | // loader: 'eslint-loader',
27 | // options: {
28 | // formatter: require('eslint-friendly-formatter')
29 | // }
30 | // }
31 | // },
32 | {
33 | test: /\.scss$/,
34 | use: ['vue-style-loader', 'css-loader', 'sass-loader']
35 | },
36 | {
37 | test: /\.sass$/,
38 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
39 | },
40 | {
41 | test: /\.less$/,
42 | use: ['vue-style-loader', 'css-loader', 'less-loader']
43 | },
44 | {
45 | test: /\.css$/,
46 | use: ['vue-style-loader', 'css-loader']
47 | },
48 | {
49 | test: /\.html$/,
50 | use: 'vue-html-loader'
51 | },
52 | {
53 | test: /\.js$/,
54 | use: 'babel-loader',
55 | include: [path.resolve(__dirname, '../src/renderer')],
56 | exclude: /node_modules/
57 | },
58 | {
59 | test: /\.vue$/,
60 | use: {
61 | loader: 'vue-loader',
62 | options: {
63 | extractCSS: true,
64 | loaders: {
65 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
66 | scss: 'vue-style-loader!css-loader!sass-loader',
67 | less: 'vue-style-loader!css-loader!less-loader'
68 | }
69 | }
70 | }
71 | },
72 | {
73 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
74 | use: {
75 | loader: 'url-loader',
76 | query: {
77 | limit: 10000,
78 | name: 'imgs/[name].[ext]'
79 | }
80 | }
81 | },
82 | {
83 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
84 | use: {
85 | loader: 'url-loader',
86 | query: {
87 | limit: 10000,
88 | name: 'fonts/[name].[ext]'
89 | }
90 | }
91 | }
92 | ]
93 | },
94 | plugins: [
95 | new VueLoaderPlugin(),
96 | new MiniCssExtractPlugin({ filename: 'styles.css' }),
97 | new HtmlWebpackPlugin({
98 | filename: 'index.html',
99 | template: path.resolve(__dirname, '../src/index.ejs'),
100 | minify: {
101 | collapseWhitespace: true,
102 | removeAttributeQuotes: true,
103 | removeComments: true
104 | },
105 | nodeModules: false
106 | }),
107 | new webpack.DefinePlugin({
108 | 'process.env.IS_WEB': 'true'
109 | }),
110 | new webpack.HotModuleReplacementPlugin(),
111 | new webpack.NoEmitOnErrorsPlugin()
112 | ],
113 | optimization: {
114 | minimizer: []
115 | },
116 | output: {
117 | filename: '[name].js',
118 | path: path.join(__dirname, '../dist/web')
119 | },
120 | resolve: {
121 | alias: {
122 | '@': path.join(__dirname, '../src/renderer'),
123 | vue$: 'vue/dist/vue.esm.js'
124 | },
125 | extensions: ['.js', '.vue', '.json', '.css']
126 | },
127 | target: 'web'
128 | }
129 |
130 | /**
131 | * Adjust webConfig for production settings
132 | */
133 | if (process.env.NODE_ENV === 'production') {
134 | const terserOptions = require('./terserOptions')
135 | webConfig.optimization.minimizer.push(new TerserPlugin(terserOptions()))
136 |
137 | webConfig.devtool = ''
138 |
139 | webConfig.plugins.push(
140 | new CopyWebpackPlugin([
141 | {
142 | from: path.join(__dirname, '../static'),
143 | to: path.join(__dirname, '../dist/web/static'),
144 | ignore: ['.*']
145 | }
146 | ]),
147 | new webpack.DefinePlugin({
148 | 'process.env.NODE_ENV': '"production"'
149 | }),
150 | new webpack.LoaderOptionsPlugin({
151 | minimize: true
152 | })
153 | )
154 | }
155 |
156 | module.exports = webConfig
157 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | build:
10 |
11 | runs-on: ${{ matrix.os }}
12 |
13 | strategy:
14 | matrix:
15 | os: [macOS-latest, ubuntu-18.04, windows-latest]
16 |
17 | steps:
18 | - uses: actions/checkout@v1
19 |
20 | - name: Hardcode tag to file
21 | run: (node -p "require('./package.json').version") > TAG
22 |
23 | - name: Set action ENV
24 | run: echo "::set-env name=TAG::$(cat TAG)"
25 |
26 | - name: yarn install
27 | run: yarn install
28 |
29 | - name: Build
30 | run: yarn run build
31 |
32 | - name: Upload Mac artifacts (zip)
33 | uses: actions/upload-artifact@v1
34 | if: startsWith(matrix.os, 'macOS')
35 | with:
36 | name: mac-zip
37 | path: build/massCode-${{ env.TAG }}-mac.zip
38 |
39 | - name: Upload Mac artifacts (dmg)
40 | uses: actions/upload-artifact@v1
41 | if: startsWith(matrix.os, 'macOS')
42 | with:
43 | name: mac-dmg
44 | path: build/massCode-${{ env.TAG }}.dmg
45 |
46 | - name: Upload Linux artifacts
47 | uses: actions/upload-artifact@v1
48 | if: startsWith(matrix.os, 'ubuntu')
49 | with:
50 | name: linux
51 | path: build/massCode-${{ env.TAG }}.AppImage
52 |
53 | - name: Upload Win artifacts
54 | uses: actions/upload-artifact@v1
55 | if: startsWith(matrix.os, 'windows')
56 | with:
57 | name: win
58 | path: build/massCode Setup ${{ env.TAG }}.exe
59 |
60 | assets:
61 | needs: build
62 | runs-on: ubuntu-latest
63 |
64 | steps:
65 | - uses: actions/checkout@v1
66 |
67 | - name: Hardcode tag to file
68 | run: (node -p "require('./package.json').version") > TAG
69 |
70 | - name: Set action ENV
71 | run: echo "::set-env name=TAG::$(cat TAG)"
72 |
73 | - name: Create Release
74 | id: create_release
75 | uses: actions/create-release@v1
76 | env:
77 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
78 | with:
79 | tag_name: ${{ github.ref }}
80 | release_name: Release ${{ github.ref }}
81 | draft: true
82 | prerelease: false
83 |
84 | - name: Dowload Mac Artifact (zip)
85 | uses: actions/download-artifact@v1
86 | with:
87 | name: mac-zip
88 |
89 | - name: Dowload Mac Artifact (dmg)
90 | uses: actions/download-artifact@v1
91 | with:
92 | name: mac-dmg
93 |
94 | - name: Dowload Linux Artifact
95 | uses: actions/download-artifact@v1
96 | with:
97 | name: linux
98 |
99 | - name: Dowload Win Artifact
100 | uses: actions/download-artifact@v1
101 | with:
102 | name: win
103 |
104 | - name: Upload Release Mac Asset (zip)
105 | uses: actions/upload-release-asset@v1.0.1
106 | env:
107 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
108 | with:
109 | upload_url: ${{ steps.create_release.outputs.upload_url }}
110 | asset_path: mac-zip/massCode-${{ env.TAG }}-mac.zip
111 | asset_name: massCode-${{ env.TAG }}-mac.zip
112 | asset_content_type: application/zip
113 |
114 | - name: Upload Release Mac Asset (dmg)
115 | uses: actions/upload-release-asset@v1.0.1
116 | env:
117 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
118 | with:
119 | upload_url: ${{ steps.create_release.outputs.upload_url }}
120 | asset_path: mac-dmg/massCode-${{ env.TAG }}.dmg
121 | asset_name: massCode-${{ env.TAG }}.dmg
122 | asset_content_type: application/x-apple-diskimage
123 |
124 | - name: Upload Release Linux Asset
125 | uses: actions/upload-release-asset@v1.0.1
126 | env:
127 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128 | with:
129 | upload_url: ${{ steps.create_release.outputs.upload_url }}
130 | asset_path: linux/massCode-${{ env.TAG }}.AppImage
131 | asset_name: massCode-${{ env.TAG }}.AppImage
132 | asset_content_type: application/vnd.appimage
133 |
134 | - name: Upload Release Win Asset
135 | uses: actions/upload-release-asset@v1.0.1
136 | env:
137 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
138 | with:
139 | upload_url: ${{ steps.create_release.outputs.upload_url }}
140 | asset_path: win/massCode Setup ${{ env.TAG }}.exe
141 | asset_name: massCode Setup ${{ env.TAG }}.exe
142 | asset_content_type: application/octet-stream
143 |
--------------------------------------------------------------------------------
/src/renderer/components/editor/MarkdownPreview.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
168 |
169 |
185 |
--------------------------------------------------------------------------------
/src/renderer/components/snippets/SnippetList.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
9 |
13 |
21 |
22 |
26 |
27 |
28 |
29 |
149 |
150 |
193 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # massCode
2 |
3 | > Project is no longer maintained, and may have bugs and security issues. Feel free to fork but no pull request or security alert will be answered.
4 | >
5 | > This is the repository for massCode v1. A new version of massCode is available in this [repository](https://github.com/massCodeIO/massCode).
6 |
7 |
8 | A free and open source code snippets manager for developers.
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | Built with Electron, Vue & Monaco editor.
24 |
25 | Inspired by applications like SnippetsLab and Quiver.
26 |
27 |
28 | Official Website | Documentation | Change Log
29 |
30 |
31 | ## Supporting
32 |
33 | **massCode** is open source project and completely free to use.
34 |
35 | However, the amount of effort needed to maintain and develop new features for the project is not sustainable without proper financial backing. You can support massCode development via the following methods:
36 |
37 |
38 |
39 |
40 | [](https://opencollective.com/masscode)
41 | [](https://www.patreon.com/antonreshetov)
42 | [](https://paypal.me/antonreshetov)
43 | [](https://blockchain.com/btc/payment_request?address=1GnNU7UGrXyKx5Zd3uDfhCLL716AYBJwAJ&amount=0.00010450&message=Contribution%20to%20massCode)
44 |
45 |
46 |
47 | ## Overview
48 |
49 | The goal of creating this application was mostly my own growth as a developer. Also, I wanted this project to absorb the best of such applications already on the market (both free and paid). At the same time, I wanted this project to be an open source project.
50 |
51 | massCode allows you to organize snippets using multi-level folders as well as tags. Each snippet has fragments - tabs, which gives even greater level of organization
52 |
53 | A snippets manager must not only provide organization of snippets but also have a good code editor. That's why under the hood of massCode there's [Monaco editor](https://microsoft.github.io/monaco-editor/). Monaco is a modern code editor from Microsoft which is used in one of the most popular editor [VS Code](https://code.visualstudio.com/). The editor provides IntelliSense, validation for TypeScript, JavaScript, CSS, LESS, SCSS, JSON, HTML. It's also added a super productive [Emmet](https://emmet.io/) and [Prettier](https://prettier.io/) to code formatter.
54 |
55 | ## Development
56 |
57 | ```bash
58 | # install dependencies
59 | yarn
60 | # serve with hot reload
61 | yarn dev
62 | ```
63 |
64 | ## Building
65 |
66 | ```bash
67 | ## build application for production
68 | yarn build
69 | ```
70 |
71 | ## License
72 |
73 | [AGPL-3.0](https://github.com/antonreshetov/massCode/blob/master/LICENSE)
74 |
75 | Copyright (c) 2019-present, Anton Reshetov.
76 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "masscode",
3 | "productName": "massCode",
4 | "version": "1.3.1",
5 | "author": "Anton Reshetov ",
6 | "description": "A free and open source code snippets manager for developers",
7 | "license": "AGPL-3.0",
8 | "main": "./dist/electron/main.js",
9 | "scripts": {
10 | "build": "node .electron-vue/build.js && electron-builder",
11 | "build:dir": "node .electron-vue/build.js && electron-builder --dir",
12 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
13 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js",
14 | "dev": "node .electron-vue/dev-runner.js",
15 | "lint": "eslint --ext .js,.vue src --fix",
16 | "lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src",
17 | "pack": "npm run pack:main && npm run pack:renderer",
18 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js",
19 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js",
20 | "release": "sh scripts/release.sh",
21 | "postinstall": "npm run lint:fix"
22 | },
23 | "lint-staged": {
24 | "*.{js,vue}": [
25 | "prettier --write",
26 | "eslint --fix",
27 | "git add"
28 | ]
29 | },
30 | "husky": {
31 | "hooks": {
32 | "pre-commit": "lint-staged",
33 | "commit-msg": "node scripts/verify-commit.js"
34 | }
35 | },
36 | "build": {
37 | "productName": "massCode",
38 | "appId": "io.masscode.app",
39 | "directories": {
40 | "output": "build"
41 | },
42 | "files": [
43 | "dist/electron/**/*"
44 | ],
45 | "dmg": {
46 | "contents": [
47 | {
48 | "x": 410,
49 | "y": 150,
50 | "type": "link",
51 | "path": "/Applications"
52 | },
53 | {
54 | "x": 130,
55 | "y": 150,
56 | "type": "file"
57 | }
58 | ]
59 | },
60 | "mac": {
61 | "icon": "build/icons/icon.icns"
62 | },
63 | "win": {
64 | "icon": "build/icons/icon.ico"
65 | },
66 | "linux": {
67 | "icon": "build/icons"
68 | }
69 | },
70 | "dependencies": {
71 | "@babel/plugin-proposal-optional-chaining": "^7.10.1",
72 | "@johmun/vue-tags-input": "^2.1.0",
73 | "axios": "^0.19.1",
74 | "date-fns": "^2.8.1",
75 | "electron-store": "^5.1.0",
76 | "emmet-monaco-es": "^4.3.3",
77 | "feather-icons": "^4.25.0",
78 | "fs-extra": "^8.1.0",
79 | "highlight.js": "^9.18.1",
80 | "interactjs": "^1.8.0-alpha.6",
81 | "junk": "^3.1.0",
82 | "lodash-es": "^4.17.15",
83 | "markdown-it": "^10.0.0",
84 | "markdown-it-link-attributes": "^3.0.0",
85 | "monaco-editor": "^0.19.0",
86 | "monaco-editor-webpack-plugin": "^1.8.1",
87 | "mousetrap": "^1.6.3",
88 | "nedb": "^1.8.0",
89 | "perfect-scrollbar": "^1.4.0",
90 | "popper.js": "^1.16.0",
91 | "prismjs": "^1.19.0",
92 | "rimraf": "^3.0.2",
93 | "sanitize-html": "^1.21.1",
94 | "shortid": "^2.2.15",
95 | "universal-analytics": "^0.4.20",
96 | "vue": "^2.5.16",
97 | "vue-draggable-nested-tree": "github:massCodeIO/vue-draggable-nested-tree",
98 | "vue-electron": "^1.0.6",
99 | "vue-router": "^3.1.3",
100 | "vuex": "^3.0.1"
101 | },
102 | "devDependencies": {
103 | "@babel/core": "^7.7.7",
104 | "@babel/plugin-transform-runtime": "^7.7.6",
105 | "@babel/polyfill": "^7.7.0",
106 | "@babel/preset-env": "^7.7.7",
107 | "@babel/register": "^7.7.7",
108 | "@babel/runtime": "^7.7.7",
109 | "@vue/eslint-config-standard": "^5.0.1",
110 | "ajv": "^6.5.0",
111 | "babel-eslint": "^10.0.3",
112 | "babel-loader": "^8.0.6",
113 | "cfonts": "^2.1.2",
114 | "chalk": "^3.0.0",
115 | "copy-webpack-plugin": "^5.1.1",
116 | "cross-env": "^6.0.3",
117 | "css-loader": "^3.4.0",
118 | "del": "^5.1.0",
119 | "devtron": "^1.4.0",
120 | "electron": "^7.1.6",
121 | "electron-builder": "^22.4.1",
122 | "electron-debug": "^3.0.1",
123 | "electron-devtools-installer": "^2.2.4",
124 | "eslint": "^6.7.2",
125 | "eslint-config-standard": "^14.1.0",
126 | "eslint-friendly-formatter": "^4.0.1",
127 | "eslint-loader": "^3.0.3",
128 | "eslint-plugin-html": "^6.0.0",
129 | "eslint-plugin-import": "^2.19.1",
130 | "eslint-plugin-node": "^10.0.0",
131 | "eslint-plugin-promise": "^4.2.1",
132 | "eslint-plugin-standard": "^4.0.1",
133 | "eslint-plugin-vue": "^6.0.1",
134 | "file-loader": "^5.0.2",
135 | "html-webpack-plugin": "^3.2.0",
136 | "husky": "^3.1.0",
137 | "lint-staged": "^9.5.0",
138 | "mini-css-extract-plugin": "0.8.2",
139 | "multispinner": "^0.2.1",
140 | "node-loader": "^0.6.0",
141 | "node-sass": "^4.9.2",
142 | "prettier": "^1.19.1",
143 | "sass-loader": "^8.0.0",
144 | "style-loader": "^1.0.2",
145 | "terser-webpack-plugin": "^2.3.1",
146 | "url-loader": "^3.0.0",
147 | "vue-html-loader": "^1.2.4",
148 | "vue-loader": "^15.8.3",
149 | "vue-style-loader": "^4.1.0",
150 | "vue-template-compiler": "^2.5.16",
151 | "webpack": "^4.41.3",
152 | "webpack-cli": "^3.0.8",
153 | "webpack-dev-server": "^3.10.0",
154 | "webpack-hot-middleware": "^2.22.2",
155 | "webpack-merge": "^4.1.3"
156 | },
157 | "volta": {
158 | "node": "14.19.1",
159 | "yarn": "1.22.18"
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
12 |
16 |
17 |
18 |
22 |
23 |
24 |
28 |
29 |
30 |
31 |
49 |
62 |
66 |
67 |
68 |
69 |
70 |
71 |
130 |
131 |
235 |
--------------------------------------------------------------------------------
/.electron-vue/dev-runner.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chalk = require('chalk')
4 | const electron = require('electron')
5 | const path = require('path')
6 | const { say } = require('cfonts')
7 | const { spawn } = require('child_process')
8 | const webpack = require('webpack')
9 | const WebpackDevServer = require('webpack-dev-server')
10 | const webpackHotMiddleware = require('webpack-hot-middleware')
11 |
12 | const mainConfig = require('./webpack.main.config')
13 | const rendererConfig = require('./webpack.renderer.config')
14 |
15 | let electronProcess = null
16 | let manualRestart = false
17 | let hotMiddleware
18 |
19 | function logStats(proc, data) {
20 | let log = ''
21 |
22 | log += chalk.yellow.bold(
23 | `┏ ${proc} Process ${new Array(19 - proc.length + 1).join('-')}`
24 | )
25 | log += '\n\n'
26 |
27 | if (typeof data === 'object') {
28 | data
29 | .toString({
30 | colors: true,
31 | chunks: false
32 | })
33 | .split(/\r?\n/)
34 | .forEach(line => {
35 | log += ' ' + line + '\n'
36 | })
37 | } else {
38 | log += ` ${data}\n`
39 | }
40 |
41 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n'
42 |
43 | console.log(log)
44 | }
45 |
46 | function startRenderer() {
47 | return new Promise((resolve, reject) => {
48 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(
49 | rendererConfig.entry.renderer
50 | )
51 | rendererConfig.mode = 'development'
52 | const compiler = webpack(rendererConfig)
53 | hotMiddleware = webpackHotMiddleware(compiler, {
54 | log: false,
55 | heartbeat: 2500
56 | })
57 |
58 | compiler.hooks.compilation.tap('compilation', compilation => {
59 | compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync(
60 | 'html-webpack-plugin-after-emit',
61 | (data, cb) => {
62 | hotMiddleware.publish({ action: 'reload' })
63 | cb()
64 | }
65 | )
66 | })
67 |
68 | compiler.hooks.done.tap('done', stats => {
69 | logStats('Renderer', stats)
70 | })
71 |
72 | const server = new WebpackDevServer(compiler, {
73 | contentBase: path.join(__dirname, '../'),
74 | quiet: true,
75 | hot: true,
76 | before(app, ctx) {
77 | ctx.middleware.waitUntilValid(() => {
78 | resolve()
79 | })
80 | }
81 | })
82 |
83 | server.listen(9080)
84 | })
85 | }
86 |
87 | function startMain() {
88 | return new Promise((resolve, reject) => {
89 | mainConfig.entry.main = [
90 | path.join(__dirname, '../src/main/index.dev.js')
91 | ].concat(mainConfig.entry.main)
92 | mainConfig.mode = 'development'
93 | const compiler = webpack(mainConfig)
94 |
95 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => {
96 | logStats('Main', chalk.white.bold('compiling...'))
97 | hotMiddleware.publish({ action: 'compiling' })
98 | done()
99 | })
100 |
101 | compiler.watch({}, (err, stats) => {
102 | if (err) {
103 | console.log(err)
104 | return
105 | }
106 |
107 | logStats('Main', stats)
108 |
109 | if (electronProcess && electronProcess.kill) {
110 | manualRestart = true
111 | process.kill(electronProcess.pid)
112 | electronProcess = null
113 | startElectron()
114 |
115 | setTimeout(() => {
116 | manualRestart = false
117 | }, 5000)
118 | }
119 |
120 | resolve()
121 | })
122 | })
123 | }
124 |
125 | function startElectron() {
126 | var args = [
127 | '--inspect=5858',
128 | path.join(__dirname, '../dist/electron/main.js')
129 | ]
130 |
131 | // detect yarn or npm and process commandline args accordingly
132 | if (process.env.npm_execpath.endsWith('yarn.js')) {
133 | args = args.concat(process.argv.slice(3))
134 | } else if (process.env.npm_execpath.endsWith('npm-cli.js')) {
135 | args = args.concat(process.argv.slice(2))
136 | }
137 |
138 | electronProcess = spawn(electron, args)
139 |
140 | electronProcess.stdout.on('data', data => {
141 | electronLog(data, 'blue')
142 | })
143 | electronProcess.stderr.on('data', data => {
144 | electronLog(data, 'red')
145 | })
146 |
147 | electronProcess.on('close', () => {
148 | if (!manualRestart) process.exit()
149 | })
150 | }
151 |
152 | function electronLog(data, color) {
153 | let log = ''
154 | data = data.toString().split(/\r?\n/)
155 | data.forEach(line => {
156 | log += ` ${line}\n`
157 | })
158 | if (/[0-9A-z]+/.test(log)) {
159 | console.log(
160 | chalk[color].bold('┏ Electron -------------------') +
161 | '\n\n' +
162 | log +
163 | chalk[color].bold('┗ ----------------------------') +
164 | '\n'
165 | )
166 | }
167 | }
168 |
169 | function greeting() {
170 | const cols = process.stdout.columns
171 | let text = ''
172 |
173 | if (cols > 104) text = 'electron-vue'
174 | else if (cols > 76) text = 'electron-|vue'
175 | else text = false
176 |
177 | if (text) {
178 | say(text, {
179 | colors: ['yellow'],
180 | font: 'simple3d',
181 | space: false
182 | })
183 | } else console.log(chalk.yellow.bold('\n electron-vue'))
184 | console.log(chalk.blue(' getting ready...') + '\n')
185 | }
186 |
187 | function init() {
188 | greeting()
189 |
190 | Promise.all([startRenderer(), startMain()])
191 | .then(() => {
192 | startElectron()
193 | })
194 | .catch(err => {
195 | console.error(err)
196 | })
197 | }
198 |
199 | init()
200 |
--------------------------------------------------------------------------------
/.electron-vue/webpack.renderer.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'renderer'
4 |
5 | const path = require('path')
6 | const { dependencies } = require('../package.json')
7 | const webpack = require('webpack')
8 |
9 | const TerserPlugin = require('terser-webpack-plugin')
10 | const CopyWebpackPlugin = require('copy-webpack-plugin')
11 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
12 | const HtmlWebpackPlugin = require('html-webpack-plugin')
13 | const { VueLoaderPlugin } = require('vue-loader')
14 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
15 |
16 | /**
17 | * List of node_modules to include in webpack bundle
18 | *
19 | * Required for specific packages like Vue UI libraries
20 | * that provide pure *.vue files that need compiling
21 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
22 | */
23 | let whiteListedModules = ['vue']
24 |
25 | let rendererConfig = {
26 | devtool: '#cheap-module-eval-source-map',
27 | entry: {
28 | renderer: path.join(__dirname, '../src/renderer/main.js')
29 | },
30 | externals: [
31 | ...Object.keys(dependencies || {}).filter(
32 | d => !whiteListedModules.includes(d)
33 | )
34 | ],
35 | module: {
36 | rules: [
37 | // {
38 | // test: /\.(js|vue)$/,
39 | // enforce: 'pre',
40 | // exclude: /node_modules/,
41 | // use: {
42 | // loader: 'eslint-loader',
43 | // options: {
44 | // formatter: require('eslint-friendly-formatter')
45 | // }
46 | // }
47 | // },
48 | {
49 | test: /\.scss$/,
50 | use: ['vue-style-loader', 'css-loader', 'sass-loader']
51 | },
52 | {
53 | test: /\.sass$/,
54 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
55 | },
56 | {
57 | test: /\.less$/,
58 | use: ['vue-style-loader', 'css-loader', 'less-loader']
59 | },
60 | {
61 | test: /\.css$/,
62 | use: ['vue-style-loader', 'css-loader']
63 | },
64 | {
65 | test: /\.html$/,
66 | use: 'vue-html-loader'
67 | },
68 | {
69 | test: /\.js$/,
70 | use: 'babel-loader',
71 | exclude: /node_modules/
72 | },
73 | {
74 | test: /\.node$/,
75 | use: 'node-loader'
76 | },
77 | {
78 | test: /\.vue$/,
79 | use: {
80 | loader: 'vue-loader',
81 | options: {
82 | extractCSS: process.env.NODE_ENV === 'production',
83 | loaders: {
84 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
85 | scss: 'vue-style-loader!css-loader!sass-loader',
86 | less: 'vue-style-loader!css-loader!less-loader'
87 | }
88 | }
89 | }
90 | },
91 | {
92 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
93 | use: {
94 | loader: 'url-loader',
95 | query: {
96 | limit: 10000,
97 | name: 'imgs/[name]--[folder].[ext]'
98 | }
99 | }
100 | },
101 | {
102 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
103 | loader: 'url-loader',
104 | options: {
105 | limit: 10000,
106 | name: 'media/[name]--[folder].[ext]'
107 | }
108 | },
109 | {
110 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
111 | use: {
112 | loader: 'url-loader',
113 | query: {
114 | limit: 10000,
115 | name: 'fonts/[name]--[folder].[ext]'
116 | }
117 | }
118 | }
119 | ]
120 | },
121 | node: {
122 | __dirname: process.env.NODE_ENV !== 'production',
123 | __filename: process.env.NODE_ENV !== 'production'
124 | },
125 | plugins: [
126 | new VueLoaderPlugin(),
127 | new MiniCssExtractPlugin({ filename: 'styles.css' }),
128 | new HtmlWebpackPlugin({
129 | filename: 'index.html',
130 | template: path.resolve(__dirname, '../src/index.ejs'),
131 | minify: {
132 | collapseWhitespace: true,
133 | removeAttributeQuotes: true,
134 | removeComments: true
135 | },
136 | isBrowser: false,
137 | isDevelopment: process.env.NODE_ENV !== 'production',
138 | nodeModules:
139 | process.env.NODE_ENV !== 'production'
140 | ? path.resolve(__dirname, '../node_modules')
141 | : false
142 | }),
143 | new webpack.HotModuleReplacementPlugin(),
144 | new webpack.NoEmitOnErrorsPlugin(),
145 | new MonacoWebpackPlugin()
146 | ],
147 | optimization: {
148 | minimizer: []
149 | },
150 | output: {
151 | filename: '[name].js',
152 | libraryTarget: 'commonjs2',
153 | path: path.join(__dirname, '../dist/electron')
154 | },
155 | resolve: {
156 | alias: {
157 | '@': path.join(__dirname, '../src/renderer'),
158 | '@@': path.join(__dirname, '../src/main'),
159 | vue$: 'vue/dist/vue.esm.js'
160 | },
161 | extensions: ['.js', '.vue', '.json', '.css', '.node']
162 | },
163 | target: 'electron-renderer'
164 | }
165 |
166 | /**
167 | * Adjust rendererConfig for development settings
168 | */
169 | if (process.env.NODE_ENV !== 'production') {
170 | rendererConfig.plugins.push(
171 | new webpack.DefinePlugin({
172 | __static: `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
173 | })
174 | )
175 | }
176 |
177 | /**
178 | * Adjust rendererConfig for production settings
179 | */
180 | if (process.env.NODE_ENV === 'production') {
181 | const terserOptions = require('./terserOptions')
182 | rendererConfig.optimization.minimizer.push(new TerserPlugin(terserOptions()))
183 |
184 | rendererConfig.devtool = ''
185 |
186 | rendererConfig.plugins.push(
187 | new CopyWebpackPlugin([
188 | {
189 | from: path.join(__dirname, '../static'),
190 | to: path.join(__dirname, '../dist/electron/static'),
191 | ignore: ['.*']
192 | }
193 | ]),
194 | new webpack.DefinePlugin({
195 | 'process.env.NODE_ENV': '"production"'
196 | }),
197 | new webpack.LoaderOptionsPlugin({
198 | minimize: true
199 | })
200 | )
201 | }
202 |
203 | module.exports = rendererConfig
204 |
--------------------------------------------------------------------------------
/src/renderer/components/preferences/Storage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
19 |
20 | To use sync services like iCloud Drive, Google Drive of Dropbox, simply
21 | move storage to the corresponding synced folders.
22 |
23 |
24 |
25 |
40 |
41 | Backup will be created automatically when massCode is running.
42 |
43 | Backups
44 |
48 |
53 |
54 | {{ i.label }}
55 | Restore
59 |
60 |
61 |
62 |
63 | {{ countText }}
64 |
65 |
66 |
67 |
68 |
208 |
209 |
241 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppPopper.vue:
--------------------------------------------------------------------------------
1 |
2 |
20 |
21 |
22 |
100 |
101 |
260 |
--------------------------------------------------------------------------------
/src/renderer/views/Tray.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
28 |
32 |
33 |
42 |
43 |
44 |
45 |
46 |
47 |
204 |
205 |
259 |
--------------------------------------------------------------------------------
/src/renderer/datastore.js:
--------------------------------------------------------------------------------
1 | import Store from 'nedb'
2 | import path from 'path'
3 | import electronStore from '@@/store'
4 | import fs from 'fs-extra'
5 | import shortid from 'shortid'
6 | import { format, min, max, isSameDay } from 'date-fns'
7 | import rimraf from 'rimraf'
8 | import junk from 'junk'
9 |
10 | class DataStore {
11 | constructor () {
12 | this._path = electronStore.preferences.get('storagePath')
13 | this._backupPath = electronStore.preferences.get('backupPath')
14 | this._backupLimit = 30
15 | this._dbFiles = ['masscode.db', 'snippets.db', 'tags.db']
16 |
17 | this.init()
18 | }
19 |
20 | async init () {
21 | this.createDB('masscode', false)
22 | this.createDB('snippets')
23 | this.createDB('tags')
24 |
25 | this.masscode.loadDatabase(err => {
26 | if (err) throw err
27 |
28 | const defaultFolder = {
29 | list: [
30 | {
31 | id: shortid(),
32 | name: 'Default',
33 | open: false,
34 | defaultLanguage: 'text'
35 | }
36 | ],
37 | _id: 'folders'
38 | }
39 | this.masscode.insert(defaultFolder)
40 | })
41 |
42 | await this.createBackupDir()
43 | this.autoBackup()
44 | }
45 |
46 | async createDB (name, autoload = true) {
47 | this[name] = new Store({
48 | autoload,
49 | filename: path.join(this._path, `/${name}.db`),
50 | onload: err => {
51 | if (err) {
52 | this.createDB(name)
53 | console.log(`db ${name} is restarted`)
54 | }
55 | }
56 | })
57 | console.log(`db ${name} is created`)
58 | }
59 |
60 | updatePath () {
61 | this._path = electronStore.preferences.get('storagePath')
62 | this.init()
63 | }
64 |
65 | import (from) {
66 | electronStore.preferences.set('storagePath', from)
67 | this.updatePath()
68 | }
69 |
70 | move (to) {
71 | return new Promise((resolve, reject) => {
72 | const src = this._dbFiles.map(i => path.resolve(this._path, i))
73 | const dist = this._dbFiles.map(i => path.resolve(to, i))
74 |
75 | fs.readdir(to, (err, files) => {
76 | if (err) reject(err)
77 |
78 | const isExist = this._dbFiles.some(i => files.includes(i))
79 |
80 | if (isExist) {
81 | reject(new Error('Folder already contains db files.'))
82 | }
83 |
84 | src.forEach((file, index) => {
85 | fs.moveSync(file, dist[index])
86 | })
87 | electronStore.preferences.set('storagePath', to)
88 | this.updatePath()
89 | resolve()
90 | })
91 | })
92 | }
93 |
94 | compact () {
95 | this.masscode.persistence.compactDatafile()
96 | this.snippets.persistence.compactDatafile()
97 | this.tags.persistence.compactDatafile()
98 | }
99 |
100 | async createBackupDir () {
101 | await fs.ensureDir(this._backupPath)
102 | }
103 |
104 | createBackupDirByDate (date) {
105 | date = date || new Date()
106 | const backupFolderDatePattern = 'yyyy-MM-dd_HH-mm-ss'
107 | const suffixFolder = 'massCode'
108 | const dirName = `${format(date, backupFolderDatePattern)}_${suffixFolder}`
109 |
110 | return path.resolve(this._backupPath, dirName)
111 | }
112 |
113 | async backup () {
114 | const dir = this.createBackupDirByDate()
115 | const src = this._dbFiles.map(i => path.resolve(this._path, i))
116 | const dest = this._dbFiles.map(i => path.resolve(dir, i))
117 |
118 | this.compact()
119 |
120 | await fs.ensureDir(dir)
121 |
122 | src.forEach((file, index) => {
123 | fs.copy(file, dest[index])
124 | })
125 | }
126 |
127 | autoBackup () {
128 | const start = async () => {
129 | const now = new Date()
130 | const isEmpty = await this.isBackupEmpty()
131 |
132 | if (isEmpty) {
133 | await this.backup()
134 | } else {
135 | const { date } = await this.getLatestBackupDir()
136 |
137 | if (!isSameDay(now, date)) {
138 | await this.removeEarliestBackup()
139 | await this.backup()
140 | }
141 | }
142 | console.log('autobackup is started')
143 | }
144 |
145 | start()
146 | setInterval(() => {
147 | start()
148 | }, 1000 * 60 * 60 * 12)
149 | }
150 |
151 | restoreFromBackup (date) {
152 | return new Promise((resolve, reject) => {
153 | const dir = this.createBackupDirByDate(date)
154 | const src = this._dbFiles.map(i => path.resolve(dir, i))
155 | const dest = this._dbFiles.map(i => path.resolve(this._path, i))
156 |
157 | src.forEach((file, index) => {
158 | fs.copySync(file, dest[index])
159 | })
160 |
161 | this.init()
162 | resolve()
163 | })
164 | }
165 |
166 | async moveBackup (to) {
167 | const dirs = await this.getBackupDirs()
168 | const src = dirs.map(i => path.resolve(this._backupPath, i))
169 | const dest = dirs.map(i => path.resolve(to, i))
170 |
171 | src.forEach((dir, index) => {
172 | fs.moveSync(dir, dest[index], { overwrite: true })
173 | })
174 |
175 | this._backupPath = to
176 | electronStore.preferences.set('backupPath', to)
177 | }
178 |
179 | async getBackupDirs () {
180 | let dirs = await fs.readdir(this._backupPath)
181 | dirs = dirs.filter(junk.not).filter(i => i.includes('massCode'))
182 |
183 | return dirs
184 | }
185 |
186 | async getBackupsDirsAsDate () {
187 | const dirs = await this.getBackupDirs()
188 | return this.convertBackupDirsToDate(dirs.filter(junk.not))
189 | }
190 |
191 | async getEarliestBackupDir () {
192 | const dirs = await this.getBackupDirs()
193 |
194 | const dirsDate = this.convertBackupDirsToDate(dirs)
195 | const minDate = min(dirsDate).getTime()
196 | const dir = dirs[dirsDate.indexOf(minDate)]
197 |
198 | return {
199 | date: minDate,
200 | dir,
201 | path: dir ? path.resolve(this._backupPath, dir) : null
202 | }
203 | }
204 |
205 | async getLatestBackupDir () {
206 | const dirs = await this.getBackupDirs()
207 |
208 | const dirsDate = this.convertBackupDirsToDate(dirs)
209 | const maxDate = max(dirsDate).getTime()
210 | const dir = dirs[dirsDate.indexOf(maxDate)]
211 |
212 | return {
213 | date: maxDate,
214 | dir,
215 | path: dir ? path.resolve(this._backupPath, dir) : null
216 | }
217 | }
218 |
219 | async removeEarliestBackup () {
220 | const dirs = await this.getBackupDirs()
221 | const { path } = await this.getEarliestBackupDir()
222 |
223 | if (dirs.length > this._backupLimit) {
224 | rimraf(path, err => {
225 | if (err) throw Error(err)
226 | })
227 | }
228 | }
229 |
230 | convertBackupDirsToDate (dirs) {
231 | return dirs.map(i => {
232 | const arr = i.split('_').splice(0, 2)
233 | arr[1] = `T${arr[1].replace(/-/g, ':')}`
234 | const date = new Date(arr.join('')).getTime()
235 |
236 | return date
237 | })
238 | }
239 |
240 | async isBackupEmpty () {
241 | let dirs = await this.getBackupDirs()
242 | dirs = dirs.filter(junk.not)
243 |
244 | return dirs.length === 0
245 | }
246 | }
247 |
248 | export default new DataStore()
249 |
--------------------------------------------------------------------------------
/src/renderer/components/uikit/AppInputTags.vue:
--------------------------------------------------------------------------------
1 |
2 |
34 |
35 |
36 |
202 |
203 |
264 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/folders.js:
--------------------------------------------------------------------------------
1 | import db from '@/datastore'
2 | import electronStore from '@@/store'
3 | import shortid from 'shortid'
4 | import { defaultLibraryQuery } from '@/util/helpers'
5 |
6 | export default {
7 | namespaced: true,
8 | state: {
9 | list: [],
10 | selected: null,
11 | selectedId: null,
12 | selectedIds: null,
13 | editableId: null
14 | },
15 | getters: {
16 | folders (state) {
17 | return state.list
18 | },
19 | selectedId (state) {
20 | return state.selectedId
21 | },
22 | selectedIds (state) {
23 | return state.selectedIds
24 | },
25 | editableId (state) {
26 | return state.editableId
27 | },
28 | defaultLanguage (state) {
29 | if (state.selected) {
30 | return state.selected.defaultLanguage
31 | }
32 | },
33 | defaultQueryBySystemFolder (state) {
34 | let query
35 | if (state.selectedId === 'trash') {
36 | query = { isDeleted: true }
37 | }
38 | if (state.selectedId === 'favorites') {
39 | query = { isFavorites: true }
40 | }
41 | if (state.selectedId === 'allSnippets') {
42 | query = {}
43 | }
44 | if (state.selectedId === 'inBox') {
45 | query = { folderId: null }
46 | }
47 |
48 | return query
49 | },
50 | isSystemFolder (state) {
51 | return (
52 | state.selectedId === 'trash' ||
53 | state.selectedId === 'favorites' ||
54 | state.selectedId === 'allSnippets' ||
55 | state.selectedId === 'inBox'
56 | )
57 | }
58 | },
59 | mutations: {
60 | SET_FOLDERS (state, data) {
61 | state.list = data
62 | },
63 | SET_SELECTED_ID (state, id) {
64 | state.selectedId = id
65 | },
66 | SET_SELECTED_IDS (state, ids) {
67 | state.selectedIds = ids
68 | },
69 | SET_SELECTED (state, data) {
70 | state.selected = data
71 | },
72 | SET_EDITABLE (state, id) {
73 | state.editableId = id
74 | }
75 | },
76 | actions: {
77 | getFolders ({ commit }) {
78 | return new Promise((resolve, reject) => {
79 | db.masscode.findOne({ _id: 'folders' }, (err, doc) => {
80 | if (err) return
81 | if (doc) {
82 | commit('SET_FOLDERS', doc.list)
83 | resolve()
84 | }
85 | })
86 | })
87 | },
88 | setSelectedFolder ({ state, commit, dispatch, getters }, id) {
89 | const libraryItems = ['inBox', 'favorites', 'allSnippets', 'trash']
90 |
91 | if (!id) {
92 | commit('SET_SELECTED_ID', null)
93 | electronStore.app.delete('selectedFolderId')
94 | return
95 | }
96 |
97 | commit('SET_SELECTED_ID', id)
98 | electronStore.app.set('selectedFolderId', id)
99 |
100 | if (libraryItems.includes(id)) {
101 | commit('SET_SELECTED', null)
102 | commit('SET_SELECTED_IDS', null)
103 | return
104 | }
105 |
106 | const { list } = state
107 |
108 | function findFolderById (folders, id) {
109 | let found
110 | folders.forEach(i => {
111 | if (i.id === id) found = i
112 |
113 | if (i.children && i.children.length) {
114 | findFolderById(i.children, id)
115 | }
116 | })
117 |
118 | if (found) {
119 | commit('SET_SELECTED', found)
120 | }
121 | }
122 |
123 | findFolderById(list, id)
124 |
125 | dispatch('setSelectedIds')
126 | },
127 | setSelectedIds ({ state, commit }) {
128 | if (!state.selected) return
129 |
130 | const ids = []
131 |
132 | function getIds (arr) {
133 | arr.forEach(i => {
134 | ids.push(i.id)
135 |
136 | if (i.children && i.children.length) {
137 | getIds(i.children)
138 | }
139 | })
140 | }
141 |
142 | getIds([state.selected])
143 |
144 | commit('SET_SELECTED_IDS', ids)
145 | },
146 | addFolder ({ state, commit, dispatch }) {
147 | const folder = {
148 | id: shortid(),
149 | name: 'Untitled',
150 | open: false,
151 | defaultLanguage: 'text'
152 | }
153 |
154 | db.masscode.update(
155 | { _id: 'folders' },
156 | { $push: { list: folder } },
157 | (err, doc) => {
158 | if (err) return
159 |
160 | commit('SET_EDITABLE', folder.id)
161 | commit('SET_SELECTED', folder)
162 | commit('SET_SELECTED_ID', folder.id)
163 | commit('SET_SELECTED_IDS', [folder.id])
164 | dispatch('getFolders')
165 | }
166 | )
167 | },
168 | updateFolderName ({ dispatch, rootGetters }, { id, payload }) {
169 | db.masscode.findOne({ _id: 'folders' }, (err, doc) => {
170 | if (err) return
171 |
172 | const { list } = doc
173 |
174 | function findAndUpdate (arr) {
175 | arr.forEach((i, index) => {
176 | if (i.id === id) {
177 | i.name = payload
178 | }
179 |
180 | if (i.children && i.children.length) {
181 | findAndUpdate(i.children)
182 | }
183 | })
184 | }
185 | findAndUpdate(list)
186 |
187 | const ids = rootGetters['folders/selectedIds']
188 | const folderId = rootGetters['folders/selectedId']
189 | const defaultQuery = { folderId: { $in: ids } }
190 | const query = defaultLibraryQuery(defaultQuery, folderId)
191 |
192 | dispatch('updateFolders', list)
193 | dispatch('snippets/getSnippets', query, { root: true })
194 | })
195 | },
196 | updateFolderLanguage ({ dispatch, rootGetters }, { id, payload }) {
197 | db.masscode.findOne({ _id: 'folders' }, async (err, doc) => {
198 | if (err) return
199 |
200 | const { list } = doc
201 |
202 | function findAndUpdate (arr) {
203 | arr.forEach((i, index) => {
204 | if (i.id === id) {
205 | i.defaultLanguage = payload
206 | }
207 |
208 | if (i.children && i.children.length) {
209 | findAndUpdate(i.children)
210 | }
211 | })
212 | }
213 | findAndUpdate(list)
214 |
215 | await dispatch('updateFolders', list)
216 | const folderId = rootGetters['folders/selectedId']
217 |
218 | if (folderId === id) {
219 | dispatch('setSelectedFolder', id)
220 | }
221 | })
222 | },
223 | updateFolders ({ dispatch }, list) {
224 | return new Promise((resolve, reject) => {
225 | db.masscode.update({ _id: 'folders' }, { list }, async (err, doc) => {
226 | if (err) return
227 | await dispatch('getFolders')
228 | resolve()
229 | })
230 | })
231 | },
232 | deleteFolder ({ state, commit, dispatch, rootGetters }, id) {
233 | const ids = rootGetters['folders/selectedIds']
234 | const folderId = rootGetters['folders/selectedId']
235 | const defaultQuery = { folderId: { $in: ids } }
236 | const query = defaultLibraryQuery(defaultQuery, folderId)
237 | // Перемещаем все сниппеты из удаленной папки,
238 | // включая вложенные сниппеты в подпапках, в корзину
239 | db.snippets.update(
240 | { folderId: { $in: ids } },
241 | { $set: { isDeleted: true } },
242 | { multi: true },
243 | (err, doc) => {
244 | if (err) return
245 | dispatch('snippets/getSnippets', query, { root: true })
246 | }
247 | )
248 | // Удаляем папку, включая все подпапки
249 | db.masscode.findOne({ _id: 'folders' }, (err, doc) => {
250 | if (err) return
251 |
252 | const { list } = doc
253 |
254 | function findAndRemove (arr) {
255 | arr.forEach((i, index) => {
256 | if (i.id === id) {
257 | return arr.splice(index, 1)
258 | }
259 |
260 | if (i.children && i.children.length) {
261 | findAndRemove(i.children)
262 | }
263 | })
264 | }
265 | findAndRemove(list)
266 |
267 | db.masscode.update({ _id: 'folders' }, { list }, (err, doc) => {
268 | if (err) return
269 |
270 | dispatch('getFolders')
271 | dispatch('snippets/setSelected', null, { root: true })
272 | dispatch('setSelectedFolder', 'allSnippets')
273 | })
274 | })
275 | }
276 | }
277 | }
278 |
--------------------------------------------------------------------------------