Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions browser/components/CodeEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
49 changes: 49 additions & 0 deletions browser/lib/wakatime-plugin.js
Original file line number Diff line number Diff line change
@@ -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 }
) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last 3 variable is all booleans. I suggest we change to object type for more clarification. For example, if we change this function to:

function sendWakatimeHeartBeat(storagePath, noteKey, storageName, {isWrite, hasFileChanges, isFileChange}) {
  ...
}

We can keep the code inside the same but when we call this function, we will call it like this:

sendWakatimeHeartBeat(
  storagePath,
  noteKey,
  storageName,
  { isWrite: true, hasFileChanges: false, isFileChange: true}
)

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 }
2 changes: 2 additions & 0 deletions browser/main/Detail/SnippetNoteDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -870,6 +870,8 @@ class SnippetNoteDetail extends React.Component {
enableSmartPaste={config.editor.enableSmartPaste}
hotkey={config.hotkey}
autoDetect={autoDetect}
storageKey={storageKey}
noteKey={note.key}
/>
)}
</div>
Expand Down
11 changes: 10 additions & 1 deletion browser/main/lib/ConfigManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,10 @@ export const DEFAULT_CONFIG = {
username: '',
password: ''
},
coloredTags: {}
coloredTags: {},
wakatime: {
key: null
}
}

function validate(config) {
Expand Down Expand Up @@ -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,
Expand Down
207 changes: 207 additions & 0 deletions browser/main/modals/PreferencesModal/PluginsTab.js
Original file line number Diff line number Diff line change
@@ -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 ? (
<p className={`alert ${pluginsAlert.type}`}>{pluginsAlert.message}</p>
) : null

const wakatimeAlert = this.state.wakatimePluginAlert
const wakatimePluginAlertElement =
wakatimeAlert != null ? (
<p className={`alert ${wakatimeAlert.type}`}>{wakatimeAlert.message}</p>
) : null

const { config } = this.state

return (
<div styleName='root'>
<div styleName='group'>
<div styleName='group-header'>{i18n.__('Plugins')}</div>
<div styleName='group-header2'>{i18n.__('Wakatime')}</div>
<div styleName='group-checkBoxSection'>
<label>
<input
onChange={e => this.handleIsWakatimePluginActiveChange(e)}
checked={config.wakatime.isActive}
ref='wakatimeIsActive'
type='checkbox'
/>
&nbsp;
{i18n.__('Enable Wakatime')}
</label>
</div>
<div styleName='group-section'>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Wakatime isn't the only plugin that we support so it's better to create a section for Wakatime in this plugin setting page. Just a simple section with title Wakatime and bellow is a check box saying enable wakatime? and below it is the text box for wakatime key. If we plan to only have 1 plugin then this plugin page should change the name to Wakatime which I think will be worst cus harder to change when we want to support more plugins

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done 👍

<div styleName='group-section-label'>{i18n.__('Wakatime key')}</div>
<div styleName='group-section-control'>
<input
styleName='group-section-control-input'
onChange={e => this.handleWakatimeKeyChange(e)}
disabled={!config.wakatime.isActive}
ref='wakatimeKey'
value={config.wakatime.key}
type='text'
/>
{wakatimePluginAlertElement}
</div>
</div>
<div styleName='group-control'>
<button
styleName='group-control-rightButton'
onClick={e => this.handleSaveButtonClick(e)}
>
{i18n.__('Save')}
</button>
{pluginsAlertElement}
</div>
</div>
</div>
)
}
}

PluginsTab.propTypes = {
dispatch: PropTypes.func,
haveToSave: PropTypes.func
}

export default CSSModules(PluginsTab, styles)
12 changes: 11 additions & 1 deletion browser/main/modals/PreferencesModal/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -82,6 +83,14 @@ class Preferences extends React.Component {
)
case 'SNIPPET':
return <SnippetTab dispatch={dispatch} config={config} data={data} />
case 'PLUGINS':
return (
<PluginsTab
dispatch={dispatch}
config={config}
haveToSave={alert => this.setState({ PluginsAlert: alert })}
/>
)
case 'STORAGES':
default:
return (
Expand Down Expand Up @@ -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 => {
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading