├── .editorconfig ├── .gitignore ├── LICENSE ├── LICENSE.md ├── README.md ├── _config.yml ├── angular.json ├── common ├── ipc-channels.ts ├── models │ └── updateCheckResult.interface.ts └── utils.ts ├── dev-app-update.yml ├── e2e ├── app.e2e-spec.ts ├── app.po.ts ├── protractor.conf.js └── tsconfig.e2e.json ├── electron-builder.json ├── hexo-note-image-save-image.png ├── hexo-note-image.png ├── hexo-note-logo.svg ├── logo-angular.jpg ├── logo-electron.jpg ├── main.ts ├── main ├── autoUpdater.ts ├── menu.ts └── utils.ts ├── package.json ├── postcss.config.js ├── postinstall-web.js ├── postinstall.js ├── renderer ├── update-progress.html └── update-window.html ├── src ├── app │ ├── Models │ │ ├── Article.interface.ts │ │ ├── Article.ts │ │ ├── Category.interface.ts │ │ ├── Config.Interface.ts │ │ ├── Post.interface.ts │ │ └── Tag.interface.ts │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.spec.ts │ ├── app.component.ts │ ├── app.module.ts │ ├── components │ │ ├── article-list-item │ │ │ ├── article-list-item.component.html │ │ │ ├── article-list-item.component.scss │ │ │ ├── article-list-item.component.spec.ts │ │ │ └── article-list-item.component.ts │ │ ├── article-md-editor │ │ │ ├── article-md-editor.component.html │ │ │ ├── article-md-editor.component.scss │ │ │ ├── article-md-editor.component.spec.ts │ │ │ └── article-md-editor.component.ts │ │ ├── custom-md-editor │ │ │ ├── custom-md-editor.component.html │ │ │ ├── custom-md-editor.component.scss │ │ │ ├── custom-md-editor.component.spec.ts │ │ │ └── custom-md-editor.component.ts │ │ ├── new-article-form │ │ │ ├── new-article-form.component.html │ │ │ ├── new-article-form.component.scss │ │ │ ├── new-article-form.component.spec.ts │ │ │ └── new-article-form.component.ts │ │ ├── new-blog-modal │ │ │ ├── new-blog-modal.component.html │ │ │ ├── new-blog-modal.component.scss │ │ │ ├── new-blog-modal.component.spec.ts │ │ │ └── new-blog-modal.component.ts │ │ ├── rename-article-modal │ │ │ ├── rename-article-modal.component.html │ │ │ ├── rename-article-modal.component.scss │ │ │ ├── rename-article-modal.component.spec.ts │ │ │ └── rename-article-modal.component.ts │ │ ├── save-article-image-modal │ │ │ ├── save-article-image-modal.component.html │ │ │ ├── save-article-image-modal.component.scss │ │ │ ├── save-article-image-modal.component.spec.ts │ │ │ └── save-article-image-modal.component.ts │ │ └── sidebar │ │ │ ├── sidebar.component.html │ │ │ ├── sidebar.component.scss │ │ │ ├── sidebar.component.spec.ts │ │ │ └── sidebar.component.ts │ ├── directives │ │ └── webview.directive.ts │ ├── guard │ │ ├── app-init.guard.spec.ts │ │ ├── app-init.guard.ts │ │ ├── can-deactivate.guard.spec.ts │ │ ├── can-deactivate.guard.ts │ │ ├── config-init.guard.spec.ts │ │ ├── config-init.guard.ts │ │ ├── hexo-init.guard.spec.ts │ │ └── hexo-init.guard.ts │ ├── pages │ │ ├── dashboard │ │ │ ├── article │ │ │ │ ├── article-detail │ │ │ │ │ ├── article-detail.component.html │ │ │ │ │ ├── article-detail.component.scss │ │ │ │ │ ├── article-detail.component.spec.ts │ │ │ │ │ └── article-detail.component.ts │ │ │ │ ├── article.component.html │ │ │ │ ├── article.component.scss │ │ │ │ ├── article.component.spec.ts │ │ │ │ └── article.component.ts │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.spec.ts │ │ │ ├── dashboard.component.ts │ │ │ └── settings │ │ │ │ ├── settings.component.html │ │ │ │ ├── settings.component.scss │ │ │ │ ├── settings.component.spec.ts │ │ │ │ └── settings.component.ts │ │ └── not-project-found │ │ │ ├── not-project-found.component.html │ │ │ ├── not-project-found.component.scss │ │ │ ├── not-project-found.component.spec.ts │ │ │ └── not-project-found.component.ts │ └── services │ │ ├── article.service.spec.ts │ │ ├── article.service.ts │ │ ├── asset.service.spec.ts │ │ ├── asset.service.ts │ │ ├── config.service.spec.ts │ │ ├── config.service.ts │ │ ├── electron.service.ts │ │ ├── hexo.service.spec.ts │ │ ├── hexo.service.ts │ │ ├── scaffold.service.spec.ts │ │ ├── scaffold.service.ts │ │ ├── server.service.spec.ts │ │ ├── server.service.ts │ │ ├── system-settings.service.spec.ts │ │ ├── system-settings.service.ts │ │ ├── utils.service.spec.ts │ │ └── utils.service.ts ├── assets │ ├── .gitkeep │ ├── background.jpg │ ├── hexo-logo.svg │ ├── hexo-note-logo.svg │ └── i18n │ │ └── en.json ├── environments │ ├── environment.common.ts │ ├── environment.dev.ts │ ├── environment.prod.ts │ └── environment.ts ├── favicon.256x256.png ├── favicon.512x512.png ├── favicon.icns ├── favicon.ico ├── favicon.png ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills-test.ts ├── polyfills.ts ├── style │ ├── _color.scss │ ├── _index.scss │ ├── _size.scss │ └── _spacing.scss ├── styles.scss ├── test.ts ├── theme.less ├── tsconfig.app.json ├── tsconfig.spec.json └── typings.d.ts ├── tsconfig-serve.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | /app-builds 8 | /release 9 | main.js 10 | src/**/*.js 11 | *.js.map 12 | /main/**/*.js 13 | /common/**/*.js 14 | 15 | # dependencies 16 | /node_modules 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | testem.log 41 | /typings 42 | package-lock.json 43 | 44 | # e2e 45 | /e2e/*.js 46 | !/e2e/protractor.conf.js 47 | /e2e/*.map 48 | 49 | # System Files 50 | .DS_Store 51 | Thumbs.db 52 | 53 | /private 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2018 - Maxime GRIS 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Hexo-Note 2 | 3 | A Hexojs Client project 4 | 5 | hexojs 客户端 6 | 7 | ## Highlights / 特点 8 | 9 | ![Hexo note](./hexo-note-image.png) 10 | 11 | You Can Save Image!!! 12 | 13 | 可以保存图片!!! 14 | ![Hexo note](./hexo-note-image-save-image.png) 15 | 16 | ## Installation / 安装 17 | 18 | Windows, Mac and AppImage binaries are available in the [Releases / 下载](https://github.com/tmirun/Hexo-Note/releases) 19 | 20 | Or you can also install from repo, 21 | 1. Clone this project to a local folder / 下载项目到本地 22 | 2. Run `npm start` / 打开执行 `npm start` 23 | 3. More scripts are available in [package.json](package.json) / 更多命令看 [package.json](package.json) 24 | 25 | ## Prerequisites / 安装需求 26 | 27 | - Hexo, see the [installation guide](https://hexo.io/docs/) / 请看 [安装教程](https://hexo.io/zh-cn/docs/) 28 | - At least a blog post, see the [writing guide](https://hexo.io/docs/writing) / 创建至少一篇文章,请看 [写作教程](https://hexo.io/zh-cn/docs/writing) 29 | 30 | ## Tech stack / 在这项目中使用了: 31 | - Framework / 框架: [angular6 + electron](https://github.com/maximegris/angular-electron) 32 | - UI / 设计: [ng-zorro ant design](https://github.com/NG-ZORRO/ng-zorro-antd) 33 | - Customize the theme in in `src/theme.less` / 你可以在 `src/theme.less` 自定义主题 34 | - Icon / 图标: [font awesome](https://fontawesome.com/icons?from=io) 35 | - Editor / 写作框: [code mirror](https://codemirror.net/) 36 | 37 | ## Architecture / 软件框架图 38 | TODO 39 | 40 | ## Feedback 41 | 42 | If you have any opinion or suggestion for improvement, please raise an [Issue](https://github.com/tmirun/Hexo-Note/issues) and we will discuss it together. 43 | 44 | You can write it with English, Spanish or Chinese. 45 | 46 | 如果你有什么想法 意见 或者 改善的地方 可以直接写到 [Issue](https://github.com/tmirun/Hexo-Note/issues) 里,我们来一起讨论. 47 | 48 | This documentation is work in progress, if you found any mistake, tell me please. Thanks! 49 | 50 | ## Contribute / 开发: 51 | 52 | Contact me at tmirun@hotmail.com if you wish to develop this program with me. 53 | 54 | 如果哪位同学用兴趣一起完善这个项目,请联系我 tmirun@hotmail.com 55 | 56 | ## TODO LIST 57 | 公开 trello [地址](https://trello.com/b/F20B7ufQ) 58 | 59 | * [x] 获取 hexo 当地地址 DONE 60 | * [x] 获取文章(区分草稿和已发布) DONE 61 | * [x] 新增文章 DONE 62 | * [x] 编辑文章 DONE 63 | * [x] 删除文章 DONE 64 | * [ ] 强化 mk 编译器, 65 | * [x] 添加 toolbar DONE 66 | * [ ] 可自定义 toolbar 67 | * [x] 可以显示本地文章图片(用{% %}方法加的) DONE 68 | * [x] 添加 Read More DONE 69 | * [x] 黏贴 imagen: 在 post asset 自动创建如果打开的话 70 | * [x] 打开文章的图片文件 71 | * [ ] 分类 72 | * [ ] Tag 73 | * [ ] 搜索文章 74 | * [x] 标题 DONE 75 | * [ ] 按分类 76 | * [ ] 按 Tag 77 | * [x] 热键 78 | * [x] 保存 cmd + s / ctrl + s, 79 | * [x] 黑体 cmd + b / ctrl + b, 80 | * [x] italic cmd + i / ctrl + i, 81 | * [x] h1-h6 cmd + [1-6] / ctrl + [1-6], 82 | * [x] 启动 HEXO 服务器 DONE 83 | * [x] 软件偏好设置 -- yml DONE 84 | * [x] 一键编译、发布博客 DONE 85 | * [x] 预览博客 DONE 86 | * [ ] 操作日志记录 87 | * [x] 将文章保存为草稿 DONE 88 | * [ ] 自动保存文章 89 | * [ ] 多语言 90 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "angular-electron": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "architect": { 11 | "build": { 12 | "builder": "@angular-devkit/build-angular:browser", 13 | "options": { 14 | "outputPath": "dist", 15 | "index": "src/index.html", 16 | "main": "src/main.ts", 17 | "tsConfig": "src/tsconfig.app.json", 18 | "polyfills": "src/polyfills.ts", 19 | "assets": [ 20 | "src/assets", 21 | "src/favicon.ico", 22 | "src/favicon.png", 23 | "src/favicon.icns", 24 | "src/favicon.256x256.png", 25 | "src/favicon.512x512.png", 26 | { 27 | "glob": "**/*", 28 | "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/", 29 | "output": "/assets/" 30 | } 31 | ], 32 | "styles": [ 33 | "src/theme.less", 34 | "src/styles.scss", 35 | "node_modules/prismjs/themes/prism-okaidia.css", 36 | "node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss", 37 | "node_modules/@fortawesome/fontawesome-free/scss/solid.scss", 38 | "node_modules/@fortawesome/fontawesome-free/scss/brands.scss" 39 | ], 40 | "scripts": [ 41 | "node_modules/prismjs/prism.js", 42 | "node_modules/marked/lib/marked.js" 43 | ] 44 | }, 45 | "configurations": { 46 | "dev": { 47 | "optimization": false, 48 | "outputHashing": "all", 49 | "sourceMap": true, 50 | "extractCss": true, 51 | "namedChunks": false, 52 | "aot": false, 53 | "extractLicenses": true, 54 | "vendorChunk": false, 55 | "buildOptimizer": false, 56 | "fileReplacements": [ 57 | { 58 | "replace": "src/environments/environment.ts", 59 | "with": "src/environments/environment.dev.ts" 60 | } 61 | ] 62 | }, 63 | "production": { 64 | "optimization": true, 65 | "outputHashing": "all", 66 | "sourceMap": false, 67 | "extractCss": true, 68 | "namedChunks": false, 69 | "aot": true, 70 | "extractLicenses": true, 71 | "vendorChunk": false, 72 | "buildOptimizer": true, 73 | "fileReplacements": [ 74 | { 75 | "replace": "src/environments/environment.ts", 76 | "with": "src/environments/environment.prod.ts" 77 | } 78 | ] 79 | } 80 | } 81 | }, 82 | "serve": { 83 | "builder": "@angular-devkit/build-angular:dev-server", 84 | "options": { 85 | "browserTarget": "angular-electron:build" 86 | }, 87 | "configurations": { 88 | "dev": { 89 | "browserTarget": "angular-electron:build:dev" 90 | }, 91 | "production": { 92 | "browserTarget": "angular-electron:build:production" 93 | } 94 | } 95 | }, 96 | "extract-i18n": { 97 | "builder": "@angular-devkit/build-angular:extract-i18n", 98 | "options": { 99 | "browserTarget": "angular-electron:build" 100 | } 101 | }, 102 | "test": { 103 | "builder": "@angular-devkit/build-angular:karma", 104 | "options": { 105 | "main": "src/test.ts", 106 | "polyfills": "src/polyfills-test.ts", 107 | "tsConfig": "src/tsconfig.spec.json", 108 | "karmaConfig": "src/karma.conf.js", 109 | "assets": [ 110 | "src/assets", 111 | "src/favicon.ico", 112 | "src/favicon.png", 113 | "src/favicon.icns", 114 | "src/favicon.256x256.png", 115 | "src/favicon.512x512.png" 116 | ], 117 | "styles": [ 118 | "src/theme.less", 119 | "src/styles.scss", 120 | "node_modules/prismjs/themes/prism-okaidia.css", 121 | "node_modules/@fortawesome/fontawesome-free/scss/fontawesome.scss", 122 | "node_modules/@fortawesome/fontawesome-free/scss/solid.scss", 123 | "node_modules/@fortawesome/fontawesome-free/scss/brands.scss" 124 | ], 125 | "scripts": [ 126 | "node_modules/prismjs/prism.js", 127 | "node_modules/marked/lib/marked.js" 128 | ] 129 | } 130 | }, 131 | "lint": { 132 | "builder": "@angular-devkit/build-angular:tslint", 133 | "options": { 134 | "tsConfig": [ 135 | "src/tsconfig.app.json", 136 | "src/tsconfig.spec.json" 137 | ], 138 | "exclude": [ 139 | "**/node_modules/**" 140 | ] 141 | } 142 | } 143 | } 144 | }, 145 | "angular-electron-e2e": { 146 | "root": "e2e", 147 | "projectType": "application", 148 | "architect": { 149 | "e2e": { 150 | "builder": "@angular-devkit/build-angular:protractor", 151 | "options": { 152 | "protractorConfig": "e2e/protractor.conf.js", 153 | "devServerTarget": "angular-electron:serve" 154 | } 155 | }, 156 | "lint": { 157 | "builder": "@angular-devkit/build-angular:tslint", 158 | "options": { 159 | "tsConfig": [ 160 | "e2e/tsconfig.e2e.json" 161 | ], 162 | "exclude": [ 163 | "**/node_modules/**" 164 | ] 165 | } 166 | } 167 | } 168 | } 169 | }, 170 | "defaultProject": "angular-electron", 171 | "schematics": { 172 | "@schematics/angular:component": { 173 | "prefix": "app", 174 | "styleext": "scss" 175 | }, 176 | "@schematics/angular:directive": { 177 | "prefix": "app" 178 | } 179 | } 180 | } -------------------------------------------------------------------------------- /common/ipc-channels.ts: -------------------------------------------------------------------------------- 1 | export const IPC_CHANNELS = { 2 | AUTO_UPLOAD_DOWNLOAD_PROGRESS: 'auto-upload-download-progress', 3 | AUTO_UPLOAD_ACCEPT: 'auto-upload-accept' 4 | }; 5 | -------------------------------------------------------------------------------- /common/models/updateCheckResult.interface.ts: -------------------------------------------------------------------------------- 1 | export interface UpdateCheckResult { 2 | version: string; 3 | files: any[]; 4 | path: string; 5 | sha512: string; 6 | releaseDate: string; 7 | releaseName: string; 8 | releaseNotes: string; 9 | } 10 | -------------------------------------------------------------------------------- /common/utils.ts: -------------------------------------------------------------------------------- 1 | import isDev from 'electron-is-dev'; 2 | 3 | export const utils = { 4 | isDev() { return isDev; }, 5 | 6 | isPro() { return !this.isDev(); } 7 | }; 8 | -------------------------------------------------------------------------------- /dev-app-update.yml: -------------------------------------------------------------------------------- 1 | owner: tmirun 2 | repo: Hexo-Note 3 | provider: github 4 | -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AngularElectronPage } from './app.po'; 2 | import { browser, element, by } from 'protractor'; 3 | 4 | describe('angular-electron App', () => { 5 | let page: AngularElectronPage; 6 | 7 | beforeEach(() => { 8 | page = new AngularElectronPage(); 9 | }); 10 | 11 | it('should display message saying App works !', () => { 12 | page.navigateTo('/'); 13 | expect(element(by.css('app-home h1')).getText()).toMatch('App works !'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /e2e/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, element, by } from 'protractor'; 2 | 3 | /* tslint:disable */ 4 | export class AngularElectronPage { 5 | navigateTo(route: string) { 6 | return browser.get(route); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 25000, 8 | delayBrowserTimeInSeconds: 0, 9 | specs: [ 10 | './**/*.e2e-spec.ts' 11 | ], 12 | capabilities: { 13 | 'browserName': 'chrome', 14 | chromeOptions: { 15 | args: ["--no-sandbox", "--headless", "--disable-gpu"] 16 | } 17 | }, 18 | chromeOnly: true, 19 | directConnect: true, 20 | baseUrl: 'http://localhost:4200/', 21 | framework: 'jasmine2', 22 | jasmineNodeOpts: { 23 | showColors: true, 24 | defaultTimeoutInterval: 30000, 25 | print: function () { }, 26 | realtimeFailure: true 27 | }, 28 | useAllAngular2AppRoots: true, 29 | beforeLaunch: function () { 30 | require('ts-node').register({ 31 | project: 'e2e/tsconfig.e2e.json' 32 | }); 33 | }, 34 | onPrepare() { 35 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types":[ 8 | "jasmine", 9 | "node" 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /electron-builder.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "Hexo Note", 3 | "directories": { 4 | "output": "release/" 5 | }, 6 | "files": [ 7 | "**/*", 8 | "!*.ts", 9 | "!*.code-workspace", 10 | "!LICENSE.md", 11 | "!package.json", 12 | "!package-lock.json", 13 | "!src/", 14 | "!e2e/", 15 | "!hooks/", 16 | "!.angular-cli.json", 17 | "!_config.yml", 18 | "!karma.conf.js", 19 | "!tsconfig.json", 20 | "!tslint.json", 21 | "!private/" 22 | ], 23 | "win": { 24 | "publish": ["github"], 25 | "icon": "dist", 26 | "certificateFile": "private/hexo-note.p12", 27 | "verifyUpdateCodeSignature": false, 28 | "target": [ 29 | "nsis" 30 | ] 31 | }, 32 | "mac": { 33 | "publish": ["github"], 34 | "icon": "dist", 35 | "category": "public.app-category.productivity", 36 | "target": [ 37 | "dmg", 38 | "zip" 39 | ] 40 | }, 41 | "linux": { 42 | "publish": ["github"], 43 | "icon": "dist", 44 | "category": "Office", 45 | "target": [ 46 | "AppImage" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /hexo-note-image-save-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/hexo-note-image-save-image.png -------------------------------------------------------------------------------- /hexo-note-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/hexo-note-image.png -------------------------------------------------------------------------------- /hexo-note-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /logo-angular.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/logo-angular.jpg -------------------------------------------------------------------------------- /logo-electron.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/logo-electron.jpg -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, screen, shell } from 'electron'; 2 | import * as path from 'path'; 3 | import * as url from 'url'; 4 | import { autoUploadCheck } from './main/autoUpdater'; 5 | import { createMenu } from './main/menu'; 6 | import { utils } from './main/utils'; 7 | 8 | let win, serve; 9 | const args = process.argv.slice(1); 10 | serve = args.some(val => val === '--serve'); 11 | 12 | function createWindow() { 13 | 14 | const electronScreen = screen; 15 | const size = electronScreen.getPrimaryDisplay().workAreaSize; 16 | 17 | // Create the browser window. 18 | win = new BrowserWindow({ 19 | x: 0, 20 | y: 0, 21 | width: size.width, 22 | height: size.height, 23 | webPreferences: { 24 | devTools: true 25 | } 26 | }); 27 | 28 | // Open In Browser External Links 29 | win.webContents.on('will-navigate', function(e, openedUrl) { 30 | if (openedUrl.includes('http')) { 31 | e.preventDefault(); 32 | shell.openExternal(openedUrl); 33 | } 34 | }); 35 | 36 | if (serve) { 37 | require('electron-reload')(__dirname, { 38 | electron: require(`${__dirname}/node_modules/electron`) 39 | }); 40 | win.loadURL('http://localhost:4200'); 41 | } else { 42 | win.loadURL(url.format({ 43 | pathname: path.join(__dirname, 'dist/index.html'), 44 | protocol: 'file:', 45 | slashes: true 46 | })); 47 | } 48 | 49 | if (utils.isDev()) { 50 | win.webContents.openDevTools(); 51 | } 52 | 53 | // Emitted when the window is closed. 54 | win.on('closed', () => { 55 | // Dereference the window object, usually you would store window 56 | // in an array if your app supports multi windows, this is the time 57 | // when you should delete the corresponding element. 58 | win = null; 59 | }); 60 | } 61 | 62 | try { 63 | 64 | // This method will be called when Electron has finished 65 | // initialization and is ready to create browser windows. 66 | // Some APIs can only be used after this event occurs. 67 | app.on('ready', () => { 68 | createWindow(); 69 | createMenu(); 70 | if (utils.isPro()) { 71 | setTimeout(autoUploadCheck, 2000); 72 | } 73 | }); 74 | 75 | // Quit when all windows are closed. 76 | app.on('window-all-closed', () => { 77 | // On OS X it is common for applications and their menu bar 78 | // to stay active until the user quits explicitly with Cmd + Q 79 | if (process.platform !== 'darwin') { 80 | app.quit(); 81 | } 82 | }); 83 | 84 | app.on('activate', () => { 85 | // On OS X it's common to re-create a window in the app when the 86 | // dock icon is clicked and there are no other windows open. 87 | if (win === null) { 88 | createWindow(); 89 | } 90 | }); 91 | 92 | } catch (e) { 93 | // Catch Error 94 | // throw e; 95 | } 96 | -------------------------------------------------------------------------------- /main/autoUpdater.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from 'electron-updater'; 2 | import * as electronLog from 'electron-log'; 3 | import { dialog, BrowserWindow, ipcMain } from 'electron'; 4 | import { UpdateCheckResult } from '../common/models/updateCheckResult.interface'; 5 | import { IPC_CHANNELS } from '../common/ipc-channels'; 6 | 7 | autoUpdater.logger = electronLog as any; 8 | 9 | export function autoUploadCheck() { 10 | autoUpdater.checkForUpdates(); 11 | 12 | // if exist new version 13 | autoUpdater.on('update-available', ( result: UpdateCheckResult) => { 14 | 15 | // create update information window 16 | let updateWin = new BrowserWindow({ 17 | width: 500, 18 | height: 300, 19 | autoHideMenuBar: true, 20 | maximizable: false, 21 | fullscreen: false, 22 | fullscreenable: false, 23 | resizable: false 24 | }); 25 | 26 | updateWin['custom'] = result; 27 | 28 | updateWin.loadURL(`file://${__dirname}/../renderer/update-window.html`); 29 | 30 | updateWin.on('close', () => { updateWin = null; }); 31 | 32 | ipcMain.on(IPC_CHANNELS.AUTO_UPLOAD_ACCEPT, (event) => { 33 | openDownloadProgressWindow(); 34 | event.returnValue = true; 35 | }); 36 | 37 | function openDownloadProgressWindow() { 38 | // creating progress window; 39 | let progressWin = new BrowserWindow({ 40 | width: 400, 41 | height: 80, 42 | autoHideMenuBar: true, 43 | maximizable: false, 44 | fullscreen: false, 45 | fullscreenable: false, 46 | resizable: false 47 | }); 48 | 49 | progressWin.loadURL(`file://${__dirname}/../renderer/update-progress.html`); 50 | 51 | progressWin.on('close', () => { progressWin = null; }); 52 | 53 | // sent progress percent to window; 54 | let progressPercent = 0; 55 | 56 | ipcMain.on(IPC_CHANNELS.AUTO_UPLOAD_DOWNLOAD_PROGRESS, (event) => { 57 | event.returnValue = progressPercent; 58 | }); 59 | 60 | autoUpdater.on('download-progress', (download) => { 61 | progressPercent = download.percent; 62 | }); 63 | 64 | autoUpdater.on('update-downloaded', () => { 65 | if (progressWin) { progressWin.close(); } 66 | 67 | dialog.showMessageBox( { 68 | type: 'info', 69 | title: 'Update Ready', 70 | message: 'Then new version is downloaded, Quit and install now?', 71 | buttons: ['Yes', 'Late'], 72 | }, 73 | (buttonIndex) => { 74 | if (buttonIndex === 0) { autoUpdater.quitAndInstall(); } 75 | }); 76 | 77 | }); 78 | } 79 | 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /main/menu.ts: -------------------------------------------------------------------------------- 1 | import { app, Menu } from 'electron'; 2 | 3 | export function createMenu() { 4 | const template = [ 5 | { 6 | label: 'Edit', 7 | submenu: [ 8 | {role: 'undo'}, 9 | {role: 'redo'}, 10 | {type: 'separator'}, 11 | {role: 'cut'}, 12 | {role: 'copy'}, 13 | {role: 'paste'}, 14 | {role: 'pasteandmatchstyle'}, 15 | {role: 'delete'}, 16 | {role: 'selectall'} 17 | ] 18 | }, 19 | { 20 | label: 'View', 21 | submenu: [ 22 | {role: 'reload'}, 23 | {role: 'forcereload'}, 24 | {role: 'toggledevtools'}, 25 | {type: 'separator'}, 26 | {role: 'resetzoom'}, 27 | {role: 'zoomin'}, 28 | {role: 'zoomout'}, 29 | {type: 'separator'}, 30 | {role: 'togglefullscreen'} 31 | ] 32 | }, 33 | { 34 | role: 'window', 35 | submenu: [ 36 | {role: 'minimize'}, 37 | {role: 'close'} 38 | ] 39 | }, 40 | { 41 | role: 'help', 42 | submenu: [ 43 | { 44 | label: 'Learn More', 45 | click () { require('electron').shell.openExternal('https://electronjs.org') } 46 | } 47 | ] 48 | } 49 | ] as any; 50 | 51 | if (process.platform === 'darwin') { 52 | template.unshift( { 53 | label: app.getName(), 54 | submenu: [ 55 | {role: 'about'}, 56 | {type: 'separator'}, 57 | {role: 'services', submenu: []}, 58 | {type: 'separator'}, 59 | {role: 'hide'}, 60 | {role: 'hideothers'}, 61 | {role: 'unhide'}, 62 | {type: 'separator'}, 63 | {role: 'quit'} 64 | ] 65 | }); 66 | 67 | // Edit menu 68 | template[1].submenu.push( 69 | {type: 'separator'}, 70 | { 71 | label: 'Speech', 72 | submenu: [ 73 | {role: 'startspeaking'}, 74 | {role: 'stopspeaking'} 75 | ] 76 | } 77 | ); 78 | 79 | // Window menu 80 | template[3].submenu = [ 81 | {role: 'close'}, 82 | {role: 'minimize'}, 83 | {role: 'zoom'}, 84 | {type: 'separator'}, 85 | {role: 'front'} 86 | ]; 87 | } 88 | 89 | Menu.setApplicationMenu(Menu.buildFromTemplate(template)); 90 | } 91 | -------------------------------------------------------------------------------- /main/utils.ts: -------------------------------------------------------------------------------- 1 | import { utils as commonUtils } from '../common/utils'; 2 | 3 | export const utils = { 4 | ...commonUtils 5 | }; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hexo-note", 3 | "version": "0.4.0", 4 | "productName": "hexo-note", 5 | "description": "Hexo Note is a desktop client for Hexo.js", 6 | "homepage": "", 7 | "author": { 8 | "name": "Tmirun", 9 | "email": "tmirun@hotmail.com" 10 | }, 11 | "keywords": [ 12 | "hexo", 13 | "hexo note", 14 | "hexonote", 15 | "hexo client", 16 | "hexoclient", 17 | "hexojs", 18 | "angular", 19 | "angular 6", 20 | "electron", 21 | "typescript", 22 | "sass" 23 | ], 24 | "main": "main.js", 25 | "private": true, 26 | "scripts": { 27 | "postinstall": "npm run postinstall:electron && npx electron-builder install-app-deps", 28 | "postinstall:web": "node postinstall-web", 29 | "postinstall:electron": "node postinstall", 30 | "ng": "ng", 31 | "start": "npm run postinstall:electron && npm-run-all -p ng:serve electron:serve", 32 | "build": "npm run postinstall:electron && npm run electron:serve-tsc && ng build", 33 | "build:dev": "npm run build -- -c dev", 34 | "build:prod": "npm run build -- -c production", 35 | "ng:serve": "ng serve", 36 | "ng:serve:web": "npm run postinstall:web && ng serve -o", 37 | "electron:serve-tsc": "tsc -p tsconfig-serve.json", 38 | "electron:serve": "wait-on http-get://localhost:4200/ && npm run electron:serve-tsc && electron . --serve", 39 | "electron:local": "npm run build:prod && electron .", 40 | "electron:linux": "npm run build:prod && npx electron-builder build --linux", 41 | "electron:windows": "npm run build:prod && npx electron-builder build --windows", 42 | "electron:mac": "npm run build:prod && npx electron-builder build --mac", 43 | "electron:all": "npm run build:prod && npx electron-builder build -wml", 44 | "publish:linux": "npm run build:prod && npx electron-builder build -l -p'onTagOrDraft'", 45 | "publish:windows": "npm run build:prod && npx electron-builder build -w -p 'onTagOrDraft'", 46 | "publish:mac": "npm run build:prod && npx electron-builder build -m -p 'onTagOrDraft'", 47 | "publish:all": "npm run build:prod && npx electron-builder build -wml -p 'onTagOrDraft'", 48 | "test": "npm run postinstall:web && ng test", 49 | "e2e": "npm run postinstall:web && ng e2e" 50 | }, 51 | "dependencies": { 52 | "electron-is-dev": "^1.0.1", 53 | "electron-log": "^2.2.17", 54 | "electron-reload": "1.2.2", 55 | "electron-settings": "^3.2.0", 56 | "electron-updater": "^4.0.6", 57 | "fix-path": "^2.1.0", 58 | "fs-extra": "^7.0.0", 59 | "hexo": "^3.7.1", 60 | "jquery": "^3.3.1", 61 | "tslib": "^1.9.0" 62 | }, 63 | "devDependencies": { 64 | "@angular-devkit/build-angular": "~0.12.0", 65 | "@angular/animations": "^7.2.0", 66 | "@angular/cli": "7.2.1", 67 | "@angular/common": "7.2.0", 68 | "@angular/compiler": "7.2.0", 69 | "@angular/compiler-cli": "7.2.0", 70 | "@angular/core": "7.2.0", 71 | "@angular/flex-layout": "^7.0.0-beta.23", 72 | "@angular/forms": "7.2.0", 73 | "@angular/http": "7.2.0", 74 | "@angular/language-service": "7.2.0", 75 | "@angular/platform-browser": "7.2.0", 76 | "@angular/platform-browser-dynamic": "7.2.0", 77 | "@angular/router": "7.2.0", 78 | "@ctrl/ngx-codemirror": "^1.3.8", 79 | "@fortawesome/fontawesome-free": "^5.3.1", 80 | "@ngx-translate/core": "10.0.2", 81 | "@ngx-translate/http-loader": "3.0.1", 82 | "@types/jasmine": "2.8.8", 83 | "@types/jasminewd2": "2.0.3", 84 | "@types/node": "8.9.4", 85 | "@types/promise.prototype.finally": "^2.0.2", 86 | "chokidar": "^2.0.4", 87 | "codelyzer": "4.2.1", 88 | "codemirror": "^5.40.2", 89 | "core-js": "2.5.6", 90 | "electron": "4.0.1", 91 | "electron-builder": "^20.38.4", 92 | "jasmine-core": "3.1.0", 93 | "jasmine-spec-reporter": "4.2.1", 94 | "js-yaml": "^3.12.0", 95 | "karma": "2.0.2", 96 | "karma-chrome-launcher": "2.2.0", 97 | "karma-coverage-istanbul-reporter": "2.0.1", 98 | "karma-jasmine": "1.1.2", 99 | "karma-jasmine-html-reporter": "1.1.0", 100 | "less": "^2.7.3", 101 | "moment": "^2.22.2", 102 | "ng-zorro-antd": "^7.0.0-rc.3", 103 | "ngx-markdown": "^6.2.0", 104 | "npm-run-all": "4.1.3", 105 | "npx": "10.2.0", 106 | "promise.prototype.finally": "^3.1.0", 107 | "protractor": "5.3.2", 108 | "rxjs": "6.3.3", 109 | "rxjs-compat": "^6.3.2", 110 | "ts-node": "6.0.3", 111 | "tslint": "5.10.0", 112 | "typescript": "3.2.2", 113 | "uuid": "^3.3.2", 114 | "wait-on": "2.1.0", 115 | "webdriver-manager": "12.0.6", 116 | "zone.js": "0.8.27" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /postinstall-web.js: -------------------------------------------------------------------------------- 1 | // Allow angular using electron module (native node modules) 2 | const fs = require('fs'); 3 | const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js'; 4 | 5 | fs.readFile(f_angular, 'utf8', function (err, data) { 6 | if (err) { 7 | return console.log(err); 8 | } 9 | var result = data.replace(/target: "electron-renderer",/g, ''); 10 | var result = result.replace(/target: "web",/g, ''); 11 | var result = result.replace(/return \{/g, 'return {target: "web",'); 12 | 13 | fs.writeFile(f_angular, result, 'utf8', function (err) { 14 | if (err) return console.log(err); 15 | }); 16 | }); -------------------------------------------------------------------------------- /postinstall.js: -------------------------------------------------------------------------------- 1 | // Allow angular using electron module (native node modules) 2 | const fs = require('fs'); 3 | const f_angular = 'node_modules/@angular-devkit/build-angular/src/angular-cli-files/models/webpack-configs/browser.js'; 4 | 5 | fs.readFile(f_angular, 'utf8', function (err, data) { 6 | if (err) { 7 | return console.log(err); 8 | } 9 | var result = data.replace(/target: "electron-renderer",/g, ''); 10 | var result = result.replace(/target: "web",/g, ''); 11 | var result = result.replace(/return \{/g, 'return {target: "electron-renderer",'); 12 | 13 | fs.writeFile(f_angular, result, 'utf8', function (err) { 14 | if (err) return console.log(err); 15 | }); 16 | }); -------------------------------------------------------------------------------- /renderer/update-progress.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Download Hexo Note update 5 | 13 | 14 | 15 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /renderer/update-window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 45 | 46 | 47 |
48 |

