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 ( +