├── .vscode ├── settings.json └── launch.json ├── config ├── devconfig.example.json ├── webpack.config.eslint.js ├── webpack.config.base.babel.js ├── webpack.config.electron.babel.js ├── webpack.config.renderer.dev.babel.js ├── gulp.babel.js └── webpack.config.renderer.prod.babel.js ├── assets ├── loading.gif └── icons │ ├── win │ └── app.ico │ └── osx │ └── app.icns ├── .eslintignore ├── app ├── main │ ├── resource │ │ ├── app.png │ │ ├── start.png │ │ ├── tray-icon.png │ │ ├── tray-icon@2x.png │ │ └── tray-icon@3x.png │ ├── oauthConfig.js │ ├── index.html │ ├── schedule.js │ ├── package.json │ ├── Oauth2.js │ ├── webview │ │ └── webview.html │ ├── pdf.js │ └── index.js ├── views │ ├── assets │ │ ├── images │ │ │ ├── icon.png │ │ │ ├── logo.png │ │ │ └── onedrive.png │ │ └── scss │ │ │ ├── preview.scss │ │ │ ├── common.scss │ │ │ ├── color.scss │ │ │ ├── themes.scss │ │ │ ├── trash.scss │ │ │ ├── drive.scss │ │ │ └── index.scss │ ├── sagas │ │ ├── sagas.js │ │ ├── app.js │ │ ├── projects.js │ │ └── drive.js │ ├── actions │ │ ├── user.js │ │ ├── exportQueue.js │ │ ├── drive.js │ │ ├── note.js │ │ ├── markdown.js │ │ ├── app.js │ │ └── projects.js │ ├── component │ │ ├── share │ │ │ ├── Loading.jsx │ │ │ ├── search │ │ │ │ ├── search.scss │ │ │ │ └── Search.jsx │ │ │ ├── SVGIcon.jsx │ │ │ └── notebook │ │ │ │ └── NoteItem.jsx │ │ ├── cloud │ │ │ ├── Notebooks.jsx │ │ │ ├── Notes.jsx │ │ │ ├── Cloud.jsx │ │ │ └── Drive.jsx │ │ ├── trash │ │ │ ├── ToolBar.jsx │ │ │ ├── Files.jsx │ │ │ ├── Projects.jsx │ │ │ ├── Trash.jsx │ │ │ └── HOCList.jsx │ │ ├── AppToolBar.jsx │ │ ├── note │ │ │ ├── Explorer.jsx │ │ │ ├── Note.jsx │ │ │ └── ToolBar.jsx │ │ └── editor │ │ │ ├── Markdown.jsx │ │ │ ├── Preview.jsx │ │ │ └── Editor.jsx │ ├── reducers │ │ ├── reducers.js │ │ ├── user.js │ │ ├── exportQueue.js │ │ ├── note.js │ │ ├── drive.js │ │ ├── markdown.js │ │ └── app.js │ ├── index.jsx │ ├── services │ │ ├── CommonServices.js │ │ └── OneDrive.js │ └── utils │ │ ├── db │ │ └── DB.js │ │ └── utils.js └── webview │ └── webview-pre.js ├── .github └── ISSUE_TEMPLATE │ ├── Custom.md │ ├── Feature_request.md │ └── Bug_report.md ├── .travis.yml ├── .editorconfig ├── .eslintrc.yml ├── templete └── index.html ├── scripts ├── packageDMG.js ├── packageDeb.js └── packageSetup.js ├── .babelrc ├── .gitignore ├── CHANGELOG.md ├── README.md ├── .sass-lint.yml └── package.json /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /config/devconfig.example.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensions": [] 3 | } 4 | -------------------------------------------------------------------------------- /assets/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/assets/loading.gif -------------------------------------------------------------------------------- /assets/icons/win/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/assets/icons/win/app.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | lib 4 | app/views/utils/highlight.min.js 5 | out 6 | releases 7 | -------------------------------------------------------------------------------- /app/main/resource/app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/main/resource/app.png -------------------------------------------------------------------------------- /assets/icons/osx/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/assets/icons/osx/app.icns -------------------------------------------------------------------------------- /app/main/resource/start.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/main/resource/start.png -------------------------------------------------------------------------------- /app/main/resource/tray-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/main/resource/tray-icon.png -------------------------------------------------------------------------------- /app/main/resource/tray-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/main/resource/tray-icon@2x.png -------------------------------------------------------------------------------- /app/main/resource/tray-icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/main/resource/tray-icon@3x.png -------------------------------------------------------------------------------- /app/views/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/views/assets/images/icon.png -------------------------------------------------------------------------------- /app/views/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/views/assets/images/logo.png -------------------------------------------------------------------------------- /app/views/assets/images/onedrive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rivafarabi/Yosoro/master/app/views/assets/images/onedrive.png -------------------------------------------------------------------------------- /config/webpack.config.eslint.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | 3 | module.exports = require('./webpack.config.base.babel'); 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Custom.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Custom issue template 3 | about: Describe this issue template's purpose here. 4 | 5 | --- 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/views/sagas/sagas.js: -------------------------------------------------------------------------------- 1 | import app from './app'; 2 | import projects from './projects'; 3 | import drive from './drive'; 4 | 5 | export default [ 6 | ...app, 7 | ...projects, 8 | ...drive, 9 | ]; 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: node_js 4 | 5 | node_js: 6 | - 8 7 | - 9 8 | 9 | cache: 10 | npm: true 11 | directories: 12 | - node_modules 13 | 14 | install: 15 | - yarn install 16 | 17 | script: 18 | - node --version 19 | - npm test 20 | -------------------------------------------------------------------------------- /app/views/actions/user.js: -------------------------------------------------------------------------------- 1 | export const GET_USER_AVATAR = 'GET_USER_AVATAR'; 2 | export const GET_USER_AVATAR_SUCCESS = 'GET_USER_AVATAR_SUCCESS'; 3 | export const GET_USER_AVATAR_FAILED = 'GET_USER_AVATAR_FAILED'; 4 | 5 | export const SET_USER_LOCAL_AVATAR = 'SET_USER_LOCAL_AVATAR'; 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | 9 | [*.{js,jsx}] 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [*.scss] 14 | indent_style = tab 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /app/views/actions/exportQueue.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 文件导出队列 3 | */ 4 | 5 | export const EXPORT_INIT_QUEUE = 'EXPORT_INIT_QUEUE'; 6 | export const EXPORT_SUCCESS_SINGLE = 'EXPORT_SUCCESS_SINGLE'; 7 | export const EXPORT_FAILED_SINGLE = 'EXPORT_FAILED_SINGLE'; 8 | export const EXPORT_COMPOLETE = 'EXPORT_COMPOLETE'; 9 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | env: 2 | es6: true 3 | node: true 4 | browser: true 5 | parserOptions: 6 | ecmaVersion: 6 7 | sourceType: 'module' 8 | parser: 'babel-eslint' 9 | extends: alchemy 10 | plugins: 11 | - react 12 | - jsx-a11y 13 | - import 14 | rules: 15 | no-console: 16 | - error 17 | - 'allow': 18 | - 'warn' 19 | - 'error' 20 | - 'info' 21 | import/no-extraneous-dependencies: 0 22 | -------------------------------------------------------------------------------- /app/views/assets/scss/preview.scss: -------------------------------------------------------------------------------- 1 | .preview-root { 2 | background-color: #fcfdfe; 3 | overflow: hidden; 4 | position: relative; 5 | 6 | &.pre-mode { 7 | margin: 0 auto; 8 | background-color: inherit; 9 | 10 | .preview-body { 11 | width: 100%; 12 | padding: 0; 13 | } 14 | } 15 | } 16 | 17 | .preview-body { 18 | width: 100%; 19 | height: 100%; 20 | overflow: hidden; 21 | box-sizing: border-box; 22 | padding: 0.5rem; 23 | } 24 | -------------------------------------------------------------------------------- /app/main/oauthConfig.js: -------------------------------------------------------------------------------- 1 | const oauthConfig = { 2 | oneDrive: { 3 | clientId: '35730bb9-a23a-46f9-aebf-5c2b9d6fc06c', 4 | authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', 5 | tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', 6 | useBasicAuthorizationHeader: false, 7 | redirectUri: 'http://localhost', 8 | clientSecret: 'bcdfnjKNMK0$-!wHQO6656]', 9 | }, 10 | }; 11 | 12 | export default oauthConfig; 13 | -------------------------------------------------------------------------------- /templete/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Yosoro 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/main/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Yosoro 6 | 7 | 8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /scripts/packageDMG.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import createDMG from 'electron-installer-dmg'; 3 | import { productName, version } from '../package.json'; 4 | 5 | createDMG({ 6 | appPath: path.resolve(__dirname, '../out', `${productName}-darwin-x64/${productName}.app`), 7 | name: `${productName}-${version}`, 8 | icon: path.resolve(__dirname, '../assets/icons/osx/app.icns'), 9 | out: path.resolve(__dirname, '../out'), 10 | overwrite: true, 11 | }, (err) => { 12 | if (err) { 13 | console.warn(err); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /app/views/assets/scss/common.scss: -------------------------------------------------------------------------------- 1 | @mixin display-flex { 2 | display: flex; 3 | display: -webkit-flex; 4 | } 5 | 6 | @mixin project-item { 7 | height: 3.5rem; 8 | line-height: 3.5rem; 9 | font-size: 0.95rem; 10 | color: rgba(0, 0, 0, 0.45); 11 | cursor: pointer; 12 | } 13 | 14 | @mixin normal-ipt { 15 | height: 1.8rem; 16 | font-size: 0.9rem; 17 | padding: 0 0.2rem; 18 | color: rgba(0, 0, 0, 0.5); 19 | cursor: pointer; 20 | } 21 | 22 | @mixin text-ellipsis { 23 | overflow: hidden; 24 | white-space: nowrap; 25 | text-overflow: ellipsis; 26 | } 27 | -------------------------------------------------------------------------------- /app/views/component/share/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import loadingImg from '../../assets/images/loading.svg'; 4 | 5 | const Loading = props => ( 6 |
7 | 8 |

{props.tip}

9 |
10 | ); 11 | 12 | Loading.displayName = 'Loading'; 13 | Loading.propTypes = { 14 | tip: PropTypes.string.isRequired, 15 | }; 16 | Loading.defaultProps = { 17 | tip: 'Loading...', 18 | }; 19 | 20 | export default Loading; 21 | -------------------------------------------------------------------------------- /app/views/reducers/reducers.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { routerReducer } from 'react-router-redux'; 3 | import app from './app'; 4 | import projects from './projects'; 5 | import markdown from './markdown'; 6 | import note from './note'; 7 | import drive from './drive'; 8 | import exportQueue from './exportQueue'; 9 | import user from './user'; 10 | 11 | const rootReducer = combineReducers({ 12 | app, 13 | projects, 14 | markdown, 15 | note, 16 | drive, 17 | exportQueue, 18 | routing: routerReducer, 19 | user, 20 | }); 21 | 22 | export default rootReducer; 23 | -------------------------------------------------------------------------------- /app/views/component/share/search/search.scss: -------------------------------------------------------------------------------- 1 | .search-wrapper { 2 | font-family: "Monospace Number", "Chinese Quote", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Helvetica, Arial, sans-serif; 3 | box-sizing: border-box; 4 | margin: 0; 5 | padding: 0; 6 | list-style: none; 7 | position: relative; 8 | display: inline-block; 9 | text-align: center; 10 | 11 | .ant-input { 12 | height: 1.5rem; 13 | font-size: 0.8rem; 14 | padding: 0 1rem; 15 | } 16 | 17 | .ant-input-prefix { 18 | cursor: pointer; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/views/reducers/user.js: -------------------------------------------------------------------------------- 1 | import { 2 | GET_USER_AVATAR, 3 | GET_USER_AVATAR_SUCCESS, 4 | GET_USER_AVATAR_FAILED, 5 | SET_USER_LOCAL_AVATAR, 6 | } from '../actions/user'; 7 | 8 | const assign = Object.assign; 9 | 10 | export default function updateUser(state = { 11 | avatar: '', 12 | }, action) { 13 | switch (action.type) { 14 | case SET_USER_LOCAL_AVATAR: 15 | case GET_USER_AVATAR_SUCCESS: { 16 | return assign({}, state, { 17 | avatar: action.avatar, 18 | }); 19 | } 20 | case GET_USER_AVATAR: 21 | case GET_USER_AVATAR_FAILED: 22 | default: 23 | return state; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /app/views/reducers/exportQueue.js: -------------------------------------------------------------------------------- 1 | import { 2 | EXPORT_INIT_QUEUE, 3 | // EXPORT_SUCCESS_SINGLE, 4 | // EXPORT_FAILED_SINGLE, 5 | EXPORT_COMPOLETE, 6 | } from '../actions/exportQueue'; 7 | 8 | const assign = Object.assign; 9 | 10 | export default function exportQueue(state = { 11 | status: 0, // 0: pending 1: exporting 2: over 12 | }, action) { 13 | switch (action.type) { 14 | case EXPORT_INIT_QUEUE: 15 | return assign({}, state, { 16 | status: 1, 17 | }); 18 | // case EXPORT_SUCCESS_SINGLE: 19 | // case EXPORT_FAILED_SINGLE: 20 | case EXPORT_COMPOLETE: 21 | return assign({}, state, { 22 | status: 0, 23 | }); 24 | default: 25 | return state; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/Bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. Windows7] 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /app/main/schedule.js: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron'; 2 | import schedule from 'node-schedule'; 3 | 4 | export class Schedule { 5 | constructor() { 6 | this.releaseJob = null; 7 | } 8 | 9 | releaseSchedule() { 10 | if (this.releaseJob) { 11 | return false; 12 | } 13 | // 每隔1小时检查更新 14 | this.releaseJob = schedule.scheduleJob('0 * * * *', () => { 15 | try { 16 | BrowserWindow.getAllWindows()[0].webContents.send('fetch-releases'); 17 | } catch (ex) { 18 | console.warn(ex); 19 | } 20 | }); 21 | } 22 | 23 | cancelReleases() { 24 | if (this.releaseJob) { 25 | this.releaseJob.cancel(); 26 | this.releaseJob = null; 27 | } 28 | } 29 | } 30 | 31 | export default new Schedule(); 32 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": 8 6 | } 7 | }] 8 | ], 9 | "env": { 10 | "main": { 11 | "presets": [ 12 | ["env", { 13 | "targets": { 14 | "node": 8 15 | } 16 | }] 17 | ] 18 | }, 19 | "renderer": { 20 | "presets": [ 21 | ["env", { 22 | "targets": { 23 | "chrome": 59 24 | }, 25 | "loose": true 26 | }], 27 | "react", 28 | "react-optimize", 29 | "stage-0" 30 | ], 31 | "plugins": [ 32 | "transform-decorators-legacy", 33 | "transform-async-to-generator", 34 | ["import", { "libraryName": "antd", "style": "css" }] 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /scripts/packageDeb.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import installer from 'electron-installer-debian'; 3 | import { productName, version } from '../package.json'; 4 | 5 | const options = { 6 | bin: 'Yosoro', 7 | // name: 'Yosoro', 8 | productName, 9 | genericName: productName, 10 | categories: ['Utility'], 11 | src: path.resolve(__dirname, '../out', `${productName}-linux-x64`), 12 | dest: path.resolve(__dirname, '../out', `${productName}-linux-x64-deb-${version}`), 13 | arch: 'amd64', 14 | icon: path.resolve(__dirname, '../app/main/resource/app.png'), 15 | homepage: 'https://yosoro.coolecho.net', 16 | }; 17 | 18 | installer(options) 19 | .then(() => console.info(`Successfully created package at ${options.dest}`)) 20 | .catch((err) => { 21 | console.error(err, err.stack); 22 | process.exit(1); 23 | }); 24 | -------------------------------------------------------------------------------- /config/webpack.config.base.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | export default { 4 | target: 'electron-renderer', 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.(png|jpg|jpeg|gif)$/, 9 | loader: 'url-loader?limit=10000&name=images/[name].[ext]', 10 | }, 11 | { 12 | test: /\.(eot|woff2|woff|ttf|svg)$/, 13 | loader: 'url-loader?name=fonts/[name].[ext]', 14 | }, 15 | ], 16 | }, 17 | resolve: { 18 | extensions: ['.js', '.jsx', '.json'], 19 | modules: ['node_modules', 'app'], 20 | alias: { 21 | Components: path.resolve(__dirname, '../app/views/component/'), 22 | Utils: path.resolve(__dirname, '../app/views/utils/'), 23 | Actions: path.resolve(__dirname, '../app/views/actions/'), 24 | Services: path.resolve(__dirname, '../app/views/services/'), 25 | }, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /app/views/assets/scss/color.scss: -------------------------------------------------------------------------------- 1 | $htmlBg: rgba(255, 255, 255, 0); 2 | 3 | $navWidth: 260px; 4 | 5 | $cardBg: #1b1d24; 6 | 7 | $loadingBg: #2c3e50; 8 | $color: hsl(0, 0%, 22%); 9 | 10 | // $navBg: #25292f; 11 | $navBg: rgba(244, 244, 244, 0.7); 12 | 13 | $rootBg: rgba(244, 244, 244, 0.9); 14 | 15 | $noteLiActiveBg: rgba($color: #2e80fe, $alpha: 0.8); 16 | 17 | $noteLiHoverBg: rgba($color: #2e80fe, $alpha: 0.7); 18 | 19 | $fileLiActiveBg: rgba($color: #7f7a78, $alpha: 0.8); 20 | 21 | $fileLiHoverBg: rgba($color: #7f7a78, $alpha: 0.6); 22 | 23 | $noteLiActiveColor: #f7f8f9; 24 | 25 | $noteTitleColor: #7f7a78; 26 | 27 | $headerBg: #0c0c0c; 28 | 29 | $colorBorder: #0c0c0c; 30 | $windowBg: #2d313a; 31 | 32 | $darkBorder: #0c0c0c; 33 | 34 | $fontColor: aliceblue; 35 | $lightFontColor: #9da5af; 36 | 37 | $lightColor: rgb(214, 216, 218); 38 | 39 | $navCurBg: #424851; 40 | 41 | $lightBorderBg: #d9d9d9; 42 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "command": "npm", 7 | "isShellCommand": true, 8 | "showOutput": "always", 9 | "suppressTaskName": true, 10 | "configurations": [ 11 | { 12 | "type": "node", 13 | "request": "launch", 14 | "name": "Debug App", 15 | "cwd": "${workspaceRoot}", 16 | "env": { 17 | "NODE_ENV": "development", 18 | "BABEL_ENV": "main" 19 | }, 20 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron", 21 | "windows": { 22 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron.cmd" 23 | }, 24 | "args" : ["-r", "babel-register", "./app/main/"] 25 | } 26 | ] 27 | } 28 | -------------------------------------------------------------------------------- /app/views/actions/drive.js: -------------------------------------------------------------------------------- 1 | export const DRIVE_FETCHING_PROJECTS = 'FETCHING_PROJECTS'; 2 | export const DRIVE_FETCHING_PROJECRS_FAILED = 'DRIVE_FETCHING_PROJECRS_FAILED'; 3 | export const DRIVE_FETCHING_PROJECRS_SUCCESS = 'DRIVE_FETCHING_PROJECRS_SUCCESS'; 4 | 5 | export const DRIVE_FETCHING_NOTES = 'DRIVE_FETCHING_NOTES'; 6 | export const DRIVE_FETCHING_NOTES_SUCCESS = 'DRIVE_FETCHING_NOTES_SUCCESS'; 7 | export const DRIVE_FETCHING_NOTES_FAILED = 'DRIVE_FETCHING_NOTES_FAILED'; 8 | 9 | export const DRIVE_BACK_ROOT = 'DRIVE_BACK_ROOT'; 10 | 11 | export const DRIVE_DOWNLOAD_NOTE = 'DRIVE_DOWNLOAD_NOTE'; 12 | export const DRIVE_DOWNLOAD_NOTE_SUCCESS = 'DRIVE_DOWNLOAD_NOTE_SUCCESS'; 13 | export const DRIVE_DOWNLOAD_NOTE_FAILED = 'DRIVE_DOWNLOAD_NOTE_FAILED'; 14 | 15 | export const DRIVE_DELETE_ITEM = 'DRIVE_DELETE_ITEM'; 16 | export const DRIVE_DELETE_ITEM_SUCCESS = 'DRIVE_DELETE_ITEM_SUCCESS'; 17 | export const DRIVE_DELETE_ITEM_FAILED = 'DRIVE_DELETE_ITEM_FAILED'; 18 | -------------------------------------------------------------------------------- /app/views/actions/note.js: -------------------------------------------------------------------------------- 1 | export const SWITCH_PROJECT = 'SWITCH_PROJECT'; 2 | 3 | export function switchProject(uuid, name) { 4 | return { 5 | type: SWITCH_PROJECT, 6 | uuid, 7 | name, 8 | }; 9 | } 10 | 11 | 12 | export const SWITCH_FILE = 'SWITCH_NOTE'; 13 | 14 | export function switchFile(uuid, fileName) { 15 | return { 16 | type: SWITCH_FILE, 17 | uuid, 18 | fileName, 19 | }; 20 | } 21 | 22 | export const CLEAR_NOTE = 'CLEAR_NOTE'; 23 | 24 | export function clearNote() { 25 | return { 26 | type: CLEAR_NOTE, 27 | }; 28 | } 29 | 30 | export const CLEAR_NOTE_WORKSCAPE = 'CLEAR_NOTE_WORKSCAPE'; 31 | 32 | export function clearWorkspace() { 33 | return { 34 | type: CLEAR_NOTE_WORKSCAPE, 35 | }; 36 | } 37 | 38 | export const UPDATE_NOTE_PROJECTNAME = 'UPDATE_NOTE_PROJECTNAME'; 39 | 40 | export function updateNoteProjectName(name) { 41 | return { 42 | type: UPDATE_NOTE_PROJECTNAME, 43 | name, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /app/views/component/share/SVGIcon.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const SVGIcon = (props) => { 5 | const { className, id, viewBox, useClassName, version } = props; 6 | const useHtml = ``; 7 | return ( 8 | 14 | ); 15 | }; 16 | 17 | SVGIcon.displayName = 'SVGIcon'; 18 | SVGIcon.propTypes = { 19 | className: PropTypes.string.isRequired, 20 | id: PropTypes.string.isRequired, 21 | viewBox: PropTypes.string.isRequired, 22 | useClassName: PropTypes.string.isRequired, 23 | version: PropTypes.string, 24 | }; 25 | SVGIcon.defaultProps = { 26 | className: '', 27 | id: '', 28 | useClassName: '', 29 | version: '1.1', 30 | }; 31 | 32 | export default SVGIcon; 33 | -------------------------------------------------------------------------------- /app/views/assets/scss/themes.scss: -------------------------------------------------------------------------------- 1 | @import "color.scss"; 2 | 3 | .light { 4 | &.not-darwin { 5 | .tool-bar { 6 | background-image: linear-gradient(to left, rgb(230, 230, 230), rgb(210, 210, 210)); 7 | } 8 | } 9 | .tool-bar { 10 | background-color: $navBg; 11 | 12 | .tool-bar-bg { 13 | background-color: $navBg; 14 | // background: url("../images/blur.svg#blur"); 15 | } 16 | 17 | .menu-list { 18 | .menu-svg { 19 | fill: #ff9999; 20 | } 21 | 22 | .menu-item-radius { 23 | background-color: #ccffff; 24 | opacity: .5; 25 | } 26 | .menu-item .cur { 27 | .menu-item-radius { 28 | background-color: #ff9999; 29 | box-shadow: 0 0 1.2rem .05rem #ffffff; 30 | opacity: 1; 31 | } 32 | .menu-svg { 33 | fill: #ffffff; 34 | } 35 | } 36 | } 37 | } 38 | 39 | .note { 40 | .project-list { 41 | border-color: $lightBorderBg; 42 | } 43 | } 44 | } 45 | 46 | .dark { 47 | .tool-bar { 48 | background-color: rgb(38, 38, 38); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /config/webpack.config.electron.babel.js: -------------------------------------------------------------------------------- 1 | /** 2 | * electron 主进程打包 3 | */ 4 | import path from 'path'; 5 | // import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | 7 | export default { 8 | mode: 'production', 9 | target: 'electron-main', 10 | externals: [ 11 | 'fsevents', 12 | ], 13 | entry: { 14 | main: [ 15 | path.join(__dirname, '../app/main/index.js'), 16 | ], 17 | }, 18 | output: { 19 | filename: 'main.js', 20 | path: path.resolve(__dirname, '../lib'), 21 | }, 22 | module: { 23 | rules: [ 24 | { 25 | test: /\.js$/, 26 | use: [ 27 | 'babel-loader', 28 | ], 29 | exclude: /node_modules/, 30 | include: path.resolve(__dirname, '../'), 31 | }, 32 | ], 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.json'], 36 | }, 37 | optimization: { 38 | minimize: true, 39 | }, 40 | plugins: [], 41 | node: { 42 | global: true, 43 | process: true, 44 | Buffer: true, 45 | __dirname: false, 46 | __filename: false, 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /app/views/component/cloud/Notebooks.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import NoteItem from '../share/notebook/NoteItem'; 4 | 5 | const Notebooks = (props) => { 6 | const { projects } = props; 7 | if (projects.length === 0) { 8 | return (

List is empty.

); 9 | } 10 | return ( 11 | 30 | ); 31 | }; 32 | 33 | Notebooks.displayName = 'CloudDriveNotebooks'; 34 | Notebooks.propTypes = { 35 | projects: PropTypes.array.isRequired, 36 | // chooseProject: PropTypes.func.isRequired, 37 | openRemove: PropTypes.func.isRequired, 38 | }; 39 | 40 | export default Notebooks; 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | config/devconfig.json 61 | yosoro-darwin-x64/ 62 | lib/ 63 | out/ 64 | -------------------------------------------------------------------------------- /app/views/component/cloud/Notes.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import NoteItem from '../share/notebook/NoteItem'; 4 | 5 | const Notes = (props) => { 6 | const { notes } = props; 7 | if (notes.length === 0) { 8 | return (

List is empty.

); 9 | } 10 | return ( 11 | 35 | ); 36 | }; 37 | 38 | Notes.displayName = 'CloudDriveNotes'; 39 | Notes.propTypes = { 40 | notes: PropTypes.array.isRequired, 41 | downloadNote: PropTypes.func.isRequired, 42 | // openRemove: PropTypes.func.isRequired, 43 | }; 44 | 45 | export default Notes; 46 | -------------------------------------------------------------------------------- /app/views/reducers/note.js: -------------------------------------------------------------------------------- 1 | import { 2 | SWITCH_PROJECT, 3 | SWITCH_FILE, 4 | CLEAR_NOTE, 5 | CLEAR_NOTE_WORKSCAPE, 6 | UPDATE_NOTE_PROJECTNAME, 7 | } from '../actions/note'; 8 | 9 | const assign = Object.assign; 10 | 11 | export default function updateNote(state = { 12 | projectUuid: '-1', 13 | projectName: '', 14 | fileUuid: '-1', 15 | fileName: '', 16 | exportStatus: 0, // 0: wait 1: pending 2:success 3: failed 17 | }, action) { 18 | switch (action.type) { 19 | case SWITCH_PROJECT: { 20 | const { uuid, name } = action; 21 | return assign({}, state, { 22 | projectUuid: uuid, 23 | projectName: name, 24 | }); 25 | } 26 | case SWITCH_FILE: 27 | return assign({}, state, { 28 | fileUuid: action.uuid, 29 | fileName: action.fileName, 30 | }); 31 | case CLEAR_NOTE: 32 | return assign({}, state, { 33 | fileUuid: '-1', 34 | fileName: '', 35 | }); 36 | case CLEAR_NOTE_WORKSCAPE: 37 | return assign({}, { 38 | projectUuid: '-1', 39 | projectName: '', 40 | fileUuid: '-1', 41 | fileName: '', 42 | }); 43 | case UPDATE_NOTE_PROJECTNAME: { 44 | const { name } = action; 45 | return assign({}, state, { 46 | projectName: name, 47 | }); 48 | } 49 | default: 50 | return state; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/views/component/trash/ToolBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Icon } from 'antd'; 4 | import { trashBack } from '../../actions/projects'; 5 | 6 | export default class ToolBar extends PureComponent { 7 | static displayName = 'TrashToolBar'; 8 | static propTypes = { 9 | dispatch: PropTypes.func.isRequired, 10 | trash: PropTypes.shape({ 11 | projectName: PropTypes.string.isRequired, 12 | projectUuid: PropTypes.string.isRequired, 13 | }).isRequired, 14 | } 15 | 16 | handleClick = () => { 17 | this.props.dispatch(trashBack()); 18 | } 19 | 20 | render() { 21 | const { trash: { projectUuid, projectName } } = this.props; 22 | let isBack = false; 23 | let title = 'Trash'; 24 | if (projectUuid !== '-1' && projectName !== '') { 25 | isBack = true; 26 | title = projectName; 27 | } 28 | return ( 29 |
30 |

31 | {isBack ? () : ()} 32 | {title} 33 |

34 | {isBack ? ( 35 | 39 | 40 | 41 | ) : null} 42 |
43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/webview/webview-pre.js: -------------------------------------------------------------------------------- 1 | const ipcRenderer = require('electron').ipcRenderer; 2 | 3 | let nodeRoot = null; 4 | 5 | let currentMode = ''; 6 | 7 | /** 8 | * @description 检测编辑模式 9 | * @param {String} editorMode 编辑模式 10 | */ 11 | function checkMode(editorMode) { 12 | if (currentMode !== editorMode && currentMode === 'preview') { 13 | nodeRoot.classList.remove('preview'); 14 | } else if (currentMode !== editorMode && editorMode === 'preview') { 15 | nodeRoot.classList.add('preview'); 16 | } 17 | currentMode = editorMode; 18 | } 19 | 20 | function handleInnerClick(event) { 21 | if (!event) { 22 | return; 23 | } 24 | const node = event.target; 25 | event.preventDefault(); 26 | if (node.tagName && node.tagName.toLowerCase() === 'a' && node.href) { 27 | ipcRenderer.sendToHost('did-click-link', node.href); 28 | } 29 | } 30 | 31 | document.addEventListener('click', handleInnerClick); 32 | 33 | document.addEventListener('DOMContentLoaded', () => { 34 | ipcRenderer.sendToHost('wv-first-loaded'); 35 | 36 | // 渲染预览页面 37 | ipcRenderer.on('wv-render-html', (event, args) => { 38 | let { html, editorMode } = args; 39 | html = html || ''; 40 | editorMode = editorMode || 'normal'; 41 | if (!nodeRoot) { 42 | nodeRoot = document.getElementById('root'); 43 | } 44 | if (currentMode === '') { 45 | currentMode = editorMode; 46 | } 47 | checkMode(editorMode); 48 | nodeRoot.innerHTML = html; 49 | }); 50 | 51 | ipcRenderer.on('wv-scroll', (event, radio) => { 52 | document.body.scrollTop = nodeRoot.offsetHeight * radio; 53 | }); 54 | }); 55 | -------------------------------------------------------------------------------- /scripts/packageSetup.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import { productName, version } from '../package.json'; 4 | 5 | const electronInstaller = require('electron-winstaller'); 6 | 7 | const packageQueue = []; 8 | 9 | function getOptions(appPath, arch) { 10 | return { 11 | appDirectory: appPath, 12 | outputDirectory: path.resolve(__dirname, '../out', `${productName}-win32-${arch}-setup-${version}`), 13 | authors: 'Alchemy', 14 | description: 'Beautiful Cloud Drive Markdown NoteBook Desktop App, based on React, Redux, Webpack, React Hot Loader for rapid application development', 15 | exe: `${productName}.exe`, 16 | iconUrl: path.resolve(__dirname, '../assets/icons/win/app.ico'), 17 | setupIcon: path.resolve(__dirname, '../assets/icons/win/app.ico'), 18 | setupExe: `${productName}-win32-${arch}-setup.exe`, 19 | loadingGif: path.resolve(__dirname, '../assets/loading.gif'), 20 | noMsi: true, 21 | }; 22 | } 23 | 24 | const apps = [{ 25 | arch: 'ia32', 26 | path: path.resolve(__dirname, '../out/Yosoro-win32-ia32'), 27 | }, { 28 | arch: 'x64', 29 | path: path.resolve(__dirname, '../out/Yosoro-win32-x64'), 30 | }]; 31 | 32 | for (const app of apps) { 33 | if (fs.existsSync(app.path)) { 34 | const options = getOptions(app.path, app.arch); 35 | packageQueue.push(electronInstaller.createWindowsInstaller(options) 36 | .then(() => console.info(`${app.arch} build success.`), e => console.error(`${app.arch} build failed.\r\n ${e.message}`)) 37 | ); 38 | } 39 | } 40 | 41 | Promise.all(packageQueue) 42 | .then(() => { 43 | console.info('Build over.'); 44 | }); 45 | -------------------------------------------------------------------------------- /app/views/component/trash/Files.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import NoteItem from '../share/notebook/NoteItem'; 4 | import HOCList from './HOCList'; 5 | 6 | @HOCList('note') 7 | export default class TestList extends Component { 8 | static displayName = 'TrashProjects'; 9 | static propTypes = { 10 | notes: PropTypes.arrayOf(PropTypes.shape({ 11 | uuid: PropTypes.string.isRequired, 12 | name: PropTypes.string.isRequired, 13 | description: PropTypes.string.isRequired, 14 | labels: PropTypes.arrayOf(PropTypes.string).isRequired, 15 | status: PropTypes.number.isRequired, 16 | })).isRequired, 17 | openRestore: PropTypes.func.isRequired, 18 | openRemove: PropTypes.func.isRequired, 19 | } 20 | 21 | render() { 22 | const { notes } = this.props; 23 | if (notes.length === 0) { 24 | return ( 25 |
26 |

Notebook is empty.

27 |
28 | ); 29 | } 30 | return ( 31 |
32 | 49 |
50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/views/component/cloud/Cloud.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Route, Switch, NavLink } from 'react-router-dom'; 4 | import Drive from './Drive'; 5 | 6 | import '../../assets/scss/drive.scss'; 7 | 8 | import oneDriveLogo from '../../assets/images/onedrive.png'; 9 | 10 | const Cloud = props => ( 11 |
12 |
13 |
14 | Cloud Drive: 15 | 16 | 17 | onedrive 18 | 19 | 20 |
21 |
22 | 23 | ( 26 | 32 | )} 33 | /> 34 | 35 |
36 | ); 37 | 38 | Cloud.displayName = 'Cloud'; 39 | Cloud.propTypes = { 40 | dispatch: PropTypes.func.isRequired, 41 | drive: PropTypes.shape({ 42 | status: PropTypes.number.isRequired, 43 | projects: PropTypes.array.isRequired, 44 | notes: PropTypes.array.isRequired, 45 | currentProjectName: PropTypes.string.isRequired, 46 | }).isRequired, 47 | note: PropTypes.shape({ 48 | projectUuid: PropTypes.string.isRequired, 49 | projectName: PropTypes.string.isRequired, 50 | fileUuid: PropTypes.string.isRequired, 51 | fileName: PropTypes.string.isRequired, 52 | }).isRequired, 53 | }; 54 | 55 | export default Cloud; 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## v1.0.7 4 | 5 | ### New Features 6 | 7 | - Set user avatar 8 | 9 | ### Optimize 10 | 11 | - Automatically save files 12 | - Reduce file size 13 | - Optimize transition animation 14 | - Windows font optimization 15 | 16 | ### BugFix 17 | 18 | - Search mode switching bug 19 | 20 | ## v1.0.6 21 | 22 | ### Optimize 23 | 24 | - Use `webview` to render html 25 | - Remove js-xss and increase conversion speed 26 | 27 | ## v1.0.5 28 | 29 | ### new Features 30 | 31 | - Export note as `markdown`, `html`, `pdf` 32 | - Export notebook 33 | 34 | ### Bugfix 35 | 36 | - Cancle export 37 | - XSS 38 | 39 | ### Optimize 40 | 41 | - Upgrade to Webpack 4.0 42 | - Build for chrome >= 59 43 | 44 | ## v1.0.4 45 | 46 | ### New Features 47 | 48 | - Optimize application colors. 49 | 50 | ### BugFix 51 | 52 | - Fix bugs when downloading notes. 53 | 54 | ### Others 55 | 56 | - Optimize application operations. 57 | 58 | ## v1.0.3 59 | 60 | ### New Features 61 | 62 | - Delete notes or notebooks on oneDrive. 63 | - Optimize user operation. 64 | 65 | Date: 2018.04.19 21:57:00 66 | 67 | ---- 68 | ## v1.0.2 69 | 70 | ### New Features 71 | 72 | - Use CodeMirror 73 | 74 | ---- 75 | 76 | ## v1.0.1 77 | 78 | ### New Features 79 | 80 | - Add Update Notification 81 | - Note export as markdown or html 82 | - Disable menu items are set in grey 83 | 84 | ### Bugfix 85 | 86 | - Optimized list style 87 | - Fix can'not cancel the selection of text in `Windows` 88 | 89 | 90 | ---- 91 | 92 | ## v1.0.0 93 | 94 | ### Features 95 | 96 | - Create notebook & Write note 97 | - Support Markdown syntax 98 | - Delete & Restore 99 | - Synchronize with Cloud Drive(OneDrive is supported) 100 | -------------------------------------------------------------------------------- /app/views/actions/markdown.js: -------------------------------------------------------------------------------- 1 | export const READ_FILE = 'READING_FILE'; 2 | 3 | export function readFile(param) { 4 | return { 5 | type: READ_FILE, 6 | param, 7 | }; 8 | } 9 | 10 | export const UPDATE_MARKDOWN_HTML = 'UPDATE_MARKDOWN_HTML'; 11 | 12 | export function updateMarkdownHtml(content, uuid, start) { 13 | return { 14 | type: UPDATE_MARKDOWN_HTML, 15 | content, 16 | uuid, 17 | start, 18 | }; 19 | } 20 | 21 | export const BEFORE_SWITCH_SAVE = 'BEFORE_SWITCH_SAVE'; 22 | 23 | // 切换文件前对当前笔记进行保存 24 | export function beforeSwitchSave(projectName, needUpdateCloudStatus) { 25 | return { 26 | type: BEFORE_SWITCH_SAVE, 27 | projectName, 28 | needUpdateCloudStatus, 29 | }; 30 | } 31 | 32 | // 异步自动保存文件内容 33 | export const AUTO_SAVE_CONTENT_TO_FILE = 'AUTO_SAVE_CONTENT_TO_FILE'; 34 | 35 | export const SAVE_CONTENT_TO_TRASH_FILE = 'SAVE_CONTENT_TO_TRASH_FILE'; 36 | 37 | export function saveContentToTrashFile(projectName) { 38 | return { 39 | type: SAVE_CONTENT_TO_TRASH_FILE, 40 | projectName, 41 | }; 42 | } 43 | 44 | export const CLEAR_MARKDOWN = 'CLEAR_MARKDOWN'; 45 | 46 | // 清空markown内容 47 | export function clearMarkdown() { 48 | return { 49 | type: CLEAR_MARKDOWN, 50 | }; 51 | } 52 | 53 | export const UPDATE_CURRENT_MARKDOWN_TITLE = 'UPDATE_MARKDOWN_TITLE_CURRENT'; 54 | 55 | /** 56 | * @description 更新当前markdown标题 57 | * 58 | * @export 59 | * @param {any} uuid 文件uuid 60 | * @param {any} name 文件标题 61 | */ 62 | export function updateCurrentTitle(uuid, name) { 63 | return { 64 | type: UPDATE_CURRENT_MARKDOWN_TITLE, 65 | uuid, 66 | name, 67 | }; 68 | } 69 | 70 | export const MARKDOWN_UPLOADING = 'MARKDOWN_UPLOADING'; 71 | export const MARKDWON_UPLADING_SUCCESS = 'MARKDWON_UPLADING_SUCCESS'; 72 | export const MARKDWON_UPLADING_FAILED = 'MARKDWON_UPLADING_FAILED'; 73 | export const JUST_UPDATE_MARKDWON_HTML = 'JUST_UPDATE_MARKDWON_HTML'; 74 | -------------------------------------------------------------------------------- /app/views/component/trash/Projects.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import NoteItem from '../share/notebook/NoteItem'; 4 | import HOCList from './HOCList'; 5 | 6 | @HOCList('projects') 7 | export default class Projects extends Component { 8 | static displayName = 'TrashProjects'; 9 | static propTypes = { 10 | projects: PropTypes.arrayOf(PropTypes.shape({ 11 | uuid: PropTypes.string.isRequired, 12 | name: PropTypes.string.isRequired, 13 | description: PropTypes.string.isRequired, 14 | labels: PropTypes.arrayOf(PropTypes.string).isRequired, 15 | status: PropTypes.number.isRequired, 16 | notes: PropTypes.array.isRequired, 17 | })).isRequired, 18 | handleGoIn: PropTypes.func.isRequired, 19 | openRestore: PropTypes.func.isRequired, 20 | openRemove: PropTypes.func.isRequired, 21 | } 22 | 23 | render() { 24 | const { projects } = this.props; 25 | if (projects.length === 0) { 26 | return ( 27 |
28 |