v --

49 |

--

50 |

Release Note:

51 |
52 | --- 53 |
54 |
55 | 56 | 57 |
58 |
59 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /src/app/Models/Article.interface.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | 3 | export interface Article { 4 | _id?: string; 5 | raw?: string; // the all file content, have info part and content part 6 | info?: string; // extraction of hexo note part between --- --- 7 | content?: string; // post content part 8 | published?: boolean; 9 | asset_dir?: string; 10 | path?: string; 11 | file?: string; 12 | fileName?: string; 13 | updated?: moment.Moment; 14 | created?: moment.Moment; 15 | 16 | // hexo info 17 | date?: moment.Moment; 18 | title?: string; 19 | tags?: string[]; 20 | categories?: string | string[]; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/Models/Article.ts: -------------------------------------------------------------------------------- 1 | import * as moment from 'moment'; 2 | import * as uuid4 from 'uuid/v4'; 3 | import { Article as ArticleInterface } from './Article.interface'; 4 | import * as yaml from 'js-yaml'; 5 | 6 | export class Article implements ArticleInterface { 7 | public _id?: string; 8 | private _raw?: string; 9 | private _info?: string; 10 | private _content?: string; 11 | private _title?: string; 12 | 13 | public published?: boolean; 14 | public asset_dir?: string; 15 | public path?: string; 16 | public file?: string; 17 | public fileName?: string; 18 | public updated?: moment.Moment; 19 | public created?: moment.Moment; 20 | 21 | // hexo info 22 | public date?: moment.Moment; 23 | public tags?: string[] = []; 24 | public categories?: string | string[] = []; 25 | 26 | constructor( 27 | { title = '', 28 | raw = '', 29 | published = false, 30 | asset_dir = '', 31 | path = '', 32 | file = '', 33 | fileName = '', 34 | updated = moment(), 35 | created = moment(), 36 | date = moment() 37 | }: ArticleInterface) { 38 | 39 | this._id = `${published ? 'post' : 'draft'}-${file}`; 40 | this.created = created; 41 | this.date = date; 42 | if (! moment.isMoment(this.date)) { 43 | this.date = moment(this.date); 44 | } 45 | this.title = String(title); 46 | this.raw = raw; 47 | this.published = published; 48 | this.asset_dir = asset_dir; 49 | this.path = path; 50 | this.file = file; 51 | this.fileName = fileName; 52 | this.updated = updated; 53 | } 54 | 55 | get title(): string { 56 | return this._title; 57 | } 58 | 59 | set title(value: string) { 60 | this._title = String(value); 61 | } 62 | 63 | get raw(): string { 64 | return this._raw; 65 | } 66 | 67 | set raw(value: string) { 68 | this._raw = value; 69 | this._parseArticleInfoAndContent(this._raw); 70 | } 71 | 72 | get info(): string { 73 | return this._info; 74 | } 75 | 76 | set info(value: string) { 77 | this._info = value; 78 | this._raw = this._info + this._content; 79 | } 80 | 81 | get content(): string { 82 | return this._content; 83 | } 84 | 85 | set content(value: string) { 86 | this._content = value; 87 | this._raw = this._info + '\n' + this._content; 88 | } 89 | 90 | public refreshInfoAndContent() { 91 | this._parseArticleInfoAndContent(this._raw); 92 | } 93 | 94 | private _parseArticleInfoAndContent(raw: string) { 95 | const regex = /(---([.\s\S]+?)---)\n([.\s\S]*)/g; 96 | const match = regex.exec(raw); 97 | const info = match[1]; 98 | const content = match[3]; 99 | try { 100 | const articleInfoItems = yaml.safeLoad(match[2]); 101 | this._parseArticleInfoItems(articleInfoItems); 102 | this._info = info; 103 | this._content = content; 104 | } catch (e) { 105 | console.error(e); 106 | this._content = raw; 107 | } 108 | 109 | } 110 | 111 | private _parseArticleInfoItems(articleInfoItems = {}) { 112 | for (const key in articleInfoItems) { 113 | if (articleInfoItems.hasOwnProperty(key)) { 114 | if (articleInfoItems[key] instanceof Date) { 115 | articleInfoItems[key] = moment(articleInfoItems[key]); 116 | } 117 | 118 | // fix date if some article date string format is error 119 | if (key === 'date' && !(articleInfoItems[key] instanceof Date)) { 120 | articleInfoItems[key] = moment(articleInfoItems[key]); 121 | } 122 | 123 | this[key] = articleInfoItems[key]; 124 | } 125 | } 126 | } 127 | 128 | 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/app/Models/Category.interface.ts: -------------------------------------------------------------------------------- 1 | import { Post } from './Post.interface'; 2 | 3 | export interface PostCategory { 4 | length?: number; 5 | name?: string; 6 | parent?: string; 7 | path?: string; 8 | permalink?: string; 9 | posts?: Post[]; 10 | slug?: string; 11 | _id?: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/Models/Config.Interface.ts: -------------------------------------------------------------------------------- 1 | export interface Config { 2 | // Site 3 | title?: string; 4 | subtitle?: string; 5 | description?: string; 6 | keywords?: string; 7 | author?: string; 8 | language?: string; 9 | timezone?: string; 10 | 11 | // URL 12 | url?: string; 13 | root?: string; 14 | permalink?: string; 15 | permalink_defaults?: string; 16 | 17 | // Directory 18 | source_dir?: string; 19 | public_dir?: string; 20 | tag_dir?: string; 21 | archive_dir?: string; 22 | category_dir?: string; 23 | code_dir?: string; 24 | i18n_dir?: string; 25 | skip_render?: any; 26 | 27 | // Writing 28 | new_post_name?: string; 29 | default_layout?: string; 30 | titlecase?: boolean; 31 | external_link?: boolean; 32 | filename_case?: number; 33 | render_drafts?: boolean; 34 | post_asset_folder?: boolean; 35 | relative_link?: boolean; 36 | future?: boolean; 37 | highlight?: { 38 | enable?: boolean; 39 | line_number?: boolean; 40 | auto_detect?: boolean; 41 | tab_replace?: boolean; 42 | }; 43 | 44 | // Home page setting 45 | index_generator ?: { 46 | path?: string; 47 | per_page?: number; 48 | order_by?: string; 49 | }; 50 | 51 | // Category & Tag 52 | default_category?: string; 53 | category_map?: any; 54 | tag_map?: any; 55 | 56 | // Date / Time format 57 | date_format?: string; 58 | time_format?: string; 59 | 60 | // Pagination 61 | // Set per_page to 0 to disable pagination 62 | per_page?: number; 63 | pagination_dir?: string; 64 | 65 | // Extensions 66 | theme?: string; 67 | 68 | // Deployment 69 | deploy?: { 70 | type?: string; 71 | repo?: string; 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/app/Models/Post.interface.ts: -------------------------------------------------------------------------------- 1 | // HEXO API POST INTERFACE 2 | 3 | import * as moment from 'moment'; 4 | import { PostCategory } from './Category.interface'; 5 | import { PostTag } from './Tag.interface'; 6 | 7 | export interface Post { 8 | asset_dir?: string; 9 | canonical_path?: string; 10 | categories?: { 11 | data?: PostCategory[]; 12 | length?: number 13 | }; 14 | comments?: boolean; 15 | content?: string; 16 | date?: moment.Moment; 17 | excerpt?: string; 18 | full_source?: string; 19 | lang?: string; 20 | layout?: string; 21 | link?: string; 22 | more?: string; 23 | path?: string; 24 | permalink?: string; 25 | photos?: string[]; 26 | published?: boolean; 27 | raw?: string; 28 | site?: { 29 | data?: any 30 | }; 31 | slug?: string; 32 | source?: string; 33 | tags?: PostTag; 34 | title?: string; 35 | updated?: moment.Moment; 36 | __post?: boolean; 37 | _content?: string; 38 | _id?: string; 39 | } 40 | -------------------------------------------------------------------------------- /src/app/Models/Tag.interface.ts: -------------------------------------------------------------------------------- 1 | import { Post } from './Post.interface'; 2 | 3 | export interface PostTag { 4 | length: number; 5 | name: string; 6 | parent: string; 7 | path: string; 8 | permalink: string; 9 | posts: Post[]; 10 | slug: string; 11 | _id: string; 12 | } 13 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { DashboardComponent } from './pages/dashboard/dashboard.component'; 2 | import { ArticleComponent } from './pages/dashboard/article/article.component'; 3 | import { ArticleDetailComponent } from './pages/dashboard/article/article-detail/article-detail.component'; 4 | import { NgModule } from '@angular/core'; 5 | import { Routes, RouterModule } from '@angular/router'; 6 | import { SettingsComponent } from './pages/dashboard/settings/settings.component'; 7 | import { CanDeactivateGuard } from './guard/can-deactivate.guard'; 8 | import { AppInitGuard } from './guard/app-init.guard'; 9 | import { ConfigInitGuard} from './guard/config-init.guard'; 10 | import { NotProjectFoundComponent } from './pages/not-project-found/not-project-found.component'; 11 | 12 | const routes: Routes = [ 13 | { 14 | path: 'not-project-found', 15 | component: NotProjectFoundComponent, 16 | }, 17 | { 18 | path: 'dashboard', 19 | component: DashboardComponent, 20 | canActivate: [ AppInitGuard ], 21 | children: [ 22 | { 23 | path: 'settings', 24 | component: SettingsComponent 25 | }, 26 | { 27 | path: 'article', 28 | component: ArticleComponent, 29 | canActivate: [ ConfigInitGuard ], 30 | children: [ 31 | { path: ':id', 32 | component: ArticleDetailComponent, 33 | canDeactivate: [CanDeactivateGuard], 34 | }, 35 | ] 36 | }, 37 | ] 38 | }, 39 | { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, 40 | ]; 41 | 42 | @NgModule({ 43 | imports: [RouterModule.forRoot(routes, { useHash: true})], 44 | exports: [RouterModule] 45 | }) 46 | export class AppRoutingModule { } 47 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | @import "~@fortawesome/fontawesome-free/scss/fontawesome.scss"; 2 | 3 | :host { 4 | .app-content-loading { 5 | position: fixed; 6 | height: 70px; 7 | width: 70px; 8 | top: 50%; 9 | left: 50%; 10 | transform: translate(-50%,-50%); 11 | background: rgba(200, 200, 200, 0.2); 12 | } 13 | 14 | .app-content-loading-icon { 15 | font-size: 50px; 16 | margin-left: auto; 17 | margin-right: auto; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | import { TranslateModule } from '@ngx-translate/core'; 5 | import { ElectronService } from './services/electron.service'; 6 | 7 | describe('AppComponent', () => { 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [ 11 | AppComponent 12 | ], 13 | providers: [ 14 | ElectronService 15 | ], 16 | imports: [ 17 | RouterTestingModule, 18 | TranslateModule.forRoot() 19 | ] 20 | }).compileComponents(); 21 | })); 22 | 23 | it('should create the app', async(() => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.debugElement.componentInstance; 26 | expect(app).toBeTruthy(); 27 | })); 28 | }); 29 | 30 | class TranslateServiceStub { 31 | setDefaultLang(lang: string): void { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { ElectronService } from './services/electron.service'; 3 | import { TranslateService } from '@ngx-translate/core'; 4 | import { AppConfig } from '../environments/environment'; 5 | import { HexoService } from './services/hexo.service'; 6 | import { ScaffoldService } from './services/scaffold.service'; 7 | import { timer } from 'rxjs'; 8 | import 'rxjs/add/operator/delay'; 9 | import 'rxjs/add/operator/delayWhen'; 10 | import {ConfigService} from './services/config.service'; 11 | import {Router} from '@angular/router'; 12 | 13 | @Component({ 14 | selector: 'app-root', 15 | templateUrl: './app.component.html', 16 | styleUrls: ['./app.component.scss'] 17 | }) 18 | export class AppComponent { 19 | 20 | public isLoading = false; 21 | 22 | constructor( 23 | public electronService: ElectronService, 24 | private hexoService: HexoService, 25 | private router: Router, 26 | private configService: ConfigService, 27 | private translate: TranslateService) { 28 | 29 | translate.setDefaultLang('en'); 30 | console.log('AppConfig', AppConfig); 31 | 32 | if (electronService.isElectron()) { 33 | console.log('Mode electron'); 34 | } else { 35 | console.log('Mode web'); 36 | } 37 | 38 | if (this.hexoService.isCurrentDirectoryProjectFolder()) { 39 | this.router.navigate(['dashboard/article']); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import 'zone.js/dist/zone-mix'; 2 | import 'reflect-metadata'; 3 | import '../polyfills'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { NgModule } from '@angular/core'; 6 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 7 | import { HttpClientModule, HttpClient } from '@angular/common/http'; 8 | import { AppRoutingModule } from './app-routing.module'; 9 | import { NgZorroAntdModule } from 'ng-zorro-antd'; 10 | import { FlexLayoutModule } from '@angular/flex-layout'; 11 | import { CodemirrorModule } from '@ctrl/ngx-codemirror'; 12 | 13 | /** 14 | * register language package 15 | */ 16 | import { registerLocaleData } from '@angular/common'; 17 | import en from '@angular/common/locales/en'; 18 | registerLocaleData(en); 19 | import { NZ_I18N, en_US } from 'ng-zorro-antd'; 20 | import { MarkdownModule } from 'ngx-markdown'; 21 | 22 | 23 | // NG Translate 24 | import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; 25 | import { TranslateHttpLoader } from '@ngx-translate/http-loader'; 26 | 27 | import { ElectronService } from './services/electron.service'; 28 | 29 | import { WebviewDirective } from './directives/webview.directive'; 30 | 31 | import { AppComponent } from './app.component'; 32 | import { SidebarComponent } from './components/sidebar/sidebar.component'; 33 | import { DashboardComponent } from './pages/dashboard/dashboard.component'; 34 | import { HexoService } from './services/hexo.service'; 35 | import { ArticleComponent } from './pages/dashboard/article/article.component'; 36 | import { ArticleService } from './services/article.service'; 37 | import { HexoInitGuard } from './guard/hexo-init.guard'; 38 | import { ArticleDetailComponent } from './pages/dashboard/article/article-detail/article-detail.component'; 39 | import { SystemSettingsService } from './services/system-settings.service'; 40 | import { SettingsComponent } from './pages/dashboard/settings/settings.component'; 41 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 42 | import { ConfigService } from './services/config.service'; 43 | import { UtilsService } from './services/utils.service'; 44 | import { ServerService } from './services/server.service'; 45 | import { NewArticleFormComponent } from './components/new-article-form/new-article-form.component'; 46 | import { CanDeactivateGuard } from './guard/can-deactivate.guard'; 47 | import { AppInitGuard } from './guard/app-init.guard'; 48 | import { ArticleListItemComponent } from './components/article-list-item/article-list-item.component'; 49 | import { RenameArticleModalComponent } from './components/rename-article-modal/rename-article-modal.component'; 50 | import { SaveArticleImageModalComponent } from './components/save-article-image-modal/save-article-image-modal.component'; 51 | import { AssetService } from './services/asset.service'; 52 | import { ArticleMdEditorComponent } from './components/article-md-editor/article-md-editor.component'; 53 | import { CustomMdEditorComponent } from './components/custom-md-editor/custom-md-editor.component'; 54 | import { NotProjectFoundComponent } from './pages/not-project-found/not-project-found.component'; 55 | import { NewBlogModalComponent } from './components/new-blog-modal/new-blog-modal.component'; 56 | 57 | // AoT requires an exported function for factories 58 | export function HttpLoaderFactory(http: HttpClient) { 59 | return new TranslateHttpLoader(http, './assets/i18n/', '.json'); 60 | } 61 | 62 | @NgModule({ 63 | declarations: [ 64 | AppComponent, 65 | WebviewDirective, 66 | SidebarComponent, 67 | DashboardComponent, 68 | ArticleComponent, 69 | ArticleDetailComponent, 70 | SettingsComponent, 71 | NewArticleFormComponent, 72 | ArticleListItemComponent, 73 | RenameArticleModalComponent, 74 | SaveArticleImageModalComponent, 75 | ArticleMdEditorComponent, 76 | CustomMdEditorComponent, 77 | NotProjectFoundComponent, 78 | NewBlogModalComponent, 79 | ], 80 | entryComponents: [ 81 | RenameArticleModalComponent, 82 | SaveArticleImageModalComponent, 83 | NewBlogModalComponent, 84 | NewArticleFormComponent 85 | ], 86 | imports: [ 87 | BrowserModule, 88 | FormsModule, 89 | ReactiveFormsModule, 90 | HttpClientModule, 91 | AppRoutingModule, 92 | BrowserAnimationsModule, 93 | NgZorroAntdModule, 94 | FlexLayoutModule, 95 | CodemirrorModule, 96 | MarkdownModule.forRoot(), 97 | TranslateModule.forRoot({ 98 | loader: { 99 | provide: TranslateLoader, 100 | useFactory: (HttpLoaderFactory), 101 | deps: [HttpClient] 102 | } 103 | }) 104 | ], 105 | providers: [ 106 | AssetService, 107 | ElectronService, 108 | HexoService, 109 | ArticleService, 110 | ConfigService, 111 | SystemSettingsService, 112 | UtilsService, 113 | HexoInitGuard, 114 | CanDeactivateGuard, 115 | AppInitGuard, 116 | ServerService, 117 | { provide: NZ_I18N, useValue: en_US } 118 | ], 119 | bootstrap: [AppComponent], 120 | }) 121 | export class AppModule { } 122 | -------------------------------------------------------------------------------- /src/app/components/article-list-item/article-list-item.component.html: -------------------------------------------------------------------------------- 1 |
4 |
6 | 7 |
8 |
{{article.file}}
9 |
{{article.date.format('YYYY-MM-DD')}}
10 |
11 |
12 |
13 | 14 | 15 | 27 | 28 |
29 |
30 |
31 |
32 | -------------------------------------------------------------------------------- /src/app/components/article-list-item/article-list-item.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../styles"; 2 | 3 | .article-list-item { 4 | cursor: pointer; 5 | padding-left: $spacing-m; 6 | padding-right: $spacing-m; 7 | 8 | &.active { 9 | outline: none; 10 | background: $grey-3; 11 | } 12 | 13 | &:active { 14 | outline: none; 15 | } 16 | 17 | &:hover { 18 | background-color: $grey-2; 19 | .article-list-item-actions { 20 | display: block !important; 21 | } 22 | } 23 | 24 | .article-list-item-container { 25 | padding-top: $spacing-m; 26 | padding-bottom: $spacing-m; 27 | border-bottom: thin solid $grey-3; 28 | 29 | /deep/ .ant-list-item-content { 30 | overflow: hidden; 31 | } 32 | 33 | .article-list-item-icon { 34 | padding-right: $spacing-s; 35 | } 36 | 37 | .article-list-item-info { 38 | white-space: nowrap; 39 | overflow: hidden; 40 | text-overflow: ellipsis; 41 | 42 | .article-list-item-title { 43 | white-space: nowrap; 44 | overflow: hidden; 45 | text-overflow: ellipsis; 46 | color: $blue-6; 47 | } 48 | 49 | .article-list-item-subtitle { 50 | color: $grey-6; 51 | } 52 | } 53 | 54 | .article-list-item-actions { 55 | cursor: pointer; 56 | display: none; 57 | 58 | > * { 59 | margin-left: $spacing-xxs; 60 | } 61 | 62 | .article-list-item-actions-more { 63 | border-color: transparent; 64 | color: $grey-9; 65 | } 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/app/components/article-list-item/article-list-item.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleListItemComponent } from './article-list-item.component'; 4 | 5 | describe('ArticleListItemComponent', () => { 6 | let component: ArticleListItemComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleListItemComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleListItemComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/article-list-item/article-list-item.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnDestroy, OnInit} from '@angular/core'; 2 | import {Article} from '../../Models/Article'; 3 | import {NzMessageService, NzModalService} from 'ng-zorro-antd'; 4 | import {ArticleService} from '../../services/article.service'; 5 | import {RenameArticleModalComponent} from '../rename-article-modal/rename-article-modal.component'; 6 | import {ConfigService} from '../../services/config.service'; 7 | import {Subscription} from 'rxjs'; 8 | 9 | @Component({ 10 | selector: 'app-article-list-item', 11 | templateUrl: './article-list-item.component.html', 12 | styleUrls: ['./article-list-item.component.scss'] 13 | }) 14 | export class ArticleListItemComponent implements OnInit, OnDestroy { 15 | 16 | @Input() article: Article; 17 | 18 | public isPostAssetFolderActive = false; 19 | private configJsonSubscription: Subscription; 20 | 21 | constructor( 22 | private articleService: ArticleService, 23 | private modalService: NzModalService, 24 | private message: NzMessageService, 25 | private configService: ConfigService 26 | ) { } 27 | 28 | ngOnInit() { 29 | this.configJsonSubscription = this.configService.configJson$.subscribe((configJson) => { 30 | this.isPostAssetFolderActive = configJson.post_asset_folder; 31 | }); 32 | } 33 | 34 | ngOnDestroy() { 35 | this.configJsonSubscription.unsubscribe(); 36 | } 37 | 38 | public removeArticle() { 39 | this.modalService.confirm({ 40 | nzTitle: 'REMOVE ARTICLE', 41 | nzContent: 'DO YOU WANT REMOVE ARTICLE:' + this.article.title, 42 | nzOnOk: () => new Promise((resolve, reject) => { 43 | this.articleService.delete(this.article).then(() => { 44 | resolve(); 45 | }).catch((error) => { 46 | reject(error); 47 | }); 48 | }).catch((error) => { 49 | console.log('ERROR REMOVE ARTICLE', error); 50 | this.message.error('ERROR REMOVE ARTICLE ' + error); 51 | }) 52 | }); 53 | } 54 | 55 | public rename() { 56 | this.modalService.create({ 57 | nzTitle: 'RENAME FILE', 58 | nzContent: RenameArticleModalComponent, 59 | nzComponentParams: { 60 | article: this.article 61 | }, 62 | nzFooter: null 63 | }); 64 | } 65 | 66 | public openAssetFolder() { 67 | const isOpened = this.articleService.openAssetFolder(this.article.asset_dir); 68 | if (isOpened) { 69 | this.message.success('FOLDER IS OPENED'); 70 | } else { 71 | this.message.error('OPEN FOLDER FAIL, MAY BE NOT EXIST'); 72 | } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/app/components/article-md-editor/article-md-editor.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 | 9 | 13 | 14 | 15 | 16 | 17 | 19 | 21 | 23 | 25 | 27 | 29 | 30 | 31 | 32 | 33 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 45 | 47 |
49 | 55 |
56 | 57 | 58 | 59 |
60 | 61 | 62 |
64 | 67 |
68 |
69 | 70 | 71 | 76 | 77 | 78 | 79 |
80 |
81 | 85 | 89 | 96 |
97 |
98 | 99 |
101 |
102 | 103 | 104 | 110 | 111 | 112 |
113 |
114 |
115 | 121 | 122 |
123 | 124 | 125 |
126 |
127 | 128 | 129 |
130 | {{ article.title }} 131 | 132 | 133 | {{ category }} 134 | 135 | 136 | {{ article.categories }} 137 | 138 | 139 | 140 | {{ tag }} 141 | 142 | 143 | {{ article.tags }} 144 |
145 |
146 |
147 | -------------------------------------------------------------------------------- /src/app/components/article-md-editor/article-md-editor.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/index"; 2 | $editor-toolbar-height: 48px; 3 | 4 | :host { 5 | > *[tabindex]{ 6 | &:focus, &:hover{ 7 | outline: none; 8 | } 9 | } 10 | .article-md-editor { 11 | .article-md-editor-action-bar { 12 | > * { 13 | margin-left: $spacing-m; 14 | } 15 | .article-md-editor-preview-icon { 16 | padding-right: $spacing-xs; 17 | font-size: $size-icon-m; 18 | } 19 | } 20 | 21 | .article-md-editor-editor-toolbar { 22 | /deep/ .ant-btn-group { 23 | margin-right: $spacing-xs; 24 | > .ant-btn { 25 | line-height: initial; 26 | } 27 | } 28 | } 29 | 30 | .article-md-editor-divider { 31 | margin-top: $spacing-s; 32 | margin-bottom: $spacing-s; 33 | } 34 | 35 | .article-md-editor-form { 36 | border: thin solid $grey-3; 37 | 38 | /deep/ { 39 | .ant-collapse-item { 40 | border-color: $grey-3; 41 | } 42 | 43 | .CodeMirror { 44 | height: auto; 45 | min-height: 150px; 46 | padding: $spacing-s; 47 | font: inherit; 48 | 49 | * { 50 | font-weight: normal; 51 | } 52 | } 53 | 54 | } 55 | 56 | .article-md-editor-info-wrapper { 57 | /deep/ { 58 | .ant-collapse-content{ 59 | background: $grey-3; 60 | } 61 | .CodeMirror { 62 | padding: 0; 63 | background: $grey-3; 64 | } 65 | } 66 | } 67 | 68 | .article-md-editor-content-wrapper { 69 | position: relative; 70 | /deep/ .CodeMirror { 71 | position: absolute; 72 | top: 0; 73 | bottom: 0; 74 | width: 100%; 75 | height: 100%; 76 | 77 | .CodeMirror-code{ 78 | padding-bottom: $spacing-CodeMirror-padding-scroll; 79 | } 80 | } 81 | 82 | 83 | } 84 | } 85 | /deep/ .article-md-editor-markdown-preview { 86 | background: $grey-1; 87 | padding: $spacing-m; 88 | overflow: scroll; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/app/components/article-md-editor/article-md-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleMdEditorComponent } from './article-md-editor.component'; 4 | 5 | describe('ArticleMdEditorComponent', () => { 6 | let component: ArticleMdEditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleMdEditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleMdEditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/article-md-editor/article-md-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, ViewChild, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core'; 2 | import { ArticleService } from '../../services/article.service'; 3 | import { Subscription } from 'rxjs'; 4 | import { Article } from '../../Models/Article'; 5 | import 'rxjs/add/operator/filter'; 6 | import 'rxjs/add/operator/switchMap'; 7 | import 'rxjs/add/operator/auditTime'; 8 | import * as moment from 'moment'; 9 | import { 10 | FormBuilder, 11 | FormGroup, 12 | Validators 13 | } from '@angular/forms'; 14 | import { NzMessageService } from 'ng-zorro-antd'; 15 | import { SystemSettingsService } from '../../services/system-settings.service'; 16 | import { NzModalService } from 'ng-zorro-antd'; 17 | import { ConfigService } from '../../services/config.service'; 18 | import { UtilsService } from '../../services/utils.service'; 19 | import { ElectronService } from '../../services/electron.service'; 20 | import { SaveArticleImageModalComponent } from '../save-article-image-modal/save-article-image-modal.component'; 21 | import { CustomMdEditorComponent } from '../custom-md-editor/custom-md-editor.component'; 22 | 23 | @Component({ 24 | selector: 'app-article-md-editor', 25 | templateUrl: './article-md-editor.component.html', 26 | styleUrls: ['./article-md-editor.component.scss'] 27 | }) 28 | export class ArticleMdEditorComponent implements OnInit, OnDestroy, OnChanges { 29 | 30 | @Input() article: Article = {} as Article; 31 | @Output() articleChange = new EventEmitter
(); 32 | 33 | @Input() isChanged = false; 34 | @Output() isChangedChange = new EventEmitter(); 35 | 36 | @ViewChild('editorContent') editorContent: CustomMdEditorComponent; 37 | @ViewChild('editorInfo') editorInfo: CustomMdEditorComponent; 38 | 39 | public form: FormGroup; 40 | public title: string; 41 | public isActivePreview = false; 42 | public codeMirrorOptions = { 43 | theme: 'hexo-note', 44 | mode: 'markdown', 45 | lineWrapping: true, 46 | lineNumbers: false 47 | }; 48 | public isSaving = false; 49 | public isPublishing = false; 50 | public disablePostAsset = true; 51 | public needPostAssetFolderActiveText = 'Need Active post_asset_folder property of config file for enble this option'; 52 | 53 | private _configSubscription: Subscription; 54 | 55 | constructor( 56 | private articleService: ArticleService, 57 | private fb: FormBuilder, 58 | private message: NzMessageService, 59 | private electronService: ElectronService, 60 | private systemSettingsService: SystemSettingsService, 61 | private modalService: NzModalService, 62 | private configService: ConfigService, 63 | public utils: UtilsService 64 | ) { 65 | this.form = this.fb.group({ 66 | info: [ '', [ Validators.required ] ], 67 | content: [ '', [ Validators.required ] ] 68 | }); 69 | 70 | this.isActivePreview = this.systemSettingsService.getIsActivePreview(); 71 | } 72 | 73 | ngOnInit() { 74 | this.form.setValue({ 75 | info: this.article.info, 76 | content: this.article.content 77 | }); 78 | 79 | // clean history when editor init; 80 | this._cleanHistory(); 81 | 82 | this._configSubscription = this.configService.configJson$.subscribe((configJson) => { 83 | this.disablePostAsset = !configJson.post_asset_folder; 84 | }); 85 | } 86 | 87 | ngOnDestroy() { 88 | this._configSubscription.unsubscribe(); 89 | } 90 | 91 | ngOnChanges(changes: SimpleChanges) { 92 | if (changes.article) { 93 | this.form.setValue({ 94 | info: this.article.info, 95 | content: this.article.content 96 | }); 97 | this._cleanHistory(); 98 | } 99 | } 100 | 101 | public emitIsChanged () { 102 | this.isChangedChange.emit(true); 103 | } 104 | 105 | public publish() { 106 | const loadingMessageId = this.message.loading('PUBLISH').messageId; 107 | this.isPublishing = true; 108 | this.articleService.publish(this.article) 109 | .then(() => this.message.success('PUBLISH OK')) 110 | .catch(() => this.message.error('PUBLISH ERROR')) 111 | .finally( () => { 112 | this.isPublishing = false; 113 | this.message.remove(loadingMessageId); 114 | }); 115 | } 116 | 117 | public remove() { 118 | this.modalService.confirm({ 119 | nzTitle: 'REMOVE ARTICLE', 120 | nzContent: 'DO YOU WANT REMOVE ARTICLE:' + this.article.title, 121 | nzOnOk: () => new Promise((resolve, reject) => { 122 | this.articleService.delete(this.article).then(() => { 123 | resolve(); 124 | }).catch((error) => { 125 | reject(error); 126 | }); 127 | }).catch((error) => console.log('ERROR REMOVE ARTICLE', error)) 128 | }); 129 | } 130 | 131 | public save() { 132 | const loadingMessageId = this.message.loading('SAVING').messageId; 133 | this.isSaving = true; 134 | this.article.info = this.form.value.info; 135 | this.article.content = this.form.value.content; 136 | 137 | this.articleService.update(this.article) 138 | .then(() => { 139 | this.message.success('SAVING OK'); 140 | this.isChangedChange.emit(false); 141 | }) 142 | .catch(() => this.message.error('SAVING ERROR')) 143 | .finally( () => { 144 | this.isSaving = false; 145 | this.message.remove(loadingMessageId); 146 | }); 147 | 148 | this.articleChange.emit(this.article); 149 | } 150 | 151 | public onKeyDown($event): void { 152 | const charCode = $event.key.toLowerCase(); 153 | // matekey: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey 154 | const ctrlCmdKey = this.utils.isMac() ? $event.metaKey : $event.ctrlKey; 155 | 156 | if (ctrlCmdKey && charCode === 's') { this.save(); $event.preventDefault(); } 157 | 158 | this.emitIsChanged(); 159 | } 160 | 161 | public onPreviewClick() { 162 | this.systemSettingsService.saveIsActivePreview(this.isActivePreview); 163 | } 164 | 165 | public openAssetFolder() { 166 | const isOpened = this.articleService.openAssetFolder(this.article.asset_dir); 167 | if (isOpened) { 168 | this.message.success('FOLDER IS OPENED'); 169 | } else { 170 | this.message.error('OPEN FOLDER FAIL, MAY BE NOT EXIST'); 171 | } 172 | } 173 | 174 | public onPaste($event) { 175 | if (this.utils.clipboardHasFormat('image')) { 176 | if (this.disablePostAsset ) { 177 | this.message.info('ENABLE post_asset_folder OF config.yml YOU CAN PASTE IMAGE'); 178 | return; 179 | } 180 | this._openSaveArticleImageModal(); 181 | } 182 | } 183 | 184 | private _openSaveArticleImageModal() { 185 | const clipboard = this.electronService.clipboard; 186 | const format = this.utils.clipboardHasFormat('jp') ? 'jpg' : 'png'; 187 | let fileName = 'image-' + moment().unix(); 188 | if (this.utils.clipboardHasFormat('plain')) { 189 | fileName = this.utils.removeFileExtension(clipboard.readText()); 190 | } 191 | 192 | const saveArticleModal = this.modalService.create({ 193 | nzTitle: 'SAVE IMAGE', 194 | nzContent: SaveArticleImageModalComponent, 195 | nzComponentParams: { 196 | image: clipboard.readImage(), 197 | fileName, 198 | format, 199 | article: this.article 200 | }, 201 | nzFooter: null 202 | }); 203 | 204 | saveArticleModal.afterClose.subscribe((file) => { 205 | if (file) { 206 | this.editorContent.imageLocal(file); 207 | } 208 | }); 209 | } 210 | 211 | private _cleanHistory() { 212 | if (this.editorInfo.codeMirror) { 213 | this.editorInfo.codeMirror.clearHistory(); 214 | } else { 215 | setTimeout(() => { 216 | return this.editorInfo.codeMirror ? this.editorInfo.codeMirror.clearHistory() : ''; 217 | }, 1000); 218 | } 219 | 220 | if (this.editorContent.codeMirror) { 221 | this.editorContent.codeMirror.clearHistory(); 222 | } else { 223 | setTimeout(() => { 224 | return this.editorContent.codeMirror ? this.editorContent.codeMirror.clearHistory() : ''; 225 | }, 1000); 226 | } 227 | } 228 | 229 | public isArray(object): boolean { 230 | return Array.isArray(object); 231 | } 232 | 233 | public ctrlOrCmd(): string { 234 | return this.utils.isMac() ? 'Cmd' : 'Ctrl'; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/app/components/custom-md-editor/custom-md-editor.component.html: -------------------------------------------------------------------------------- 1 |
2 | 10 |
11 | -------------------------------------------------------------------------------- /src/app/components/custom-md-editor/custom-md-editor.component.scss: -------------------------------------------------------------------------------- 1 | // simplemde Editor Class: 2 | // https://github.com/sparksuite/simplemde-markdown-editor/blob/master/src/css/simplemde.css 3 | :host:not(.no-style) /deep/ { 4 | .CodeMirror-scroll { 5 | min-height: 300px 6 | } 7 | 8 | .CodeMirror-fullscreen { 9 | background: #fff; 10 | position: fixed !important; 11 | top: 50px; 12 | left: 0; 13 | right: 0; 14 | bottom: 0; 15 | height: auto; 16 | z-index: 9; 17 | } 18 | 19 | .CodeMirror-sided { 20 | width: 50% !important; 21 | } 22 | 23 | .CodeMirror .CodeMirror-code .cm-tag { 24 | color: #63a35c; 25 | } 26 | 27 | .CodeMirror .CodeMirror-code .cm-attribute { 28 | color: #795da3; 29 | } 30 | 31 | .CodeMirror .CodeMirror-code .cm-string { 32 | color: #183691; 33 | } 34 | 35 | .CodeMirror .CodeMirror-selected { 36 | background: #d9d9d9; 37 | } 38 | 39 | .CodeMirror .CodeMirror-code .cm-header-1 { 40 | font-size: 180%; 41 | line-height: 180%; 42 | } 43 | 44 | .CodeMirror .CodeMirror-code .cm-header-2 { 45 | font-size: 160%; 46 | line-height: 160%; 47 | } 48 | 49 | .CodeMirror .CodeMirror-code .cm-header-3 { 50 | font-size: 125%; 51 | line-height: 125%; 52 | } 53 | 54 | .CodeMirror .CodeMirror-code .cm-header-4 { 55 | font-size: 110%; 56 | line-height: 110%; 57 | } 58 | 59 | .CodeMirror .CodeMirror-code .cm-comment { 60 | background: rgba(0, 0, 0, .05); 61 | border-radius: 2px; 62 | } 63 | 64 | .CodeMirror .CodeMirror-code .cm-link { 65 | color: #7f8c8d; 66 | } 67 | 68 | .CodeMirror .CodeMirror-code .cm-url { 69 | color: #aab2b3; 70 | } 71 | 72 | .CodeMirror .CodeMirror-code .cm-strikethrough { 73 | text-decoration: line-through; 74 | } 75 | 76 | .CodeMirror .CodeMirror-placeholder { 77 | opacity: .5; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/app/components/custom-md-editor/custom-md-editor.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { CustomMdEditorComponent } from './custom-md-editor.component'; 4 | 5 | describe('CustomMdEditorComponent', () => { 6 | let component: CustomMdEditorComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ CustomMdEditorComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(CustomMdEditorComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/custom-md-editor/custom-md-editor.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, KeyValueDiffers, NgZone, OnInit, forwardRef } from '@angular/core'; 2 | import { CodemirrorComponent } from '@ctrl/ngx-codemirror'; 3 | import { NG_VALUE_ACCESSOR } from '@angular/forms'; 4 | import { UtilsService } from '../../services/utils.service'; 5 | 6 | /** 7 | * This Component creating all md editor action 8 | * */ 9 | 10 | @Component({ 11 | selector: 'app-custom-md-editor', 12 | templateUrl: './custom-md-editor.component.html', 13 | styleUrls: ['./custom-md-editor.component.scss'], 14 | providers: [ 15 | { 16 | provide: NG_VALUE_ACCESSOR, 17 | multi: true, 18 | useExisting: forwardRef(() => CustomMdEditorComponent), 19 | } 20 | ] 21 | 22 | }) 23 | export class CustomMdEditorComponent extends CodemirrorComponent implements OnInit { 24 | 25 | public codeMirror: any; 26 | 27 | constructor( 28 | _differs: KeyValueDiffers, 29 | _ngZone: NgZone, 30 | public utils: UtilsService, 31 | ) { 32 | super(_differs, _ngZone); 33 | } 34 | 35 | ngOnInit() { 36 | } 37 | 38 | public onKeyDown($event: KeyboardEvent): void { 39 | const charCode = $event.key.toLowerCase(); 40 | const ctrlCmdKey = this.utils.isMac() ? $event.metaKey : $event.ctrlKey; 41 | const alt = $event.altKey; 42 | const shift = $event.shiftKey; 43 | 44 | if (ctrlCmdKey && shift && charCode === 'i') { this.image(); $event.preventDefault(); return; } 45 | if (ctrlCmdKey && shift && charCode === 'c') { this.codeBlock(); $event.preventDefault(); return; } 46 | 47 | if (ctrlCmdKey && charCode === 's') { $event.preventDefault(); } 48 | if (ctrlCmdKey && charCode === '1') { this.header(1); $event.preventDefault(); return; } 49 | if (ctrlCmdKey && charCode === '2') { this.header(2); $event.preventDefault(); return; } 50 | if (ctrlCmdKey && charCode === '3') { this.header(3); $event.preventDefault(); return; } 51 | if (ctrlCmdKey && charCode === '4') { this.header(4); $event.preventDefault(); return; } 52 | if (ctrlCmdKey && charCode === '5') { this.header(5); $event.preventDefault(); return; } 53 | if (ctrlCmdKey && charCode === '6') { this.header(6); $event.preventDefault(); return; } 54 | if (ctrlCmdKey && charCode === 'b') { this.bold(); $event.preventDefault(); return; } 55 | if (ctrlCmdKey && charCode === 'i') { this.italic(); $event.preventDefault(); return; } 56 | if (ctrlCmdKey && charCode === 'l') { this.listUl(); $event.preventDefault(); return; } 57 | if (ctrlCmdKey && charCode === 'k') { this.link(); $event.preventDefault(); return; } 58 | } 59 | 60 | 61 | // All below method you can consulting in https://github.com/pandao/editor.md/blob/master/src/editormd.js 62 | 63 | public undo() { this.codeMirror.undo(); } 64 | 65 | public redo() { this.codeMirror.redo(); } 66 | 67 | public header(headerNumber = 1) { 68 | const cm = this.codeMirror; 69 | const cursor = cm.getCursor(); 70 | const selection = cm.getSelection(); 71 | const prefix = '#'.repeat(headerNumber); 72 | 73 | if (cursor.ch !== 0) { 74 | cm.setCursor(cursor.line, 0); 75 | cm.replaceSelection(`${prefix} ${selection}` ); 76 | cm.setCursor(cursor.line, cursor.ch + 4); 77 | } else { 78 | cm.replaceSelection(`${prefix} ${selection}` ); 79 | } 80 | cm.focus(); 81 | } 82 | 83 | public bold() { 84 | const cm = this.codeMirror; 85 | const cursor = cm.getCursor(); 86 | const selection = cm.getSelection(); 87 | 88 | cm.replaceSelection('**' + selection + '**'); 89 | 90 | if (selection === '') { 91 | cm.setCursor(cursor.line, cursor.ch + 2); 92 | } 93 | cm.focus(); 94 | } 95 | 96 | public del() { 97 | const cm = this.codeMirror; 98 | const cursor = cm.getCursor(); 99 | const selection = cm.getSelection(); 100 | 101 | cm.replaceSelection('~~' + selection + '~~'); 102 | 103 | if (selection === '') { 104 | cm.setCursor(cursor.line, cursor.ch + 2); 105 | } 106 | cm.focus(); 107 | } 108 | 109 | public italic() { 110 | const cm = this.codeMirror; 111 | const cursor = cm.getCursor(); 112 | const selection = cm.getSelection(); 113 | 114 | cm.replaceSelection('*' + selection + '*'); 115 | 116 | if (selection === '') { 117 | cm.setCursor(cursor.line, cursor.ch + 1); 118 | } 119 | cm.focus(); 120 | } 121 | 122 | public quote() { 123 | const cm = this.codeMirror; 124 | const cursor = cm.getCursor(); 125 | const selection = cm.getSelection(); 126 | 127 | if (cursor.ch !== 0) { 128 | cm.setCursor(cursor.line, 0); 129 | cm.replaceSelection('> ' + selection); 130 | cm.setCursor(cursor.line, cursor.ch + 2); 131 | } else { 132 | cm.replaceSelection('> ' + selection); 133 | } 134 | cm.focus(); 135 | } 136 | 137 | public listUl() { 138 | const cm = this.codeMirror; 139 | const cursor = cm.getCursor(); 140 | const selection = cm.getSelection(); 141 | 142 | if (selection === '') { 143 | cm.replaceSelection('- ' + selection); 144 | } else { 145 | const selectionText = selection.split('\n'); 146 | 147 | for (let i = 0, len = selectionText.length; i < len; i++) { 148 | selectionText[i] = (selectionText[i] === '') ? '' : '- ' + selectionText[i]; 149 | } 150 | 151 | cm.replaceSelection(selectionText.join('\n')); 152 | } 153 | cm.focus(); 154 | } 155 | 156 | public listOl() { 157 | const cm = this.codeMirror; 158 | const cursor = cm.getCursor(); 159 | const selection = cm.getSelection(); 160 | 161 | if (selection === '') { 162 | cm.replaceSelection('1. ' + selection); 163 | } else { 164 | const selectionText = selection.split('\n'); 165 | 166 | for (let i = 0, len = selectionText.length; i < len; i++) { 167 | selectionText[i] = (selectionText[i] === '') ? '' : (i + 1) + '. ' + selectionText[i]; 168 | } 169 | 170 | cm.replaceSelection(selectionText.join('\n')); 171 | } 172 | cm.focus(); 173 | } 174 | 175 | public hr() { 176 | const cm = this.codeMirror; 177 | const cursor = cm.getCursor(); 178 | 179 | cm.replaceSelection(((cursor.ch !== 0) ? '\n\n' : '\n') + '------------\n\n'); 180 | cm.focus(); 181 | } 182 | 183 | 184 | public link() { 185 | const cm = this.codeMirror; 186 | const cursor = cm.getCursor(); 187 | const selection = cm.getSelection(); 188 | 189 | if (this.utils.isURL(selection)) { 190 | cm.replaceSelection(`[](${selection})`); 191 | } else { 192 | cm.replaceSelection(`[${selection}]()`); 193 | } 194 | 195 | if (selection === '') { 196 | cm.setCursor(cursor.line, cursor.ch + 3); 197 | } 198 | cm.focus(); 199 | } 200 | 201 | public image() { 202 | const cm = this.codeMirror; 203 | const cursor = cm.getCursor(); 204 | const selection = cm.getSelection(); 205 | 206 | if (this.utils.isURL(selection)) { 207 | cm.replaceSelection(`![](${selection})`); 208 | } else { 209 | cm.replaceSelection(`![${selection}]()`); 210 | } 211 | 212 | if (selection === '') { 213 | cm.setCursor(cursor.line, cursor.ch + 4); 214 | } 215 | cm.focus(); 216 | } 217 | 218 | public imageLocal(text = '') { 219 | const cm = this.codeMirror; 220 | 221 | const resultText = this.utils.isImageFormat(text) ? 222 | `{% asset_img "${text}" "some description"%}` : 223 | `{% asset_img "image.jpeg" "sime description"%}`; 224 | 225 | cm.replaceSelection(resultText); 226 | cm.focus(); 227 | } 228 | 229 | public code() { 230 | const cm = this.codeMirror; 231 | const cursor = cm.getCursor(); 232 | const selection = cm.getSelection(); 233 | 234 | cm.replaceSelection('`' + selection + '`'); 235 | 236 | if (selection === '') { 237 | cm.setCursor(cursor.line, cursor.ch + 1); 238 | } 239 | cm.focus(); 240 | } 241 | 242 | public codeBlock() { 243 | const cm = this.codeMirror; 244 | const cursor = cm.getCursor(); 245 | const selection = cm.getSelection(); 246 | 247 | cm.replaceSelection('```\n' + selection + '\n```'); 248 | 249 | if (selection === '') { 250 | cm.setCursor(cursor.line + 1, 0); 251 | } 252 | cm.focus(); 253 | } 254 | 255 | public table() { 256 | const cm = this.codeMirror; 257 | const tableText = 258 | '\nheader1 | header2 | header3\n' + 259 | '--- | --- | ---\n' + 260 | 'text1 | text2 | text3\n'; 261 | 262 | cm.replaceSelection(tableText); 263 | cm.focus(); 264 | } 265 | 266 | public readMore() { 267 | const cm = this.codeMirror; 268 | cm.replaceSelection('\n\n'); 269 | cm.focus(); 270 | } 271 | 272 | 273 | 274 | } 275 | -------------------------------------------------------------------------------- /src/app/components/new-article-form/new-article-form.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | PLEASE REQUIRE TITLE 6 | THIS TITLE JUST EXIST 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | -------------------------------------------------------------------------------- /src/app/components/new-article-form/new-article-form.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/app/components/new-article-form/new-article-form.component.scss -------------------------------------------------------------------------------- /src/app/components/new-article-form/new-article-form.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NewArticleFormComponent } from './new-article-form.component'; 4 | 5 | describe('NewArticleFormComponent', () => { 6 | let component: NewArticleFormComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NewArticleFormComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NewArticleFormComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/new-article-form/new-article-form.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit, Input, Output, EventEmitter, OnDestroy} from '@angular/core'; 2 | import { Article } from '../../Models/Article'; 3 | import { 4 | FormGroup, 5 | FormBuilder, 6 | Validators 7 | } from '@angular/forms'; 8 | import { Subscription } from 'rxjs'; 9 | import { ArticleService } from '../../services/article.service'; 10 | import { NzMessageService, NzModalRef } from 'ng-zorro-antd'; 11 | import 'rxjs/add/operator/debounceTime'; 12 | import 'rxjs/add/operator/toPromise'; 13 | import 'rxjs/add/operator/take'; 14 | import { Router } from '@angular/router'; 15 | 16 | @Component({ 17 | selector: 'app-new-article-form', 18 | templateUrl: './new-article-form.component.html', 19 | styleUrls: ['./new-article-form.component.scss'] 20 | }) 21 | export class NewArticleFormComponent implements OnInit, OnDestroy { 22 | 23 | @Input() post: Article; 24 | @Output() postChange = new EventEmitter
(); 25 | 26 | public isCreating = false; 27 | public form: FormGroup; 28 | private formSubscription: Subscription; 29 | private formTitleChangeSubscription: Subscription; 30 | 31 | constructor( 32 | private fb: FormBuilder, 33 | private articleService: ArticleService, 34 | private modal: NzModalRef, 35 | private message: NzMessageService, 36 | private router: Router 37 | ) { 38 | this.form = this.fb.group({ 39 | title: [ '', [ Validators.required ] ], 40 | published: [ false, [ Validators.required ] ] 41 | }); 42 | } 43 | 44 | ngOnInit() { 45 | this.formTitleChangeSubscription = this.form.controls['title'] 46 | .valueChanges 47 | .debounceTime(300) 48 | .subscribe(title => { 49 | const isTitleExist = this.articleService.checkIfExistFileName(title); 50 | if (isTitleExist) { 51 | this.form.controls['title'].setErrors( {'exist': true}); 52 | } else if (!this.form.controls['title'].errors) { 53 | this.form.controls['title'].setErrors( null); 54 | } 55 | }); 56 | 57 | this.formSubscription = this.form.valueChanges 58 | .subscribe(value => { 59 | this.postChange.emit(value); 60 | }); 61 | } 62 | 63 | ngOnDestroy() { 64 | this.formTitleChangeSubscription.unsubscribe(); 65 | this.formSubscription.unsubscribe(); 66 | } 67 | 68 | public onSubmit() { 69 | this.isCreating = true; 70 | 71 | // validate form 72 | for (const i in this.form.controls) { 73 | if (this.form.controls[i]) { 74 | this.form.controls[i].markAsDirty(); 75 | this.form.controls[i].updateValueAndValidity(); 76 | } 77 | } 78 | 79 | if (this.form.invalid) { 80 | return; 81 | } 82 | 83 | const title = this.form.value.title; 84 | const published = this.form.value.published; 85 | 86 | this.articleService.create({ title, published}) 87 | .then(() => { 88 | this.isCreating = false; 89 | this.message.success('CREATE POST OK'); 90 | this.articleService.articles$ 91 | .debounceTime(500) 92 | .take(1) 93 | .toPromise() 94 | .then(() => { 95 | const currentArticle = this.articleService.getArticleByLocalByTitle(title); 96 | this.router.navigate(['/dashboard', 'article', currentArticle._id]); 97 | this.modal.close(); 98 | }); 99 | }) 100 | .catch((err) => { 101 | this.message.error(`CREATE POST ERROR: ${err}`); 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/components/new-blog-modal/new-blog-modal.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | PATH 4 | 5 | 6 | REQUIRE 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /src/app/components/new-blog-modal/new-blog-modal.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/index"; 2 | 3 | :host { 4 | .new-blog-modal-form-open-folder-icon { 5 | margin: 7px $spacing-s; 6 | font-size: $size-text-l; 7 | 8 | &:hover { 9 | color: $primary; 10 | cursor: pointer; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/app/components/new-blog-modal/new-blog-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NewBlogModalComponent } from './new-blog-modal.component'; 4 | 5 | describe('NewBlogModalComponent', () => { 6 | let component: NewBlogModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NewBlogModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NewBlogModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/new-blog-modal/new-blog-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormBuilder, Validators } from '@angular/forms'; 3 | import { ElectronService } from '../../services/electron.service'; 4 | import {UtilsService} from '../../services/utils.service'; 5 | import {HexoService} from '../../services/hexo.service'; 6 | 7 | 8 | @Component({ 9 | selector: 'app-new-blog-modal', 10 | templateUrl: './new-blog-modal.component.html', 11 | styleUrls: ['./new-blog-modal.component.scss'] 12 | }) 13 | export class NewBlogModalComponent implements OnInit { 14 | 15 | public form: FormGroup; 16 | private isCreating = false; 17 | 18 | constructor( 19 | private fb: FormBuilder, 20 | private hexoService: HexoService, 21 | private electronService: ElectronService, 22 | private utils: UtilsService 23 | ) { 24 | this.form = this.fb.group({ 25 | directory: [ this.electronService.app.getPath('documents') || '', [ Validators.required ]] 26 | }); 27 | } 28 | 29 | ngOnInit() { 30 | } 31 | 32 | changePath() { 33 | const directory = this.utils.openDirectoryDialog(); 34 | if (directory) { 35 | this.form.setValue({ directory }); 36 | } 37 | } 38 | 39 | public submitForm(): void { 40 | for (const i in this.form.controls) { 41 | if (this.form.controls[i]) { 42 | this.form.controls[i].markAsDirty(); 43 | this.form.controls[i].updateValueAndValidity(); 44 | } 45 | } 46 | 47 | if (this.form.invalid) { 48 | return; 49 | } 50 | 51 | this.isCreating = true; 52 | 53 | this.hexoService.newBlog(this.form.value.directory); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/components/rename-article-modal/rename-article-modal.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | PLEASE REQUIRE FILENAME 6 | THIS FILENAME JUST EXIST 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | -------------------------------------------------------------------------------- /src/app/components/rename-article-modal/rename-article-modal.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/app/components/rename-article-modal/rename-article-modal.component.scss -------------------------------------------------------------------------------- /src/app/components/rename-article-modal/rename-article-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { RenameArticleModalComponent } from './rename-article-modal.component'; 4 | 5 | describe('RenameArticleModalComponent', () => { 6 | let component: RenameArticleModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ RenameArticleModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(RenameArticleModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/rename-article-modal/rename-article-modal.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, OnDestroy} from '@angular/core'; 2 | import {FormBuilder, FormGroup, Validators} from '@angular/forms'; 3 | import {Subscription} from 'rxjs'; 4 | import {ArticleService} from '../../services/article.service'; 5 | import {NzMessageService, NzModalRef} from 'ng-zorro-antd'; 6 | import {Article} from '../../Models/Article'; 7 | 8 | @Component({ 9 | selector: 'app-rename-article-modal', 10 | templateUrl: './rename-article-modal.component.html', 11 | styleUrls: ['./rename-article-modal.component.scss'] 12 | }) 13 | export class RenameArticleModalComponent implements OnInit, OnDestroy { 14 | 15 | @Input() article: Article; 16 | @Input() subtitle: string; 17 | 18 | public isRenaming = false; 19 | public isSame = true; 20 | public form: FormGroup; 21 | private _formFileNameChangeSubscription: Subscription; 22 | 23 | constructor( 24 | private fb: FormBuilder, 25 | private articleService: ArticleService, 26 | private message: NzMessageService, 27 | private modal: NzModalRef 28 | ) { 29 | } 30 | 31 | ngOnInit() { 32 | this.form = this.fb.group({ 33 | fileName: [ '', [ Validators.required ] ] 34 | }); 35 | 36 | this._formFileNameChangeSubscription = this.form.controls['fileName'] 37 | .valueChanges 38 | .debounceTime(300) 39 | .subscribe(fileName => { 40 | const isFileNameExist = this.articleService.checkIfExistFileName(fileName); 41 | if (isFileNameExist) { 42 | this.form.controls['fileName'].setErrors( {'exist': true}); 43 | } else if (!this.form.controls['fileName'].errors) { 44 | this.form.controls['fileName'].setErrors( null); 45 | } 46 | }); 47 | 48 | this.form.setValue({ 49 | fileName: this.article.fileName 50 | }); 51 | } 52 | 53 | ngOnDestroy() { 54 | this._formFileNameChangeSubscription.unsubscribe(); 55 | } 56 | 57 | public onSubmit() { 58 | this.isRenaming = true; 59 | 60 | // validate form 61 | for (const i in this.form.controls) { 62 | if (this.form.controls[i]) { 63 | this.form.controls[i].markAsDirty(); 64 | this.form.controls[i].updateValueAndValidity(); 65 | } 66 | } 67 | 68 | if (this.form.invalid) { 69 | return; 70 | } 71 | 72 | this.articleService.rename(this.article, this.form.value.fileName) 73 | .then(() => { 74 | this.isRenaming = false; 75 | this.message.success('RENAME ARTICLE OK'); 76 | this.modal.close(); 77 | }) 78 | .catch((err) => { 79 | this.message.error(`RENAME ARTICLE ERROR: ${err}`); 80 | }); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/components/save-article-image-modal/save-article-image-modal.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | SIZE: {{this.imageSize.width}} x {{this.imageSize.height}} 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | PLEASE REQUIRE FILENAME 21 | THIS FILENAME JUST EXIST 22 | PLEASE REQUIRE FORMAT 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/app/components/save-article-image-modal/save-article-image-modal.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/index"; 2 | 3 | .save-article-image-modal { 4 | .save-article-image-modal-preview-wrapper { 5 | background-color: $grey-3; 6 | padding: $spacing-xs; 7 | } 8 | 9 | .save-article-image-modal-preview { 10 | max-height: 200px; 11 | background-repeat: no-repeat; 12 | background-position: center center; 13 | background-size: contain; 14 | } 15 | 16 | .save-article-image-modal-info { 17 | font-size: $size-text-xs; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/components/save-article-image-modal/save-article-image-modal.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SaveArticleImageModalComponent } from './save-article-image-modal.component'; 4 | 5 | describe('SaveArticleImageModalComponent', () => { 6 | let component: SaveArticleImageModalComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SaveArticleImageModalComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SaveArticleImageModalComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/save-article-image-modal/save-article-image-modal.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, Input, ViewChild, ElementRef } from '@angular/core'; 2 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 3 | import { NzMessageService, NzModalRef, NzModalService } from 'ng-zorro-antd'; 4 | import * as electron from 'electron'; 5 | import { ElectronService } from '../../services/electron.service'; 6 | import { UtilsService } from '../../services/utils.service'; 7 | import { Subscription } from 'rxjs'; 8 | import { AssetService } from '../../services/asset.service'; 9 | import { Article } from '../../Models/Article.interface'; 10 | import 'rxjs/add/operator/debounceTime'; 11 | 12 | @Component({ 13 | selector: 'app-save-article-image-modal', 14 | templateUrl: './save-article-image-modal.component.html', 15 | styleUrls: ['./save-article-image-modal.component.scss'] 16 | }) 17 | export class SaveArticleImageModalComponent implements OnInit { 18 | 19 | @Input() image: typeof electron.NativeImage | any; 20 | @Input() fileName = ''; 21 | @Input() format = 'png'; 22 | @Input() article: Article = {}; 23 | 24 | @ViewChild('imagePreview') imagePreview: ElementRef; 25 | 26 | public form: FormGroup; 27 | public imageSize: {width: number, height: number}; 28 | public isSaving: boolean; 29 | 30 | private _formFileNameChangeSubscription: Subscription; 31 | 32 | constructor( 33 | private fb: FormBuilder, 34 | private assetService: AssetService, 35 | private electronService: ElectronService, 36 | private modalService: NzModalService, 37 | private utilsService: UtilsService, 38 | private message: NzMessageService, 39 | private modal: NzModalRef) { 40 | } 41 | 42 | ngOnInit() { 43 | this.form = this.fb.group({ 44 | fileName: [ this.fileName, [ Validators.required ] ], 45 | format: [ this.format, [ Validators.required ] ] 46 | }); 47 | 48 | this._formFileNameChangeSubscription = this.form.valueChanges 49 | .debounceTime(300) 50 | .subscribe((values) => { 51 | const file = `${values.fileName}.${values.format}`; 52 | const isFileExist = this.assetService.checkIfExistFileName(this.article.asset_dir, file); 53 | if (isFileExist) { 54 | this.form.controls['fileName'].setErrors( {'exist': true}); 55 | } else if (!this.form.controls['fileName'].errors) { 56 | this.form.controls['fileName'].setErrors( null); 57 | } 58 | }); 59 | 60 | // set image data 61 | this.imageSize = this.image.getSize(); 62 | this.imagePreview.nativeElement.style.height = this.imageSize.height + 'px'; 63 | this.imagePreview.nativeElement.style.backgroundImage = `url(${this.image.toDataURL()})`; 64 | } 65 | 66 | public onSubmit() { 67 | this.isSaving = true; 68 | 69 | // validate form 70 | for (const i in this.form.controls) { 71 | if (this.form.controls[i]) { 72 | this.form.controls[i].markAsDirty(); 73 | this.form.controls[i].updateValueAndValidity(); 74 | } 75 | } 76 | 77 | if (this.form.invalid) { 78 | return; 79 | } 80 | 81 | let imageData = ''; 82 | const fileName = this.form.value.fileName; 83 | const format = this.form.value.format; 84 | const file = `${fileName}.${format}`; 85 | 86 | switch (this.form.value.format) { 87 | case 'png': 88 | imageData = this.image.toPNG(); 89 | break; 90 | case 'jpg': 91 | imageData = this.image.toJPEG(); 92 | break; 93 | } 94 | 95 | this.assetService.saveImageToAssetDir(this.article.asset_dir, `${fileName}.${format}`, imageData) 96 | .then(() => { 97 | this.message.success('save asset ok'); 98 | this.modal.triggerOk(); 99 | this.modal.close(file); 100 | }) 101 | .catch((err) => { 102 | this.message.error('save asset error: ' + err); 103 | this.modal.triggerCancel(); 104 | this.modal.close(false); 105 | }); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/app/components/sidebar/sidebar.component.html: -------------------------------------------------------------------------------- 1 | 58 | -------------------------------------------------------------------------------- /src/app/components/sidebar/sidebar.component.scss: -------------------------------------------------------------------------------- 1 | @import '../../../style/index'; 2 | 3 | :host { 4 | .sidebar { 5 | height: 100%; 6 | background-color: $sidebar-color; 7 | padding: $spacing-m 0px; 8 | border-right: $grey-6 thin solid; 9 | 10 | button { 11 | margin-top: $spacing-s; 12 | margin-bottom: $spacing-s; 13 | 14 | &.server-running { 15 | color: white; 16 | background-color: $success !important; 17 | } 18 | } 19 | 20 | hr { 21 | width: 90%; 22 | display: block; 23 | height: 1px; 24 | border: 0; 25 | border-top: thin solid $border-color; 26 | margin-left: auto; 27 | margin-right: auto; 28 | padding: 0; 29 | margin: $spacing-m; 30 | } 31 | 32 | .sidebar-report-issue:hover { 33 | color: $warning; 34 | } 35 | } 36 | 37 | .ant-menu { 38 | border-right: none; 39 | background: none; 40 | } 41 | 42 | .ant-menu-item { 43 | outline: none; 44 | i { 45 | font-size: 18px; 46 | } 47 | } 48 | 49 | .sidebar-actions { 50 | button { 51 | border: none; 52 | color: $transparent-dark-65; 53 | &:hover { 54 | color: $black; 55 | } 56 | } 57 | } 58 | 59 | #sidebar-hexo-logo { 60 | width: 40px; 61 | margin: $spacing-s auto; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/app/components/sidebar/sidebar.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SidebarComponent } from './sidebar.component'; 4 | 5 | describe('SidebarComponent', () => { 6 | let component: SidebarComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SidebarComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SidebarComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/components/sidebar/sidebar.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { NzModalService } from 'ng-zorro-antd'; 3 | import { UtilsService } from './../../services/utils.service'; 4 | import { NzMessageService } from 'ng-zorro-antd'; 5 | import { HexoService } from '../../services/hexo.service'; 6 | import { ServerService } from '../../services/server.service'; 7 | import { AppConfig } from '../../../environments/environment'; 8 | import { ElectronService } from '../../services/electron.service'; 9 | import {RenameArticleModalComponent} from '../rename-article-modal/rename-article-modal.component'; 10 | import {NewArticleFormComponent} from '../new-article-form/new-article-form.component'; 11 | 12 | @Component({ 13 | selector: 'app-sidebar', 14 | templateUrl: './sidebar.component.html', 15 | styleUrls: ['./sidebar.component.scss'] 16 | }) 17 | export class SidebarComponent implements OnInit { 18 | 19 | public isDeploying = false; 20 | public version: string = AppConfig.version; 21 | 22 | constructor( 23 | private modalService: NzModalService, 24 | private utilsService: UtilsService, 25 | private hexoService: HexoService, 26 | private message: NzMessageService, 27 | private electronService: ElectronService, 28 | public serverService: ServerService 29 | ) { 30 | } 31 | 32 | ngOnInit() { 33 | } 34 | 35 | public openNewArticleModal() { 36 | this.modalService.create({ 37 | nzTitle: 'RENAME FILE', 38 | nzContent: NewArticleFormComponent, 39 | nzFooter: null 40 | }); 41 | } 42 | 43 | public openTerminal() { 44 | this.utilsService.openTerminal(); 45 | } 46 | 47 | public confirmDeploy() { 48 | this.modalService.confirm({ 49 | nzTitle: 'DEPLOY', 50 | nzContent: 'DO YOU WANT DEPLOY THE PROJECT?', 51 | nzOnOk: () => { this.deploy(); } 52 | }); 53 | } 54 | 55 | public deploy() { 56 | this.isDeploying = true; 57 | const deployMessageId = this.message.loading('Deploy in process..', { nzDuration: 0 }).messageId; 58 | this.hexoService.deployChildProcess() 59 | .then(() => this.message.success('DEPLOY OK')) 60 | .catch((error) => this.message.error(`DEPLOY ERROR ${error}`)) 61 | .finally(() => { 62 | this.isDeploying = false; 63 | this.message.remove(deployMessageId); 64 | }); 65 | } 66 | 67 | public switchServer() { 68 | if (this.serverService.isServerRunning) { 69 | this.serverService.stopServer(); 70 | this.message.info('STOP SERVER'); 71 | } else { 72 | this.message.info('STARTING SERVER'); 73 | this.serverService.startServer().catch((error) => { 74 | this.message.error(`SERVER STARTING ERROR ${error}`); 75 | }); 76 | } 77 | } 78 | 79 | public openIssuePage() { 80 | this.electronService.shell.openExternal('https://github.com/tmirun/Hexo-Note/issues'); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/app/directives/webview.directive.ts: -------------------------------------------------------------------------------- 1 | import { Directive } from '@angular/core'; 2 | 3 | @Directive({ 4 | selector: 'webview' 5 | }) 6 | export class WebviewDirective { 7 | 8 | constructor() { } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/guard/app-init.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { AppInitGuard } from './app-init.guard'; 4 | 5 | describe('AppInitGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AppInitGuard] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([AppInitGuard], (guard: AppInitGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/guard/app-init.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router} from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { ConfigService } from '../services/config.service'; 5 | import {HexoService} from '../services/hexo.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class AppInitGuard implements CanActivate { 11 | constructor( 12 | private configService: ConfigService, 13 | private hexoService: HexoService, 14 | private router: Router 15 | ) {} 16 | 17 | canActivate( 18 | next: ActivatedRouteSnapshot, 19 | state: RouterStateSnapshot): Observable | Promise | boolean { 20 | if (this.hexoService.isCurrentDirectoryProjectFolder()) { 21 | return true; 22 | } else { 23 | this.router.navigate(['not-project-found']); 24 | return false; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/guard/can-deactivate.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { CanDeactivateGuard } from './can-deactivate.guard'; 4 | 5 | describe('CanDesactiveGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [CanDeactivateGuard] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([CanDeactivateGuard], (guard: CanDeactivateGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/guard/can-deactivate.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, CanDeactivate } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | 5 | export interface CanComponentDeactivate { 6 | canDeactivate: () => Observable | Promise | boolean; 7 | } 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class CanDeactivateGuard implements CanDeactivate { 13 | canDeactivate(component: CanComponentDeactivate) { 14 | return component.canDeactivate ? component.canDeactivate() : true; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/app/guard/config-init.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { ConfigInitGuard } from './config-init.guard'; 4 | 5 | describe('ConfigInitGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ConfigInitGuard] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([ConfigInitGuard], (guard: ConfigInitGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/guard/config-init.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import {ConfigService} from '../services/config.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ConfigInitGuard implements CanActivate { 10 | 11 | constructor( 12 | private configService: ConfigService 13 | ) {} 14 | 15 | canActivate( 16 | next: ActivatedRouteSnapshot, 17 | state: RouterStateSnapshot): Observable | Promise | boolean { 18 | 19 | this.configService.getConfigYml(); 20 | 21 | return this.configService.configYml$ 22 | .filter(configYmlData => !!configYmlData) // only pass if not empty 23 | .map((configYmlData) => { 24 | return !!configYmlData; 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/app/guard/hexo-init.guard.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, async, inject } from '@angular/core/testing'; 2 | 3 | import { HexoInitGuard } from './hexo-init.guard'; 4 | 5 | describe('HexoInitGuard', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [HexoInitGuard] 9 | }); 10 | }); 11 | 12 | it('should ...', inject([HexoInitGuard], (guard: HexoInitGuard) => { 13 | expect(guard).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/guard/hexo-init.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; 3 | import { Observable } from 'rxjs'; 4 | import { HexoService } from '../services/hexo.service'; 5 | import 'rxjs/add/operator/map'; 6 | import 'rxjs/add/operator/filter'; 7 | import {ConfigService} from '../services/config.service'; 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class HexoInitGuard implements CanActivate { 13 | constructor( 14 | private hexoService: HexoService, 15 | private configService: ConfigService 16 | ) {} 17 | 18 | canActivate( 19 | next: ActivatedRouteSnapshot, 20 | state: RouterStateSnapshot): Observable | Promise | boolean { 21 | return this.configService.configYml$.map(x => true); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article-detail/article-detail.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article-detail/article-detail.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../../style/index"; 2 | $editor-toolbar-height: 48px; 3 | 4 | :host { 5 | > *[tabindex]{ 6 | &:focus, &:hover{ 7 | outline: none; 8 | } 9 | } 10 | .article-detail { 11 | padding: $spacing-m; 12 | .article-detail-content { 13 | 14 | } 15 | 16 | .article-detail-action-bar { 17 | > * { 18 | margin-left: $spacing-m; 19 | } 20 | .article-detail-preview-icon { 21 | padding-right: $spacing-xs; 22 | font-size: $size-icon-m; 23 | } 24 | } 25 | 26 | .article-detail-editor-toolbar { 27 | /deep/ .ant-btn-group { 28 | margin-right: $spacing-xs; 29 | > .ant-btn { 30 | line-height: initial; 31 | } 32 | } 33 | } 34 | 35 | .article-detail-divider { 36 | margin-top: $spacing-s; 37 | margin-bottom: $spacing-s; 38 | } 39 | 40 | .article-detail-form-wrapper { 41 | border: thin solid $grey-3; 42 | .article-detail-form { 43 | 44 | /deep/ { 45 | .ant-collapse-item { 46 | border-color: $grey-3; 47 | } 48 | 49 | .CodeMirror { 50 | height: auto; 51 | min-height: 150px; 52 | padding: $spacing-s; 53 | font: inherit; 54 | 55 | * { 56 | font-weight: normal; 57 | } 58 | } 59 | 60 | } 61 | 62 | .article-detail-info-wrapper { 63 | /deep/ { 64 | .ant-collapse-content{ 65 | background: $grey-3; 66 | } 67 | .CodeMirror { 68 | padding: 0; 69 | background: $grey-3; 70 | } 71 | } 72 | } 73 | 74 | .article-detail-content-wrapper { 75 | position: relative; 76 | /deep/ .CodeMirror { 77 | position: absolute; 78 | top: 0; 79 | bottom: 0; 80 | width: 100%; 81 | height: 100%; 82 | 83 | .CodeMirror-code{ 84 | padding-bottom: $spacing-CodeMirror-padding-scroll; 85 | } 86 | } 87 | 88 | // simplemde Editor Class: 89 | // https://github.com/sparksuite/simplemde-markdown-editor/blob/master/src/css/simplemde.css 90 | /deep/ { 91 | .CodeMirror-scroll { 92 | min-height: 300px 93 | } 94 | 95 | .CodeMirror-fullscreen { 96 | background: #fff; 97 | position: fixed !important; 98 | top: 50px; 99 | left: 0; 100 | right: 0; 101 | bottom: 0; 102 | height: auto; 103 | z-index: 9; 104 | } 105 | 106 | .CodeMirror-sided { 107 | width: 50% !important; 108 | } 109 | 110 | .CodeMirror .CodeMirror-code .cm-tag { 111 | color: #63a35c; 112 | } 113 | 114 | .CodeMirror .CodeMirror-code .cm-attribute { 115 | color: #795da3; 116 | } 117 | 118 | .CodeMirror .CodeMirror-code .cm-string { 119 | color: #183691; 120 | } 121 | 122 | .CodeMirror .CodeMirror-selected { 123 | background: #d9d9d9; 124 | } 125 | 126 | .CodeMirror .CodeMirror-code .cm-header-1 { 127 | font-size: 180%; 128 | line-height: 180%; 129 | } 130 | 131 | .CodeMirror .CodeMirror-code .cm-header-2 { 132 | font-size: 160%; 133 | line-height: 160%; 134 | } 135 | 136 | .CodeMirror .CodeMirror-code .cm-header-3 { 137 | font-size: 125%; 138 | line-height: 125%; 139 | } 140 | 141 | .CodeMirror .CodeMirror-code .cm-header-4 { 142 | font-size: 110%; 143 | line-height: 110%; 144 | } 145 | 146 | .CodeMirror .CodeMirror-code .cm-comment { 147 | background: rgba(0, 0, 0, .05); 148 | border-radius: 2px; 149 | } 150 | 151 | .CodeMirror .CodeMirror-code .cm-link { 152 | color: #7f8c8d; 153 | } 154 | 155 | .CodeMirror .CodeMirror-code .cm-url { 156 | color: #aab2b3; 157 | } 158 | 159 | .CodeMirror .CodeMirror-code .cm-strikethrough { 160 | text-decoration: line-through; 161 | } 162 | 163 | .CodeMirror .CodeMirror-placeholder { 164 | opacity: .5; 165 | } 166 | } 167 | } 168 | } 169 | /deep/ .article-detail-markdown-preview { 170 | background: $grey-1; 171 | padding: $spacing-m; 172 | overflow: scroll; 173 | } 174 | } 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article-detail/article-detail.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleDetailComponent } from './article-detail.component'; 4 | 5 | describe('ArticleDetailComponent', () => { 6 | let component: ArticleDetailComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleDetailComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleDetailComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article-detail/article-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; 2 | import { ArticleService } from '../../../../services/article.service'; 3 | import { Subscription } from 'rxjs'; 4 | import { Article } from '../../../../Models/Article'; 5 | import { ActivatedRoute, Router } from '@angular/router'; 6 | import 'rxjs/add/operator/filter'; 7 | import 'rxjs/add/operator/switchMap'; 8 | import 'rxjs/add/operator/auditTime'; 9 | import * as moment from 'moment'; 10 | import { NzMessageService } from 'ng-zorro-antd'; 11 | import { SystemSettingsService } from '../../../../services/system-settings.service'; 12 | import { CanDeactivateGuard } from '../../../../guard/can-deactivate.guard'; 13 | import { NzModalService } from 'ng-zorro-antd'; 14 | import { ElectronService } from '../../../../services/electron.service'; 15 | 16 | @Component({ 17 | selector: 'app-post-detail', 18 | templateUrl: './article-detail.component.html', 19 | styleUrls: ['./article-detail.component.scss'] 20 | }) 21 | export class ArticleDetailComponent implements OnInit, OnDestroy, CanDeactivateGuard { 22 | 23 | public article: Article = {} as Article; 24 | public title: string; 25 | public date: moment.Moment; 26 | public isEditorChanged = false; 27 | 28 | private _routeSubscription: Subscription; 29 | 30 | constructor( 31 | private articleService: ArticleService, 32 | private route: ActivatedRoute, 33 | private router: Router, 34 | private message: NzMessageService, 35 | private electronService: ElectronService, 36 | private systemSettingsService: SystemSettingsService, 37 | private modalService: NzModalService, 38 | ) { 39 | } 40 | 41 | ngOnInit() { 42 | this._routeSubscription = this.route.params 43 | .switchMap(params => { 44 | return this.articleService.articles$ 45 | .map(articles => articles.find(article => article._id === params.id)); 46 | }) 47 | .map((article) => { 48 | if (!article) { 49 | this.router.navigate(['/dashboard/article']); 50 | return ; 51 | } 52 | this.article = article; 53 | 54 | this.isEditorChanged = false; 55 | 56 | return article; 57 | }) 58 | .subscribe(() => {}); 59 | 60 | } 61 | 62 | ngOnDestroy() { 63 | this._routeSubscription.unsubscribe(); 64 | } 65 | 66 | canDeactivate() { 67 | if ( this.isEditorChanged ) { 68 | return this.modalService.confirm({ 69 | nzTitle : 'YOU HAVE CHANGED SOME THINK', 70 | nzContent : 'DO YOU WANT SURE TO LEAVE THIS PAGE?', 71 | nzOnOk : () => true, 72 | }).afterClose; 73 | } else { 74 | return true; 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article.component.html: -------------------------------------------------------------------------------- 1 |
2 | 15 |
16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../styles"; 2 | 3 | :host { 4 | .article-sidebar { 5 | overflow-y: auto; 6 | width: $help-sidebar-width; 7 | border-right: $border-color solid thin; 8 | 9 | .article-sidebar-search { 10 | padding: $spacing-s $spacing-m; 11 | border-bottom: $border-color solid thin; 12 | } 13 | 14 | .article-sidebar-list-wrapper { 15 | overflow: scroll; 16 | 17 | .article-sidebar-list-title { 18 | padding: $spacing-m $spacing-m $spacing-xs ; 19 | border-bottom: $border-color solid thin; 20 | font-weight: bold; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ArticleComponent } from './article.component'; 4 | 5 | describe('ArticleComponent', () => { 6 | let component: ArticleComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ ArticleComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ArticleComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/article/article.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { ArticleService } from '../../../services/article.service'; 3 | import { Article } from '../../../Models/Article'; 4 | import { Subscription } from 'rxjs'; 5 | import { FormControl } from '@angular/forms'; 6 | import 'rxjs/add/operator/shareReplay'; 7 | 8 | @Component({ 9 | selector: 'app-post', 10 | templateUrl: './article.component.html', 11 | styleUrls: ['./article.component.scss'] 12 | }) 13 | export class ArticleComponent implements OnInit, OnDestroy { 14 | 15 | public posts: Article[]; 16 | public drafts: Article[]; 17 | public searchFormControl = new FormControl(''); 18 | 19 | private postsSubscription: Subscription; 20 | private draftsSubscription: Subscription; 21 | 22 | constructor( 23 | private articleService: ArticleService, 24 | ) { 25 | 26 | const searchFormObservable = this.searchFormControl.valueChanges.shareReplay(1).debounceTime(300); 27 | 28 | this.postsSubscription = this.articleService.posts$ 29 | .switchMap( (posts: Article[]) => { 30 | return searchFormObservable.map((query) => { 31 | return posts.filter(post => query ? post.title.toUpperCase().includes(query.toUpperCase()) : true); 32 | }); 33 | }) 34 | .subscribe((posts: Article[]) => { 35 | this.posts = posts; 36 | this.posts.sort((a, b) => b.date.valueOf() - a.date.valueOf()); 37 | }); 38 | 39 | this.draftsSubscription = this.articleService.drafts$ 40 | .switchMap( (drafts: Article[]) => { 41 | return searchFormObservable.map((query) => { 42 | return drafts.filter(draft => query ? draft.title.toUpperCase().includes(query.toUpperCase()) : true); 43 | }); 44 | }) 45 | .subscribe((drafts: Article[]) => { 46 | this.drafts = drafts; 47 | this.drafts.sort((a, b) => b.date.valueOf() - a.date.valueOf()); 48 | }); 49 | 50 | this.searchFormControl.patchValue(''); 51 | } 52 | 53 | ngOnInit() { 54 | this.articleService.getArticles(); 55 | this.articleService.startWatchArticle(); 56 | } 57 | 58 | ngOnDestroy() { 59 | this.postsSubscription.unsubscribe(); 60 | this.draftsSubscription.unsubscribe(); 61 | this.articleService.stopWatchArticle(); 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | width: 100%; 3 | 4 | .dashboard { 5 | overflow: hidden; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { DashboardComponent } from './dashboard.component'; 4 | 5 | describe('DashboardComponent', () => { 6 | let component: DashboardComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ DashboardComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(DashboardComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core'; 2 | import { HexoService } from '../../services/hexo.service'; 3 | import { ConfigService } from '../../services/config.service'; 4 | import { Subscription } from 'rxjs'; 5 | import { Router } from '@angular/router'; 6 | 7 | @Component({ 8 | selector: 'app-dashboard', 9 | templateUrl: './dashboard.component.html', 10 | styleUrls: ['./dashboard.component.scss'] 11 | }) 12 | export class DashboardComponent implements OnInit, OnDestroy { 13 | 14 | constructor( 15 | ) { } 16 | 17 | ngOnInit() { 18 | } 19 | 20 | ngOnDestroy() { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/settings/settings.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | HEXO PATH: 4 | {{ hexoPath }} 5 | 6 |
7 | 8 | 9 | 10 |
14 | 15 |
16 | 23 |
24 |
25 |
26 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/settings/settings.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../../style/index"; 2 | 3 | :host { 4 | .settings { 5 | padding: $spacing-l; 6 | 7 | .settings-config-yml-form { 8 | 9 | .settings-config-yml-form-button { 10 | position: absolute; 11 | bottom: $spacing-xxl; 12 | right: $spacing-xxl; 13 | z-index: 99; 14 | } 15 | 16 | .settings-config-yml-content-wrapper { 17 | position: relative; 18 | /deep/ .CodeMirror { 19 | position: absolute; 20 | top: 0; 21 | bottom: 0; 22 | width: 100%; 23 | height: 100%; 24 | 25 | .CodeMirror-code{ 26 | padding-bottom: $spacing-CodeMirror-padding-scroll; 27 | } 28 | } 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/settings/settings.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { SettingsComponent } from './settings.component'; 4 | 5 | describe('SettingsComponent', () => { 6 | let component: SettingsComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ SettingsComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(SettingsComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/dashboard/settings/settings.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, OnDestroy } from '@angular/core'; 2 | import { 3 | FormBuilder, 4 | FormGroup 5 | } from '@angular/forms'; 6 | import { SystemSettingsService } from '../../../services/system-settings.service'; 7 | import { ConfigService } from '../../../services/config.service'; 8 | import { Subscription } from 'rxjs'; 9 | import { NzMessageService } from 'ng-zorro-antd'; 10 | import { UtilsService } from '../../../services/utils.service'; 11 | 12 | @Component({ 13 | selector: 'app-settings', 14 | templateUrl: './settings.component.html', 15 | styleUrls: ['./settings.component.scss'] 16 | }) 17 | export class SettingsComponent implements OnInit, OnDestroy { 18 | 19 | public hexoPath = ''; 20 | public configYmlForm: FormGroup; 21 | 22 | private configYmlSubscription: Subscription; 23 | 24 | constructor( 25 | private fb: FormBuilder, 26 | private systemSettingsService: SystemSettingsService, 27 | private configService: ConfigService, 28 | private message: NzMessageService, 29 | private utils: UtilsService 30 | ) { 31 | 32 | this.hexoPath = this.systemSettingsService.getHexoPath(); 33 | this.configService.getConfigYml(); 34 | 35 | this.configYmlForm = this.fb.group({ 36 | configYml: '' 37 | }); 38 | } 39 | 40 | ngOnInit() { 41 | this.configYmlSubscription = this.configService.configYml$.subscribe((configYml) => { 42 | this.configYmlForm.setValue({ 43 | configYml 44 | }); 45 | }); 46 | } 47 | 48 | ngOnDestroy() { 49 | this.configYmlSubscription.unsubscribe(); 50 | } 51 | 52 | public showSelectHexoPath() { 53 | const path = this.utils.openDirectoryDialog(); 54 | if (!path) { return; } 55 | if (this.utils.isHexoProjectFolder(path)) { 56 | this.hexoPath = path; 57 | location.reload(); 58 | } else { 59 | this.utils.showNotHexoProjectPathAlert(); 60 | this.showSelectHexoPath(); 61 | } 62 | } 63 | 64 | public save() { 65 | const loadingMessageId = this.message.loading('SAVING').messageId; 66 | 67 | this.configService.updateConfigYml(this.configYmlForm.value.configYml) 68 | .then(() => this.message.success('SAVING OK')) 69 | .catch(() => this.message.error('SAVING ERROR')) 70 | .finally( () => this.message.remove(loadingMessageId)); 71 | } 72 | 73 | 74 | public onKeyDown($event): void { 75 | if (this.utils.isMac()) { 76 | this.handleMacKeyEvents($event); 77 | } else { 78 | this.handleWindowsKeyEvents($event); 79 | } 80 | } 81 | 82 | handleMacKeyEvents($event) { 83 | const charCode = $event.key.toLowerCase(); 84 | // matekey: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/metaKey 85 | if ($event.metaKey && charCode === 's') { this.save(); $event.preventDefault(); } 86 | } 87 | 88 | handleWindowsKeyEvents($event) { 89 | $event.preventDefault(); 90 | const charCode = $event.key.toLowerCase(); 91 | if ($event.ctrlKey && charCode === 's') { this.save(); $event.preventDefault(); } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/app/pages/not-project-found/not-project-found.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 |
10 | 13 | 14 | HOW SETUP ? 15 |
16 | 17 |
18 |
19 | -------------------------------------------------------------------------------- /src/app/pages/not-project-found/not-project-found.component.scss: -------------------------------------------------------------------------------- 1 | @import "../../../style/index"; 2 | 3 | :host{ 4 | .not-project-found { 5 | padding: $spacing-xxl; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/pages/not-project-found/not-project-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { NotProjectFoundComponent } from './not-project-found.component'; 4 | 5 | describe('NotProjectFoundComponent', () => { 6 | let component: NotProjectFoundComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async(() => { 10 | TestBed.configureTestingModule({ 11 | declarations: [ NotProjectFoundComponent ] 12 | }) 13 | .compileComponents(); 14 | })); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(NotProjectFoundComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/app/pages/not-project-found/not-project-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { HexoService } from '../../services/hexo.service'; 3 | import { NzModalService } from 'ng-zorro-antd'; 4 | import { NewBlogModalComponent } from '../../components/new-blog-modal/new-blog-modal.component'; 5 | import { ElectronService } from '../../services/electron.service'; 6 | import { Route, Router } from '@angular/router'; 7 | import {SystemSettingsService} from '../../services/system-settings.service'; 8 | 9 | @Component({ 10 | selector: 'app-not-project-found', 11 | templateUrl: './not-project-found.component.html', 12 | styleUrls: ['./not-project-found.component.scss'] 13 | }) 14 | export class NotProjectFoundComponent implements OnInit { 15 | 16 | constructor( 17 | private hexoService: HexoService, 18 | private modalService: NzModalService, 19 | private electronService: ElectronService, 20 | private system: SystemSettingsService, 21 | private router: Router 22 | ) { } 23 | 24 | ngOnInit() { 25 | } 26 | 27 | public openNewBlogDialog() { 28 | this.modalService.create({ 29 | nzTitle: 'NEW HEXO BLOG', 30 | nzContent: NewBlogModalComponent, 31 | nzFooter: null 32 | }); 33 | } 34 | 35 | public openExistingProjectDialog() { 36 | const path = this.hexoService.openSelectHexoDirectoryDialog(); 37 | if (path) { 38 | this.system.saveHexoPath(path); 39 | this.electronService.remote.getCurrentWindow().reload(); 40 | } 41 | } 42 | 43 | public openHexoSetupPage() { 44 | this.electronService.shell.openExternal('https://hexo.io/docs/setup'); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/app/services/article.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ArticleService } from './article.service'; 4 | 5 | describe('ArticleService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ArticleService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ArticleService], (service: ArticleService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/article.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { HexoService } from './hexo.service'; 3 | import { BehaviorSubject } from 'rxjs'; 4 | import { Article } from '../Models/Article'; 5 | import { Article as ArticleInterface } from '../Models/Article.interface'; 6 | import { ElectronService } from './electron.service'; 7 | import { SystemSettingsService } from './system-settings.service'; 8 | import { ConfigService } from './config.service'; 9 | import { UtilsService } from './utils.service'; 10 | import * as moment from 'moment'; 11 | import 'rxjs/add/operator/combineLatest'; 12 | 13 | @Injectable({ 14 | providedIn: 'root' 15 | }) 16 | export class ArticleService { 17 | 18 | private _articleWatcher; 19 | 20 | public articles$: BehaviorSubject = new BehaviorSubject([]); // save posts and drafts 21 | public posts$: BehaviorSubject = new BehaviorSubject([]); // only save posts 22 | public drafts$: BehaviorSubject = new BehaviorSubject([]); // only save drafts 23 | 24 | constructor( 25 | private hexoService: HexoService, 26 | private electronService: ElectronService, 27 | private systemSettings: SystemSettingsService, 28 | private configService: ConfigService, 29 | private utilsService: UtilsService 30 | ) { 31 | this.posts$.combineLatest(this.drafts$).subscribe(([posts, drafts]) => { 32 | this.articles$.next([...posts, ...drafts]); 33 | }); 34 | } 35 | 36 | public startWatchArticle() { 37 | this._articleWatcher = this.electronService.watcher.watch([this.getDraftPath(), this.getPostPath()], 38 | { 39 | interval: 100, 40 | ignoreInitial: true, 41 | depth: 0 42 | }); 43 | 44 | this._articleWatcher 45 | .on('add', path => { 46 | this.getArticles(); 47 | console.log(`watch file ${path} has been added`); 48 | }) 49 | .on('unlink', path => { 50 | this.getArticles(); 51 | console.log(`watch file ${path} has been removed`); 52 | }); 53 | } 54 | 55 | public stopWatchArticle() { 56 | if (this._articleWatcher) { this._articleWatcher.close(); } 57 | } 58 | 59 | public getArticles() { 60 | this.getPosts(); 61 | this.getDrafts(); 62 | } 63 | 64 | public getPosts(): Article[] { 65 | const articles = this.getPostsPath().map((path) => { 66 | const article = this._parseArticleFromPath(path); 67 | article.published = true; 68 | return article; 69 | }); 70 | this.posts$.next(articles); 71 | return articles; 72 | } 73 | 74 | public getDrafts(): Article[] { 75 | const articles = this.getDraftsPath().map((path) => { 76 | const article = this._parseArticleFromPath(path); 77 | article.published = false; 78 | return article; 79 | }); 80 | this.drafts$.next(articles); 81 | return articles; 82 | } 83 | 84 | public getPostsPath(): string[] { 85 | return this.utilsService.findFilesInDir(this.getPostPath(), '.md'); 86 | } 87 | 88 | public getDraftsPath(): string[] { 89 | return this.utilsService.findFilesInDir(this.getDraftPath(), '.md'); 90 | } 91 | 92 | public getArticleLocalById(postId: string): Article { 93 | return this.articles$.getValue().find( (post) => post._id === postId); 94 | } 95 | 96 | public getArticleByLocalByTitle(title: string): Article { 97 | return this.articles$.getValue().find((article) => article.title === title); 98 | } 99 | 100 | private _parseArticleFromPath(path: string): Article { 101 | const file = this.electronService.path.basename(path); 102 | const asset_dir = path.replace(/\.[^/.]+$/, ''); 103 | const fileName = this.electronService.path.basename(path, '.md'); 104 | const raw = this.electronService.fs.readFileSync(path, 'utf8'); 105 | const stat = this.electronService.fs.statSync(path); 106 | const updated = moment(stat.mtime); 107 | const created = moment(stat.ctime); 108 | return new Article({ fileName, file, path, raw, updated, created, asset_dir}); 109 | } 110 | 111 | public checkIfExistFileName(fileName: string): boolean { 112 | const articles = this.articles$.getValue(); 113 | return articles.findIndex(article => { 114 | return article.fileName === fileName; 115 | }) === -1 ? false : true; 116 | } 117 | 118 | public create(article: ArticleInterface): Promise { 119 | const layout = article.published ? 'post' : 'draft'; 120 | return this.hexoService.exec(`hexo new ${layout} "${article.title}"`); 121 | } 122 | 123 | public update(updateArticle: Article): Promise { 124 | return this.electronService.fs.writeFile(updateArticle.path, updateArticle.raw) 125 | .then(() => { 126 | console.log('update article ok'); 127 | this._updateLocalArticle(updateArticle); 128 | updateArticle.refreshInfoAndContent(); 129 | return true; 130 | }) 131 | .catch((error) => { 132 | console.error('update article error', error); 133 | throw error; 134 | }); 135 | } 136 | 137 | public delete(article: Article): any { 138 | // TODO Remove folder if exist 139 | 140 | if (article.asset_dir && this.electronService.fs.existsSync(article.asset_dir)) { 141 | this.utilsService.rmdir(article.asset_dir); 142 | } 143 | return this.electronService.fs.unlink(article.path) 144 | .then(() => { 145 | console.log('delete article ok'); 146 | }) 147 | .catch((error) => { 148 | console.log('delete article error', error); 149 | throw error; 150 | }); 151 | } 152 | 153 | public _updateLocalArticle(updateArticle: Article) { 154 | const articles = this.articles$.getValue(); 155 | 156 | const articleIndex = articles.findIndex(article => article._id === updateArticle._id); 157 | articles[articleIndex] = updateArticle; 158 | 159 | this.articles$.next(articles); 160 | } 161 | 162 | public publish(article: Article): Promise
{ 163 | return this.hexoService.exec(`hexo publish "${article.fileName}"`) 164 | .then(() => { 165 | article.published = true; 166 | return article; 167 | }); 168 | } 169 | 170 | public getPostPath(): string { 171 | const hexoPath = this.systemSettings.getHexoPath(); 172 | const sourcePath = this.configService.configJson$.getValue().source_dir; 173 | 174 | if (!hexoPath || ! sourcePath ) { return undefined; } 175 | return `${hexoPath}/${sourcePath}/_posts`; 176 | } 177 | 178 | public getDraftPath(): string { 179 | const hexoPath = this.systemSettings.getHexoPath(); 180 | const sourcePath = this.configService.configJson$.getValue().source_dir; 181 | 182 | if (!hexoPath || ! sourcePath ) { return undefined; } 183 | return `${hexoPath}/${sourcePath}/_drafts`; 184 | } 185 | 186 | public rename(article: Article, newFileName: string): any { 187 | const promises = []; 188 | const existAssetDir = this.electronService.fs.existsSync(article.path); 189 | const newFile = article.file.replace(article.fileName, newFileName); 190 | const newFilePath = article.path.replace(article.file, newFile); 191 | if (existAssetDir) { 192 | const newAssetDir = article.asset_dir.replace(article.fileName, newFileName); 193 | promises.push(this.electronService.fs.rename(article.asset_dir, newAssetDir)); 194 | } 195 | promises.push(this.electronService.fs.rename(article.path, newFilePath)); 196 | return Promise.all(promises) 197 | .then((...arg) => { 198 | console.log('renamed post', arg); 199 | }) 200 | .catch((err) => { 201 | console.error('rename post error', err); 202 | }); 203 | } 204 | 205 | public openAssetFolder(assetFolderPah: string): boolean { 206 | this.utilsService.createDirIfNotExist(assetFolderPah); 207 | const isOpened = this.electronService.shell.openItem(assetFolderPah); 208 | if (isOpened) { 209 | console.log('open asset folder', assetFolderPah); 210 | } else { 211 | console.error('open asset folder error: may be asset folder not exist'); 212 | } 213 | return isOpened; 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/app/services/asset.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { AssetService } from './asset.service'; 4 | 5 | describe('AssetService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [AssetService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([AssetService], (service: AssetService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/asset.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ElectronService } from './electron.service'; 3 | import { UtilsService } from './utils.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class AssetService { 9 | 10 | constructor( 11 | private electronService: ElectronService, 12 | private utilsService: UtilsService 13 | ) {} 14 | 15 | public saveImageToAssetDir(assetDir = '', file = '', imageData = ''): Promise { 16 | this.utilsService.createDirIfNotExist(assetDir); 17 | const path = this.electronService.path; 18 | const fs = this.electronService.fs; 19 | const imageFilePath = path.join(assetDir, file); 20 | return this.electronService.fs.writeFile(imageFilePath, imageData) 21 | .then((...arg) => { 22 | console.log('save image ok'); 23 | return true; 24 | }) 25 | .catch((err) => { 26 | console.error('save image error', err); 27 | throw err; 28 | }); 29 | } 30 | 31 | public getAssetsPath(assetDir: string): string[] { 32 | return this.utilsService.findFilesInDir(assetDir); 33 | } 34 | 35 | public checkIfExistFileName(assetDir: string, fileName: string): boolean { 36 | const path = this.electronService.path; 37 | const files = this.getAssetsPath(assetDir).map((filesPath) => path.basename(filesPath)); 38 | for (let i = 0; i < files.length; i++ ) { 39 | if (files.includes(fileName)) { return true; } 40 | } 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/app/services/config.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ConfigService } from './config.service'; 4 | 5 | describe('ConfigService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ConfigService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ConfigService], (service: ConfigService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/config.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ElectronService } from './electron.service'; 3 | import { SystemSettingsService } from './system-settings.service'; 4 | import { BehaviorSubject } from 'rxjs'; 5 | import { Config } from '../Models/Config.Interface'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class ConfigService { 11 | 12 | configYml$: BehaviorSubject = new BehaviorSubject(''); 13 | configJson$: BehaviorSubject = new BehaviorSubject({}); 14 | 15 | constructor( 16 | private electronService: ElectronService, 17 | private systemSettings: SystemSettingsService, 18 | ) { 19 | this.configYml$.subscribe((configYmlData) => { 20 | const configJson = this.electronService.yaml.safeLoad(configYmlData) as Config; 21 | this.configJson$.next(configJson); 22 | }); 23 | } 24 | 25 | public getConfigYml (): string { 26 | return this.electronService.fs.readFile(this.getConfigYmlPath(), 'utf8') 27 | .then((configYmlData) => { 28 | this.configYml$.next(configYmlData); 29 | console.log('get config yml'); 30 | return configYmlData; 31 | }) 32 | .catch((error) => { 33 | console.error(error); 34 | throw error; 35 | }); 36 | } 37 | 38 | public getConfigYmlPath(): string | undefined { 39 | const hexoPath = this.systemSettings.getHexoPath(); 40 | if (!hexoPath ) { return undefined; } 41 | return `${hexoPath}/_config.yml`; 42 | } 43 | 44 | public updateConfigYml(content: string): Promise { 45 | return this.electronService.fs.writeFile(this.getConfigYmlPath(), content) 46 | .then(() => { 47 | this.configYml$.next(content); 48 | return content; 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/app/services/electron.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | // If you import a module but never use any of the imported values other than as TypeScript types, 4 | // the resulting javascript file will look as if you never imported the module at all. 5 | import { ipcRenderer, webFrame, remote, shell, clipboard} from 'electron'; 6 | import * as childProcess from 'child_process'; 7 | import * as fs from 'fs-extra'; 8 | import * as yaml from 'js-yaml'; 9 | import * as path from 'path'; 10 | 11 | import chokidar from 'chokidar'; 12 | 13 | @Injectable() 14 | export class ElectronService { 15 | 16 | ipcRenderer: typeof ipcRenderer; 17 | webFrame: typeof webFrame; 18 | remote: typeof remote; 19 | process: any; 20 | shell: typeof shell; 21 | clipboard: typeof clipboard; 22 | app: typeof remote.app; 23 | 24 | childProcess: typeof childProcess; 25 | fs: typeof fs; 26 | yaml: typeof yaml; 27 | path: typeof path; 28 | 29 | watcher: typeof chokidar; 30 | 31 | constructor() { 32 | // Conditional imports 33 | if (this.isElectron()) { 34 | this.ipcRenderer = window.require('electron').ipcRenderer; 35 | this.webFrame = window.require('electron').webFrame; 36 | this.remote = window.require('electron').remote; 37 | this.process = this.remote.process; 38 | this.shell = window.require('electron').shell; 39 | this.clipboard = window.require('electron').clipboard; 40 | this.app = this.remote.app; 41 | 42 | this.childProcess = window.require('child_process'); 43 | this.fs = window.require('fs-extra'); 44 | this.yaml = window.require('js-yaml'); 45 | this.path = window.require('path'); 46 | 47 | this.watcher = window.require('chokidar'); 48 | } 49 | } 50 | 51 | isElectron = () => { 52 | return window && window.process && window.process.type; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/app/services/hexo.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { HexoService } from './hexo.service'; 4 | 5 | describe('HexoService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [HexoService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([HexoService], (service: HexoService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/hexo.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { SystemSettingsService } from './system-settings.service'; 3 | import { ElectronService } from './electron.service'; 4 | import { UtilsService } from './utils.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class HexoService { 10 | 11 | constructor( 12 | private systemSettings: SystemSettingsService, 13 | private electronService: ElectronService, 14 | private utils: UtilsService 15 | ) { 16 | } 17 | 18 | public newBlog(directory: string): Promise { 19 | return this.exec('hexo init', {cwd: directory}); 20 | } 21 | 22 | public isCurrentDirectoryProjectFolder(): boolean { 23 | return this.utils.isHexoProjectFolder(this.systemSettings.getHexoPath()); 24 | } 25 | 26 | public openSelectHexoDirectoryDialog(): string { 27 | const directory = this.utils.openDirectoryDialog(); 28 | if (! directory) { 29 | this.utils.showNotHexoProjectPathAlert(); 30 | return ''; 31 | } 32 | return directory; 33 | } 34 | 35 | public deployChildProcess(): Promise { 36 | return this.exec(`hexo clean`) 37 | .then(() => this.exec('hexo g')) 38 | .then(() => this.exec('hexo d')); 39 | } 40 | 41 | public exec(command: string, options = {}, callback = (...arg: any[]) => {}): Promise { 42 | const customOption = { 43 | cwd: this.systemSettings.getHexoPath() 44 | }; 45 | return new Promise((resolve, reject) => { 46 | this.electronService.childProcess 47 | .exec(command, {...customOption, ...options}, 48 | function (error, stdout, stderr) { 49 | callback(error, stdout, stderr); 50 | console.log('stdout: ' + stdout); 51 | console.warn('stderr: ' + stderr); 52 | if (error !== null) { 53 | console.error('exec error: ' + error); 54 | reject(); 55 | } 56 | resolve(); 57 | } 58 | ); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/services/scaffold.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ScaffoldService } from './scaffold.service'; 4 | 5 | describe('ScaffoldService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ScaffoldService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ScaffoldService], (service: ScaffoldService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/scaffold.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class ScaffoldService { 7 | constructor( 8 | ) { } 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/app/services/server.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { ServerService } from './server.service'; 4 | 5 | describe('ServerService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [ServerService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([ServerService], (service: ServerService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/server.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { UtilsService } from './utils.service'; 3 | import { ElectronService } from './electron.service'; 4 | import { SystemSettingsService } from './system-settings.service'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class ServerService { 10 | 11 | public isServerRunning = false; 12 | public isServerLoading = false; 13 | private _serverRunningChildProcess = null; 14 | private _startServerWaitingTime = 3000; 15 | 16 | constructor( 17 | private electronService: ElectronService, 18 | private systemSettings: SystemSettingsService 19 | ) { } 20 | 21 | public startServer(): Promise { 22 | 23 | console.log('starting server'); 24 | this.isServerRunning = true; 25 | this.isServerLoading = true; 26 | setTimeout(() => {this.isServerLoading = false}, this._startServerWaitingTime); 27 | 28 | return new Promise((resolve, reject) => { 29 | this._serverRunningChildProcess = this.electronService.childProcess 30 | .exec('hexo server', { 31 | cwd: this.systemSettings.getHexoPath() 32 | }, (error, stdout, stderr) => { 33 | console.log('stdout: ' + stdout); 34 | console.log('stderr: ' + stderr); 35 | if (error !== null) { 36 | this.isServerRunning = false; 37 | reject(error); 38 | console.log('exec error: ' + error); 39 | } else { 40 | resolve(); 41 | } 42 | }); 43 | }); 44 | } 45 | 46 | public stopServer() { 47 | console.log('stop server'); 48 | this.isServerRunning = false; 49 | this._serverRunningChildProcess.kill('SIGINT'); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/app/services/system-settings.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { SystemSettingsService } from './system-settings.service'; 4 | 5 | describe('SystemSettingsService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [SystemSettingsService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([SystemSettingsService], (service: SystemSettingsService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/system-settings.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import * as settings from 'electron-settings'; 3 | import { ElectronService } from './electron.service'; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class SystemSettingsService { 9 | 10 | private _settings: typeof settings; 11 | 12 | constructor( 13 | private electronService: ElectronService 14 | ) { 15 | // Conditional imports 16 | if (this.electronService.isElectron()) { 17 | this._settings = window.require('electron-settings'); 18 | } 19 | } 20 | 21 | public getHexoPath(): string { 22 | return this._settings.get('hexoPath'); 23 | } 24 | 25 | public saveHexoPath(path: string) { 26 | this._settings.set('hexoPath', path); 27 | } 28 | 29 | public getIsActivePreview(): boolean { 30 | return this._settings.get('isActivePreview') || false; 31 | } 32 | 33 | public saveIsActivePreview(isActive: boolean = false) { 34 | this._settings.set('isActivePreview', isActive); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/app/services/utils.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed, inject } from '@angular/core/testing'; 2 | 3 | import { UtilsService } from './utils.service'; 4 | 5 | describe('UtilsService', () => { 6 | beforeEach(() => { 7 | TestBed.configureTestingModule({ 8 | providers: [UtilsService] 9 | }); 10 | }); 11 | 12 | it('should be created', inject([UtilsService], (service: UtilsService) => { 13 | expect(service).toBeTruthy(); 14 | })); 15 | }); 16 | -------------------------------------------------------------------------------- /src/app/services/utils.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ElectronService } from './electron.service'; 3 | import { SystemSettingsService } from './system-settings.service'; 4 | import { utils } from '../../../common/utils'; 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class UtilsService { 10 | 11 | constructor( 12 | private electronService: ElectronService, 13 | private systemSettingsService: SystemSettingsService, 14 | ) { 15 | } 16 | 17 | public isWindows() { 18 | return process.platform === 'win32'; 19 | } 20 | 21 | public isMac() { 22 | return navigator.platform.match('Mac'); 23 | } 24 | 25 | public isURL(str: string): boolean { 26 | const pattern = /(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/ 27 | return pattern.test(str); 28 | } 29 | 30 | public isImageFormat(str): boolean { 31 | return (/\.(jpg|jpeg|png)$/i).test(str); 32 | } 33 | 34 | public isDev() { return utils.isDev(); } 35 | 36 | public isPro() { return utils.isPro(); } 37 | 38 | public isHexoProjectFolder(path: string): boolean { 39 | if (!path) { return false; } 40 | return this.electronService.fs.existsSync(`${path}/_config.yml`); 41 | } 42 | 43 | public openTerminal() { 44 | const path = this.systemSettingsService.getHexoPath(); 45 | const childProcess = this.electronService.childProcess; 46 | const { spawn } = childProcess; 47 | switch (process.platform) { 48 | case 'win32': 49 | childProcess.exec(`start cmd.exe /K cd /D {path}`); 50 | break; 51 | case 'linux': 52 | const terminal = 'gnome-terminal'; 53 | spawn (terminal, { cwd: path } as any); 54 | // openTerminalAtPath.on ('error', (err) => { console.log (err); }); 55 | break; 56 | case 'darwin': 57 | spawn ('open', [ '-a', 'Terminal', path ]); 58 | // openTerminalAtPath.on ('error', (err) => { console.log (err); }); 59 | break; 60 | } 61 | } 62 | 63 | public openDirectoryDialog(): string | undefined { 64 | const remote = this.electronService.remote; 65 | const dialog = remote.dialog; 66 | 67 | const paths = dialog.showOpenDialog({ 68 | properties: ['openDirectory'] 69 | }); 70 | 71 | if (!paths) { 72 | return paths as undefined; 73 | } 74 | 75 | return paths[0]; 76 | } 77 | 78 | public showNotHexoProjectPathAlert() { 79 | const remote = this.electronService.remote; 80 | const dialog = this.electronService.remote.dialog; 81 | dialog.showMessageBox( 82 | remote.getCurrentWindow(), 83 | { 84 | type: 'warning', 85 | title: 'ALERT', 86 | message: 'THE FOLDER DONT HEAVE _config.yml FILE, PLIZ CHOOSE THE CORRECT HEXO PROJECT FOLDER?' 87 | }); 88 | } 89 | 90 | public removeFileExtension(filename): string { 91 | return filename.replace(/\.[^/.]+$/, ''); 92 | } 93 | 94 | public clipboardHasFormat (format) { 95 | const clipboard = this.electronService.clipboard; 96 | const formats = clipboard.availableFormats(); 97 | for (let i = 0; i < formats.length; i++) { 98 | if (formats[i].includes(format)) { 99 | return true; 100 | } 101 | } 102 | return false; 103 | } 104 | 105 | public createDirIfNotExist(path: string): boolean { 106 | if (this.electronService.fs.existsSync(path)) { 107 | return true; 108 | } else { 109 | this.electronService.fs.mkdirSync(path); 110 | console.warn(`file is not exist, creating: ${path}`); 111 | return false; 112 | } 113 | } 114 | 115 | public findFilesInDir(startPath: string, filter?: string): string[] { 116 | const results = []; 117 | const fs = this.electronService.fs; 118 | const path = this.electronService.path; 119 | if (!fs.existsSync(startPath)) { 120 | console.log('no dir ', startPath); 121 | return results; 122 | } 123 | 124 | const files = fs.readdirSync(startPath); 125 | for (let i = 0; i < files.length; i++) { 126 | const filename = path.join(startPath, files[i]); 127 | if (!filter || filename.indexOf(filter) >= 0) { 128 | results.push(filename); 129 | } 130 | } 131 | return results; 132 | } 133 | 134 | // https://stackoverflow.com/questions/31917891/node-how-to-remove-a-directory-if-exists 135 | public rmdir (dir) { 136 | const list = this.electronService.fs.readdirSync(dir); 137 | for (let i = 0; i < list.length; i++) { 138 | const filename = this.electronService.path.join(dir, list[i]); 139 | const stat = this.electronService.fs.statSync(filename); 140 | 141 | if (filename === '.' || filename === '..') { 142 | // pass these files 143 | } else if (stat.isDirectory()) { 144 | // rmdir recursively 145 | this.rmdir(filename); 146 | } else { 147 | // rm fiilename 148 | this.electronService.fs.unlinkSync(filename); 149 | } 150 | } 151 | console.log('dir delete:', dir); 152 | this.electronService.fs.rmdirSync(dir); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/assets/background.jpg -------------------------------------------------------------------------------- /src/assets/hexo-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/hexo-note-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /src/assets/i18n/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "PAGES": { 3 | "HOME": { 4 | "TITLE": "App works !" 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/environments/environment.common.ts: -------------------------------------------------------------------------------- 1 | export const AppConfigCommon = { 2 | version: require('../../package.json').version 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.dev.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `index.ts`, but if you do 3 | // `ng build --env=prod` then `index.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | import {AppConfigCommon} from './environment.common'; 7 | 8 | export const AppConfig = { 9 | production: false, 10 | environment: 'DEV', 11 | ...AppConfigCommon 12 | }; 13 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | import {AppConfigCommon} from './environment.common'; 2 | 3 | export const AppConfig = { 4 | production: true, 5 | environment: 'PROD', 6 | ...AppConfigCommon 7 | }; 8 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | import { AppConfigCommon } from './environment.common'; 2 | 3 | export const AppConfig = { 4 | production: false, 5 | environment: 'LOCAL', 6 | ...AppConfigCommon 7 | }; 8 | -------------------------------------------------------------------------------- /src/favicon.256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/favicon.256x256.png -------------------------------------------------------------------------------- /src/favicon.512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/favicon.512x512.png -------------------------------------------------------------------------------- /src/favicon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/favicon.icns -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/favicon.ico -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tmirun/Hexo-Note/bb54e4039bd583fa528099f72916c840c7573914/src/favicon.png -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hexo Note 6 | 7 | 8 | 9 | 10 | 86 | 87 | 88 | 89 |
90 |
91 |
92 |
93 |
94 |
95 |
H
96 |
97 |
98 | 99 | 100 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/0.13/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: [ 'html', 'lcovonly' ], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: true 30 | }); 31 | }; 32 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { AppConfig } from './environments/environment'; 6 | 7 | import 'codemirror/mode/markdown/markdown'; 8 | import 'codemirror/mode/yaml/yaml'; 9 | import * as fixPath from 'fix-path'; 10 | import { utils } from '../common/utils'; 11 | 12 | import { shim } from 'promise.prototype.finally'; 13 | shim(); 14 | 15 | if (AppConfig.production) { 16 | enableProdMode(); 17 | } 18 | 19 | if (utils.isPro()) { 20 | fixPath(); 21 | } 22 | 23 | platformBrowserDynamic() 24 | .bootstrapModule(AppModule, { 25 | preserveWhitespaces: false 26 | }) 27 | .catch(err => console.error(err)); 28 | -------------------------------------------------------------------------------- /src/polyfills-test.ts: -------------------------------------------------------------------------------- 1 | import 'core-js/es7/reflect'; 2 | import 'zone.js/dist/zone'; 3 | 4 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 37 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 38 | /** IE10 and IE11 requires the following for the Reflect API. */ 39 | // import 'core-js/es6/reflect'; 40 | 41 | /** Evergreen browsers require these. **/ 42 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 43 | 44 | 45 | 46 | /** 47 | * Required to support Web Animations `@angular/platform-browser/animations`. 48 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 49 | **/ 50 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 51 | /** 52 | * By default, zone.js will patch all possible macroTask and DomEvents 53 | * user can disable parts of macroTask/DomEvents patch by setting following flags 54 | */ 55 | 56 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 57 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 58 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 59 | /* 60 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 61 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 62 | */ 63 | // (window as any).__Zone_enable_cross_context_check = true; 64 | /*************************************************************************************************** 65 | * Zone JS is required by default for Angular itself. 66 | */ 67 | import 'zone.js/dist/zone-mix'; // Included with Angular CLI. 68 | /** 69 | * You can load zone-patch-electron to allow electron native APIs 70 | * (Such as dialog/shortcut/menu/getFileIcon/shell/session/ 71 | * desktopCapturer/onEvent) in ngZone 72 | */ 73 | // import 'zone.js/dist/zone-patch-electron'; // add zone-patch-electron to patch Electron native API 74 | /*************************************************************************************************** 75 | * APPLICATION IMPORTS 76 | */ 77 | -------------------------------------------------------------------------------- /src/style/_color.scss: -------------------------------------------------------------------------------- 1 | $white: white; 2 | $black: black; 3 | $grey-1: #fbfbfb; 4 | $grey-2: #f7f7f7; 5 | $grey-3: #f5f5f5; // most user 6 | $grey-4: #e9e9e9; 7 | $grey-5: #d9d9d9; 8 | $grey-6: #bfbfbf; // most user 9 | $grey-7: #919191; 10 | $grey-8: #5a5a5a; 11 | $grey-9: #404040; // most user 12 | $grey-10: #222222; 13 | $grey-11: #121212; 14 | 15 | 16 | $blue-1: #ecf6fd; 17 | $blue-2: #d2eafb; 18 | $blue-3: #add8f7; 19 | $blue-4: #7ec2f3; 20 | $blue-5: #49a9ee; 21 | $blue-6: #108ee9; 22 | $blue-7: #0e77ca; 23 | $blue-8: #0c60aa; 24 | $blue-9: #09488a; 25 | $blue-10: #073069; 26 | 27 | $green-4: #95de64; 28 | $green-6: #52c41a; 29 | 30 | $transparent-light-85: rgba(255, 255, 255, .85); 31 | $transparent-light-65: rgba(255, 255, 255, .65); 32 | $transparent-light-45: rgba(255, 255, 255, .45); 33 | $transparent-light-25: rgba(255, 255, 255, .25); 34 | $transparent-light-15: rgba(255, 255, 255, .15); 35 | $transparent-light-9: rgba(255, 255, 255, .09); 36 | $transparent-light-4: rgba(255, 255, 255, .04); 37 | $transparent-light-2: rgba(255, 255, 255, .02); 38 | 39 | $transparent-dark-85: rgba(0, 0, 0, .85); 40 | $transparent-dark-65: rgba(0, 0, 0, .65); 41 | $transparent-dark-45: rgba(0, 0, 0, .45); 42 | $transparent-dark-25: rgba(0, 0, 0, .25); 43 | $transparent-dark-15: rgba(0, 0, 0, .15); 44 | $transparent-dark-9: rgba(0, 0, 0, .09); 45 | $transparent-dark-4: rgba(0, 0, 0, .04); 46 | $transparent-dark-2: rgba(0, 0, 0, .02); 47 | 48 | $success: $green-4; 49 | $primary: $blue-6; 50 | $danger: #f5222d; 51 | $warning: #faad14; 52 | $hexo-color: #0D83CD; 53 | $border-color: $transparent-dark-25; 54 | $sidebar-color: $white; 55 | 56 | -------------------------------------------------------------------------------- /src/style/_index.scss: -------------------------------------------------------------------------------- 1 | @import "color"; 2 | @import "spacing"; 3 | @import "size"; 4 | -------------------------------------------------------------------------------- /src/style/_size.scss: -------------------------------------------------------------------------------- 1 | $size-text-xs: 10px; 2 | $size-text-s: 12px; 3 | $size-text-m: 14px; 4 | $size-text-l: 18px; 5 | 6 | $size-icon-m: 18px; 7 | -------------------------------------------------------------------------------- /src/style/_spacing.scss: -------------------------------------------------------------------------------- 1 | $spacing-xxs: 2px; 2 | $spacing-xs: 4px; 3 | $spacing-s: 8px; 4 | $spacing-m: 16px; 5 | $spacing-l: 24px; 6 | $spacing-xl: 32px; 7 | $spacing-xxl: 64px; 8 | 9 | $spacing-CodeMirror-padding-scroll: 500px; 10 | 11 | $help-sidebar-width: 280px; 12 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | @import "style/index"; 2 | @import "~codemirror/lib/codemirror.css"; 3 | @import "~codemirror/theme/material.css"; 4 | 5 | /* You can add global styles to this file, and also import other style files */ 6 | html, body { 7 | margin: 0; 8 | padding: 0; 9 | 10 | height: 100%; 11 | } 12 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "baseUrl": "", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills-test.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ], 19 | "exclude": [ 20 | "dist", 21 | "release", 22 | "node_modules" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var nodeModule: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | 7 | declare var window: Window; 8 | interface Window { 9 | process: any; 10 | require: any; 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig-serve.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "sourceMap": true, 4 | "declaration": false, 5 | "moduleResolution": "node", 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "target": "es5", 9 | "typeRoots": [ 10 | "node_modules/@types" 11 | ], 12 | "lib": [ 13 | "es2017", 14 | "es2016", 15 | "es2015", 16 | "dom" 17 | ] 18 | }, 19 | "include": [ 20 | "main.ts" 21 | ], 22 | "exclude": [ 23 | "node_modules", 24 | "**/*.spec.ts" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "importHelpers": true, 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "es2016", 18 | "es2015", 19 | "dom" 20 | ] 21 | }, 22 | "include": [ 23 | "main.ts", 24 | "src/**/*" 25 | ], 26 | "exclude": [ 27 | "node_modules" 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-shadowed-variable": true, 69 | "no-string-literal": false, 70 | "no-string-throw": true, 71 | "no-switch-case-fall-through": true, 72 | "no-trailing-whitespace": true, 73 | "no-unnecessary-initializer": true, 74 | "no-unused-expression": true, 75 | "no-use-before-declare": true, 76 | "no-var-keyword": true, 77 | "object-literal-sort-keys": false, 78 | "one-line": [ 79 | true, 80 | "check-open-brace", 81 | "check-catch", 82 | "check-else", 83 | "check-whitespace" 84 | ], 85 | "prefer-const": true, 86 | "quotemark": [ 87 | true, 88 | "single" 89 | ], 90 | "radix": true, 91 | "semicolon": [ 92 | true, 93 | "always" 94 | ], 95 | "triple-equals": [ 96 | true, 97 | "allow-null-check" 98 | ], 99 | "typedef-whitespace": [ 100 | true, 101 | { 102 | "call-signature": "nospace", 103 | "index-signature": "nospace", 104 | "parameter": "nospace", 105 | "property-declaration": "nospace", 106 | "variable-declaration": "nospace" 107 | } 108 | ], 109 | "unified-signatures": true, 110 | "variable-name": false, 111 | "whitespace": [ 112 | true, 113 | "check-branch", 114 | "check-decl", 115 | "check-operator", 116 | "check-separator", 117 | "check-type" 118 | ], 119 | "component-selector": [ 120 | true, 121 | "element", 122 | "app", 123 | "kebab-case" 124 | ], 125 | "no-output-on-prefix": true, 126 | "use-input-property-decorator": true, 127 | "use-output-property-decorator": true, 128 | "use-host-property-decorator": true, 129 | "no-input-rename": true, 130 | "no-output-rename": true, 131 | "use-life-cycle-interface": true, 132 | "use-pipe-transform-interface": true, 133 | "component-class-suffix": true, 134 | "directive-class-suffix": true 135 | } 136 | } 137 | --------------------------------------------------------------------------------