diff --git a/browser/components/CodeEditor.js b/browser/components/CodeEditor.js index faa04e9a..056e7fff 100644 --- a/browser/components/CodeEditor.js +++ b/browser/components/CodeEditor.js @@ -21,6 +21,8 @@ const buildEditorContextMenu = require('browser/lib/contextMenuBuilder') import { createTurndownService } from '../lib/turndown' import { languageMaps } from '../lib/CMLanguageList' import snippetManager from '../lib/SnippetManager' +import { findStorage } from 'browser/lib/findStorage' +import { sendWakatimeHeartBeat } from 'browser/lib/wakatime-plugin' import { generateInEditor, tocExistsInEditor @@ -113,6 +115,16 @@ export default class CodeEditor extends React.Component { this.editorActivityHandler = () => this.handleEditorActivity() this.turndownService = createTurndownService() + + // wakatime + const { storageKey, noteKey } = this.props + const storage = findStorage(storageKey) + if (storage) + sendWakatimeHeartBeat(storage.path, noteKey, storage.name, { + isWrite: false, + hasFileChanges: false, + isFileChange: true + }) } handleSearch(msg) { @@ -793,9 +805,23 @@ export default class CodeEditor extends React.Component { this.updateHighlight(editor, changeObject) this.value = editor.getValue() + + const { storageKey, noteKey } = this.props + const storage = findStorage(storageKey) if (this.props.onChange) { this.props.onChange(editor) } + + const isWrite = !!this.props.onChange + const hasFileChanges = isWrite + + if (storage) { + sendWakatimeHeartBeat(storage.path, noteKey, storage.name, { + isWrite, + hasFileChanges, + isFileChange: false + }) + } } linePossibleContainsHeadline(currentLine) { @@ -923,6 +949,16 @@ export default class CodeEditor extends React.Component { this.restartHighlighting() this.editor.on('change', this.changeHandler) this.editor.refresh() + + // wakatime + const { storageKey, noteKey } = this.props + const storage = findStorage(storageKey) + if (storage) + sendWakatimeHeartBeat(storage.path, noteKey, storage.name, { + isWrite: false, + hasFileChanges: false, + isFileChange: true + }) } setValue(value) { diff --git a/browser/lib/wakatime-plugin.js b/browser/lib/wakatime-plugin.js new file mode 100644 index 00000000..9b1233df --- /dev/null +++ b/browser/lib/wakatime-plugin.js @@ -0,0 +1,49 @@ +import config from 'browser/main/lib/ConfigManager' +const exec = require('child_process').exec +const path = require('path') +let lastHeartbeat = 0 + +function sendWakatimeHeartBeat( + storagePath, + noteKey, + storageName, + { isWrite, hasFileChanges, isFileChange } +) { + if ( + config.get().wakatime.isActive && + !!config.get().wakatime.key && + (new Date().getTime() - lastHeartbeat > 120000 || isFileChange) + ) { + const notePath = path.join(storagePath, 'notes', noteKey + '.cson') + + if (!isWrite && !hasFileChanges && !isFileChange) { + return + } + + lastHeartbeat = new Date() + const wakatimeKey = config.get().wakatime.key + if (wakatimeKey) { + exec( + `wakatime --file ${notePath} --project '${storageName}' --key ${wakatimeKey} --plugin Boostnote-wakatime`, + (error, stdOut, stdErr) => { + if (error) { + console.log(error) + lastHeartbeat = 0 + } else { + console.log( + 'wakatime', + 'isWrite', + isWrite, + 'hasChanges', + hasFileChanges, + 'isFileChange', + isFileChange + ) + } + } + ) + } + } +} + +export { sendWakatimeHeartBeat } diff --git a/browser/main/Detail/SnippetNoteDetail.js b/browser/main/Detail/SnippetNoteDetail.js index 82723092..01a819f2 100644 --- a/browser/main/Detail/SnippetNoteDetail.js +++ b/browser/main/Detail/SnippetNoteDetail.js @@ -870,6 +870,8 @@ class SnippetNoteDetail extends React.Component { enableSmartPaste={config.editor.enableSmartPaste} hotkey={config.hotkey} autoDetect={autoDetect} + storageKey={storageKey} + noteKey={note.key} /> )} diff --git a/browser/main/lib/ConfigManager.js b/browser/main/lib/ConfigManager.js index a0b23fc2..5c3c8db2 100644 --- a/browser/main/lib/ConfigManager.js +++ b/browser/main/lib/ConfigManager.js @@ -138,7 +138,10 @@ export const DEFAULT_CONFIG = { username: '', password: '' }, - coloredTags: {} + coloredTags: {}, + wakatime: { + key: null + } } function validate(config) { @@ -254,6 +257,12 @@ function assignConfigValues(originalConfig, rcConfig) { originalConfig.hotkey, rcConfig.hotkey ) + config.wakatime = Object.assign( + {}, + DEFAULT_CONFIG.wakatime, + originalConfig.wakatime, + rcConfig.wakatime + ) config.blog = Object.assign( {}, DEFAULT_CONFIG.blog, diff --git a/browser/main/modals/PreferencesModal/PluginsTab.js b/browser/main/modals/PreferencesModal/PluginsTab.js new file mode 100644 index 00000000..ceaa383a --- /dev/null +++ b/browser/main/modals/PreferencesModal/PluginsTab.js @@ -0,0 +1,207 @@ +import PropTypes from 'prop-types' +import React from 'react' +import CSSModules from 'browser/lib/CSSModules' +import styles from './ConfigTab.styl' +import ConfigManager from 'browser/main/lib/ConfigManager' +import { store } from 'browser/main/store' +import _ from 'lodash' +import i18n from 'browser/lib/i18n' +import { sync as commandExists } from 'command-exists' +const electron = require('electron') +const ipc = electron.ipcRenderer +const { remote } = electron +const { dialog } = remote +class PluginsTab extends React.Component { + constructor(props) { + super(props) + + this.state = { + config: props.config + } + } + + componentDidMount() { + this.handleSettingDone = () => { + this.setState({ + pluginsAlert: { + type: 'success', + message: i18n.__('Successfully applied!') + } + }) + } + this.handleSettingError = err => { + this.setState({ + pluginsAlert: { + type: 'error', + message: + err.message != null ? err.message : i18n.__('An error occurred!') + } + }) + } + this.oldWakatimeConfig = this.state.config.wakatime + ipc.addListener('APP_SETTING_DONE', this.handleSettingDone) + ipc.addListener('APP_SETTING_ERROR', this.handleSettingError) + } + + componentWillUnmount() { + ipc.removeListener('APP_SETTING_DONE', this.handleSettingDone) + ipc.removeListener('APP_SETTING_ERROR', this.handleSettingError) + } + + checkWakatimePluginRequirement() { + const { wakatime } = this.state.config + if (wakatime.isActive && !commandExists('wakatime')) { + this.setState({ + wakatimePluginAlert: { + type: i18n.__('Warning'), + message: i18n.__('Missing wakatime cli') + } + }) + + const alertConfig = { + type: 'warning', + message: i18n.__('Missing Wakatime CLI'), + detail: i18n.__( + `Please install Wakatime CLI to use Wakatime tracker feature.` + ), + buttons: [i18n.__('OK')] + } + dialog.showMessageBox(remote.getCurrentWindow(), alertConfig) + } else { + this.setState({ + wakatimePluginAlert: null + }) + } + } + + handleSaveButtonClick(e) { + const newConfig = { + wakatime: { + isActive: this.state.config.wakatime.isActive, + key: this.state.config.wakatime.key + } + } + + ConfigManager.set(newConfig) + + store.dispatch({ + type: 'SET_CONFIG', + config: newConfig + }) + this.clearMessage() + this.props.haveToSave() + this.checkWakatimePluginRequirement() + } + + handleIsWakatimePluginActiveChange(e) { + const { config } = this.state + config.wakatime.isActive = !config.wakatime.isActive + this.setState({ + config + }) + if (_.isEqual(this.oldWakatimeConfig.isActive, config.wakatime.isActive)) { + this.props.haveToSave() + } else { + this.props.haveToSave({ + tab: 'Plugins', + type: 'warning', + message: i18n.__('Unsaved Changes!') + }) + } + } + + handleWakatimeKeyChange(e) { + const { config } = this.state + config.wakatime = { + isActive: true, + key: this.refs.wakatimeKey.value + } + this.setState({ + config + }) + if (_.isEqual(this.oldWakatimeConfig.key, config.wakatime.key)) { + this.props.haveToSave() + } else { + this.props.haveToSave({ + tab: 'Plugins', + type: 'warning', + message: i18n.__('Unsaved Changes!') + }) + } + } + + clearMessage() { + _.debounce(() => { + this.setState({ + pluginsAlert: null + }) + }, 2000)() + } + + render() { + const pluginsAlert = this.state.pluginsAlert + const pluginsAlertElement = + pluginsAlert != null ? ( +

{pluginsAlert.message}

+ ) : null + + const wakatimeAlert = this.state.wakatimePluginAlert + const wakatimePluginAlertElement = + wakatimeAlert != null ? ( +

{wakatimeAlert.message}

+ ) : null + + const { config } = this.state + + return ( +
+
+
{i18n.__('Plugins')}
+
{i18n.__('Wakatime')}
+
+ +
+
+
{i18n.__('Wakatime key')}
+
+ this.handleWakatimeKeyChange(e)} + disabled={!config.wakatime.isActive} + ref='wakatimeKey' + value={config.wakatime.key} + type='text' + /> + {wakatimePluginAlertElement} +
+
+
+ + {pluginsAlertElement} +
+
+
+ ) + } +} + +PluginsTab.propTypes = { + dispatch: PropTypes.func, + haveToSave: PropTypes.func +} + +export default CSSModules(PluginsTab, styles) diff --git a/browser/main/modals/PreferencesModal/index.js b/browser/main/modals/PreferencesModal/index.js index 2c14e6c7..36abd734 100644 --- a/browser/main/modals/PreferencesModal/index.js +++ b/browser/main/modals/PreferencesModal/index.js @@ -7,6 +7,7 @@ import InfoTab from './InfoTab' import Crowdfunding from './Crowdfunding' import StoragesTab from './StoragesTab' import SnippetTab from './SnippetTab' +import PluginsTab from './PluginsTab' import Blog from './Blog' import ModalEscButton from 'browser/components/ModalEscButton' import CSSModules from 'browser/lib/CSSModules' @@ -82,6 +83,14 @@ class Preferences extends React.Component { ) case 'SNIPPET': return + case 'PLUGINS': + return ( + this.setState({ PluginsAlert: alert })} + /> + ) case 'STORAGES': default: return ( @@ -122,7 +131,8 @@ class Preferences extends React.Component { { target: 'INFO', label: i18n.__('About') }, { target: 'CROWDFUNDING', label: i18n.__('Crowdfunding') }, { target: 'BLOG', label: i18n.__('Blog'), Blog: this.state.BlogAlert }, - { target: 'SNIPPET', label: i18n.__('Snippets') } + { target: 'SNIPPET', label: i18n.__('Snippets') }, + { target: 'PLUGINS', label: i18n.__('Plugins') } ] const navButtons = tabs.map(tab => { diff --git a/package.json b/package.json index c4286993..c57399e8 100644 --- a/package.json +++ b/package.json @@ -61,6 +61,7 @@ "chart.js": "^2.7.2", "codemirror": "^5.40.2", "codemirror-mode-elixir": "^1.1.1", + "command-exists": "^1.2.9", "connected-react-router": "^6.4.0", "electron-config": "^1.0.0", "electron-gh-releases": "^2.0.4", diff --git a/prettier.config b/prettier.config index 66e7e941..515c6cd5 100644 --- a/prettier.config +++ b/prettier.config @@ -1,6 +1,5 @@ { - "trailingComma": "es5", - "tabWidth": 2, + "singleQuote": true, "semi": false, - "singleQuote": true + "jsxSingleQuote": true } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d348d36e..f3a2badb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1966,6 +1966,11 @@ combined-stream@1.0.6, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" +command-exists@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/command-exists/-/command-exists-1.2.9.tgz#c50725af3808c8ab0260fd60b01fbfa25b954f69" + integrity sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w== + commander@2: version "2.16.0" resolved "http://registry.npm.taobao.org/commander/download/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50"