Trash can is empty.

29 |
30 | ); 31 | } 32 | return ( 33 |
34 | 53 |
54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/views/component/trash/Trash.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import ToolBar from './ToolBar'; 4 | import Projects from './Projects'; 5 | import Files from './Files'; 6 | 7 | import '../../assets/scss/trash.scss'; 8 | 9 | export default class ImageHosting extends Component { 10 | static displayName = 'Transh'; 11 | static propTypes = { 12 | dispatch: PropTypes.func.isRequired, 13 | projects: PropTypes.arrayOf(PropTypes.shape({ 14 | uuid: PropTypes.string.isRequired, 15 | name: PropTypes.string.isRequired, 16 | description: PropTypes.string.isRequired, 17 | labels: PropTypes.arrayOf(PropTypes.string).isRequired, 18 | status: PropTypes.number.isRequired, 19 | notes: PropTypes.array.isRequired, 20 | })).isRequired, 21 | trash: PropTypes.shape({ 22 | projectName: PropTypes.string.isRequired, 23 | projectUuid: PropTypes.string.isRequired, 24 | }).isRequired, 25 | } 26 | 27 | getNotes() { 28 | const { trash: { projectUuid }, projects } = this.props; 29 | let notes; 30 | const length = projects.length; 31 | for (let i = 0; i < length; i++) { 32 | if (projectUuid === projects[i].uuid) { 33 | notes = projects[i].notes; 34 | break; 35 | } 36 | } 37 | return notes || []; 38 | } 39 | 40 | render() { 41 | const { projects, trash, dispatch } = this.props; 42 | let isRoot = true; 43 | if (trash.projectName !== '' && trash.projectUuid !== '-1') { 44 | isRoot = false; 45 | } 46 | const notes = this.getNotes(); 47 | return ( 48 |
49 | 53 | {isRoot ? ( 54 | 55 | ) : ( 56 | 57 | )} 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/views/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { createStore, applyMiddleware, compose } from 'redux'; 4 | import { Provider } from 'react-redux'; 5 | import { createLogger } from 'redux-logger'; 6 | import { AppContainer } from 'react-hot-loader'; 7 | import createSagaMiddleware from 'redux-saga'; 8 | // import { hashHistory } from 'react-router'; 9 | import { syncHistoryWithStore } from 'react-router-redux'; 10 | import { createHashHistory } from 'history'; 11 | import App from './container/App'; 12 | import rootReducer from './reducers/reducers'; 13 | import sagas from './sagas/sagas'; 14 | 15 | const loggerMiddleware = createLogger(); 16 | const sagaMiddleware = createSagaMiddleware(); 17 | 18 | /* eslint-disable no-underscore-dangle */ 19 | const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose; 20 | /* eslint-enable */ 21 | 22 | const enhancer = composeEnhancers( 23 | applyMiddleware( 24 | loggerMiddleware, 25 | sagaMiddleware, 26 | ), 27 | ); 28 | 29 | const store = createStore( 30 | rootReducer, 31 | enhancer, 32 | ); 33 | 34 | const sagaLen = sagas.length; 35 | for (let i = 0; i < sagaLen; i++) { 36 | sagaMiddleware.run(sagas[i]); 37 | } 38 | 39 | const history = createHashHistory(); 40 | syncHistoryWithStore(createHashHistory(), store); 41 | // const history = syncHistoryWithStore(createHashHistory(), store); 42 | // const history = syncHistoryWithStore(hashHistory, store); 43 | 44 | const render = (Component) => { 45 | ReactDOM.render( 46 | 47 | 48 | 49 | 50 | , 51 | document.querySelector('#root') 52 | ); 53 | }; 54 | 55 | render(App); 56 | 57 | if (module.hot) { 58 | module.hot.accept('./container/App', () => { 59 | const newApp = require('./container/App').default; 60 | render(newApp); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /app/views/services/CommonServices.js: -------------------------------------------------------------------------------- 1 | export default class CommonServices { 2 | constructor() { 3 | this.apiGitHubRoot = 'https://api.github.com'; 4 | this.githubRepoAddr = 'IceEnd/Yosoro'; 5 | } 6 | 7 | xhr = (url, method, param) => new Promise((resolve, reject) => { 8 | let targetUrl = url; 9 | let body = null; 10 | if (/get/ig.test(method) && param) { 11 | let queryString = ''; 12 | for (const key in param) { 13 | queryString += `&${key}=${param[key]}`; 14 | } 15 | queryString = queryString.replace(/^&/ig, ''); 16 | queryString = encodeURIComponent(queryString); 17 | targetUrl += queryString; 18 | } else if (/post/ig.test(method)) { 19 | body = JSON.stringify(param); 20 | } 21 | fetch(targetUrl, { 22 | method, 23 | body, 24 | }) 25 | .then((response) => { 26 | if (response.status === 200 || response.status === 201) { 27 | return response.json(); 28 | } 29 | throw new Error('Fetching Failed.'); 30 | }) 31 | .then(json => resolve(json)) 32 | .catch(ex => reject(ex)); 33 | }); 34 | 35 | /** 36 | * @desc 获取github仓库release列表 37 | */ 38 | getReleases() { 39 | const { apiGitHubRoot, githubRepoAddr } = this; 40 | const url = `${apiGitHubRoot}/repos/${githubRepoAddr}/releases`; 41 | return this.xhr(url, 'GET'); 42 | } 43 | 44 | getLatestRelease() { 45 | const { apiGitHubRoot, githubRepoAddr } = this; 46 | const url = `${apiGitHubRoot}/repos/${githubRepoAddr}/releases/latest`; 47 | return this.xhr(url, 'GET'); 48 | } 49 | 50 | getLatestVersion = () => new Promise((resolve, reject) => { 51 | this.getLatestRelease() 52 | .then((release) => { 53 | if (release) { 54 | const latestReleaseName = release.name; 55 | resolve(latestReleaseName); 56 | } else { 57 | throw new Error('Data is invild.'); 58 | } 59 | }) 60 | .catch((ex) => { 61 | reject(ex); 62 | }); 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /app/main/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yosoro", 3 | "productName": "Yosoro", 4 | "version": "1.0.7", 5 | "description": "Beautiful Cloud Drive Markdown NoteBook Desktop App.", 6 | "main": "./main.js", 7 | "scripts": { 8 | "dev:main": "cross-env NODE_ENV='development' BABEL_ENV=main electron -r babel-register ./app/main/", 9 | "dev:renderer": "cross-env NODE_ENV='development' BABEL_ENV=renderer webpack-dev-server --config config/webpack.config.renderer.dev.babel.js --colors --progress", 10 | "build:renderer": "cross-env NODE_ENV='production' BABEL_ENV=renderer gulp --gulpfile config/gulp.babel.js --cwd . --mode renderer", 11 | "build:main": "cross-env NODE_ENV='production' BABEL_ENV=main gulp --gulpfile config/gulp.babel.js --cwd . --mode main", 12 | "build:all": "concurrently \"npm run build:renderer\" \"npm run build:main\"", 13 | "packager:mac:app": "electron-packager ./lib Yosoro --overwrite --platform=darwin --arch=x64 --out=out --icon=assets/icons/osx/app.icns", 14 | "packager:mac:dmg":"babel-node ./scripts/packageDMG.js", 15 | "packager:mac": "npm run packager:mac:app && npm run packager:mac:dmg", 16 | "packager:win": "electron-packager ./lib Yosoro --overwrite --platform=win32 --arch=ia32 --out=out --icon=assets/icons/win/app.ico", 17 | "packager:win:64": "electron-packager ./lib Yosoro --overwrite --platform=win32 --arch=x64 --out=out --icon=assets/icons/win/app.ico", 18 | "packager:linux": "electron-packager ./lib Yosoro --overwrite --platform=linux --arch=x64 --out=out", 19 | "lint": "concurrently \"eslint app config --ext .js,.jsx\" \"sass-lint -v -q\"", 20 | "test": "npm run lint" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "https://github.com/IceEnd/Yosoro.git" 25 | }, 26 | "author": { 27 | "name": "Alchemy", 28 | "email": "min@coolecho.net", 29 | "url": "https://github.com/IceEnd" 30 | }, 31 | "mac": { 32 | "icon": "asstes/icons/osx/app.icns" 33 | }, 34 | "win": { 35 | "icon": "asstes/icons/win/app.ico" 36 | }, 37 | "license": "GPL-3.0" 38 | } 39 | -------------------------------------------------------------------------------- /app/views/sagas/app.js: -------------------------------------------------------------------------------- 1 | import { put, call, takeLatest } from 'redux-saga/effects'; 2 | import { message } from 'antd'; 3 | // import { ipcRenderer } from 'electron'; 4 | import { 5 | FETCHING_ONEDRIVE_TOKEN, 6 | FETCHING_ONEDRIVE_TOKEN_FAILED, 7 | FETCHING_ONEDRIVE_TOKEN_SUCCESS, 8 | FETCHING_GITHUB_RELEASES, 9 | FETCHING_GITHUB_RELEASES_FAILED, 10 | FETCHING_GITHUB_RELEASES_SUCCESS, 11 | } from '../actions/app'; 12 | import { 13 | GET_USER_AVATAR, 14 | } from '../actions/user'; 15 | 16 | import OneDrive from '../services/OneDrive'; 17 | import CommonServices from '../services/CommonServices'; 18 | 19 | const oneDrive = new OneDrive(); 20 | const commonServices = new CommonServices(); 21 | 22 | function* oneDriveToken(action) { 23 | const { code } = action; 24 | try { 25 | const data = yield call(oneDrive.getTokenByCode, code); 26 | const token = data.access_token; 27 | const refreshToken = data.refresh_token; 28 | const expiresDate = Date.parse(new Date()) + (data.expires_in * 1000); 29 | message.success('Authorized success.'); 30 | yield put({ type: FETCHING_ONEDRIVE_TOKEN_SUCCESS, token, refreshToken, expiresDate }); 31 | // token 返回成功请求头像 32 | yield put({ 33 | type: GET_USER_AVATAR, 34 | driveName: 'oneDrive', 35 | }); 36 | } catch (ex) { 37 | message.error('Authorization failed, please try again.'); 38 | yield put({ type: FETCHING_ONEDRIVE_TOKEN_FAILED, error: ex }); 39 | } 40 | } 41 | 42 | function* fetchingOneDriveToken() { 43 | yield takeLatest(FETCHING_ONEDRIVE_TOKEN, oneDriveToken); 44 | } 45 | 46 | function* handleReleaseFetch() { 47 | try { 48 | const latestVersion = yield call(commonServices.getLatestVersion); 49 | yield put({ type: FETCHING_GITHUB_RELEASES_SUCCESS, latestVersion }); 50 | } catch (ex) { 51 | yield put({ type: FETCHING_GITHUB_RELEASES_FAILED, error: ex }); 52 | } 53 | } 54 | 55 | function* getReleases() { 56 | yield takeLatest(FETCHING_GITHUB_RELEASES, handleReleaseFetch); 57 | } 58 | 59 | export default [ 60 | fetchingOneDriveToken, 61 | getReleases, 62 | ]; 63 | -------------------------------------------------------------------------------- /app/views/actions/app.js: -------------------------------------------------------------------------------- 1 | // import { checkServers, setDefaults } from '../utils/lowdb'; 2 | 3 | export const GET_APP_INFO = 'APP_GET_INFO'; 4 | export const APP_LOAD_FIRST = 'APP_LOAD_FIRST'; 5 | export const APP_LOUNCH_DEFAULT = 'APP_LOUNCH_DEFAULT'; 6 | export const APP_LOUNCH = 'APP_LOUNCH'; 7 | 8 | export function appLounch() { 9 | return { 10 | type: APP_LOUNCH, 11 | }; 12 | } 13 | 14 | export function appLoadFirst() { 15 | return { 16 | type: APP_LOAD_FIRST, 17 | }; 18 | } 19 | 20 | export function appLounchDefault() { 21 | return { 22 | type: APP_LOUNCH_DEFAULT, 23 | }; 24 | } 25 | 26 | export function appLounched() { 27 | return { 28 | type: APP_LOUNCH, 29 | }; 30 | } 31 | 32 | export const APP_ADJUST_MARKDOWN = 'APP_ADJUST_MARKDOWN'; 33 | 34 | export function appMarkdownAdjust(param) { 35 | return { 36 | type: APP_ADJUST_MARKDOWN, 37 | param, 38 | }; 39 | } 40 | 41 | export const APP_SWITCH_EDIT_MODE = 'APP_SWITCH_EDIT_MODE'; 42 | 43 | export function appSwitchEditMode(currentMode) { 44 | return { 45 | type: APP_SWITCH_EDIT_MODE, 46 | currentMode, 47 | }; 48 | } 49 | 50 | export const APP_SET_TOKEN = 'APP_SET_TOKEN'; 51 | 52 | export function setToken(name, token) { 53 | return { 54 | type: APP_SET_TOKEN, 55 | name, 56 | token, 57 | }; 58 | } 59 | 60 | export const FETCHING_ONEDRIVE_TOKEN = 'FETCHING_ONEDRIVE_TOKEN'; 61 | export const FETCHING_ONEDRIVE_TOKEN_FAILED = 'FETCHING_ONEDRIVE_TOKEN_FAILED'; 62 | export const FETCHING_ONEDRIVE_TOKEN_SUCCESS = 'FETCHING_ONEDRIVE_TOKEN_SUCCESS'; 63 | 64 | export const ONEDRIVE_ALL_UPLOAD = 'ONEDRIVE_ALL_UPLOAD'; 65 | export const ONEDRIVE_ALL_UPLOAD_SUCCESS = 'ONEDRIVE_ALL_UPLOAD_SUCCESS'; 66 | export const ONEDRIVE_ALL_UPLOAD_FAILED = 'ONEDRIVE_ALL_UPLOAD_FAILED'; 67 | 68 | export const FETCHING_GITHUB_RELEASES = 'FETCHING_GITHUB_RELEASES'; 69 | export const FETCHING_GITHUB_RELEASES_FAILED = 'FETCHING_GITHUB_RELEASES_FAILED'; 70 | export const FETCHING_GITHUB_RELEASES_SUCCESS = 'FETCHING_GITHUB_RELEASES_SUCCESS'; 71 | export const CLOSE_UPDATE_NOTIFICATION = 'CLOSE_UPDATE_NOTIFICATION'; 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

Yosoro

4 |

Beautiful Cloud Drive Markdown NoteBook Desktop App

5 |

6 | 7 |

8 |

9 | 10 | 11 | 12 |

13 |

14 | 15 |

16 |

17 | 18 | ## Download 19 | 20 | The latest version of Yosoro for macOS, linux and Windows is available [here](https://github.com/IceEnd/Yosoro/releases). 21 | 22 | **macOS 10.9+, Windows 7+ & Linux are supported.** 23 | 24 | ## Features 25 | 26 | - Create notebook & Write note 27 | - Support Markdown syntax 28 | - Delete & Restore 29 | - Synchronize with Cloud Drive(OneDrive is supported) 30 | - Export notes as markdown or html or pdf 31 | - Update Notification 32 | 33 | You can read the [CHANGELOG](./CHANGELOG.md) to get more information. 34 | 35 | ## Demo 36 | 37 | ### Write Notes 38 | 39 | ![write](https://t1.picb.cc/uploads/2018/05/13/2vBxK7.gif) 40 | 41 | ### File Syncing 42 | 43 | ![sync](https://t1.picb.cc/uploads/2018/05/13/2vBRbs.gif) 44 | 45 | ## Screenshots 46 | 47 | ### macOS 48 | 49 | ![screenshot-osx](https://s1.ax1x.com/2018/06/30/PFoMsP.png) 50 | 51 | ### Windows 52 | 53 | ![screenshot-windows](https://s1.ax1x.com/2018/05/13/CDZC5t.png) 54 | 55 | ### linux 56 | 57 | ![screenshot-linux](https://s1.ax1x.com/2018/05/13/CDZF8f.png) 58 | 59 | ## Quick Start 60 | 61 | ### Install 62 | 63 | ```shell 64 | yarn 65 | ``` 66 | 67 | ### Dev Tools Extension 68 | 69 | ```shell 70 | cp ./config/devconfig.example.json ./config/devconfig.json 71 | ``` 72 | 73 | ### Run Main Process 74 | 75 | ```shell 76 | npm run dev:main 77 | ``` 78 | 79 | ### Run Renderer Process 80 | 81 | ```shell 82 | npm run dev:renderer 83 | ``` 84 | 85 | ### Build 86 | 87 | ```shell 88 | npm run build:all|main|renderer 89 | ``` 90 | 91 | ### Package 92 | 93 | ``` 94 | npm run packager:mac|win|win:64|linux 95 | ``` 96 | 97 | ## License 98 | 99 | GPL-3.0 © [Alchemy](./LICENSE) 100 | -------------------------------------------------------------------------------- /config/webpack.config.renderer.dev.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import webpack from 'webpack'; 3 | import merge from 'webpack-merge'; 4 | import baseConfig from './webpack.config.base.babel'; 5 | 6 | export default merge.smart(baseConfig, { 7 | mode: 'development', 8 | node: { 9 | fs: 'empty', 10 | }, 11 | context: path.resolve(__dirname, '../app/views'), 12 | entry: { 13 | 'js/bundle': [ 14 | 'whatwg-fetch', 15 | 'react-hot-loader/patch', 16 | 'webpack/hot/only-dev-server', 17 | path.resolve(__dirname, '../app/views/index.jsx'), 18 | ], 19 | 'webview/webview-pre': [ 20 | 'webpack/hot/only-dev-server', 21 | path.resolve(__dirname, '../app/webview/webview-pre.js'), 22 | ], 23 | }, 24 | output: { 25 | path: path.resolve(__dirname, '../build/'), 26 | filename: '[name].js', 27 | chunkFilename: '[name].js', 28 | publicPath: 'http://localhost:3000/static/', 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.(js|jsx)$/, 34 | use: [ 35 | 'react-hot-loader/webpack', 36 | { 37 | loader: 'babel-loader', 38 | options: { 39 | cacheDirectory: true, 40 | }, 41 | }, 42 | ], 43 | exclude: /node_modules/, 44 | include: path.resolve(__dirname, '../'), 45 | }, 46 | { 47 | test: /\.css$/, 48 | use: ['style-loader', 'css-loader'], 49 | }, 50 | { 51 | test: /\.scss$/, 52 | use: ['style-loader', 'css-loader', 'sass-loader'], 53 | }, 54 | { 55 | test: /\.less$/, 56 | use: [ 57 | 'style-loader', 58 | 'css-loader', 59 | { 60 | loader: 'less-loader', 61 | options: { 62 | javascriptEnabled: true, 63 | }, 64 | }, 65 | ], 66 | }, 67 | ], 68 | }, 69 | devServer: { 70 | hot: true, 71 | contentBase: path.resolve(__dirname, '../app/renderer'), 72 | publicPath: 'http://localhost:3000/static/', 73 | port: 3000, 74 | compress: true, 75 | noInfo: true, 76 | inline: true, 77 | historyApiFallback: { 78 | verbose: true, 79 | disableDotRule: false, 80 | }, 81 | }, 82 | devtool: 'cheap-eval-source-map', 83 | optimization: { 84 | occurrenceOrder: true, 85 | }, 86 | plugins: [ 87 | new webpack.HotModuleReplacementPlugin(), 88 | new webpack.NoEmitOnErrorsPlugin(), 89 | ], 90 | }); 91 | -------------------------------------------------------------------------------- /app/views/component/AppToolBar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { 4 | NavLink, 5 | } from 'react-router-dom'; 6 | import SVGIcon from './share/SVGIcon'; 7 | 8 | import logo from '../assets/images/logo.png'; 9 | 10 | const cloudActive = (match, location) => { 11 | const { pathname } = location; 12 | if (/\/cloud\//ig.test(pathname)) { 13 | return true; 14 | } 15 | return false; 16 | }; 17 | const AppToolBar = (props) => { 18 | const defaultDriver = props.defaultDrive.toLowerCase(); 19 | const avatar = props.avatar; 20 | 21 | return ( 22 |
23 |
24 |
25 | {avatar ? ( 26 | avatar 27 | ) : ( 28 | logo 29 | )} 30 |
31 |
    32 |
  • 33 | 34 | 35 | 41 | 42 | 43 |
  • 44 |
  • 45 | 50 | 51 | 57 | 58 | 59 |
  • 60 |
  • 61 | 62 | 63 | 69 | 70 | 71 |
  • 72 |
73 |
74 | ); 75 | }; 76 | 77 | AppToolBar.displayName = 'AppToolBar'; 78 | AppToolBar.propTypes = { 79 | defaultDrive: PropTypes.string.isRequired, 80 | avatar: PropTypes.string.isRequired, 81 | }; 82 | 83 | export default AppToolBar; 84 | -------------------------------------------------------------------------------- /app/views/component/note/Explorer.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import Project from './Project'; 4 | import Files from './Files'; 5 | import { pushStateToStorage, mergeStateFromStorage } from '../../utils/utils'; 6 | 7 | export default class Explorer extends Component { 8 | static displayName = 'NoteExplorer'; 9 | static propTypes = { 10 | dispatch: PropTypes.func.isRequired, 11 | projects: PropTypes.arrayOf(PropTypes.shape({ 12 | uuid: PropTypes.string.isRequired, 13 | name: PropTypes.string.isRequired, 14 | description: PropTypes.string.isRequired, 15 | labels: PropTypes.arrayOf(PropTypes.string).isRequired, 16 | notes: PropTypes.array.isRequired, 17 | status: PropTypes.number.isRequired, 18 | })).isRequired, 19 | note: PropTypes.shape({ 20 | projectUuid: PropTypes.string.isRequired, 21 | projectName: PropTypes.string.isRequired, 22 | fileUuid: PropTypes.string.isRequired, 23 | }).isRequired, 24 | editorMode: PropTypes.string.isRequired, 25 | searchStatus: PropTypes.number.isRequired, 26 | hasEdit: PropTypes.bool.isRequired, 27 | }; 28 | 29 | constructor() { 30 | super(); 31 | this.state = mergeStateFromStorage('noteExplorerState', { 32 | searchStatus: 0, // 0: 未搜索 1: 搜索中 2: 搜索完成 33 | }); 34 | } 35 | 36 | componentWillUnmount() { 37 | pushStateToStorage('noteExplorerState', this.state); 38 | } 39 | 40 | getNotes() { 41 | const { note: { projectUuid } } = this.props; 42 | const { projects } = this.props; 43 | let notes = []; 44 | for (let i = 0, length = projects.length; i < length; i++) { 45 | if (projectUuid === projects[i].uuid) { 46 | notes = projects[i].notes; 47 | } 48 | } 49 | return notes; 50 | } 51 | 52 | render() { 53 | const { editorMode, projects, dispatch, note: { projectUuid, fileUuid, projectName }, searchStatus, hasEdit } = this.props; 54 | const notes = this.getNotes(); 55 | return ( 56 | 57 | 67 | { projectUuid === '-1' ? (null) : ( 68 | 78 | )} 79 | 80 | ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /app/views/reducers/drive.js: -------------------------------------------------------------------------------- 1 | import { 2 | DRIVE_FETCHING_PROJECTS, 3 | DRIVE_FETCHING_PROJECRS_FAILED, 4 | DRIVE_FETCHING_PROJECRS_SUCCESS, 5 | DRIVE_FETCHING_NOTES, 6 | DRIVE_FETCHING_NOTES_SUCCESS, 7 | DRIVE_FETCHING_NOTES_FAILED, 8 | DRIVE_BACK_ROOT, 9 | DRIVE_DOWNLOAD_NOTE, 10 | DRIVE_DOWNLOAD_NOTE_SUCCESS, 11 | DRIVE_DOWNLOAD_NOTE_FAILED, 12 | DRIVE_DELETE_ITEM, 13 | DRIVE_DELETE_ITEM_SUCCESS, 14 | DRIVE_DELETE_ITEM_FAILED, 15 | } from '../actions/drive'; 16 | 17 | const assign = Object.assign; 18 | 19 | function updateDriver(state = { 20 | status: 0, // 0: fething 1: success 2:failed 21 | projects: [], 22 | notes: [], 23 | currentProjectName: '', 24 | }, action) { 25 | switch (action.type) { 26 | case DRIVE_FETCHING_PROJECTS: 27 | return assign({}, state, { 28 | status: 0, 29 | }); 30 | case DRIVE_FETCHING_PROJECRS_SUCCESS: { 31 | const { list } = action; 32 | state.projects = list; 33 | state.notes = []; 34 | state.status = 1; 35 | return assign({}, state); 36 | } 37 | case DRIVE_FETCHING_PROJECRS_FAILED: 38 | return assign({}, state, { 39 | status: 2, 40 | }); 41 | case DRIVE_FETCHING_NOTES: 42 | return assign({}, state, { 43 | status: 0, 44 | currentProjectName: action.folder, 45 | }); 46 | case DRIVE_FETCHING_NOTES_SUCCESS: { 47 | const { list } = action; 48 | state.notes = list; 49 | state.status = 1; 50 | return assign({}, state); 51 | } 52 | case DRIVE_FETCHING_NOTES_FAILED: 53 | return assign({}, state, { 54 | status: 2, 55 | }); 56 | case DRIVE_BACK_ROOT: 57 | return assign({}, state, { 58 | currentProjectName: '', 59 | notes: [], 60 | state: 1, 61 | }); 62 | case DRIVE_DOWNLOAD_NOTE: 63 | return assign({}, state, { 64 | status: 0, 65 | }); 66 | case DRIVE_DOWNLOAD_NOTE_SUCCESS: 67 | case DRIVE_DOWNLOAD_NOTE_FAILED: 68 | return assign({}, state, { 69 | status: 1, 70 | }); 71 | case DRIVE_DELETE_ITEM: 72 | return assign({}, state, { 73 | status: 0, 74 | }); 75 | case DRIVE_DELETE_ITEM_SUCCESS: { 76 | const { deleteType, itemId, jsonItemId } = action; 77 | if (deleteType === 'note') { 78 | const notes = state.notes.filter(item => item.id !== itemId && item.id !== jsonItemId); 79 | state.notes = notes; 80 | } else if (deleteType === 'notebook') { 81 | const projects = state.projects.filter(item => item.id !== itemId); 82 | state.projects = projects; 83 | } 84 | state.status = 1; 85 | return assign({}, state); 86 | } 87 | case DRIVE_DELETE_ITEM_FAILED: 88 | return assign({}, state, { 89 | status: 1, 90 | }); 91 | default: 92 | return state; 93 | } 94 | } 95 | 96 | export default updateDriver; 97 | -------------------------------------------------------------------------------- /config/gulp.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import gulp from 'gulp'; 3 | import gutil from 'gulp-util'; 4 | import del from 'del'; 5 | import webpack from 'webpack'; 6 | import webpackWeb from './webpack.config.renderer.prod.babel'; 7 | import webpackElectron from './webpack.config.electron.babel'; 8 | 9 | gulp.task('clean:web', () => { 10 | del.sync([ 11 | path.join(__dirname, '../lib/css/**'), 12 | path.join(__dirname, '../lib/vendor*'), 13 | path.join(__dirname, '../lib/*.html'), 14 | path.join(__dirname, '../lib/index*'), 15 | path.join(__dirname, '../lib/images/**'), 16 | path.join(__dirname, '../lib/webview/**'), 17 | ]); 18 | }); 19 | 20 | gulp.task('clean:electron', () => { 21 | del.sync([ 22 | path.join(__dirname, '../lib/main.js'), 23 | path.join(__dirname, '../lib/main.js.map'), 24 | path.join(__dirname, '../lib/resource'), 25 | path.join(__dirname, '../lib/assets'), 26 | ]); 27 | }); 28 | 29 | gulp.task('webpack:web', ['clean:web'], (cb) => { 30 | webpack(webpackWeb, (err, stats) => { 31 | if (err) { 32 | throw new gutil.PluginError('webpack:web', err); 33 | } 34 | gutil.log('[webpack:web]', stats.toString({ 35 | colors: true, 36 | })); 37 | cb(null); 38 | }); 39 | }); 40 | 41 | /** 42 | * @description electron 主进程 43 | */ 44 | gulp.task('webpack:electron', ['clean:electron'], (cb) => { 45 | webpack(webpackElectron, (err, stats) => { 46 | if (err) { 47 | throw new gutil.PluginError('webpack:electron', err); 48 | } 49 | gutil.log('[webpack:electron]', stats.toString({ 50 | colors: true, 51 | })); 52 | cb(null); 53 | }); 54 | }); 55 | 56 | /** 57 | * @description electron 主进程静态资源 58 | */ 59 | gulp.task('electron:resource', () => { 60 | gulp.src(path.join(__dirname, '../app/main/resource/**')) 61 | .pipe(gulp.dest(path.join(__dirname, '../lib/resource'))); 62 | gulp.src(path.join(__dirname, '../app/main/package.json')) 63 | .pipe(gulp.dest(path.join(__dirname, '../lib'))); 64 | gulp.src(path.join(__dirname, '../LICENSE')) 65 | .pipe(gulp.dest(path.join(__dirname, '../lib'))); 66 | gulp.src(path.join(__dirname, ('../assets/**'))) 67 | .pipe(gulp.dest(path.join(__dirname, '../lib/assets'))); 68 | }); 69 | 70 | // 渲染进程打包 71 | gulp.task('build:web', ['webpack:web']); 72 | 73 | // 主进程打包任务 74 | gulp.task('build:electron', ['webpack:electron', 'electron:resource']); 75 | 76 | const index = process.argv.findIndex(value => value === '--mode'); 77 | let taskArr = ['clean', 'webpack:web']; 78 | if (index === -1) { 79 | taskArr = ['clean', 'build:web']; 80 | } else { 81 | const mode = process.argv[index + 1]; 82 | switch (mode) { 83 | case 'renderer': 84 | taskArr = ['build:web']; 85 | break; 86 | case 'main': 87 | taskArr = ['build:electron']; 88 | break; 89 | default: 90 | taskArr = ['build:web']; 91 | break; 92 | } 93 | } 94 | 95 | gulp.task('default', taskArr); 96 | -------------------------------------------------------------------------------- /app/views/assets/scss/trash.scss: -------------------------------------------------------------------------------- 1 | @import "common.scss"; 2 | 3 | .trash { 4 | @include display-flex; 5 | flex: 1 1 auto; 6 | height: 100%; 7 | overflow: hidden; 8 | flex-direction: column; 9 | background-color: rgba(248, 248, 248, 1); 10 | color: rgba(0, 0, 0, 0.5); 11 | 12 | .trash-toolbar { 13 | flex: 0 0 3rem; 14 | width: 100%; 15 | -webkit-app-region: drag; 16 | position: relative; 17 | 18 | &:after { 19 | content: ""; 20 | z-index: 999; 21 | position: absolute; 22 | width: 90%; 23 | bottom: 0; 24 | left: 5%; 25 | height: 1px; 26 | background-color: #d9d9d9; 27 | box-shadow: 0 1px 10px #d9d9d9; 28 | } 29 | 30 | .title { 31 | line-height: 3rem; 32 | font-size: 1.2rem; 33 | // font-weight: normal; 34 | width: 80%; 35 | margin: 0 auto; 36 | text-align: center; 37 | @include text-ellipsis; 38 | color: rgba(0, 0, 0, 0.7); 39 | } 40 | .icon { 41 | margin-right: .3em; 42 | color: #92a3ad; 43 | } 44 | 45 | .back { 46 | z-index: 999; 47 | padding-top: 0.5rem; 48 | position: absolute; 49 | height: 100%; 50 | width: auto; 51 | left: 5%; 52 | margin-left: 0.5em; 53 | top: 0; 54 | line-height: 3rem; 55 | cursor: pointer; 56 | 57 | .anticon { 58 | font-size: 1.8rem; 59 | cursor: pointer; 60 | color: rgba(0, 0, 0, 0.3); 61 | transition: color ease 0.2s; 62 | &:hover { 63 | color: rgba(0, 0, 0, 0.5); 64 | } 65 | } 66 | } 67 | } 68 | 69 | .content { 70 | flex: 1 1 auto; 71 | padding: 0 5%; 72 | overflow-y: auto; 73 | position: relative; 74 | 75 | .tips { 76 | position: absolute; 77 | top: 30%; 78 | left: 0; 79 | text-align: center; 80 | font-size: 1.2rem; 81 | width: 100%; 82 | height: 2rem; 83 | line-height: 2rem; 84 | color: cadetblue; 85 | } 86 | 87 | .list { 88 | @include display-flex; 89 | margin: 1.5rem 1rem; 90 | flex-wrap: wrap; 91 | } 92 | 93 | .list-item { 94 | flex: 0 0 14rem; 95 | padding: 1rem 2rem; 96 | cursor: pointer; 97 | border: 2px solid transparent; 98 | transition: border-color, background-color ease-in-out 0.5s; 99 | border-radius: 0.5rem; 100 | 101 | &:hover { 102 | background-color: rgba(146, 136, 173, 0.2); 103 | border-color: #92a3ad; 104 | } 105 | } 106 | 107 | .list-item__img { 108 | padding: 0.5rem 1rem; 109 | } 110 | .list-item__option { 111 | padding: 0.25em 1rem; 112 | text-align: center; 113 | cursor: pointer; 114 | .list-item__options__item { 115 | display: inline-block; 116 | line-height: 1.5rem; 117 | height: 1.5rem; 118 | width: 2.2rem; 119 | text-align: center; 120 | cursor: pointer; 121 | &:hover { 122 | .anticon { 123 | font-size: 1.2rem; 124 | } 125 | } 126 | } 127 | .anticon { 128 | font-size: 1rem; 129 | margin: 0 0.5em; 130 | cursor: pointer; 131 | transition: font-size ease 0.2s; 132 | } 133 | } 134 | .list-item__title { 135 | margin: 0 1rem; 136 | text-align: center; 137 | @include text-ellipsis; 138 | text-align: center; 139 | width: 10rem; 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /config/webpack.config.renderer.prod.babel.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import merge from 'webpack-merge'; 3 | import ExtractTextPlugin from 'extract-text-webpack-plugin'; 4 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 5 | import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer'; 6 | import baseConfig from './webpack.config.base.babel'; 7 | 8 | process.traceDeprecation = true; 9 | 10 | const extractTextConf = (loaders = []) => ExtractTextPlugin.extract({ 11 | fallback: 'style-loader', 12 | use: [ 13 | { 14 | loader: 'css-loader', 15 | options: { minimize: true }, 16 | }, 17 | ...loaders, 18 | ], 19 | }); 20 | 21 | export default merge.smart(baseConfig, { 22 | mode: 'production', 23 | entry: { 24 | index: path.join(__dirname, '../app/views/index.jsx'), 25 | 'webview/webview-pre': [ 26 | path.resolve(__dirname, '../app/webview/webview-pre.js'), 27 | ], 28 | }, 29 | output: { 30 | filename: '[name].js', 31 | path: path.resolve(__dirname, '../lib'), 32 | }, 33 | module: { 34 | rules: [ 35 | { 36 | test: /\.(js|jsx)$/, 37 | use: [ 38 | { 39 | loader: 'babel-loader', 40 | options: { 41 | cacheDirectory: true, 42 | }, 43 | }, 44 | ], 45 | exclude: /node_modules/, 46 | include: path.resolve(__dirname, '../'), 47 | }, 48 | { 49 | test: /\.css$/, 50 | use: extractTextConf(), 51 | }, 52 | { 53 | test: /\.scss$/, 54 | use: extractTextConf(['sass-loader']), 55 | }, 56 | { 57 | test: /\.less$/, 58 | use: extractTextConf([{ 59 | loader: 'less-loader', 60 | options: { 61 | javascriptEnabled: true, 62 | }, 63 | }]), 64 | }, 65 | ], 66 | }, 67 | optimization: { 68 | minimize: true, 69 | occurrenceOrder: true, 70 | splitChunks: { 71 | cacheGroups: { 72 | commons: { 73 | name: 'vendor', 74 | // test: /react|react-dom|redux|redux-saga|redux-logger|history|prop-types|antd|moment|whatwg-fetch/, 75 | test: /node_modules/, 76 | chunks: 'initial', 77 | enforce: true, 78 | }, 79 | }, 80 | }, 81 | }, 82 | plugins: [ 83 | new ExtractTextPlugin({ 84 | filename: 'css/[name].css', 85 | allChunks: true, 86 | }), 87 | // 生成html 88 | new HtmlWebpackPlugin({ 89 | filename: path.resolve(__dirname, '../lib/index.html'), 90 | template: path.resolve(__dirname, '../templete/index.html'), 91 | inject: true, 92 | minify: { 93 | removeComments: true, 94 | collapseWhitespace: true, 95 | removeAttributeQuotes: true, 96 | }, 97 | chunksSortMode: 'dependency', 98 | }), 99 | // webview html 100 | new HtmlWebpackPlugin({ 101 | filename: path.resolve(__dirname, '../lib/webview/webview.html'), 102 | template: path.resolve(__dirname, '../app/main/webview/webview.html'), 103 | inject: false, 104 | minify: { 105 | removeComments: true, 106 | collapseWhitespace: true, 107 | removeAttributeQuotes: true, 108 | minifyCSS: true, 109 | }, 110 | }), 111 | new BundleAnalyzerPlugin(), 112 | ], 113 | }); 114 | -------------------------------------------------------------------------------- /app/views/sagas/projects.js: -------------------------------------------------------------------------------- 1 | import { put, call, takeLatest } from 'redux-saga/effects'; 2 | import { ipcRenderer } from 'electron'; 3 | import { message } from 'antd'; 4 | import { 5 | UPLOAD_NOTE_ONEDRIVE, 6 | UPLOAD_NOTE_ONEDRIVE_SUCCESS, 7 | UPLOAD_NOTE_ONEDRIVE_FAILED, 8 | } from '../actions/projects'; 9 | import { 10 | // MARKDOWN_UPLOADING, 11 | MARKDWON_UPLADING_SUCCESS, 12 | MARKDWON_UPLADING_FAILED, 13 | } from '../actions/markdown'; 14 | import * as db from '../utils/db/app'; 15 | import OneDrive from '../services/OneDrive'; 16 | 17 | const oneDrive = new OneDrive(); 18 | 19 | function* oneDriveUpload(action) { 20 | const { param, toolbar } = action; 21 | const { uuid, name, projectName, projectUuid } = param; 22 | let content; 23 | let labels; 24 | let description; 25 | try { 26 | // yield put({ type: MARKDOWN_UPLOADING }); 27 | if (typeof param.content === 'undefined') { 28 | const data = ipcRenderer.sendSync('read-file', { 29 | projectName, 30 | fileName: name, 31 | }); 32 | if (data.success) { 33 | content = data.data; 34 | } else { 35 | throw new Error('Read file content failed'); 36 | } 37 | } else { 38 | content = param.content; 39 | } 40 | if (typeof param.labels === 'undefined' || typeof param.description === 'undefined') { 41 | const note = db.getNote(uuid); 42 | if (note) { 43 | labels = note.labels; 44 | description = note.description; 45 | } else { 46 | throw new Error('Can not find note in localStorage.'); 47 | } 48 | } else { 49 | labels = param.labels; 50 | description = param.description; 51 | } 52 | const tokens = db.getTokens(); 53 | const { oneDriver: { token, refreshToken, expiresDate } } = tokens; 54 | let currentToken = token; 55 | if (Date.parse(new Date()) > expiresDate) { // token过期刷新token 56 | const refreshData = yield call(oneDrive.refreshToken, refreshToken); 57 | const newToken = refreshData.access_token; 58 | const newRefreshToken = refreshData.refresh_token; 59 | const newExpiresDate = Date.parse(new Date()) + (refreshData.expires_in * 1000); 60 | currentToken = newToken; 61 | db.setToken('oneDrive', newToken, newRefreshToken, newExpiresDate); 62 | } 63 | // 上传文件 64 | yield call(oneDrive.uploadSingleFile, currentToken, `/drive/special/approot:/${projectName}/${name}.md:/content`, content); 65 | // 上传文件信息 66 | yield call(oneDrive.uploadSingleFile, currentToken, `/drive/special/approot:/${projectName}/${name}.json:/content`, JSON.stringify({ 67 | description, 68 | labels, 69 | })); 70 | yield put({ 71 | type: UPLOAD_NOTE_ONEDRIVE_SUCCESS, 72 | param: { 73 | uuid, 74 | projectUuid, 75 | }, 76 | }); 77 | if (toolbar) { 78 | yield put({ 79 | type: MARKDWON_UPLADING_SUCCESS, 80 | }); 81 | } 82 | } catch (ex) { 83 | message.error('Upload failed.'); 84 | console.error(ex); 85 | yield put({ 86 | type: UPLOAD_NOTE_ONEDRIVE_FAILED, 87 | param: { 88 | uuid, 89 | projectUuid, 90 | }, 91 | error: ex, 92 | }); 93 | if (toolbar) { 94 | yield put({ 95 | type: MARKDWON_UPLADING_FAILED, 96 | }); 97 | } 98 | } 99 | } 100 | 101 | function* noteToOneDrive() { 102 | yield takeLatest(UPLOAD_NOTE_ONEDRIVE, oneDriveUpload); 103 | } 104 | 105 | export default [ 106 | noteToOneDrive, 107 | ]; 108 | -------------------------------------------------------------------------------- /.sass-lint.yml: -------------------------------------------------------------------------------- 1 | options: 2 | merge-default-rules: false 3 | files: 4 | include: 5 | - './app/views/assets/scss/**/*.scss' 6 | rules: 7 | # Extends 8 | extends-before-declarations: 2 9 | extends-before-mixins: 2 10 | placeholder-in-extend: 0 11 | 12 | # Mixins 13 | mixins-before-declarations: 0 14 | 15 | # Line Spacing 16 | empty-line-between-blocks: 0 17 | one-declaration-per-line: 2 18 | single-line-per-selector: 0 19 | 20 | # Disallow 21 | no-color-keywords: 0 22 | no-color-literals: 0 23 | no-css-comments: 0 24 | no-debug: 2 25 | no-empty-rulesets: 2 26 | no-extends: 0 27 | no-ids: 0 28 | no-important: 0 29 | no-invalid-hex: 2 30 | no-mergeable-selectors: 2 31 | no-misspelled-properties: 0 32 | no-qualifying-elements: 0 33 | no-trailing-whitespace: 2 34 | no-trailing-zero: 0 35 | no-transition-all: 0 36 | no-url-protocols: 2 37 | no-vendor-prefixes: 38 | - 2 39 | - 40 | excluded-identifiers: 41 | - 'webkit' 42 | - 'moz' 43 | - 'ms' 44 | no-warn: 2 45 | property-units: 0 46 | 47 | # Nesting 48 | force-attribute-nesting: 0 49 | force-element-nesting: 0 50 | force-pseudo-nesting: 0 51 | 52 | # Name Formats 53 | class-name-format: 54 | - 0 55 | - 56 | allow-leading-underscore: false 57 | convention: 'hyphenatedlowercase' 58 | function-name-format: 59 | - 2 60 | - 61 | allow-leading-underscore: false 62 | convention: 'hyphenatedlowercase' 63 | id-name-format: 64 | - 2 65 | - 66 | allow-leading-underscore: false 67 | convention: 'hyphenatedlowercase' 68 | mixin-name-format: 69 | - 2 70 | - 71 | allow-leading-underscore: false 72 | convention: 'hyphenatedlowercase' 73 | placeholder-name-format: 74 | - 2 75 | - 76 | allow-leading-underscore: false 77 | convention: 'hyphenatedlowercase' 78 | variable-name-format: 79 | - 2 80 | - 81 | allow-leading-underscore: false 82 | convention: 'camelcase' 83 | 84 | # Style Guide 85 | bem-depth: 0 86 | border-zero: 87 | - 2 88 | - 89 | convention: '0' 90 | brace-style: 91 | - 2 92 | - 93 | style: '1tbs' 94 | allow-single-line: false 95 | clean-import-paths: 0 96 | empty-args: 97 | - 2 98 | - 99 | include: false 100 | hex-length: 0 101 | hex-notation: 102 | - 2 103 | - 104 | style: 'lowercase' 105 | indentation: 106 | - 2 107 | - 108 | size: 'tab' 109 | leading-zero: 0 110 | nesting-depth: 0 111 | property-sort-order: 0 112 | quotes: 113 | - 2 114 | - 115 | style: 'double' 116 | shorthand-values: 0 117 | url-quotes: 0 118 | variable-for-property: 0 119 | zero-unit: 120 | - 2 121 | - 122 | include: false 123 | 124 | # Inner Spacing 125 | space-after-bang: 126 | - 2 127 | - 128 | include: false 129 | space-after-colon: 130 | - 2 131 | - 132 | include: true 133 | space-after-comma: 134 | - 2 135 | - 136 | include: true 137 | space-around-operator: 138 | - 2 139 | - 140 | include: true 141 | space-before-bang: 142 | - 2 143 | - 144 | include: true 145 | space-before-brace: 146 | - 2 147 | - 148 | include: true 149 | space-before-colon: 150 | - 2 151 | - 152 | include: false 153 | space-between-parens: 154 | - 2 155 | - 156 | include: false 157 | 158 | # Final Items 159 | final-newline: 160 | - 2 161 | - 162 | include: true 163 | trailing-semicolon: 164 | - 2 165 | - 166 | include: true 167 | -------------------------------------------------------------------------------- /app/views/utils/db/DB.js: -------------------------------------------------------------------------------- 1 | export default class DB { 2 | constructor() { 3 | this.getValue = null; 4 | this.getName = null; 5 | this.findIndex = null; 6 | this.findFlag = false; 7 | this.findValue = null; 8 | this.searchValue = null; 9 | this.searchFlag = false; 10 | } 11 | 12 | // 判断是否包含项目 13 | has = (name) => { 14 | this.getValue = window.localStorage.getItem(name); 15 | return this; 16 | } 17 | 18 | // 设置项目 19 | set = (name, data) => { 20 | window.localStorage.setItem(name, JSON.stringify(data)); 21 | } 22 | 23 | /** 24 | * @description 获取仓库 25 | * @param {String} name 仓库名称 26 | */ 27 | get = (name) => { 28 | const value = JSON.parse(window.localStorage.getItem(name)); 29 | this.getValue = value; 30 | this.getName = name; 31 | return this; 32 | } 33 | 34 | // 查找,只遍历一层 35 | find = (param) => { 36 | const arr = this.getValue; 37 | this.findFlag = true; 38 | this.findIndex = []; 39 | const items = arr.filter((item, index) => { 40 | let flag = false; 41 | for (const key in param) { 42 | if (item[key] === param[key]) { 43 | flag = true; 44 | } else { 45 | flag = false; 46 | break; 47 | } 48 | } 49 | if (flag) { 50 | this.findIndex.push(index); 51 | } 52 | return flag; 53 | }); 54 | this.findValue = items; 55 | return this; 56 | } 57 | 58 | // 查找匹配 59 | search = (regList, reg) => { 60 | const arr = this.getValue; 61 | this.searchFlag = true; 62 | const items = arr.filter((item) => { 63 | let flag = false; 64 | const length = regList.length; 65 | for (let i = 0; i < length; i++) { 66 | if (reg.test(item[regList[i]]) && item.status === 1) { 67 | flag = true; 68 | break; 69 | } 70 | } 71 | return flag; 72 | }); 73 | this.searchValue = items; 74 | return this; 75 | } 76 | 77 | // 更新记录 78 | assign = (param) => { 79 | const length = this.findIndex.length; 80 | for (let i = 0; i < length; i++) { 81 | const item = Object.assign({}, this.findValue[i], param); 82 | this.getValue[this.findIndex[i]] = item; 83 | } 84 | return this; 85 | } 86 | 87 | // 删除记录 88 | remove = (param) => { 89 | const arr = this.getValue; 90 | const newArr = arr.filter((item) => { 91 | let flag = true; 92 | for (const key in param) { 93 | if (item[key] === param[key]) { 94 | flag = true; 95 | } else { 96 | flag = false; 97 | break; 98 | } 99 | } 100 | return !flag; 101 | }); 102 | this.getValue = newArr; 103 | return this; 104 | } 105 | 106 | // 删除记录 107 | del = () => { 108 | this.getValue.splice(this.index, 1); 109 | return this; 110 | } 111 | 112 | // 向数组之前插入 113 | push = (item) => { 114 | this.getValue.push(item); 115 | return this; 116 | } 117 | 118 | clear = () => { 119 | this.getName = null; 120 | this.getValue = null; 121 | this.findValue = null; 122 | this.findFlag = false; 123 | this.findIndex = null; 124 | this.searchValue = null; 125 | this.searchFlag = false; 126 | } 127 | 128 | // 写入值 129 | write = () => { 130 | this.set(this.getName, this.getValue); 131 | this.clear(); 132 | } 133 | 134 | value = () => { 135 | let value; 136 | if (this.findFlag) { 137 | value = this.findValue; 138 | } else { 139 | value = this.getValue; 140 | } 141 | if (this.searchFlag) { 142 | value = this.searchValue; 143 | } 144 | this.clear(); 145 | return value; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/views/component/note/Note.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import classnames from 'classnames'; 4 | import Explorer from './Explorer'; 5 | import Markdown from '../editor/Markdown'; 6 | import ToolBar from './ToolBar'; 7 | import Loading from '../share/Loading'; 8 | 9 | import '../../assets/scss/note.scss'; 10 | 11 | const NoteWorkspace = (props) => { 12 | const { projects, markdown, dispatch, note, markdownSettings, editorMode, searchStatus, searchResult, exportQueue: { status: exportStatus } } = props; 13 | let projectData; 14 | if (searchStatus === 0) { 15 | projectData = projects; 16 | } else if (searchStatus === 1) { 17 | projectData = searchResult; 18 | } 19 | const blur = exportStatus === 1; 20 | const contClass = classnames('note-root-cont', { 21 | 'note-blur': blur, 22 | }); 23 | return ( 24 |
25 | {blur ? ( 26 | 27 | ) : null} 28 | 36 |
37 | 45 | 52 |
53 |
54 | ); 55 | }; 56 | 57 | NoteWorkspace.displayName = 'NoteSpace'; 58 | NoteWorkspace.propTypes = { 59 | dispatch: PropTypes.func.isRequired, 60 | projects: PropTypes.arrayOf(PropTypes.shape({ 61 | uuid: PropTypes.string.isRequired, 62 | name: PropTypes.string.isRequired, 63 | description: PropTypes.string.isRequired, 64 | labels: PropTypes.arrayOf(PropTypes.string).isRequired, 65 | status: PropTypes.number.isRequired, 66 | notes: PropTypes.array.isRequired, 67 | })).isRequired, 68 | searchResult: PropTypes.arrayOf(PropTypes.shape({ 69 | uuid: PropTypes.string.isRequired, 70 | name: PropTypes.string.isRequired, 71 | description: PropTypes.string.isRequired, 72 | labels: PropTypes.arrayOf(PropTypes.string).isRequired, 73 | status: PropTypes.number.isRequired, 74 | notes: PropTypes.array.isRequired, 75 | })).isRequired, 76 | searchStatus: PropTypes.number.isRequired, 77 | markdown: PropTypes.shape({ 78 | parentsId: PropTypes.string.isRequired, 79 | uuid: PropTypes.string.isRequired, 80 | createDate: PropTypes.string.isRequired, 81 | latestDate: PropTypes.string.isRequired, 82 | name: PropTypes.string.isRequired, 83 | content: PropTypes.string.isRequired, 84 | html: PropTypes.string.isRequired, 85 | status: PropTypes.number.isRequired, 86 | start: PropTypes.number.isRequired, 87 | hasEdit: PropTypes.bool.isRequired, 88 | uploadStatus: PropTypes.number.isRequired, 89 | }).isRequired, 90 | note: PropTypes.shape({ 91 | projectUuid: PropTypes.string.isRequired, 92 | projectName: PropTypes.string.isRequired, 93 | fileUuid: PropTypes.string.isRequired, 94 | exportStatus: PropTypes.number.isRequired, 95 | }).isRequired, 96 | markdownSettings: PropTypes.shape({ 97 | editorWidth: PropTypes.number.isRequired, 98 | }).isRequired, 99 | editorMode: PropTypes.string.isRequired, 100 | exportQueue: PropTypes.shape({ 101 | status: PropTypes.number.isRequired, 102 | }).isRequired, 103 | }; 104 | 105 | export default NoteWorkspace; 106 | -------------------------------------------------------------------------------- /app/views/component/share/search/Search.jsx: -------------------------------------------------------------------------------- 1 | /** 2 | *_______________#########________________________________ 3 | *______________############______________________________ 4 | *______________#############_____________________________ 5 | *_____________##__###########____________________________ 6 | *____________###__######_#####____________##_____##______ 7 | *____________###_#######___####___________##____##_______ 8 | *___________###__##########_####__________##___##________ 9 | *__________####__###########_####_________######_________ 10 | *________#####___###########__#####_______###____________ 11 | *_______######___###_########___#####____##______________ 12 | *_______#####___###___########___######_##_______________ 13 | *______######___###__###########___######________________ 14 | *_____######___####_##############__######_______________ 15 | *____#######__#####################_########_____________ 16 | *____#######__##############################_____________ 17 | *___#######__######_#################_#######____________ 18 | *___#######__######_######_#########___######____________ 19 | *___#######____##__######___######_____######____________ 20 | *___#######________######____#####_____#####_____________ 21 | *____######________#####_____#####_____####______________ 22 | *_____#####________####______#####_____###_______________ 23 | *______#####______;###________###______#_________________ 24 | *________##_______####________####_______________________ 25 | */ 26 | 27 | /** 28 | * @name Search 笔记搜索组件 29 | * @prop {number} searchStatus 搜索状态 0: 未搜索 1: 搜索中 2: 搜索完成 30 | * @prop {string} placeholder 输入框placeholder 31 | * @prop {object} style 搜索框样式 32 | * @prop {function} onSearch 搜索方法回调 33 | * @prop {function} onClose 关闭搜索回调 34 | * @class 35 | */ 36 | import React, { PureComponent } from 'react'; 37 | import PropTypes from 'prop-types'; 38 | import { Input, Icon } from 'antd'; 39 | 40 | import './search.scss'; 41 | 42 | export default class Search extends PureComponent { 43 | static displayName = 'Search'; 44 | static propTypes = { 45 | searchStatus: PropTypes.number.isRequired, // 0: 未搜索 1: 搜索中 2: 搜索完成 46 | placeholder: PropTypes.string.isRequired, 47 | style: PropTypes.object, 48 | onSearch: PropTypes.func.isRequired, 49 | onClose: PropTypes.func.isRequired, 50 | }; 51 | static defaultProps = { 52 | searchStatus: 0, 53 | placeholder: 'input search text', 54 | }; 55 | 56 | constructor() { 57 | super(); 58 | this.state = { 59 | target: '', 60 | }; 61 | } 62 | 63 | // 处理搜索事件 64 | handleSearch = () => { 65 | const { target } = this.state; 66 | if (target === '') { 67 | return false; 68 | } 69 | this.props.onSearch(target); 70 | } 71 | 72 | // 关闭搜索 73 | handleClose = () => { 74 | this.setState({ 75 | target: '', 76 | }); 77 | this.props.onClose(); 78 | } 79 | 80 | // 监听input改变 81 | handleChange = (event) => { 82 | this.setState({ 83 | target: event.target.value, 84 | }); 85 | } 86 | 87 | // 监听键盘事件 88 | handleKeyDown = (event) => { 89 | if (event.keyCode === 13) { // 键盘回车 90 | this.handleSearch(); 91 | } 92 | } 93 | 94 | renderSeachIcon = () => ( 95 | 96 | ); 97 | 98 | renderCloseIcon = () => ( 99 | 100 | ) 101 | 102 | renderLoadingIcon = () => () 103 | 104 | render() { 105 | const { placeholder, style, searchStatus } = this.props; 106 | const { target } = this.state; 107 | const options = { 108 | prefix: this.renderSeachIcon(), 109 | type: 'text', 110 | placeholder, 111 | }; 112 | if (searchStatus === 2) { 113 | options.suffix = this.renderCloseIcon(); 114 | } else if (searchStatus === 1) { 115 | options.suffix = this.renderLoadingIcon(); 116 | } 117 | return ( 118 | 122 | 128 | 129 | ); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /app/views/assets/scss/drive.scss: -------------------------------------------------------------------------------- 1 | @import "common.scss"; 2 | 3 | .cloud { 4 | @include display-flex; 5 | flex: 1 1 auto; 6 | height: 100%; 7 | overflow: hidden; 8 | flex-direction: column; 9 | background-color: rgba(248, 248, 248, 1); 10 | color: rgba(0, 0, 0, 0.5); 11 | position: relative; 12 | 13 | .cloud-bar { 14 | flex: 0 0 3rem; 15 | width: 100%; 16 | -webkit-app-region: drag; 17 | position: relative; 18 | overflow: hidden; 19 | 20 | &:after { 21 | content: ""; 22 | z-index: 999; 23 | position: absolute; 24 | width: 90%; 25 | bottom: 0; 26 | left: 5%; 27 | height: 1px; 28 | background-color: #d9d9d9; 29 | box-shadow: 0 1px 10px #d9d9d9; 30 | } 31 | 32 | .cloud-bar-content { 33 | width: 90%; 34 | height: 3rem; 35 | margin: 0 auto; 36 | } 37 | 38 | .label { 39 | color: #000000; 40 | line-height: 3rem; 41 | } 42 | 43 | .cloud-item { 44 | cursor: pointer; 45 | display: inline-block; 46 | vertical-align: top; 47 | position: relative; 48 | 49 | &.cur { 50 | &:after { 51 | content: ""; 52 | position: absolute; 53 | bottom: 0; 54 | border-top: 0.6rem solid transparent; 55 | border-right: 0.5rem solid transparent; 56 | border-bottom: 0.6rem solid #d9d9d9; 57 | border-left: 0.5rem solid transparent; 58 | left: 50%; 59 | margin-left: -0.3rem; 60 | } 61 | } 62 | } 63 | 64 | .logo { 65 | display: inline-block; 66 | width: 4rem; 67 | height: 3rem; 68 | text-align: center; 69 | cursor: pointer; 70 | 71 | img { 72 | display: inline-block; 73 | width: 2.6rem; 74 | height: 2.6rem; 75 | margin: 0.2rem auto auto auto; 76 | cursor: pointer; 77 | } 78 | } 79 | } 80 | 81 | .loading { 82 | position: absolute; 83 | z-index: 999; 84 | width: 100%; 85 | height: 100%; 86 | margin-top: 3rem; 87 | text-align: center; 88 | padding-top: 30%; 89 | } 90 | 91 | .bread-bar { 92 | flex: 0 0 3rem; 93 | 94 | .bread-container { 95 | position: relative; 96 | width: 90%; 97 | margin: 0 auto; 98 | height: 3rem; 99 | line-height: 3rem; 100 | } 101 | .ant-breadcrumb { 102 | height: 3rem; 103 | line-height: 3rem; 104 | } 105 | 106 | .tools { 107 | position: absolute; 108 | z-index: 999; 109 | height: 3rem; 110 | line-height: 3rem; 111 | top: 0; 112 | right: 1rem; 113 | font-size: 1.8rem; 114 | } 115 | } 116 | 117 | .blur { 118 | filter: blur(1rem); 119 | -webkit-filter: blur(1rem); 120 | } 121 | 122 | .content { 123 | flex: 1 1 auto; 124 | padding: 0 5%; 125 | overflow-y: auto; 126 | position: relative; 127 | 128 | .tips { 129 | position: absolute; 130 | top: 30%; 131 | left: 0; 132 | text-align: center; 133 | font-size: 1.2rem; 134 | width: 100%; 135 | height: 2rem; 136 | line-height: 2rem; 137 | color: cadetblue; 138 | 139 | .anticon { 140 | margin-right: 0.5em; 141 | } 142 | } 143 | 144 | .list { 145 | @include display-flex; 146 | margin: 1.5rem 1rem; 147 | flex-wrap: wrap; 148 | } 149 | 150 | .list-item { 151 | flex: 0 0 14rem; 152 | padding: 1rem 2rem; 153 | cursor: pointer; 154 | border: 2px solid transparent; 155 | transition: border-color, background-color ease-in-out 0.5s; 156 | border-radius: 0.5rem; 157 | 158 | &:hover { 159 | background-color: rgba(146, 136, 173, 0.2); 160 | border-color: #92a3ad; 161 | } 162 | } 163 | 164 | .list-item__img { 165 | padding: 0.5rem 1rem; 166 | } 167 | 168 | .list-item__title { 169 | margin: 0 1rem; 170 | width: 10rem; 171 | text-align: center; 172 | @include text-ellipsis; 173 | text-align: center; 174 | } 175 | 176 | .list-item__option { 177 | padding: 0.25em 1rem; 178 | text-align: center; 179 | cursor: pointer; 180 | .list-item__options__item { 181 | display: inline-block; 182 | line-height: 1.5rem; 183 | height: 1.5rem; 184 | width: 2.2rem; 185 | text-align: center; 186 | cursor: pointer; 187 | &:hover { 188 | .anticon { 189 | font-size: 1.2rem; 190 | } 191 | } 192 | } 193 | .anticon { 194 | font-size: 1rem; 195 | margin: 0 0.5em; 196 | cursor: pointer; 197 | transition: font-size ease 0.2s; 198 | } 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /app/views/assets/scss/index.scss: -------------------------------------------------------------------------------- 1 | @import "color.scss"; 2 | * { 3 | padding: 0; 4 | margin: 0; 5 | box-sizing: border-box; 6 | } 7 | 8 | li { 9 | list-style-type: none; 10 | } 11 | 12 | .anticon { 13 | cursor: pointer; 14 | } 15 | 16 | html { 17 | font-family: "BlinkMacSystemFont", "Helvetica Neue", Helvetica, "Microsoft YaHei", "微软雅黑", "Arial", "Lucida Grande", "Segoe UI", Ubuntu, Cantarell, sans-serif; 18 | font-size: 14px; 19 | line-height: 1.5; 20 | overflow: hidden; 21 | color: $color; 22 | } 23 | 24 | html, body, #root { 25 | width: 100%; 26 | height: 100%; 27 | background-color: $htmlBg; 28 | } 29 | 30 | #root { 31 | min-width: 978px; 32 | } 33 | 34 | #blur { 35 | position: fixed; 36 | z-index: -1; 37 | filter: blur(10rem); 38 | -webkit-filter: blur(10rem); 39 | width: 100%; 40 | height: 100%; 41 | left: 0; 42 | top: 0; 43 | background-color: rgba($color: #ffffff, $alpha: 0.66); 44 | } 45 | 46 | .container { 47 | display: flex; 48 | width: 100%; 49 | height: 100%; 50 | position: relative; 51 | 52 | &.darwin:before { 53 | content: ""; 54 | position: absolute; 55 | z-index: -1; 56 | width: 100%; 57 | height: 100%; 58 | left: 0; 59 | top: 0; 60 | background-color: rgba($color: #ffffff, $alpha: 0.5); 61 | } 62 | } 63 | 64 | .no-select { 65 | -webkit-user-select: none; 66 | user-select: none; 67 | cursor: default; 68 | } 69 | 70 | /* 工具栏 */ 71 | .tool-bar { 72 | flex: 0 0 5.4rem; 73 | width: 5.4rem; 74 | height: 100%; 75 | -webkit-app-region: drag; 76 | overflow: hidden; 77 | position: relative; 78 | 79 | .app-title-bar { 80 | width: 100%; 81 | height: 3rem; 82 | } 83 | 84 | .logo { 85 | text-align: center; 86 | margin-top: 1.6rem; 87 | img { 88 | width: 4rem; 89 | height: 4rem; 90 | border-radius: 50%; 91 | } 92 | 93 | .avatar { 94 | width: 4rem; 95 | height: 4rem; 96 | border: 3px solid #e4d3cc; 97 | animation: a-fade-in .5s ease; 98 | } 99 | } 100 | 101 | .menu-list { 102 | margin-top: 3rem; 103 | 104 | .menu-item { 105 | text-align: center; 106 | margin-bottom: 1.2rem; 107 | cursor: pointer; 108 | &:last-child { 109 | margin-bottom: 0; 110 | } 111 | } 112 | 113 | .menu-item-radius { 114 | border-radius: 50%; 115 | height: 3rem; 116 | width: 3rem; 117 | display: inline-block; 118 | line-height: 3rem; 119 | transition: background-color, box-shadow .2s ease-in-out; 120 | } 121 | 122 | .menu-svg { 123 | margin-top: .75rem; 124 | width: 1.5rem; 125 | height: 1.5rem; 126 | transition: fill .2s ease-in-out; 127 | } 128 | } 129 | } 130 | 131 | .app-lounch-loading { 132 | z-index: 9999; 133 | position: fixed; 134 | width: 100%; 135 | height: 100%; 136 | background-color: $navBg; 137 | 138 | .loading-cont { 139 | position: relative; 140 | width: 30%; 141 | height: 30%; 142 | top: 35%; 143 | left: 35%; 144 | fill: none; 145 | stroke-width: 1; 146 | stroke: aliceblue; 147 | } 148 | } 149 | 150 | .nav { 151 | background-color: $navBg; 152 | position: relative; 153 | padding-top: 8.2rem; 154 | width: $navWidth; 155 | overflow-x: hidden; 156 | overflow-y: auto; 157 | 158 | border-right: 1px solid $colorBorder; 159 | 160 | .nav-header { 161 | position: fixed; 162 | width: $navWidth; 163 | top: 0; 164 | left: 0; 165 | height: 8.2rem; 166 | background-color: $navBg; 167 | border-right: 1px solid $colorBorder; 168 | } 169 | 170 | .logo-cont { 171 | background-color: $headerBg; 172 | } 173 | } 174 | 175 | .sub-window { 176 | color: $fontColor; 177 | padding: 1rem; 178 | 179 | .error-tip { 180 | color: $fontColor; 181 | font-size: 1.5rem; 182 | } 183 | 184 | .name { 185 | padding: 0.5rem; 186 | color: $fontColor; 187 | border-bottom: 1px solid $darkBorder; 188 | 189 | i { 190 | font-size: 1rem; 191 | } 192 | } 193 | } 194 | 195 | .cursor-pointer { 196 | cursor: pointer; 197 | } 198 | 199 | // 公用的loading样式 200 | .loading-default { 201 | position: absolute; 202 | z-index: 9999; 203 | width: 100%; 204 | height: 100%; 205 | text-align: center; 206 | left: 0; 207 | top: 0; 208 | background-color: rgba($color: #ffffff, $alpha: 0.7); 209 | img { 210 | margin-top: 25%; 211 | } 212 | } 213 | 214 | @keyframes a-fade-in { 215 | 0% { 216 | transform: rotate(-360deg); 217 | opacity: 0; 218 | } 219 | 100% { 220 | transform: rotate(0); 221 | opacity: 1; 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /app/main/Oauth2.js: -------------------------------------------------------------------------------- 1 | import queryString from 'querystring'; 2 | import fetch from 'node-fetch'; 3 | import nodeUrl from 'url'; 4 | import electron from 'electron'; 5 | 6 | const objectAssign = Object.assign; 7 | const BrowserWindow = electron.BrowserWindow || electron.remote.BrowserWindow; 8 | 9 | const generateRandomString = function (length) { 10 | let text = ''; 11 | const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 12 | 13 | for (let i = 0; i < length; i++) { 14 | text += possible.charAt(Math.floor(Math.random() * possible.length)); 15 | } 16 | 17 | return text; 18 | }; 19 | 20 | 21 | export default class Oauth2 { 22 | constructor(config, windowParams) { 23 | this.config = config; 24 | this.windowParams = windowParams; 25 | } 26 | 27 | getAuthorizationCode(options) { 28 | const opts = options || {}; 29 | 30 | const config = this.config; 31 | if (!config.redirectUri) { 32 | this.config.redirectUri = 'urn:ietf:wg:oauth:2.0:oob'; 33 | } 34 | 35 | const urlParams = { 36 | response_type: 'code', 37 | redirect_uri: config.redirectUri, 38 | client_id: config.clientId, 39 | state: generateRandomString(16), 40 | }; 41 | 42 | if (options.scope) { 43 | urlParams.scope = opts.scope; 44 | } 45 | 46 | if (options.accessType) { 47 | urlParams.access_type = opts.accessType; 48 | } 49 | 50 | const url = `${config.authorizationUrl}?${queryString.stringify(urlParams)}`; 51 | 52 | return new Promise((resolve, reject) => { 53 | const authWindow = new BrowserWindow(this.windowParams || { 'use-content-size': true }); 54 | 55 | authWindow.loadURL(url); 56 | authWindow.show(); 57 | 58 | authWindow.on('closed', () => { 59 | reject(new Error('window was closed by user')); 60 | }); 61 | 62 | function onCallback(cUrl) { 63 | const urlParts = nodeUrl.parse(cUrl, true); 64 | const query = urlParts.query; 65 | const code = query.code; 66 | const error = query.error; 67 | 68 | if (error !== undefined) { 69 | reject(error); 70 | authWindow.removeAllListeners('closed'); 71 | setImmediate(() => { 72 | authWindow.close(); 73 | }); 74 | } else if (code) { 75 | resolve(code); 76 | authWindow.removeAllListeners('closed'); 77 | setImmediate(() => { 78 | try { 79 | authWindow.close(); 80 | } catch (ex) { 81 | console.warn(ex); 82 | } 83 | }); 84 | } 85 | } 86 | 87 | authWindow.webContents.on('will-navigate', (event, currentUrl) => { 88 | onCallback(currentUrl); 89 | }); 90 | 91 | authWindow.webContents.on('did-get-redirect-request', (event, oldUrl, newUrl) => { 92 | onCallback(newUrl); 93 | }); 94 | }); 95 | } 96 | 97 | tokenRequest(data) { 98 | const header = { 99 | Accept: 'application/json', 100 | 'Content-Type': 'application/x-www-form-urlencoded', 101 | }; 102 | 103 | if (this.config.useBasicAuthorizationHeader) { 104 | header.Authorization = `Basic ${new Buffer(`${this.config.clientId}:{this.config.clientSecret}`).toString('base64')}`; 105 | } else { 106 | objectAssign(data, { 107 | client_id: this.config.clientId, 108 | client_secret: this.config.clientSecret, 109 | }); 110 | } 111 | 112 | return fetch(this.config.tokenUrl, { 113 | method: 'POST', 114 | headers: header, 115 | body: queryString.stringify(data), 116 | }).then(res => res.json()); 117 | } 118 | 119 | getAccessToken(opts) { 120 | const config = this.config; 121 | return this.getAuthorizationCode(opts) 122 | .then((authorizationCode) => { 123 | let tokenRequestData = { 124 | code: authorizationCode, 125 | grant_type: 'authorization_code', 126 | redirect_uri: config.redirectUri, 127 | }; 128 | tokenRequestData = Object.assign(tokenRequestData, opts.additionalTokenRequestData); 129 | return this.tokenRequest(tokenRequestData); 130 | }); 131 | } 132 | 133 | refreshToken(refreshToken) { 134 | return this.tokenRequest({ 135 | refresh_token: refreshToken, 136 | grant_type: 'refresh_token', 137 | redirect_uri: this.config.redirectUri, 138 | }); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/views/component/share/notebook/NoteItem.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Icon, Tooltip } from 'antd'; 4 | import SVGIcon from '../SVGIcon'; 5 | 6 | export default class NoteItem extends PureComponent { 7 | static displayName = 'NoteItem'; 8 | static propTypes = { 9 | type: PropTypes.oneOf(['notebook', 'note']).isRequired, 10 | className: PropTypes.string.isRequired, 11 | item: PropTypes.object.isRequired, 12 | isCloud: false, // 是否来自Cloud 13 | title: PropTypes.string, 14 | hasRestore: PropTypes.bool.isRequired, // 是否有还原按钮 15 | restoreFn: PropTypes.func.isRequired, // 复原按钮事件 16 | hasLogin: PropTypes.bool.isRequired, // 是否有进入按钮 17 | hasDownload: PropTypes.bool.isRequired, // 是否有下载按钮 18 | downloadFn: PropTypes.func.isRequired, // 下载按钮事件 19 | hasRemove: PropTypes.bool.isRequired, // 是否有删除按钮 20 | removeFn: PropTypes.func.isRequired, // 删除按钮事件 21 | itemClick: PropTypes.func.isRequired, 22 | }; 23 | static defaultProps = { 24 | className: 'list-item', 25 | hasRestore: false, 26 | hasLogin: false, 27 | hasDownload: false, 28 | hasRemove: false, 29 | isCloud: false, 30 | itemClick: () => {}, 31 | }; 32 | 33 | getUseId() { 34 | const { type } = this.props; 35 | return `#icon_svg_${type}`; 36 | } 37 | 38 | handleRemove = (e, item) => { 39 | const { isCloud, type } = this.props; 40 | if (isCloud) { 41 | this.props.removeFn(e, type, item.name, item.id, item.parentReference); 42 | } else { 43 | this.props.removeFn(e, item.uuid, item.name); 44 | } 45 | } 46 | 47 | renderRestore() { 48 | const { hasRestore, restoreFn, item, type } = this.props; 49 | if (hasRestore) { 50 | return ( 51 | restoreFn(e, item.uuid, item.name)} 54 | > 55 | 56 | 57 | 58 | 59 | ); 60 | } 61 | return null; 62 | } 63 | 64 | renderDownload() { 65 | const { hasDownload, downloadFn, item, type } = this.props; 66 | if (hasDownload) { 67 | return ( 68 | downloadFn(item.name)} 71 | > 72 | document.getElementById('app_cloud')} 76 | > 77 | 78 | 79 | 80 | ); 81 | } 82 | return null; 83 | } 84 | 85 | renderRemove() { 86 | const { hasRemove, item, type } = this.props; 87 | if (hasRemove) { 88 | return ( 89 | this.handleRemove(e, item)} 92 | > 93 | 94 | 95 | 96 | 97 | ); 98 | } 99 | return null; 100 | } 101 | 102 | renderLogin() { 103 | if (this.props.hasLogin) { 104 | const { type } = this.props; 105 | return ( 106 | 107 | 108 | 109 | 110 | 111 | ); 112 | } 113 | return null; 114 | } 115 | 116 | render() { 117 | const { className, itemClick, item, title } = this.props; 118 | return ( 119 |
  • 124 |
    125 | 131 |
    132 |

    133 | {title || item.name} 134 |

    135 |
    136 | {this.renderRestore()} 137 | {this.renderLogin()} 138 | {this.renderDownload()} 139 | {this.renderRemove()} 140 |
    141 |
  • 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/views/reducers/markdown.js: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron'; 2 | import { 3 | READ_FILE, 4 | UPDATE_MARKDOWN_HTML, 5 | BEFORE_SWITCH_SAVE, 6 | CLEAR_MARKDOWN, 7 | UPDATE_CURRENT_MARKDOWN_TITLE, 8 | SAVE_CONTENT_TO_TRASH_FILE, 9 | MARKDOWN_UPLOADING, 10 | MARKDWON_UPLADING_SUCCESS, 11 | MARKDWON_UPLADING_FAILED, 12 | JUST_UPDATE_MARKDWON_HTML, 13 | // READ_FILE_SUCCESS, 14 | // READ_FILE_FARILED, 15 | } from '../actions/markdown'; 16 | import { updateNoteInfo } from '../utils/db/app'; 17 | import { markedToHtml } from '../utils/utils'; 18 | 19 | const assign = Object.assign; 20 | 21 | const initState = { 22 | parentsId: '', 23 | uuid: '', 24 | // file: '', 25 | name: '', 26 | createDate: '', 27 | latestDate: '', 28 | content: '', 29 | html: '', 30 | status: 0, // 0 :无 1:读取成功 2:读取失败 31 | start: -1, 32 | oneDriver: 0, 33 | hasEdit: false, 34 | uploadStatus: 0, // 0: 无 1:上传中 2:上传成功 3:上传失败 35 | }; 36 | 37 | function updateMarkdown(state = initState, action) { 38 | switch (action.type) { 39 | case READ_FILE: { 40 | const info = action.param; 41 | const html = markedToHtml(info.content); 42 | info.html = html; 43 | // let uploadStatus = 0; 44 | // if (info.oneDriver === 2) { 45 | // uploadStatus = 1; 46 | // } 47 | return assign({}, info, { 48 | status: 1, 49 | start: -1, 50 | hasEdit: false, 51 | uploadStatus: 0, 52 | }); 53 | } 54 | case JUST_UPDATE_MARKDWON_HTML: { 55 | const { content } = action; 56 | const html = markedToHtml(action.content); 57 | return assign({}, state, { 58 | content, 59 | html, 60 | }); 61 | } 62 | case UPDATE_MARKDOWN_HTML: { 63 | let hasEdit = true; 64 | const html = markedToHtml(action.content); 65 | if (state.uuid !== action.uuid) { 66 | hasEdit = false; 67 | } 68 | return assign({}, state, { 69 | content: action.content, 70 | html, 71 | start: action.start, 72 | hasEdit, 73 | }); 74 | } 75 | case BEFORE_SWITCH_SAVE: { // 切换笔记前保存当前笔记 76 | const { projectName, needUpdateCloudStatus } = action; 77 | const { content, name, status, uuid } = state; 78 | if (status === 0 || status === 2) { // 不用保存 79 | return state; 80 | } 81 | const data = ipcRenderer.sendSync('save-content-to-file', { 82 | content, 83 | projectName, 84 | fileName: name, 85 | }); 86 | const date = new Date(); 87 | state.latestDate = date; 88 | const param = { 89 | latestDate: date, 90 | }; 91 | if (needUpdateCloudStatus) { 92 | param.oneDriver = 1; 93 | } 94 | updateNoteInfo(uuid, param); 95 | if (!data.success) { 96 | console.error('Save file failed.'); 97 | } 98 | return assign({}, state); 99 | } 100 | case SAVE_CONTENT_TO_TRASH_FILE: { // 删除文件后将内容保存至草稿箱 101 | const { projectName } = action; 102 | const { content, name } = state; 103 | const data = ipcRenderer.sendSync('save-content-to-trash-file', { 104 | content, 105 | projectName, 106 | name, 107 | }); 108 | if (!data.success) { 109 | console.error('Save file failed.'); 110 | } 111 | return state; 112 | } 113 | case UPDATE_CURRENT_MARKDOWN_TITLE: { 114 | const { uuid, name } = action; 115 | if (state.uuid !== uuid) { 116 | return state; 117 | } 118 | return assign({}, state, { 119 | name, 120 | }); 121 | } 122 | case CLEAR_MARKDOWN: 123 | return assign({}, initState); 124 | case MARKDOWN_UPLOADING: { 125 | const { uuid } = state; 126 | if (uuid === '' || uuid === '-1') { // 不更新 127 | return state; 128 | } 129 | return assign({}, state, { 130 | uploadStatus: 1, 131 | }); 132 | } 133 | case MARKDWON_UPLADING_SUCCESS: { 134 | const { uuid } = state; 135 | if (uuid === '' || uuid === '-1') { // 不更新 136 | return state; 137 | } 138 | return assign({}, state, { 139 | uploadStatus: 2, 140 | }); 141 | } 142 | case MARKDWON_UPLADING_FAILED: { 143 | const { uuid } = state; 144 | if (uuid === '' || uuid === '-1') { // 不更新 145 | return state; 146 | } 147 | return assign({}, state, { 148 | uploadStatus: 3, 149 | }); 150 | } 151 | default: 152 | return state; 153 | } 154 | } 155 | 156 | export default updateMarkdown; 157 | -------------------------------------------------------------------------------- /app/views/reducers/app.js: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron'; 2 | import { 3 | APP_LOUNCH, 4 | APP_ADJUST_MARKDOWN, 5 | APP_SWITCH_EDIT_MODE, 6 | APP_SET_TOKEN, 7 | FETCHING_ONEDRIVE_TOKEN, 8 | FETCHING_ONEDRIVE_TOKEN_FAILED, 9 | FETCHING_ONEDRIVE_TOKEN_SUCCESS, 10 | FETCHING_GITHUB_RELEASES, 11 | FETCHING_GITHUB_RELEASES_FAILED, 12 | FETCHING_GITHUB_RELEASES_SUCCESS, 13 | CLOSE_UPDATE_NOTIFICATION, 14 | } from '../actions/app'; 15 | import appInfo from '../../../package.json'; 16 | import { checkDefaults, getAppSettings, setMarkdownSettings, setToken } from '../utils/db/app'; 17 | import { compareVersion } from '../utils/utils'; 18 | 19 | const assign = Object.assign; 20 | 21 | export default function lounchApp(state = { 22 | status: 0, // 0: app 初始化 1: 初始化成功 23 | version: '', 24 | latestVersion: '', 25 | versionFetchStatus: 0, // 0: 请求中 1: 请求成功 2: 请求失败 26 | showUpdate: false, 27 | allowShowUpdate: true, 28 | settings: { 29 | theme: 'light', 30 | editorMode: 'normal', 31 | markdownSettings: { 32 | editorWidth: 0.5, 33 | }, 34 | defaultDrive: 'oneDrive', 35 | }, 36 | first: false, 37 | oneDriveTokenStatus: 0, // 0 未请求 1 请求中 2 成功 3 失败 38 | platform: '', 39 | }, action) { 40 | switch (action.type) { 41 | case APP_LOUNCH: { 42 | const flag = checkDefaults(); 43 | if (!flag) { // 判断是否进行过初始化 44 | state.first = true; 45 | } 46 | const settings = getAppSettings(); 47 | if (typeof settings.defaultDrive === 'undefined') { 48 | settings.defaultDrive = 'oneDrive'; 49 | } 50 | if (settings.defaultDrive === 'oneDriver') { 51 | settings.defaultDrive = 'oneDrive'; 52 | } 53 | const platform = remote.getGlobal('process').platform; 54 | const app = { 55 | status: 1, 56 | version: appInfo.version, 57 | settings, 58 | platform, 59 | }; 60 | return assign({}, state, app); 61 | } 62 | case APP_SWITCH_EDIT_MODE: { 63 | const { currentMode } = action; 64 | let mode = 'normal'; 65 | switch (currentMode) { 66 | case 'normal': 67 | mode = 'edit'; 68 | break; 69 | case 'edit': 70 | mode = 'preview'; 71 | break; 72 | case 'preview': 73 | mode = 'immersion'; 74 | break; 75 | case 'immersion': 76 | mode = 'normal'; 77 | break; 78 | default: 79 | mode = 'normal'; 80 | break; 81 | } 82 | const settings = state.settings; 83 | settings.editorMode = mode; 84 | // updateAppSettings(settings); 85 | const newState = assign({}, state, { 86 | settings, 87 | }); 88 | return newState; 89 | } 90 | case APP_ADJUST_MARKDOWN: { 91 | const { param } = action; 92 | const settings = setMarkdownSettings(param); 93 | return assign({}, state, { 94 | settings, 95 | }); 96 | } 97 | case APP_SET_TOKEN: { 98 | const { name, token } = action; 99 | setToken(name, token); 100 | return assign({}, state); 101 | } 102 | case FETCHING_ONEDRIVE_TOKEN: 103 | return assign({}, state, { 104 | oneDriveTokenStatus: 1, 105 | }); 106 | case FETCHING_ONEDRIVE_TOKEN_FAILED: 107 | return assign({}, state, { 108 | oneDriveTokenStatus: 3, 109 | }); 110 | case FETCHING_ONEDRIVE_TOKEN_SUCCESS: { 111 | const { token, refreshToken, expiresDate } = action; 112 | setToken('oneDriver', token, refreshToken, expiresDate); 113 | return assign({}, state, { 114 | oneDriveTokenStatus: 2, 115 | }); 116 | } 117 | case FETCHING_GITHUB_RELEASES: 118 | return assign({}, state, { 119 | versionFetchStatus: 0, 120 | showUpdate: false, 121 | }); 122 | case FETCHING_GITHUB_RELEASES_SUCCESS: { 123 | const { latestVersion } = action; 124 | const { allowShowUpdate, version } = state; 125 | const needUpdate = compareVersion(version, latestVersion); 126 | if (allowShowUpdate && needUpdate) { 127 | state.showUpdate = true; 128 | } 129 | return assign({}, state, { 130 | latestVersion, 131 | versionFetchStatus: 1, 132 | }); 133 | } 134 | case FETCHING_GITHUB_RELEASES_FAILED: 135 | return assign({}, state, { 136 | versionFetchStatus: 2, 137 | showUpdate: false, 138 | }); 139 | case CLOSE_UPDATE_NOTIFICATION: 140 | return assign({}, state, { 141 | allowShowUpdate: false, 142 | }); 143 | default: 144 | return state; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /app/views/actions/projects.js: -------------------------------------------------------------------------------- 1 | export const GET_PROJECT_LIST = 'GET_PROJECT_LIST'; 2 | 3 | export function getProjectList() { 4 | return { 5 | type: GET_PROJECT_LIST, 6 | }; 7 | } 8 | 9 | export const CREATE_PROJECT = 'CREATE_PROJECT'; 10 | 11 | // 新建笔记项目 12 | export function createProject(param) { 13 | return { 14 | type: CREATE_PROJECT, 15 | param, 16 | }; 17 | } 18 | 19 | export const CREATE_FILE = 'CREATE_FILE'; 20 | 21 | // 新建笔记文档 22 | export function createFile(param) { 23 | return { 24 | type: CREATE_FILE, 25 | param, 26 | }; 27 | } 28 | 29 | export const DELETE_PROJECT = 'DELETE_PROJECT'; 30 | 31 | export function deleteProject(uuid, onlyDelete) { 32 | return { 33 | type: DELETE_PROJECT, 34 | uuid, 35 | onlyDelete, 36 | }; 37 | } 38 | 39 | 40 | export const RENAME_PROJECT = 'RENAME_PROJECT'; 41 | 42 | /** 43 | * @description 重命名项目 44 | * 45 | * @export 46 | * @param {String} uuid 项目uuid 47 | * @param {String} name 新名称 48 | */ 49 | export function renameProject(uuid, name) { 50 | return { 51 | type: RENAME_PROJECT, 52 | uuid, 53 | name, 54 | }; 55 | } 56 | 57 | export const RENAME_NOTE = 'RENAME_NOTE'; 58 | 59 | /** 60 | * @description 重命名笔记 61 | * 62 | * @export 63 | * @param {String} uuid 项目uuid 64 | * @param {String} name 新名称 65 | * @param {String} parentsId 项目uuid 66 | */ 67 | export function renameNote(uuid, name, parentsId) { 68 | return { 69 | type: RENAME_NOTE, 70 | uuid, 71 | name, 72 | parentsId, 73 | }; 74 | } 75 | 76 | export const DELETE_NOTE = 'DELETE_NOTE'; 77 | 78 | export function deletNote(uuid, parentsId, name, projectName, onlyDelete = false) { 79 | return { 80 | type: DELETE_NOTE, 81 | uuid, 82 | parentsId, 83 | noteName: name, 84 | projectName, 85 | onlyDelete, 86 | }; 87 | } 88 | 89 | export const UPDATE_NOTE_DESCRIPTION = 'UPDATE_NODE_DESCRIPTION'; 90 | 91 | export function updateNoteDesc(uuid, desc, parentsId) { 92 | return { 93 | type: UPDATE_NOTE_DESCRIPTION, 94 | uuid, 95 | desc, 96 | parentsId, 97 | }; 98 | } 99 | 100 | export const SEARCH_NOTES = 'SEARCH_NOTES'; 101 | 102 | export function searchNotes(keyword) { 103 | return { 104 | type: SEARCH_NOTES, 105 | keyword, 106 | }; 107 | } 108 | 109 | export const CLEAR_SEARCH_NOTES = 'CLEAR_SEARCH_NOTES'; 110 | 111 | export function clearSearchNotes() { 112 | return { 113 | type: CLEAR_SEARCH_NOTES, 114 | }; 115 | } 116 | 117 | export const REMOVE_NOTE_PERMANENTLY = 'REMOVE_NOTE_PERMANENTLY'; 118 | 119 | /** 120 | * @description 永久删除笔记 121 | * @param {String} parentsUuid 项目uuid 122 | * @param {String} uuid 笔记uuid 123 | */ 124 | export function permantRemoveNote(parentsUuid, uuid) { 125 | return { 126 | type: REMOVE_NOTE_PERMANENTLY, 127 | parentsUuid, 128 | uuid, 129 | }; 130 | } 131 | 132 | export const REMOVE_NOTEBOOK_PERMANENTLY = 'REMOVE_NOTEBOOK_PERMANENTLY'; 133 | 134 | /** 135 | * @description 永久删除笔记本 136 | * @param {String} uuid 项目uuid 137 | */ 138 | export function permantRemoveNotebook(uuid) { 139 | return { 140 | type: REMOVE_NOTEBOOK_PERMANENTLY, 141 | uuid, 142 | }; 143 | } 144 | 145 | export const RESTORE_NOTE = 'RESTORE_NOTE'; 146 | 147 | export function restoreNote(parentsId, uuid) { 148 | return { 149 | type: RESTORE_NOTE, 150 | parentsId, 151 | uuid, 152 | }; 153 | } 154 | 155 | export const RESTORE_NOTEBOOK = 'RESTORE_NOTEBOOK'; 156 | 157 | export function restoreNotebook(uuid) { 158 | return { 159 | type: RESTORE_NOTEBOOK, 160 | uuid, 161 | }; 162 | } 163 | 164 | export const TRASH_CHOOSE_PROJECT = 'TRASH_CHOOSE_PROJECT'; 165 | 166 | export function chooseTrashProject(uuid, name) { 167 | return { 168 | type: TRASH_CHOOSE_PROJECT, 169 | uuid, 170 | name, 171 | }; 172 | } 173 | 174 | export const TRASH_BACK_ROOT = 'TRASH_BACK_ROOT'; 175 | 176 | export function trashBack() { 177 | return { 178 | type: TRASH_BACK_ROOT, 179 | }; 180 | } 181 | 182 | export const SAVE_NOTE_ON_KEYDOWN = 'SAVE_NOTE_ON_KEYDOWN'; 183 | 184 | export function saveNote(parentsId, uuid) { 185 | return { 186 | type: SAVE_NOTE_ON_KEYDOWN, 187 | parentsId, 188 | uuid, 189 | }; 190 | } 191 | 192 | export const UPLOAD_NOTE_ONEDRIVE = 'UPLOAD_NOTE_ONEDRIVE'; 193 | export const UPLOAD_NOTE_ONEDRIVE_SUCCESS = 'UPLOAD_NOTE_ONEDRIVE_SUCCESS'; 194 | export const UPLOAD_NOTE_ONEDRIVE_FAILED = 'UPLOAD_NOTE_ONEDRIVE_FAILED'; 195 | 196 | export const UPDATE_NOTE_UPLOAD_STATUS = 'UPDATE_NOTE_UPLOAD_STATUS'; 197 | export function updateNoteUploadStatus(parentsId, uuid, status) { 198 | return { 199 | type: UPDATE_NOTE_UPLOAD_STATUS, 200 | parentsId, 201 | uuid, 202 | status, 203 | }; 204 | } 205 | 206 | export const SAVE_NOTE_FROM_DRIVE = 'SAVE_NOTE_FROM_DRIVE'; 207 | -------------------------------------------------------------------------------- /app/views/services/OneDrive.js: -------------------------------------------------------------------------------- 1 | export default class OneDrive { 2 | constructor() { 3 | const root = 'https://graph.microsoft.com'; 4 | this.V1 = `${root}/v1.0`; 5 | this.BETA = `${root}/beta`; 6 | } 7 | 8 | xhr = (url, method, token, param, responseType = 'json', isBeta = false) => { 9 | let rootPath = this.V1; 10 | if (isBeta) { 11 | rootPath = this.BETA; 12 | } 13 | return new Promise((resolve, reject) => { 14 | let targetUrl = `${rootPath}${url}`; 15 | let body = null; 16 | let headers = { 17 | Authorization: `bearer ${token}`, 18 | }; 19 | if (/get/ig.test(method) && param) { 20 | let queryString = ''; 21 | for (const key in param) { 22 | queryString += `&${key}=${param[key]}`; 23 | } 24 | queryString = queryString.replace(/^&/ig, ''); 25 | queryString = encodeURIComponent(queryString); 26 | targetUrl += queryString; 27 | } else if (/post/ig.test(method)) { 28 | body = JSON.stringify(param); 29 | } else if (/put/ig.test(method)) { 30 | headers = { 31 | Authorization: `bearer ${token}`, 32 | 'Content-Type': 'text/plain; charset=UTF-8', 33 | }; 34 | body = param; 35 | } 36 | fetch(targetUrl, { 37 | headers, 38 | method, 39 | body, 40 | }) 41 | .then((response) => { 42 | const { status } = response; 43 | if (status === 200 || status === 201) { 44 | if (responseType === 'json') { 45 | return response.json(); 46 | } 47 | if (responseType === 'text') { 48 | return response.text(); 49 | } 50 | if (responseType === 'image') { 51 | return response.blob(); 52 | } 53 | return response; 54 | } else if (status === 204) { 55 | return { 56 | success: true, 57 | }; 58 | } 59 | throw new Error('Fetching Failed.'); 60 | }) 61 | .then(json => resolve(json)) 62 | .catch(ex => reject(ex)); 63 | }); 64 | } 65 | 66 | getTokenByCode = (code) => { 67 | const encodeSecret = encodeURIComponent('bcdfnjKNMK0$-!wHQO6656]'); 68 | const body = `client_id=35730bb9-a23a-46f9-aebf-5c2b9d6fc06c&redirect_uri=http://localhost&client_secret=${encodeSecret} 69 | &code=${encodeURIComponent(code)}&grant_type=authorization_code`; 70 | return new Promise((resolve, reject) => 71 | fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { 72 | headers: { 73 | 'Content-Type': 'application/x-www-form-urlencoded', 74 | }, 75 | method: 'POST', 76 | body, 77 | }) 78 | .then((response) => { 79 | if (response.status === 200) { 80 | return response.json(); 81 | } 82 | throw new Error('Auth Failed.'); 83 | }) 84 | .then(json => resolve(json)) 85 | .catch(ex => reject(ex)) 86 | ); 87 | } 88 | 89 | refreshToken = (refreshToken) => { 90 | const encodeSecret = encodeURIComponent('bcdfnjKNMK0$-!wHQO6656]'); 91 | const body = `client_id=35730bb9-a23a-46f9-aebf-5c2b9d6fc06c&redirect_uri=http://localhost&client_secret=${encodeSecret} 92 | &refresh_token=${encodeURIComponent(refreshToken)}&grant_type=refresh_token`; 93 | return new Promise((resolve, reject) => 94 | fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', { 95 | headers: { 96 | 'Content-Type': 'application/x-www-form-urlencoded', 97 | }, 98 | method: 'POST', 99 | body, 100 | }) 101 | .then((response) => { 102 | if (response.status === 200) { 103 | return response.json(); 104 | } 105 | throw new Error('Auth Failed.'); 106 | }) 107 | .then(json => resolve(json)) 108 | .catch(ex => reject(ex)) 109 | ); 110 | } 111 | 112 | getAppRoot = token => this.xhr('/drive/special/approot', 'GET', token); 113 | 114 | // 列出应用文件夹子项 115 | getAppRootChildren = token => this.xhr('/drive/special/approot/children', 'GET', token); 116 | 117 | // 列出应用文件夹子项 118 | getProjects = token => this.xhr('/drive/special/approot/children', 'GET', token); 119 | 120 | // 列出文件夹下所有笔记 121 | getNotes = (token, folder) => this.xhr(`/drive/special/approot:/${folder}:/children`, 'GET', token); 122 | 123 | getNoteContent = (token, folder, name) => this.xhr(`/drive/special/approot:/${folder}/${name}:/content`, 'GET', token, null, 'text'); 124 | 125 | uploadSingleFile = (token, url, filePath) => this.xhr(url, 'PUT', token, filePath); 126 | 127 | deleteItem = (token, url) => this.xhr(url, 'DELETE', token); 128 | 129 | // 获取用户头像 130 | getUserAvatar = token => this.xhr('/me/photo/$value', 'GET', token, null, 'image', true); 131 | } 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "yosoro", 3 | "productName": "Yosoro", 4 | "version": "1.0.7", 5 | "description": "Beautiful Cloud Drive Markdown NoteBook Desktop App.", 6 | "main": "./app/main/index.js", 7 | "scripts": { 8 | "dev:main": "cross-env NODE_ENV='development' BABEL_ENV=main electron -r babel-register ./app/main/", 9 | "dev:renderer": "cross-env NODE_ENV='development' BABEL_ENV=renderer webpack-dev-server --config config/webpack.config.renderer.dev.babel.js --colors --progress", 10 | "build:renderer": "cross-env NODE_ENV='production' BABEL_ENV=renderer gulp --gulpfile config/gulp.babel.js --cwd . --mode renderer", 11 | "build:main": "cross-env NODE_ENV='production' BABEL_ENV=main gulp --gulpfile config/gulp.babel.js --cwd . --mode main", 12 | "build:all": "concurrently \"npm run build:renderer\" \"npm run build:main\"", 13 | "packager:mac:app": "electron-packager ./lib Yosoro --overwrite --platform=darwin --arch=x64 --out=out --icon=assets/icons/osx/app.icns", 14 | "packager:mac:dmg": "babel-node ./scripts/packageDMG.js", 15 | "packager:mac": "npm run packager:mac:app && npm run packager:mac:dmg", 16 | "packager:win:32": "electron-packager ./lib Yosoro --overwrite --platform=win32 --arch=ia32 --out=out --icon=assets/icons/win/app.ico", 17 | "packager:win:64": "electron-packager ./lib Yosoro --overwrite --platform=win32 --arch=x64 --out=out --icon=assets/icons/win/app.ico", 18 | "packager:win": "concurrently \"npm run packager:win:32\" \"npm run packager:win:64\"", 19 | "packager:win:setup": "babel-node ./scripts/packageSetup.js", 20 | "packager:linux": "electron-packager ./lib Yosoro --overwrite --platform=linux --arch=x64 --out=out", 21 | "packager:linux:deb": "babel-node ./scripts/packageDeb.js", 22 | "lint": "concurrently \"eslint app config scripts --ext .js,.jsx\" \"sass-lint -v -q\"", 23 | "test": "npm run lint" 24 | }, 25 | "author": { 26 | "name": "Alchemy", 27 | "email": "min@coolecho.net", 28 | "url": "https://github.com/IceEnd" 29 | }, 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/IceEnd/Yosoro.git" 33 | }, 34 | "browserslist": "electron 2.0", 35 | "mac": { 36 | "icon": "./asstes/icons/osx/app.icns" 37 | }, 38 | "win": { 39 | "icon": "./asstes/icons/win/app.ico" 40 | }, 41 | "dependencies": { 42 | "antd": "^3.6.3", 43 | "autobind-decorator": "^2.1.0", 44 | "classnames": "^2.2.5", 45 | "codemirror": "^5.36.0", 46 | "deep-eql": "^3.0.1", 47 | "fs-extra": "^5.0.0", 48 | "history": "^4.7.2", 49 | "marked": "^0.3.19", 50 | "node-fetch": "^1.3.3", 51 | "node-schedule": "^1.3.0", 52 | "prop-types": "^15.6.1", 53 | "react": "^16.4.0", 54 | "react-dom": "^16.4.0", 55 | "react-redux": "^5.0.6", 56 | "react-resize-detector": "^2.3.0", 57 | "react-router-dom": "^4.3.1", 58 | "react-router-redux": "^4.0.8", 59 | "react-transition-group": "^2.4.0", 60 | "redux": "^3.7.2", 61 | "redux-logger": "^3.0.6", 62 | "redux-saga": "^0.16.0", 63 | "uuid": "^3.1.0", 64 | "whatwg-fetch": "^2.0.3" 65 | }, 66 | "devDependencies": { 67 | "babel-cli": "^6.26.0", 68 | "babel-core": "^6.26.3", 69 | "babel-eslint": "^8.2.3", 70 | "babel-loader": "^7.1.4", 71 | "babel-plugin-import": "^1.8.0", 72 | "babel-plugin-transform-async-to-generator": "^6.24.1", 73 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 74 | "babel-preset-env": "^1.7.0", 75 | "babel-preset-react": "^6.24.1", 76 | "babel-preset-react-hmre": "^1.1.1", 77 | "babel-preset-react-optimize": "^1.0.1", 78 | "babel-preset-stage-0": "^6.24.1", 79 | "babel-register": "^6.26.0", 80 | "babel-runtime": "^6.26.0", 81 | "concurrently": "^3.5.1", 82 | "cross-env": "^5.1.1", 83 | "css-loader": "^0.28.7", 84 | "del": "^3.0.0", 85 | "devtron": "^1.4.0", 86 | "electron": "^2.0.1", 87 | "electron-installer-debian": "^0.8.1", 88 | "electron-installer-dmg": "^1.0.0", 89 | "electron-packager": "^9.1.0", 90 | "electron-watch": "^1.0.9", 91 | "electron-winstaller": "^2.6.4", 92 | "eslint": "^4.10.0", 93 | "eslint-config-alchemy": "^0.1.13", 94 | "eslint-import-resolver-webpack": "^0.10.0", 95 | "eslint-plugin-import": "2.7.0", 96 | "eslint-plugin-jsx-a11y": "6.0.2", 97 | "eslint-plugin-react": "7.1.0", 98 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 99 | "file-loader": "^1.1.5", 100 | "gulp": "3.9.1", 101 | "gulp-util": "^3.0.8", 102 | "html-webpack-plugin": "^3.2.0", 103 | "less": "^3.0.4", 104 | "less-loader": "^4.1.0", 105 | "node-sass": "^4.7.2", 106 | "pre-commit": "1.2.2", 107 | "react-hot-loader": "^3.1.2", 108 | "sass-lint": "^1.12.1", 109 | "sass-loader": "^6.0.6", 110 | "style-loader": "^0.19.0", 111 | "url-loader": "^0.6.2", 112 | "webpack": "^4.8.3", 113 | "webpack-bundle-analyzer": "^2.9.2", 114 | "webpack-cli": "^2.1.3", 115 | "webpack-dev-server": "^3.1.4", 116 | "webpack-merge": "^4.1.2" 117 | }, 118 | "license": "GPL-3.0", 119 | "pre-commit": [ 120 | "lint" 121 | ] 122 | } 123 | -------------------------------------------------------------------------------- /app/views/component/trash/HOCList.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Modal, message } from 'antd'; 4 | import { ipcRenderer } from 'electron'; 5 | import autobind from 'autobind-decorator'; 6 | import { permantRemoveNote, restoreNote, permantRemoveNotebook, chooseTrashProject, restoreNotebook } from '../../actions/projects'; 7 | 8 | const confirm = Modal.confirm; 9 | 10 | function getDisplayName(WrappedComponent) { 11 | return WrappedComponent.displayName || 12 | WrappedComponent.name || 13 | 'Component'; 14 | } 15 | 16 | export default function HOCListFactory(listType) { 17 | return function HOCList(WrappedComponent) { 18 | return class HOC extends Component { 19 | static displayName = `HOC${getDisplayName(WrappedComponent)}`; 20 | static propTypes = { 21 | dispatch: PropTypes.func.isRequired, 22 | trash: PropTypes.shape({ 23 | projectName: PropTypes.string.isRequired, 24 | projectUuid: PropTypes.string.isRequired, 25 | }).isRequired, 26 | }; 27 | 28 | constructor() { 29 | super(); 30 | this.state = { 31 | name: '', 32 | uuid: '-1', 33 | }; 34 | } 35 | 36 | setTarget(name, uuid) { 37 | this.setState({ 38 | name, 39 | uuid, 40 | }); 41 | } 42 | 43 | @autobind 44 | handleGoIn(uuid, name) { 45 | this.props.dispatch(chooseTrashProject(uuid, name)); 46 | } 47 | 48 | // 还原项目 49 | restoreProject(uuid, name) { 50 | const data = ipcRenderer.sendSync('restore-notebook', { 51 | name, 52 | }); 53 | if (!data.success) { 54 | message.error(`Restore "${name}" failed`); 55 | return false; 56 | } 57 | this.props.dispatch(restoreNotebook(uuid)); 58 | } 59 | 60 | // 还原笔记 61 | restoreNote(uuid, name) { 62 | const { trash: { projectUuid, projectName }, dispatch } = this.props; 63 | const data = ipcRenderer.sendSync('restore-note', { 64 | projectName, 65 | name, 66 | }); 67 | if (!data.success) { 68 | message.error(`Restore "${name}" failed`); 69 | return false; 70 | } 71 | dispatch(restoreNote(projectUuid, uuid)); 72 | } 73 | 74 | // 打开还原项目弹框 75 | @autobind 76 | openRestore(e, uuid, name) { 77 | e.stopPropagation(); 78 | this.setTarget(name, uuid); 79 | confirm({ 80 | title: `Do you want to restore "${name}"?`, 81 | content: 'This operation will cover the existing files.', 82 | onCancel: () => { 83 | this.setTarget('', '-1'); 84 | }, 85 | onOk: () => { 86 | if (listType === 'projects') { 87 | this.restoreProject(uuid, name); 88 | } else { 89 | this.restoreNote(uuid, name); 90 | } 91 | }, 92 | }); 93 | } 94 | 95 | // 永久删除笔记本 96 | removeNotebook(uuid, name) { 97 | const data = ipcRenderer.sendSync('permanent-remove-notebook', { 98 | name, 99 | }); 100 | if (!data.success) { 101 | message.error('Deleting notebook failed'); 102 | return false; 103 | } 104 | if (data.code === 1) { // 笔记本不存在 105 | message.error('Notebook done note exist'); 106 | } 107 | this.props.dispatch(permantRemoveNotebook(uuid)); 108 | } 109 | 110 | // 永久删除笔记 111 | removeNote(uuid, name) { 112 | const { trash: { projectUuid, projectName }, dispatch } = this.props; 113 | const data = ipcRenderer.sendSync('permanent-remove-note', { 114 | projectName, 115 | name, 116 | }); 117 | if (!data.success) { 118 | message.error(`Deleting "${name}" failed`); 119 | return false; 120 | } 121 | if (data.code === 1) { 122 | message.error('Note does not exist'); 123 | } 124 | dispatch(permantRemoveNote(projectUuid, uuid)); 125 | } 126 | 127 | // 完全删除项目确认框 128 | @autobind 129 | openRemove(e, uuid, name) { 130 | e.stopPropagation(); 131 | this.setTarget(name, uuid, 'delete'); 132 | confirm({ 133 | title: `Do you want to permanently remove "${name}"?`, 134 | content: 'Unrestoreable after deleting.', 135 | onCancel: () => { 136 | this.setTarget('', '-1'); 137 | }, 138 | onOk: () => { 139 | if (listType === 'projects') { 140 | this.removeNotebook(uuid, name); 141 | } else { 142 | this.removeNote(uuid, name); 143 | } 144 | }, 145 | }); 146 | } 147 | 148 | render() { 149 | return ( 150 | 156 | ); 157 | } 158 | }; 159 | }; 160 | } 161 | -------------------------------------------------------------------------------- /app/views/component/editor/Markdown.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import autobind from 'autobind-decorator'; 4 | import Editor from './Editor'; 5 | import Preview from './Preview'; 6 | import { pushStateToStorage, mergeStateFromStorage, throttle } from '../../utils/utils'; 7 | // import { appMarkdownAdjust } from '../../actions/app'; 8 | 9 | let appToolWidth = null; 10 | 11 | export default class Markdown extends Component { 12 | static displayName = 'Markdown'; 13 | static propTypes = { 14 | dispatch: PropTypes.func.isRequired, 15 | markdown: PropTypes.shape({ 16 | parentsId: PropTypes.string.isRequired, 17 | uuid: PropTypes.string.isRequired, 18 | // file: PropTypes.string.isRequired, 19 | createDate: PropTypes.string.isRequired, 20 | latestDate: PropTypes.string.isRequired, 21 | name: PropTypes.string.isRequired, 22 | content: PropTypes.string.isRequired, 23 | html: PropTypes.string.isRequired, 24 | status: PropTypes.number.isRequired, 25 | start: PropTypes.number.isRequired, 26 | }).isRequired, 27 | markdownSettings: PropTypes.shape({ 28 | editorWidth: PropTypes.number.isRequired, 29 | }).isRequired, 30 | note: PropTypes.shape({ 31 | projectUuid: PropTypes.string.isRequired, 32 | projectName: PropTypes.string.isRequired, 33 | fileUuid: PropTypes.string.isRequired, 34 | fileName: PropTypes.string.isRequired, 35 | }).isRequired, 36 | editorMode: PropTypes.string.isRequired, 37 | }; 38 | 39 | constructor(props) { 40 | super(props); 41 | const { markdownSettings } = props; 42 | this.setDragWidth = throttle((e) => { 43 | if (!appToolWidth) { 44 | appToolWidth = document.getElementById('app_tool_bar').offsetWidth; 45 | } 46 | const width = this.root.offsetWidth; 47 | const rootLeft = this.root.offsetLeft + appToolWidth; 48 | const x = e.clientX; 49 | const editorWidthValue = (x - rootLeft) / width; 50 | if (editorWidthValue <= 0.2 || editorWidthValue >= 0.8) { 51 | return false; 52 | } 53 | const editorWidth = `${editorWidthValue * 100}%`; 54 | this.setState({ 55 | editorWidth, 56 | editorWidthValue, 57 | }); 58 | }, 60); 59 | this.state = mergeStateFromStorage('markdownState', { 60 | drag: false, 61 | editorWidth: `${markdownSettings.editorWidth * 100}%`, 62 | editorWidthValue: markdownSettings.editorWidth, 63 | }); 64 | } 65 | 66 | componentWillReceiveProps(nextProps) { 67 | if (this.props.markdownSettings.editorWidth !== nextProps.markdownSettings.editorWidth) { 68 | this.setState({ 69 | editorWidth: `${nextProps.markdownSettings.editorWidth * 100}%`, 70 | editorWidthValue: nextProps.markdownSettings.editorWidth, 71 | }); 72 | } 73 | } 74 | 75 | componentWillUnmount() { 76 | pushStateToStorage('markdownState', this.state); 77 | } 78 | 79 | // setWidth(markdownWidth) { 80 | // this.setState({ 81 | // markdownWidth, 82 | // }); 83 | // } 84 | 85 | @autobind 86 | setDrag(drag) { 87 | this.setState({ 88 | drag, 89 | }); 90 | } 91 | 92 | setPreiewScrollRatio = (ratio) => { 93 | this.preview.setScrollRatio(ratio); 94 | } 95 | 96 | @autobind 97 | handleMouseMove(e) { 98 | e.stopPropagation(); 99 | e.persist(); 100 | if (!this.state.drag) { 101 | return false; 102 | } 103 | e.preventDefault(); 104 | this.setDragWidth(e); 105 | } 106 | 107 | @autobind 108 | handleMouseUp(e) { 109 | // e.preventDefault(); 110 | e.stopPropagation(); 111 | if (this.state.drag) { 112 | this.setState({ 113 | drag: false, 114 | }); 115 | } 116 | } 117 | 118 | @autobind 119 | handMouseLeave(e) { 120 | e.preventDefault(); 121 | e.stopPropagation(); 122 | if (this.state.drag) { 123 | this.setState({ 124 | drag: false, 125 | }); 126 | } 127 | } 128 | 129 | render() { 130 | const { markdown: { content, status, html, start, uuid }, dispatch, editorMode, note } = this.props; 131 | const { editorWidth, drag, editorWidthValue } = this.state; 132 | if (status === 0) { 133 | return null; 134 | } 135 | return ( 136 |
    137 |
    (this.root = node)} 144 | > 145 | 158 | (this.preview = node)} 165 | /> 166 |
    167 |
    168 | ); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /app/views/utils/utils.js: -------------------------------------------------------------------------------- 1 | import marked from 'marked'; 2 | 3 | const renderer = new marked.Renderer(); 4 | 5 | renderer.listitem = function (text) { 6 | let res = text; 7 | if (/^\s*\[[x ]\]\s*/.test(text)) { 8 | res = text.replace(/^\s*\[ \]\s*/, ' ').replace(/^\s*\[x\]\s*/, ' '); 9 | return `
  • ${res}
  • `; 10 | } 11 | return `
  • ${text}
  • `; 12 | }; 13 | 14 | marked.setOptions({ 15 | renderer, 16 | gfm: true, 17 | tables: true, 18 | breaks: false, 19 | pedantic: false, 20 | sanitize: false, 21 | smartLists: true, 22 | smartypants: false, 23 | highlight: (code) => { 24 | const value = require('./highlight.min.js').highlightAuto(code).value; 25 | return value; 26 | }, 27 | }); 28 | 29 | function formatNumber(number) { 30 | if (number < 10) { 31 | return `0${number}`; 32 | } 33 | return number; 34 | } 35 | 36 | 37 | export function formatDate(date) { 38 | const newDate = new Date(date); 39 | const year = newDate.getFullYear(); 40 | const month = formatNumber(newDate.getMonth() + 1); 41 | const day = formatNumber(newDate.getDate()); 42 | const hour = formatNumber(newDate.getHours()); 43 | const minutes = formatNumber(newDate.getMinutes()); 44 | const seconds = formatNumber(newDate.getSeconds()); 45 | return `${year}-${month}-${day} ${hour}:${minutes}:${seconds}`; 46 | } 47 | 48 | /** 49 | * @param 将组件state存放至localStroge中 50 | * @param {String} componentName - 组件displayName 51 | * @param {Object} state - 组件state 52 | */ 53 | export function pushStateToStorage(componentName, state) { 54 | // const data = JSON.stringify(state); 55 | window.localStorage.setItem(componentName, JSON.stringify(state)); 56 | } 57 | 58 | /** 59 | * @description 从localStorage中读取组件state与初始state合并 60 | * @param {String} componentName - 组件displayName 61 | * @param {Object} initState - 组件初始state 62 | */ 63 | export function mergeStateFromStorage(componentName, initState) { 64 | const str = window.localStorage.getItem(componentName); 65 | let obj; 66 | if (str) { 67 | obj = JSON.parse(str); 68 | } else { 69 | obj = {}; 70 | } 71 | return Object.assign({}, initState, obj); 72 | } 73 | 74 | function formatVersion(string) { 75 | const version = string.replace(/\.|v|-beta/ig, ''); 76 | return parseInt(version, 10); 77 | } 78 | 79 | /** 80 | * @desc 比较本地版本号与线上最新版本号 81 | * @param {String} localVersion 本地版本 82 | * @param {String} latestVersion 线上版本 83 | * 84 | * @return {Boolean} flag 是否需要更新 85 | */ 86 | export function compareVersion(localVersion, latestVersion) { 87 | if (localVersion === latestVersion) { // 不需要更新 88 | return false; 89 | } 90 | let localBeta = false; 91 | let latestBeta = false; 92 | if (/-beta/ig.test(localVersion)) { 93 | localBeta = true; 94 | } 95 | if (/-beta/ig.test(latestVersion)) { 96 | latestBeta = true; 97 | } 98 | const localFormatVersion = formatVersion(localVersion); 99 | const latestFormatVersion = formatVersion(latestVersion); 100 | if (localFormatVersion === latestFormatVersion && localBeta && !latestBeta) { // 本地是测试版本 101 | return true; 102 | } 103 | if (localFormatVersion === latestFormatVersion && !localBeta && latestBeta) { // 线上是测试版本 104 | return false; 105 | } 106 | if (localFormatVersion >= latestFormatVersion) { 107 | return false; 108 | } 109 | if (localFormatVersion < latestFormatVersion) { 110 | return true; 111 | } 112 | return false; 113 | } 114 | 115 | /** 116 | * @desc markdown to html 117 | * 118 | * @export 119 | * @param {any} string markdown内容 120 | * @param {boolean} [xssWhite=false] 是否阻止xss 121 | * @returns html 122 | */ 123 | export function markedToHtml(string) { 124 | return marked(string); 125 | } 126 | 127 | 128 | /** 129 | * @desc 函数节流 返回函数连续调用时,fun 执行频率限定为 次/wait 130 | * 131 | * @param {Function} func 需要执行的函数 132 | * @param {Number} wait 执行间隔,单位是毫秒(ms),默认100 133 | * 134 | * @return {Function} 返回一个“节流”函数 135 | */ 136 | export function throttle(func, wait = 100) { 137 | let timer = null; 138 | let previous; // 上次执行时间 139 | return function (...args) { // 闭包 140 | const context = this; 141 | const currentArgs = args; 142 | const now = +new Date(); 143 | 144 | if (previous && now < previous + wait) { 145 | clearTimeout(timer); 146 | timer = setTimeout(() => { 147 | previous = now; 148 | func.apply(context, currentArgs); 149 | }, wait); 150 | } else { 151 | previous = now; 152 | func.apply(context, currentArgs); 153 | } 154 | }; 155 | } 156 | 157 | /** 158 | * @desc 函数防抖 159 | * @param {Function} fn 需要执行的函数 160 | * @param {Number} delay 执行间隔,单位是毫秒(ms),默认100 161 | * 162 | * @return {Function} 163 | */ 164 | export function debounce(fn, delay = 100) { 165 | let timer; 166 | return function (...args) { 167 | const context = this; 168 | const currentArgs = args; 169 | clearTimeout(timer); 170 | timer = setTimeout(() => { 171 | fn.apply(context, currentArgs); 172 | }, delay); 173 | }; 174 | } 175 | 176 | /** 177 | * @desc 获取组件displayName 178 | * @param {React.Component} WrappedComponent React组件 179 | */ 180 | export function getDisplayName(WrappedComponent) { 181 | return WrappedComponent.displayName || 182 | WrappedComponent.name || 183 | 'Component'; 184 | } 185 | 186 | /** 187 | * @description 获取预览插入js路径 188 | */ 189 | export function getWebviewPreJSPath() { 190 | if (process.env.NODE_ENV === 'development') { 191 | return '../webview/webview-pre.js'; 192 | } 193 | return './webview/webview-pre.js'; 194 | } 195 | 196 | /** 197 | * @description blob to base6 198 | */ 199 | export function blobToBase64(blob) { 200 | const reader = new FileReader(); 201 | return new Promise((resolve, reject) => { 202 | reader.onload = () => { 203 | const dataUrl = reader.result; 204 | resolve(dataUrl); 205 | }; 206 | reader.onerror = (error) => { 207 | reject(error); 208 | }; 209 | reader.readAsDataURL(blob); 210 | }); 211 | } 212 | -------------------------------------------------------------------------------- /app/main/webview/webview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Virtual Document 6 | 7 | 212 | 213 | 214 |
    215 |
    216 | 217 | 218 | -------------------------------------------------------------------------------- /app/views/component/editor/Preview.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import autobind from 'autobind-decorator'; 4 | import { ipcRenderer, remote } from 'electron'; 5 | import classNames from 'classnames'; 6 | import { getWebviewPreJSPath } from '../../utils/utils'; 7 | import LoadingImg from '../../assets/images/loading.svg'; 8 | 9 | import '../../assets/scss/preview.scss'; 10 | 11 | const isDEV = process.env.NODE_ENV === 'development'; 12 | 13 | const preJSPath = getWebviewPreJSPath(); 14 | 15 | const webviewPath = ipcRenderer.sendSync('get-webview-path'); 16 | 17 | export default class Preview extends PureComponent { 18 | static displayName = 'MarkdownPreview'; 19 | static propTypes = { 20 | html: PropTypes.string.isRequired, 21 | editorMode: PropTypes.string.isRequired, 22 | // editorWidth: PropTypes.string.isRequired, 23 | drag: PropTypes.bool.isRequired, 24 | editorWidthValue: PropTypes.number.isRequired, 25 | }; 26 | 27 | constructor(props) { 28 | super(props); 29 | const bodyWidth = this.getBodyWidth(props); 30 | this.state = { 31 | bodyWidth, 32 | loading: true, 33 | }; 34 | } 35 | 36 | componentDidMount() { 37 | this.noteRoot = document.getElementById('note_root_cont'); 38 | window.addEventListener('resize', this.onWindowResize); 39 | this.webview.addEventListener('ipc-message', this.onWVMessage); 40 | } 41 | 42 | componentWillReceiveProps(nextProps) { 43 | if (this.props.editorMode !== nextProps.editorMode || this.props.editorWidthValue !== nextProps.editorWidthValue) { 44 | const bodyWidth = this.getBodyWidth(nextProps); 45 | this.setState({ 46 | bodyWidth, 47 | }); 48 | } 49 | } 50 | 51 | componentDidUpdate() { 52 | const { html, editorMode } = this.props; 53 | this.webview.send('wv-render-html', { 54 | html, 55 | editorMode, 56 | }); 57 | } 58 | 59 | componentWillUnmount() { 60 | window.removeEventListener('resize', this.onWindowResize); 61 | this.webview.removeEventListener('ipc-message', this.onWVMessage); 62 | } 63 | 64 | onWindowResize = () => { 65 | const { editorMode } = this.props; 66 | if (editorMode === 'edit') { 67 | const bodyWidth = this.getBodyWidth(); 68 | this.setState({ 69 | bodyWidth, 70 | }); 71 | } 72 | } 73 | 74 | @autobind 75 | onWVMessage(event) { 76 | const channel = event.channel; 77 | const { html, editorMode } = this.props; 78 | switch (channel) { 79 | case 'wv-first-loaded': { 80 | this.webview.send('wv-render-html', { 81 | html, 82 | editorMode, 83 | }); 84 | this.setState({ 85 | loading: false, 86 | }); 87 | break; 88 | } 89 | case 'did-click-link': { 90 | let href = ''; 91 | if (event.args && event.args.length) { 92 | href = event.args[0]; 93 | } 94 | if (/^https?:\/\//i.test(href)) { 95 | remote.shell.openExternal(href); 96 | } 97 | break; 98 | } 99 | default: 100 | break; 101 | } 102 | } 103 | 104 | getBodyWidth(props) { 105 | let editorMode; 106 | let editorWidthValue; 107 | if (props) { 108 | editorMode = props.editorMode; 109 | editorWidthValue = props.editorWidthValue; 110 | } else { 111 | editorMode = this.props.editorMode; 112 | editorWidthValue = this.props.editorWidthValue; 113 | } 114 | if (editorMode === 'normal') { 115 | return '100%'; 116 | } 117 | if (editorMode === 'preview') { 118 | return '100%'; 119 | } 120 | if (!this.preview || !this.noteRoot) { 121 | return '100%'; 122 | } 123 | if (this.preview) { 124 | let parentWidth; 125 | if (editorMode === 'edit') { 126 | parentWidth = this.noteRoot.offsetWidth; 127 | } else { 128 | parentWidth = this.preview.offsetParent.offsetWidth; 129 | } 130 | let res = '100%'; 131 | if (editorWidthValue && parentWidth) { 132 | res = `${parentWidth * (1 - editorWidthValue)}px`; 133 | } 134 | return res; 135 | } 136 | return '100%'; 137 | } 138 | 139 | setScrollRatio(radio) { 140 | // const height = this.previewBody.offsetHeight; 141 | // const scrollTop = height * radio; 142 | // this.preview.scrollTop = scrollTop; 143 | this.webview.send('wv-scroll', radio); 144 | } 145 | 146 | @autobind 147 | openWVDevTools() { 148 | if (this.webview) { 149 | this.webview.openDevTools(); 150 | } 151 | } 152 | 153 | renderLoading() { 154 | if (this.state.loading) { 155 | return ( 156 | 164 | ); 165 | } 166 | } 167 | 168 | render() { 169 | const { bodyWidth } = this.state; 170 | const { editorMode, drag } = this.props; 171 | const rootClass = classNames( 172 | 'preview-root', 173 | { 174 | hide: editorMode === 'immersion', 175 | }, 176 | { 177 | 'pre-mode': editorMode === 'preview', 178 | }, 179 | { 180 | drag, 181 | } 182 | ); 183 | return ( 184 |
    (this.preview = node)} 187 | > 188 | {isDEV ? ( 189 | 193 | devtools 194 | 195 | ) : null} 196 | {this.renderLoading()} 197 |
    (this.previewBody = node)} 201 | > 202 | (this.webview = node)} 211 | /> 212 |
    213 |
    214 | ); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /app/main/pdf.js: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, app } from 'electron'; 2 | import fs from 'fs'; 3 | import { markedToHtml } from '../views/utils/utils'; 4 | 5 | export function getContent(file) { 6 | let content = fs.readFileSync(file, { 7 | encoding: 'utf8', 8 | }); 9 | content = markedToHtml(content); 10 | return `
    ${content}
    `; 22 | } 23 | 24 | export default class PDF { 25 | /** 26 | * Creates an instance of PDF. 27 | * @param {Array} notes 笔记数组 28 | * @param {String} folderPath 所在目录 29 | * @param {String} exportPath 导出目录 30 | * @memberof PDF 31 | */ 32 | constructor(notes, folderPath, exportPath, isBook = true) { 33 | this.notes = notes; 34 | this.folderPath = folderPath; 35 | this.queue = []; 36 | this.tempPath = app.getPath('temp'); 37 | this.exportPath = exportPath; 38 | this.isBook = isBook; 39 | this.initPDFQueue(); 40 | } 41 | 42 | static printPDF(win, file, tempFile, resolve) { 43 | win.webContents.printToPDF({ 44 | pageSize: 'A4', 45 | printBackground: true, 46 | }, (err, pdfData) => { 47 | win.removeAllListeners('did-finish-load'); 48 | win.removeAllListeners('did-fail-load'); 49 | win.close(); // 销毁window 50 | if (err) { 51 | throw err; 52 | } 53 | fs.writeFileSync(file, pdfData); 54 | if (fs.existsSync(tempFile)) { 55 | fs.unlinkSync(tempFile); 56 | } 57 | resolve('done'); 58 | }); 59 | } 60 | 61 | static getHtml(filePath) { 62 | return getContent(filePath); 63 | } 64 | 65 | // 初始化PDF生成队列 66 | initPDFQueue() { 67 | this.seed = 0; 68 | this.queue = []; 69 | for (const note of this.notes) { 70 | const promise = this.setPromise(note, this.seed); 71 | this.queue.push(promise); 72 | this.seed++; 73 | } 74 | return this.queue; 75 | } 76 | 77 | setPromise(note, seed) { 78 | const { folderPath, tempPath, exportPath, isBook } = this; 79 | return new Promise((resolve) => { 80 | const name = note.replace(/.md$/ig, ''); 81 | let file = `${exportPath}/${name}.pdf`; 82 | if (!isBook) { 83 | file = exportPath; 84 | } 85 | const filePath = `${folderPath}/${note}`; 86 | const content = PDF.getHtml(filePath); 87 | const tempFile = `${tempPath}/yosoro_pdf_${seed}.html`; 88 | fs.writeFileSync(tempFile, content); // 写入临时html文件 89 | let windowToPDF = new BrowserWindow({ 90 | show: false, 91 | webPreferences: { 92 | nodeIntegration: false, 93 | }, 94 | }); 95 | windowToPDF.loadURL(`file://${tempFile}`); 96 | let timer = setTimeout(() => { 97 | console.warn('waiting time over'); 98 | PDF.printPDF(windowToPDF, file, tempFile, resolve); 99 | }, 5000); 100 | windowToPDF.webContents.once('did-finish-load', () => { 101 | clearTimeout(timer); 102 | timer = null; 103 | PDF.printPDF(windowToPDF, file, tempFile, resolve); 104 | }); 105 | windowToPDF.webContents.once('did-fail-load', () => { 106 | clearTimeout(timer); 107 | timer = null; 108 | windowToPDF.removeAllListeners('did-finish-load'); 109 | windowToPDF.removeAllListeners('did-fail-load'); 110 | windowToPDF.destroy(); 111 | windowToPDF = null; 112 | resolve('fail'); 113 | }); 114 | }); 115 | } 116 | 117 | start() { 118 | return Promise.all(this.queue); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /app/views/component/note/ToolBar.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { ipcRenderer } from 'electron'; 4 | import classnames from 'classnames'; 5 | import { Icon, Tooltip, Menu, Dropdown } from 'antd'; 6 | import Search from '../share/search/Search'; 7 | import SVGIcon from '../share/SVGIcon'; 8 | 9 | import { searchNotes, clearSearchNotes, UPLOAD_NOTE_ONEDRIVE } from '../../actions/projects'; 10 | import { pushStateToStorage, mergeStateFromStorage } from '../../utils/utils'; 11 | import { appSwitchEditMode } from '../../actions/app'; 12 | import { clearWorkspace } from '../../actions/note'; 13 | import { clearMarkdown, beforeSwitchSave, MARKDOWN_UPLOADING } from '../../actions/markdown'; 14 | 15 | const MenuItem = Menu.Item; 16 | 17 | export default class Tool extends PureComponent { 18 | static displayName = 'NoteToolBar'; 19 | static propTypes = { 20 | dispatch: PropTypes.func.isRequired, 21 | markdown: PropTypes.shape({ 22 | parentsId: PropTypes.string.isRequired, 23 | uuid: PropTypes.string.isRequired, 24 | createDate: PropTypes.string.isRequired, 25 | latestDate: PropTypes.string.isRequired, 26 | name: PropTypes.string.isRequired, 27 | content: PropTypes.string.isRequired, 28 | html: PropTypes.string.isRequired, 29 | status: PropTypes.number.isRequired, 30 | start: PropTypes.number.isRequired, 31 | uploadStatus: PropTypes.number.isRequired, 32 | }).isRequired, 33 | note: PropTypes.shape({ 34 | projectUuid: PropTypes.string.isRequired, 35 | projectName: PropTypes.string.isRequired, 36 | fileUuid: PropTypes.string.isRequired, 37 | }).isRequired, 38 | editorMode: PropTypes.string.isRequired, 39 | searchStatus: PropTypes.number.isRequired, 40 | blur: PropTypes.bool.isRequired, 41 | }; 42 | 43 | static defaultProps = { 44 | blur: false, 45 | }; 46 | 47 | constructor() { 48 | super(); 49 | this.state = mergeStateFromStorage('noteExplorerState', { 50 | searchStatus: 0, // 0: 未搜索 1: 搜索中 2: 搜索完成 51 | }); 52 | } 53 | 54 | componentWillReceiveProps(nextProps) { 55 | if (nextProps.searchStatus === 1) { 56 | this.setState({ 57 | searchStatus: 2, 58 | }); 59 | } 60 | } 61 | 62 | componentWillUnmount() { 63 | pushStateToStorage('noteExplorerState', this.state); 64 | } 65 | 66 | onSearch = (value) => { 67 | if (value) { 68 | const { dispatch, note, markdown } = this.props; 69 | if (markdown.uuid && note.projectName) { 70 | dispatch(beforeSwitchSave(note.projectName)); 71 | } 72 | dispatch(clearMarkdown()); 73 | dispatch(clearWorkspace()); 74 | dispatch(searchNotes(value)); 75 | dispatch(appSwitchEditMode('')); 76 | this.setState({ 77 | searchStatus: 1, 78 | }); 79 | } 80 | } 81 | 82 | onClose = () => { 83 | this.setState({ 84 | searchStatus: 0, 85 | }); 86 | const { dispatch, markdown, note } = this.props; 87 | if (markdown.uuid && note.projectName) { 88 | dispatch(beforeSwitchSave(note.projectName)); 89 | } 90 | dispatch(clearMarkdown()); 91 | dispatch(clearWorkspace()); 92 | dispatch(clearSearchNotes()); 93 | } 94 | 95 | handleSwitchMode = () => { 96 | const { dispatch, editorMode } = this.props; 97 | dispatch(appSwitchEditMode(editorMode)); 98 | } 99 | 100 | handleClick = (type) => { 101 | if (type === 'cloud-upload-o') { 102 | this.handleUpload(); 103 | } 104 | } 105 | 106 | handleUpload = () => { 107 | const { markdown: { name, uuid, content, uploadStatus }, note: { projectUuid, projectName } } = this.props; 108 | if (uploadStatus === 1) { // 正在上传 109 | return false; 110 | } 111 | this.props.dispatch({ 112 | type: MARKDOWN_UPLOADING, 113 | }); 114 | this.props.dispatch({ 115 | type: UPLOAD_NOTE_ONEDRIVE, 116 | param: { 117 | uuid, 118 | name, 119 | projectUuid, 120 | projectName, 121 | content, 122 | }, 123 | toolbar: true, 124 | }); 125 | } 126 | 127 | handleExport = ({ key }) => { 128 | const { markdown: { content, name, html }, note: { projectName } } = this.props; 129 | let data; 130 | if (key === 'md') { 131 | data = content; 132 | } else if (key === 'html') { 133 | data = html; 134 | } else if (key === 'pdf') { 135 | data = html; 136 | } 137 | ipcRenderer.send('export-note', { 138 | content, 139 | html, 140 | type: key, 141 | fileName: name, 142 | projectName, 143 | data, 144 | }); 145 | } 146 | 147 | renderIcon = (type, desc) => { 148 | const { markdown: { uploadStatus } } = this.props; 149 | if (type === 'export') { 150 | const menu = ( 151 | 152 | Markdown 153 | Html 154 | PDF 155 | 156 | ); 157 | return ( 158 | 162 | this.handleClick(type)} 165 | > 166 | 167 | 168 | 169 | ); 170 | } 171 | return ( 172 | this.handleClick(type)} 175 | > 176 | 180 | {type === 'cloud-upload-o' && uploadStatus === 1 ? ( 181 | 182 | ) : ( 183 | 184 | )} 185 | 186 | 187 | ); 188 | } 189 | 190 | renderEditModalIcon = () => { 191 | const { editorMode } = this.props; 192 | return ( 193 | 197 | 201 | 206 | 207 | 208 | ); 209 | } 210 | 211 | render() { 212 | const { searchStatus } = this.state; 213 | const { markdown: { name, status }, blur } = this.props; 214 | const classStr = classnames('note-toolbar', { 215 | 'note-blur': blur, 216 | }); 217 | return ( 218 |
    219 |
    220 | this.onSearch(value)} 224 | onClose={() => this.onClose()} 225 | /> 226 |
    227 | {status === 1 ? ( 228 | 229 |

    {name}

    230 |
    231 | {this.renderIcon('cloud-upload-o', 'upload')} 232 | {/* {this.renderIcon('link')} 233 | {this.renderIcon('picture')} 234 | {this.renderIcon('code-o')} 235 | {this.renderIcon('smile-o')} 236 | {this.renderIcon('tag')} 237 | {this.renderIcon('arrows-alt')} */} 238 | {/* {this.renderIcon('arrows-alt')} */} 239 | {this.renderIcon('export', 'export')} 240 | {this.renderEditModalIcon()} 241 |
    242 |
    243 | ) : (null)} 244 |
    245 | ); 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /app/main/index.js: -------------------------------------------------------------------------------- 1 | import electron from 'electron'; 2 | import path from 'path'; 3 | import url from 'url'; 4 | import fs from 'fs'; 5 | import ChildProcess from 'child_process'; 6 | import { setMenu, getExplorerMenuItem, getExplorerFileMenuItem, getExplorerProjectItemMenu, getExplorerFileItemMenu } from './menu'; 7 | import { removeEventListeners, eventListener } from './event'; 8 | import schedule from './schedule'; 9 | 10 | const app = electron.app; 11 | const BrowserWindow = electron.BrowserWindow; 12 | // const ipcMain = electron.ipcMain; 13 | // const Menu = electron.Menu; 14 | // const Tray = electron.Tray; 15 | // const dialog = electron.dialog; 16 | const shell = electron.shell; 17 | 18 | app.setName('Yosoro'); 19 | 20 | function handleSquirrelEvent() { 21 | if (process.argv.length === 1) { 22 | return false; 23 | } 24 | 25 | const appFolder = path.resolve(process.execPath, '..'); 26 | const rootAtomFolder = path.resolve(appFolder, '..'); 27 | const updateDotExe = path.resolve(path.join(rootAtomFolder, 'Update.exe')); 28 | const exeName = path.basename(process.execPath); 29 | 30 | const spawn = (command, args) => { 31 | let spawnedProcess; 32 | try { 33 | spawnedProcess = ChildProcess.spawn(command, args, { detached: true }); 34 | } catch (error) { 35 | console.warn(error); 36 | } 37 | 38 | return spawnedProcess; 39 | }; 40 | 41 | const spawnUpdate = args => spawn(updateDotExe, args); 42 | const squirrelEvent = process.argv[1]; 43 | switch (squirrelEvent) { 44 | case '--squirrel-install': 45 | case '--squirrel-updated': { 46 | // Optionally do things such as: 47 | // - Add your .exe to the PATH 48 | // - Write to the registry for things like file associations and 49 | // explorer context menus 50 | 51 | // Install desktop and start menu shortcuts 52 | spawnUpdate(['--createShortcut', exeName]); 53 | setTimeout(app.quit, 1000); 54 | return true; 55 | } 56 | case '--squirrel-uninstall': { 57 | // Undo anything you did in the --squirrel-install and 58 | // --squirrel-updated handlers 59 | // Remove desktop and start menu shortcuts 60 | spawnUpdate(['--removeShortcut', exeName]); 61 | setTimeout(app.quit, 1000); 62 | return true; 63 | } 64 | case '--squirrel-obsolete': { 65 | // This is called on the outgoing version of your app before 66 | // we update to the new version - it's the opposite of 67 | // --squirrel-updated 68 | app.quit(); 69 | return true; 70 | } 71 | default: 72 | return true; 73 | } 74 | } 75 | 76 | // this should be placed at top of main.js to handle setup events quickly 77 | if (process.platform === 'win32' && handleSquirrelEvent() && process.env.NODE_ENV === 'production') { 78 | if (handleSquirrelEvent()) { 79 | // squirrel event handled and app will exit in 1000ms, so don't do anything else 80 | app.quit(); 81 | } 82 | } 83 | 84 | let mainWindow; 85 | // let tray = null; 86 | 87 | const dataPath = app.getPath('appData'); 88 | let appDataPath = `${dataPath}/Yosoro`; 89 | if (process.env.NODE_ENV === 'development') { 90 | appDataPath += 'Test'; 91 | } 92 | const profilePath = `${appDataPath}/profiledata`; 93 | const documentsPath = `${appDataPath}/documents`; 94 | const projectsPath = `${appDataPath}/documents/projects`; 95 | const trashPath = `${appDataPath}/documents/trash`; 96 | 97 | function createInitWorkSpace() { 98 | try { 99 | if (!fs.existsSync(appDataPath)) { 100 | fs.mkdirSync(appDataPath); 101 | } 102 | if (!fs.existsSync(profilePath)) { 103 | fs.mkdir(profilePath); // 异步创建 104 | } 105 | if (!fs.existsSync(documentsPath)) { 106 | fs.mkdirSync(documentsPath); 107 | } 108 | if (!fs.existsSync(projectsPath)) { 109 | fs.mkdirSync(projectsPath); 110 | } 111 | if (!fs.existsSync(trashPath)) { 112 | fs.mkdirSync(trashPath); 113 | } 114 | } catch (ex) { 115 | console.warn(ex); 116 | } 117 | } 118 | 119 | createInitWorkSpace(); 120 | 121 | if (process.env.NODE_ENV === 'development') { 122 | require('electron-watch')( 123 | __dirname, 124 | 'dev:main', 125 | process.cwd(), 126 | ); 127 | } 128 | 129 | function createWindow() { 130 | // Create the browser window. 131 | const options = { 132 | title: 'Yosoro', 133 | width: 1200, 134 | height: 786, 135 | minWidth: 1200, 136 | minHeight: 600, 137 | titleBarStyle: 'default', 138 | }; 139 | if (process.platform === 'linux') { 140 | options.icon = path.join(__dirname, './resource/app.png'); 141 | } 142 | if (process.platform === 'darwin') { 143 | options.transparent = true; 144 | options.frame = true; 145 | options.titleBarStyle = 'hiddenInset'; 146 | } 147 | mainWindow = new BrowserWindow(options); 148 | mainWindow.once('ready-to-show', () => { 149 | mainWindow.show(); 150 | }); 151 | 152 | mainWindow.setTitle('Yosoro'); 153 | 154 | // and load the index.html of the app. 155 | mainWindow.loadURL(url.format({ 156 | pathname: path.join(__dirname, './index.html'), 157 | hash: 'note', 158 | protocol: 'file:', 159 | slashes: true, 160 | })); 161 | 162 | // 设置菜单 163 | setMenu(mainWindow); 164 | 165 | const explorerMenu = getExplorerMenuItem(mainWindow); 166 | const exploereFileMenu = getExplorerFileMenuItem(mainWindow); 167 | const projectItemMenu = getExplorerProjectItemMenu(mainWindow); 168 | const fileItemMenu = getExplorerFileItemMenu(mainWindow); 169 | 170 | eventListener({ 171 | explorerMenu, 172 | exploereFileMenu, 173 | projectItemMenu, 174 | fileItemMenu, 175 | }); 176 | 177 | // Tray模块 178 | // const iconPath = path.join(__dirname, './resource/tray-icon.png'); 179 | // tray = new Tray(iconPath); 180 | // const contextMenu = Menu.buildFromTemplate([{ 181 | // label: 'Tray', 182 | // }]); 183 | // tray.setToolTip('This is my yosoyo-dektop.'); 184 | // tray.setContextMenu(contextMenu); 185 | 186 | const webContents = mainWindow.webContents; 187 | 188 | webContents.on('will-navigate', (e, linkUrl) => { 189 | e.preventDefault(); 190 | shell.openExternal(linkUrl); 191 | }); 192 | 193 | // Emitted when the window is closed. 194 | mainWindow.on('close', () => { 195 | mainWindow = null; 196 | removeEventListeners(); 197 | }); 198 | 199 | // 配置插件 200 | if (process.env.NODE_ENV === 'development') { 201 | require('devtron').install(); 202 | /* eslint-disable import/no-unresolved */ 203 | const CONFIG = require('../../config/devconfig.json'); 204 | const extensions = CONFIG.extensions; 205 | for (const ex of extensions) { 206 | BrowserWindow.addDevToolsExtension(ex); 207 | } 208 | /* eslint-enable */ 209 | } 210 | } 211 | 212 | // This method will be called when Electron has finished 213 | // initialization and is ready to create browser windows. 214 | // Some APIs can only be used after this event occurs. 215 | app.on('ready', createWindow); 216 | 217 | // Quit when all windows are closed. 218 | app.on('window-all-closed', () => { 219 | // On OS X it is common for applications and their menu bar 220 | // to stay active until the user quits explicitly with Cmd + Q 221 | // if (tray) { 222 | // tray.destroy(); 223 | // } 224 | if (process.platform !== 'darwin') { 225 | app.quit(); 226 | } 227 | }); 228 | 229 | // app.on('browser-window-created', (event, win) => { 230 | // win.webContents.on('context-menu', (e, params) => { 231 | // explorerMenu.popup(win, params.x, params.y) 232 | // }); 233 | // }); 234 | 235 | app.on('activate', () => { 236 | // On OS X it's common to re-create a window in the app when the 237 | // dock icon is clicked and there are no other windows open. 238 | if (mainWindow === null) { 239 | createWindow(); 240 | } 241 | }); 242 | 243 | app.on('before-quit', () => { 244 | // 退出应用关闭定时器 245 | schedule.cancelReleases(); 246 | }); 247 | -------------------------------------------------------------------------------- /app/views/sagas/drive.js: -------------------------------------------------------------------------------- 1 | import { takeLatest, call, put, all } from 'redux-saga/effects'; 2 | import { ipcRenderer } from 'electron'; 3 | import { message } from 'antd'; 4 | import { 5 | DRIVE_FETCHING_PROJECTS, 6 | DRIVE_FETCHING_PROJECRS_FAILED, 7 | DRIVE_FETCHING_PROJECRS_SUCCESS, 8 | DRIVE_FETCHING_NOTES, 9 | DRIVE_FETCHING_NOTES_SUCCESS, 10 | DRIVE_FETCHING_NOTES_FAILED, 11 | DRIVE_DOWNLOAD_NOTE, 12 | DRIVE_DOWNLOAD_NOTE_SUCCESS, 13 | DRIVE_DOWNLOAD_NOTE_FAILED, 14 | DRIVE_DELETE_ITEM, 15 | DRIVE_DELETE_ITEM_SUCCESS, 16 | DRIVE_DELETE_ITEM_FAILED, 17 | } from '../actions/drive'; 18 | import { 19 | GET_USER_AVATAR, 20 | GET_USER_AVATAR_SUCCESS, 21 | GET_USER_AVATAR_FAILED, 22 | } from '../actions/user'; 23 | import { SAVE_NOTE_FROM_DRIVE } from '../actions/projects'; 24 | import { JUST_UPDATE_MARKDWON_HTML } from '../actions/markdown'; 25 | import * as db from '../utils/db/app'; 26 | import OneDrive from '../services/OneDrive'; 27 | import { blobToBase64 } from '../utils/utils'; 28 | 29 | const oneDrive = new OneDrive(); 30 | 31 | function getDrive(driveName) { 32 | switch (driveName) { 33 | case 'onedriver': 34 | return { 35 | cloudDrive: oneDrive, 36 | driveType: 'oneDriver', 37 | }; 38 | default: 39 | return { 40 | cloudDrive: oneDrive, 41 | driveType: 'oneDriver', 42 | }; 43 | } 44 | } 45 | 46 | function* getToken(cloudDrive, driveType) { 47 | const tokens = db.getTokens(); 48 | const { [driveType]: { token, refreshToken, expiresDate } } = tokens; 49 | let currentToken = token; 50 | if (Date.parse(new Date()) > expiresDate) { // token过期刷新token 51 | const refreshData = yield call(cloudDrive.refreshToken, refreshToken); 52 | const newToken = refreshData.access_token; 53 | const newRefreshToken = refreshData.refresh_token; 54 | const newExpiresDate = Date.parse(new Date()) + (refreshData.expires_in * 1000); 55 | currentToken = newToken; 56 | db.setToken(driveType, newToken, newRefreshToken, newExpiresDate); 57 | } 58 | return currentToken; 59 | } 60 | 61 | function* fetchProject(action) { 62 | const { driveName } = action; 63 | let cloudDrive; 64 | let driveType; 65 | if (driveName === 'onedriver') { 66 | cloudDrive = oneDrive; 67 | driveType = 'oneDriver'; 68 | } 69 | try { 70 | const token = yield call(getToken, cloudDrive, driveType); 71 | const data = yield call(cloudDrive.getProjects, token); 72 | const list = data.value; 73 | if (!list) { 74 | throw new Error('Read result failed'); 75 | } 76 | yield put({ 77 | type: DRIVE_FETCHING_PROJECRS_SUCCESS, 78 | list, 79 | }); 80 | } catch (ex) { 81 | message.error('Fetching failed'); 82 | console.warn(ex); 83 | yield put({ 84 | type: DRIVE_FETCHING_PROJECRS_FAILED, 85 | }); 86 | } 87 | } 88 | 89 | function* fetchProjectList() { 90 | yield takeLatest(DRIVE_FETCHING_PROJECTS, fetchProject); 91 | } 92 | 93 | function* fetchNotes(action) { 94 | const { folder, driveName } = action; 95 | let cloudDrive; 96 | let driveType; 97 | if (driveName === 'onedriver') { 98 | cloudDrive = oneDrive; 99 | driveType = 'oneDriver'; 100 | } 101 | try { 102 | const token = yield call(getToken, cloudDrive, driveType); 103 | const data = yield call(cloudDrive.getNotes, token, folder); 104 | const list = data.value; 105 | if (!list) { 106 | throw new Error('Read result failed'); 107 | } 108 | yield put({ 109 | type: DRIVE_FETCHING_NOTES_SUCCESS, 110 | list, 111 | }); 112 | } catch (ex) { 113 | message.error('Fetching failed'); 114 | console.warn(ex); 115 | yield put({ 116 | type: DRIVE_FETCHING_NOTES_FAILED, 117 | }); 118 | } 119 | } 120 | 121 | function* fetchNotesList() { 122 | yield takeLatest(DRIVE_FETCHING_NOTES, fetchNotes); 123 | } 124 | 125 | function* downloadNote(action) { 126 | const { folder, name, driveName, needUpdateEditor } = action; 127 | let cloudDrive; 128 | let driveType; 129 | if (driveName === 'onedriver') { 130 | cloudDrive = oneDrive; 131 | driveType = 'oneDriver'; 132 | } 133 | const infoName = `${name.replace(/.md$/ig, '')}.json`; 134 | try { 135 | const token = yield call(getToken, cloudDrive, driveType); 136 | const contentPromise = call(cloudDrive.getNoteContent, token, folder, name); 137 | const infoPromise = call(cloudDrive.getNoteContent, token, folder, infoName); 138 | const data = yield all([contentPromise, infoPromise]); 139 | const content = data[0]; 140 | const info = JSON.parse(data[1]); 141 | yield put({ 142 | type: SAVE_NOTE_FROM_DRIVE, 143 | folder, 144 | name: name.replace(/.md$/ig, ''), 145 | content, 146 | info, 147 | driveType, 148 | }); 149 | yield put({ 150 | type: DRIVE_DOWNLOAD_NOTE_SUCCESS, 151 | }); 152 | if (needUpdateEditor) { // 需要更新MarkDown编辑器 153 | yield put({ 154 | type: JUST_UPDATE_MARKDWON_HTML, 155 | content, 156 | }); 157 | } 158 | } catch (ex) { 159 | message.error('Download note failed'); 160 | console.warn(ex); 161 | yield put({ 162 | type: DRIVE_DOWNLOAD_NOTE_FAILED, 163 | }); 164 | } 165 | } 166 | 167 | function* handleDownloadNote() { 168 | yield takeLatest(DRIVE_DOWNLOAD_NOTE, downloadNote); 169 | } 170 | 171 | 172 | function* deleteItem(action) { 173 | const { itemId, parentReference, driveName, jsonItemId, deleteType } = action; 174 | const { cloudDrive, driveType } = getDrive(driveName); 175 | try { 176 | let url = ''; 177 | let jsonUrl = ''; 178 | if (parentReference && parentReference.driveId) { 179 | url = `/drives/${encodeURIComponent(parentReference.driveId)}/items/${encodeURIComponent(itemId)}`; 180 | if (deleteType === 'note') { 181 | jsonUrl = `/drives/${encodeURIComponent(parentReference.driveId)}/items/${encodeURIComponent(jsonItemId)}`; 182 | } 183 | } else { 184 | url = `/me/drive/items/${itemId}`; 185 | if (deleteType === 'note') { 186 | jsonUrl = `/me/drive/items/${encodeURIComponent(jsonItemId)}`; 187 | } 188 | } 189 | const token = yield call(getToken, cloudDrive, driveType); 190 | const deleteItemPromise = call(cloudDrive.deleteItem, token, url); 191 | const deleteJsonPromise = call(cloudDrive.deleteItem, token, jsonUrl); 192 | const queue = [deleteItemPromise]; 193 | if (deleteType === 'note') { 194 | queue.push(deleteJsonPromise); 195 | } 196 | yield all(queue); 197 | yield put({ 198 | type: DRIVE_DELETE_ITEM_SUCCESS, 199 | deleteType, 200 | itemId, 201 | jsonItemId, 202 | }); 203 | } catch (ex) { 204 | message.error('delete failed'); 205 | yield put({ 206 | type: DRIVE_DELETE_ITEM_FAILED, 207 | }); 208 | } 209 | } 210 | 211 | function* handleDeleteItem() { 212 | yield takeLatest(DRIVE_DELETE_ITEM, deleteItem); 213 | } 214 | 215 | // 获取用户头像 216 | function* getUserAvatar(action) { 217 | const { driveName } = action; 218 | let cloudDrive; 219 | let driveType; 220 | if (driveName === 'oneDrive') { 221 | cloudDrive = oneDrive; 222 | driveType = 'oneDriver'; 223 | } 224 | try { 225 | const token = yield call(getToken, cloudDrive, driveType); 226 | const data = yield call(cloudDrive.getUserAvatar, token); 227 | const base64 = yield call(blobToBase64, data); 228 | const avatar = ipcRenderer.sendSync('save-user-avatar', base64); 229 | yield put({ 230 | type: GET_USER_AVATAR_SUCCESS, 231 | avatar: avatar.url, 232 | }); 233 | } catch (ex) { 234 | console.warn(ex); 235 | yield put({ 236 | type: GET_USER_AVATAR_FAILED, 237 | }); 238 | } 239 | } 240 | 241 | function* handleGetUserAvatar() { 242 | yield takeLatest(GET_USER_AVATAR, getUserAvatar); 243 | } 244 | 245 | export default [ 246 | fetchProjectList, 247 | fetchNotesList, 248 | handleDownloadNote, 249 | handleDeleteItem, 250 | handleGetUserAvatar, 251 | ]; 252 | -------------------------------------------------------------------------------- /app/views/component/editor/Editor.jsx: -------------------------------------------------------------------------------- 1 | import 'codemirror/lib/codemirror.css'; 2 | import React, { Component } from 'react'; 3 | import PropTypes from 'prop-types'; 4 | import { ipcRenderer } from 'electron'; 5 | import autobind from 'autobind-decorator'; 6 | import CodeMirror from 'codemirror'; 7 | import 'codemirror/addon/fold/markdown-fold'; 8 | import 'codemirror/mode/markdown/markdown'; 9 | import ReactResizeDetector from 'react-resize-detector'; 10 | import { updateMarkdownHtml } from '../../actions/markdown'; 11 | import { throttle, debounce } from '../../utils/utils'; 12 | 13 | export default class Editor extends Component { 14 | static displayName = 'MarkdownEditor'; 15 | static propTypes = { 16 | dispatch: PropTypes.func.isRequired, 17 | uuid: PropTypes.string.isRequired, 18 | defaultContent: PropTypes.string.isRequired, 19 | start: PropTypes.number.isRequired, 20 | editorWidth: PropTypes.string.isRequired, 21 | setDrag: PropTypes.func.isRequired, 22 | editorMode: PropTypes.string.isRequired, 23 | editorWidthValue: PropTypes.number.isRequired, 24 | drag: PropTypes.bool.isRequired, 25 | setPreiewScrollRatio: PropTypes.func.isRequired, 26 | note: PropTypes.shape({ 27 | projectUuid: PropTypes.string.isRequired, 28 | projectName: PropTypes.string.isRequired, 29 | fileUuid: PropTypes.string.isRequired, 30 | fileName: PropTypes.string.isRequired, 31 | }).isRequired, 32 | }; 33 | 34 | static getDerivedStateFromProps(nextProps) { 35 | return { 36 | content: nextProps.defaultContent, 37 | }; 38 | } 39 | 40 | constructor() { 41 | super(); 42 | this.codeMirror = null; 43 | this.containerResize = debounce(() => { 44 | this.codeMirror.refresh(); 45 | }, 100); 46 | this.state = { 47 | listenScroll: true, 48 | }; 49 | } 50 | 51 | componentDidMount() { 52 | this.noteRoot = document.getElementById('note_root_cont'); 53 | window.addEventListener('resize', throttle(this.onWindowResize, 60)); 54 | this.container.addEventListener('resize', this.handleContainerResize); 55 | this.setCodeMirror(); 56 | } 57 | 58 | componentDidUpdate(prevProps) { 59 | const { start } = this.props; 60 | if (start !== -1) { 61 | this.editor.selectionStart = start + 1; 62 | this.editor.selectionEnd = start + 1; 63 | } 64 | if (prevProps.uuid !== this.props.uuid) { 65 | this.removeChangeEvent(); // 取消change事件 66 | this.codeMirror.setValue(this.props.defaultContent); 67 | this.addChangeEvent(); // 重新绑定change事件 68 | } 69 | } 70 | 71 | componentWillUnmount() { 72 | window.removeEventListener('resize', throttle(this.onWindowResize, 60)); 73 | this.deleteCodeMirror(); 74 | } 75 | 76 | onWindowResize = () => { 77 | const { editorMode } = this.props; 78 | if (editorMode === 'edit') { 79 | const { editorWidthValue } = this.props; 80 | const textWidth = this.getTextWidth(editorMode, editorWidthValue); 81 | this.setState({ 82 | textWidth, 83 | }); 84 | } 85 | } 86 | 87 | getRatio = (cm) => { 88 | const currentLine = cm.getCursor().line; 89 | const lines = cm.lineCount(); 90 | return currentLine / lines; 91 | } 92 | 93 | getTextWidth(editorMode, editorWidthValue) { 94 | if (editorMode === 'normal' || editorMode === 'immersion') { 95 | return '100%'; 96 | } 97 | if (this.editorRoot) { 98 | let parentWidth; 99 | if (editorMode === 'edit') { 100 | parentWidth = this.noteRoot.offsetWidth; 101 | } else { 102 | parentWidth = this.editorRoot.offsetParent.offsetWidth; 103 | } 104 | let res = '100%'; 105 | if (editorWidthValue && parentWidth) { 106 | res = `${parentWidth * editorWidthValue}px`; 107 | } 108 | return res; 109 | } 110 | return '100%'; 111 | } 112 | 113 | setCodeMirror = () => { 114 | this.codeMirror = CodeMirror(this.container, { 115 | value: this.state.content, 116 | mode: 'markdown', 117 | lineNumbers: true, 118 | lineWrapping: true, 119 | // extraKeys: {"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }}, 120 | foldGutter: true, 121 | gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter'], 122 | }); 123 | this.addChangeEvent(); 124 | // this.codeMirror.on('change', this.handleChange); 125 | this.codeMirror.on('scroll', this.handleScroll); 126 | this.codeMirror.on('keydown', this.handleKeyDown); 127 | this.codeMirror.on('focus', this.handleFocus); 128 | } 129 | 130 | // 停止编辑500ms, 异步保存文件内容 131 | autoSave = debounce(() => { 132 | const { note: { projectName, fileName }, defaultContent } = this.props; 133 | ipcRenderer.send('auto-save-content-to-file', { 134 | projectName, 135 | fileName, 136 | content: defaultContent, 137 | }); 138 | }, 500); 139 | 140 | removeChangeEvent() { 141 | if (this.codeMirror) { 142 | this.codeMirror.off('change', this.handleChange); 143 | } 144 | } 145 | 146 | addChangeEvent() { 147 | if (this.codeMirror) { 148 | this.codeMirror.on('change', this.handleChange); 149 | } 150 | } 151 | 152 | handleChange = (cm) => { 153 | const content = cm.getValue(); 154 | const { uuid } = this.props; 155 | this.props.dispatch(updateMarkdownHtml(content, uuid, -1)); 156 | this.autoSave(); 157 | } 158 | 159 | updateCode = () => { 160 | const { editorMode, editorWidthValue, defaultContent } = this.props; 161 | const textWidth = this.getTextWidth(editorMode, editorWidthValue); 162 | this.setState({ 163 | content: defaultContent, 164 | textWidth, 165 | }); 166 | } 167 | 168 | deleteCodeMirror = () => {this.codeMirror = null;} 169 | 170 | handleScroll = (cm) => { 171 | const { listenScroll } = this.state; 172 | if (!listenScroll) { 173 | this.setState({ 174 | listenScroll: true, 175 | }); 176 | return false; 177 | } 178 | const element = cm.getScrollerElement(); 179 | const heigth = element.scrollHeight; 180 | const ratio = element.scrollTop / heigth; 181 | this.props.setPreiewScrollRatio(ratio); 182 | } 183 | 184 | handleFocus = (cm) => { 185 | const currentLine = cm.getCursor().line; 186 | const lines = cm.lineCount(); 187 | this.props.setPreiewScrollRatio(currentLine / lines); 188 | } 189 | 190 | handleKeyDown = (cm) => { 191 | this.setState({ 192 | listenScroll: false, 193 | }); 194 | const ratio = this.getRatio(cm); 195 | this.props.setPreiewScrollRatio(ratio); 196 | } 197 | 198 | @autobind 199 | handleMouseDown() { 200 | this.props.setDrag(true); 201 | } 202 | 203 | @autobind 204 | handleMouseUp() { 205 | this.props.setDrag(false); 206 | } 207 | 208 | @autobind 209 | handleCodeMirrorResize() { 210 | this.containerResize(); 211 | } 212 | 213 | render() { 214 | // const { textWidth } = this.state; 215 | const { editorWidth, editorMode, editorWidthValue, drag } = this.props; 216 | let width = editorWidth; 217 | let rootClass = ''; 218 | let split = true; 219 | let noBorder = ''; 220 | if (editorMode === 'preview') { 221 | width = '0'; 222 | rootClass = 'hide'; 223 | split = false; 224 | noBorder = 'no-border'; 225 | } else if (editorMode === 'immersion') { 226 | width = '100%'; 227 | split = false; 228 | noBorder = 'no-border'; 229 | rootClass = 'immersion-mode'; 230 | } 231 | const textWidth = this.getTextWidth(editorMode, editorWidthValue); 232 | return ( 233 |
    (this.editorRoot = node)} 237 | > 238 | 239 |
    (this.container = node)} 243 | /> 244 | { split ? ( 245 | 250 | ) : null} 251 |
    252 | ); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /app/views/component/cloud/Drive.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { Breadcrumb, message, Icon, Modal } from 'antd'; 4 | import autobind from 'autobind-decorator'; 5 | import Notebooks from './Notebooks'; 6 | import Notes from './Notes'; 7 | import Loading from '../share/Loading'; 8 | import { 9 | DRIVE_FETCHING_PROJECTS, 10 | DRIVE_FETCHING_NOTES, 11 | DRIVE_BACK_ROOT, 12 | DRIVE_DOWNLOAD_NOTE, 13 | DRIVE_DELETE_ITEM, 14 | } from '../../actions/drive'; 15 | import { getTokens } from '../../utils/db/app'; 16 | 17 | const confirm = Modal.confirm; 18 | const BreadcrumbItem = Breadcrumb.Item; 19 | 20 | export default class Drive extends Component { 21 | static displayName = 'CloudDrive'; 22 | static propTypes = { 23 | dispatch: PropTypes.func.isRequired, 24 | match: PropTypes.any.isRequired, 25 | drive: PropTypes.shape({ 26 | status: PropTypes.number.isRequired, 27 | projects: PropTypes.array.isRequired, 28 | notes: PropTypes.array.isRequired, 29 | currentProjectName: PropTypes.string.isRequired, 30 | }).isRequired, 31 | note: PropTypes.shape({ 32 | projectUuid: PropTypes.string.isRequired, 33 | projectName: PropTypes.string.isRequired, 34 | fileUuid: PropTypes.string.isRequired, 35 | fileName: PropTypes.string.isRequired, 36 | }).isRequired, 37 | } 38 | 39 | constructor(props) { 40 | super(props); 41 | this.state = { 42 | show: false, 43 | hasAuth: false, 44 | driveName: '', 45 | loadingText: 'Loading', 46 | }; 47 | } 48 | 49 | componentDidMount() { 50 | this.checkOAuth(); 51 | } 52 | 53 | getValidNotes() { 54 | const { notes } = this.props.drive; 55 | return notes.filter(note => /.md$/ig.test(note.name)); 56 | } 57 | 58 | setAutuStatus(flag) { 59 | this.setState({ 60 | show: true, 61 | hasAuth: flag, 62 | }); 63 | } 64 | 65 | setDriveName(name) { 66 | this.setState({ 67 | driveName: name, 68 | }); 69 | } 70 | 71 | checkOAuth() { 72 | let drive = this.props.match.params.drive; 73 | const { dispatch, drive: { currentProjectName } } = this.props; 74 | const oAuth = getTokens(); 75 | let auth; 76 | if (drive === 'onedrive') { 77 | drive = 'onedriver'; 78 | auth = oAuth.oneDriver; 79 | this.setDriveName('One Drive'); 80 | } else { 81 | message.error('Not support this cloud drive'); 82 | return false; 83 | } 84 | if (auth.token && auth.refreshToken) { // 已经授权 85 | this.setAutuStatus(true); 86 | this.setState({ 87 | loadingText: 'Loading...', 88 | }); 89 | if (currentProjectName) { 90 | this.props.dispatch({ 91 | type: DRIVE_FETCHING_NOTES, 92 | folder: currentProjectName, 93 | driveName: drive, 94 | }); 95 | } else { 96 | dispatch({ 97 | type: DRIVE_FETCHING_PROJECTS, 98 | driveName: drive, 99 | }); 100 | } 101 | } else { 102 | this.setAutuStatus(false); // 未授权 103 | } 104 | } 105 | 106 | @autobind 107 | handleRefresh() { 108 | this.checkOAuth(); 109 | } 110 | 111 | @autobind 112 | chooseProject(folder) { 113 | this.setState({ 114 | loadingText: 'Loading...', 115 | }); 116 | let driveName = this.props.match.params.drive; 117 | if (driveName === 'onedrive') { 118 | driveName = 'onedriver'; 119 | } 120 | this.props.dispatch({ 121 | type: DRIVE_FETCHING_NOTES, 122 | folder, 123 | driveName, 124 | }); 125 | } 126 | 127 | @autobind 128 | backRoot() { 129 | const { drive: { currentProjectName } } = this.props; 130 | if (currentProjectName) { 131 | this.props.dispatch({ 132 | type: DRIVE_BACK_ROOT, 133 | }); 134 | } else { 135 | this.handleRefresh(); 136 | } 137 | } 138 | 139 | // 下载单个笔记 140 | @autobind 141 | downloadNote(name) { 142 | const { drive: { currentProjectName }, note } = this.props; 143 | this.setState({ 144 | loadingText: 'Downloading...', 145 | }); 146 | let driveName = this.props.match.params.drive; 147 | if (driveName === 'onedrive') { 148 | driveName = 'onedriver'; 149 | } 150 | let needUpdateEditor = false; 151 | if (note.projectUuid !== '-1' && note.fileUuid !== '-1' && note.projectName === currentProjectName && `${note.fileName}.md` === name) { 152 | needUpdateEditor = true; 153 | } 154 | this.props.dispatch({ 155 | type: DRIVE_DOWNLOAD_NOTE, 156 | folder: currentProjectName, 157 | name, 158 | driveName, 159 | needUpdateEditor, 160 | }); 161 | } 162 | 163 | // 打开删除笔记提示框 164 | @autobind 165 | openDelete(e, type, name, id, parentReference) { 166 | e.stopPropagation(); 167 | confirm({ 168 | title: `Remove "${name.replace(/.md$/ig, '')}"?`, 169 | content: 'It can be reduced in the cloud drive.', 170 | onOk: () => { 171 | this.deleteItem(type, name, id, parentReference); 172 | }, 173 | }); 174 | } 175 | 176 | /** 177 | * @desc 删除单个Item 178 | * @param {String} type 'note' or 'project' 179 | */ 180 | deleteItem(type, name, id, parentReference) { 181 | this.setState({ 182 | loadingText: 'Deleting...', 183 | }); 184 | let driveName = this.props.match.params.drive; 185 | if (driveName === 'onedrive') { 186 | driveName = 'onedriver'; 187 | } 188 | let jsonItemId; 189 | if (type === 'note') { 190 | // 搜索匹配.json文件 191 | const { notes } = this.props.drive; 192 | const jsonName = `${name.replace(/.md$/ig, '')}.json`; 193 | const nl = notes.length; 194 | for (let i = 0; i < nl; i++) { 195 | if (notes[i].name === jsonName) { 196 | jsonItemId = notes[i].id; 197 | break; 198 | } 199 | } 200 | } 201 | this.props.dispatch({ 202 | type: DRIVE_DELETE_ITEM, 203 | itemId: id, 204 | parentReference, 205 | driveName, 206 | jsonItemId, 207 | deleteType: type, 208 | }); 209 | } 210 | 211 | renerList(status, driveName, currentProjectName, blur) { 212 | if (status === 2) { 213 | return ( 214 |
    215 |

    216 | 217 | Fetch data failed. 218 |

    219 |
    220 | ); 221 | } 222 | if (currentProjectName) { 223 | return this.renderNotes(status, driveName, currentProjectName, blur); 224 | } 225 | return this.renderProject(status, driveName, currentProjectName, blur); 226 | } 227 | 228 | /** 229 | * @param {Number} status 请求状态 230 | * @param {String} driveName 驱动名称 231 | * @param {String} currentProjectName 232 | * @param {String} blur 233 | */ 234 | renderNotes(status, driveName, currentProjectName, blur) { 235 | const notes = this.getValidNotes(); 236 | return ( 237 |
    238 | 243 |
    244 | ); 245 | } 246 | 247 | renderProject(status, driveName, currentProjectName, blur) { 248 | const { drive: { projects } } = this.props; 249 | return ( 250 |
    251 | 256 |
    257 | ); 258 | } 259 | 260 | renderBread(blur, driveName, currentProjectName) { 261 | return ( 262 |
    263 |
    264 | 265 | {driveName} 266 | 270 | Yosoro 271 | 272 | {currentProjectName ? ( 273 | 274 | {currentProjectName} 275 | 276 | ) : null} 277 | 278 |
    279 | 280 |
    281 |
    282 |
    283 | ); 284 | } 285 | 286 | renderLoading(status) { 287 | const { loadingText } = this.state; 288 | if (status === 0) { 289 | return ( 290 | 291 | ); 292 | } 293 | return null; 294 | } 295 | 296 | render() { 297 | const { show, hasAuth, driveName } = this.state; 298 | if (!show) { 299 | return null; 300 | } 301 | if (!hasAuth) { 302 | return ( 303 |
    304 |

    305 | 306 | Yosoro need to be authorized. 307 |

    308 |
    309 | ); 310 | } 311 | const { drive: { status, currentProjectName } } = this.props; 312 | let blur = ''; 313 | if (status === 0) { 314 | blur = 'blur'; 315 | } 316 | return ( 317 | 318 | {this.renderLoading(status)} 319 | {this.renderBread(blur, driveName, currentProjectName)} 320 | {this.renerList(status, driveName, currentProjectName, blur)} 321 | 322 | ); 323 | } 324 | } 325 | --------------------------------------------------------------------------------