mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Removing need for including build files
This commit is contained in:
parent
c838548831
commit
11ad6c6bef
40
ElectronClient/app/compile-jsx.js
Normal file
40
ElectronClient/app/compile-jsx.js
Normal file
@ -0,0 +1,40 @@
|
||||
const fs = require('fs-extra');
|
||||
const spawnSync = require('child_process').spawnSync;
|
||||
|
||||
const babelPath = __dirname + '/node_modules/.bin/babel' + (process.platform === 'win32' ? '.cmd' : '');
|
||||
const guiPath = __dirname + '/gui';
|
||||
|
||||
function fileIsNewerThan(path1, path2) {
|
||||
if (!fs.existsSync(path2)) return true;
|
||||
|
||||
const stat1 = fs.statSync(path1);
|
||||
const stat2 = fs.statSync(path2);
|
||||
|
||||
return stat1.mtime > stat2.mtime;
|
||||
}
|
||||
|
||||
fs.readdirSync(guiPath).forEach((filename) => {
|
||||
const jsxPath = guiPath + '/' + filename;
|
||||
const p = jsxPath.split('.');
|
||||
if (p.length <= 1) return;
|
||||
const ext = p[p.length - 1];
|
||||
if (ext !== 'jsx') return;
|
||||
p.pop();
|
||||
|
||||
const basePath = p.join('/');
|
||||
|
||||
const jsPath = basePath + '.min.js';
|
||||
|
||||
if (fileIsNewerThan(jsxPath, jsPath)) {
|
||||
console.info('Compiling ' + jsxPath + '...');
|
||||
const result = spawnSync(babelPath, ['--presets', 'react', '--out-file',jsPath, jsxPath]);
|
||||
if (result.status !== 0) {
|
||||
const msg = [];
|
||||
if (result.stdout) msg.push(result.stdout.toString());
|
||||
if (result.stderr) msg.push(result.stderr.toString());
|
||||
console.error(msg.join('\n'));
|
||||
if (result.error) console.error(result.error);
|
||||
process.exit(result.status);
|
||||
}
|
||||
}
|
||||
});
|
147
ElectronClient/app/gui/ConfigScreen.min.js
vendored
147
ElectronClient/app/gui/ConfigScreen.min.js
vendored
@ -1,147 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class ConfigScreenComponent extends React.Component {
|
||||
|
||||
settingToComponent(key, value) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
let output = null;
|
||||
|
||||
const rowStyle = {
|
||||
marginBottom: 10
|
||||
};
|
||||
|
||||
const labelStyle = Object.assign({}, theme.textStyle, {
|
||||
display: 'inline-block',
|
||||
marginRight: 10
|
||||
});
|
||||
|
||||
const controlStyle = {
|
||||
display: 'inline-block'
|
||||
};
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
Setting.setValue(key, value);
|
||||
};
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
|
||||
if (md.isEnum) {
|
||||
let items = [];
|
||||
const settingOptions = md.options();
|
||||
for (let k in settingOptions) {
|
||||
if (!settingOptions.hasOwnProperty(k)) continue;
|
||||
items.push(React.createElement(
|
||||
'option',
|
||||
{ value: k.toString(), key: k },
|
||||
settingOptions[k]
|
||||
));
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ key: key, style: rowStyle },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: labelStyle },
|
||||
React.createElement(
|
||||
'label',
|
||||
null,
|
||||
md.label()
|
||||
)
|
||||
),
|
||||
React.createElement(
|
||||
'select',
|
||||
{ value: value, style: controlStyle, onChange: event => {
|
||||
updateSettingValue(key, event.target.value);
|
||||
} },
|
||||
items
|
||||
)
|
||||
);
|
||||
} else if (md.type === Setting.TYPE_BOOL) {
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ key: key, style: rowStyle },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: controlStyle },
|
||||
React.createElement(
|
||||
'label',
|
||||
null,
|
||||
React.createElement('input', { type: 'checkbox', defaultChecked: !!value, onChange: event => {
|
||||
updateSettingValue(key, !!event.target.checked);
|
||||
} }),
|
||||
React.createElement(
|
||||
'span',
|
||||
{ style: labelStyle },
|
||||
' ',
|
||||
md.label()
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const settings = this.props.settings;
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
padding: 10
|
||||
};
|
||||
|
||||
let settingComps = [];
|
||||
let keys = Setting.keys(true, 'desktop');
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (key === 'sync.target') continue;
|
||||
if (!(key in settings)) {
|
||||
console.warn('Missing setting: ' + key);
|
||||
continue;
|
||||
}
|
||||
const comp = this.settingToComponent(key, settings[key]);
|
||||
if (!comp) continue;
|
||||
settingComps.push(comp);
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: style },
|
||||
React.createElement(Header, { style: headerStyle }),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: containerStyle },
|
||||
settingComps
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
settings: state.settings,
|
||||
locale: state.settings.locale
|
||||
};
|
||||
};
|
||||
|
||||
const ConfigScreen = connect(mapStateToProps)(ConfigScreenComponent);
|
||||
|
||||
module.exports = { ConfigScreen };
|
||||
|
101
ElectronClient/app/gui/Header.min.js
vendored
101
ElectronClient/app/gui/Header.min.js
vendored
@ -1,101 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class HeaderComponent extends React.Component {
|
||||
|
||||
back_click() {
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
}
|
||||
|
||||
makeButton(key, style, options) {
|
||||
let icon = null;
|
||||
if (options.iconName) {
|
||||
const iconStyle = {
|
||||
fontSize: Math.round(style.fontSize * 1.4),
|
||||
color: style.color
|
||||
};
|
||||
if (options.title) iconStyle.marginRight = 5;
|
||||
icon = React.createElement('i', { style: iconStyle, className: "fa " + options.iconName });
|
||||
}
|
||||
|
||||
const isEnabled = !('enabled' in options) || options.enabled;
|
||||
let classes = ['button'];
|
||||
if (!isEnabled) classes.push('disabled');
|
||||
|
||||
const finalStyle = Object.assign({}, style, {
|
||||
opacity: isEnabled ? 1 : 0.4
|
||||
});
|
||||
|
||||
return React.createElement(
|
||||
'a',
|
||||
{
|
||||
className: classes.join(' '),
|
||||
style: finalStyle,
|
||||
key: key,
|
||||
href: '#',
|
||||
onClick: () => {
|
||||
if (isEnabled) options.onClick();
|
||||
}
|
||||
},
|
||||
icon,
|
||||
options.title ? options.title : ''
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const showBackButton = this.props.showBackButton === undefined || this.props.showBackButton === true;
|
||||
style.height = theme.headerHeight;
|
||||
style.display = 'flex';
|
||||
style.flexDirection = 'row';
|
||||
style.borderBottom = '1px solid ' + theme.dividerColor;
|
||||
style.boxSizing = 'border-box';
|
||||
|
||||
const buttons = [];
|
||||
|
||||
const buttonStyle = {
|
||||
height: theme.headerHeight,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: theme.headerButtonHPadding,
|
||||
paddingRight: theme.headerButtonHPadding,
|
||||
color: theme.color,
|
||||
textDecoration: 'none',
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: theme.fontSize,
|
||||
boxSizing: 'border-box',
|
||||
cursor: 'default'
|
||||
};
|
||||
|
||||
if (showBackButton) {
|
||||
buttons.push(this.makeButton('back', buttonStyle, { title: _('Back'), onClick: () => this.back_click(), iconName: 'fa-chevron-left ' }));
|
||||
}
|
||||
|
||||
if (this.props.buttons) {
|
||||
for (let i = 0; i < this.props.buttons.length; i++) {
|
||||
const o = this.props.buttons[i];
|
||||
buttons.push(this.makeButton('btn_' + i + '_' + o.title, buttonStyle, o));
|
||||
}
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: 'header', style: style },
|
||||
buttons
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return { theme: state.settings.theme };
|
||||
};
|
||||
|
||||
const Header = connect(mapStateToProps)(HeaderComponent);
|
||||
|
||||
module.exports = { Header };
|
||||
|
41
ElectronClient/app/gui/IconButton.min.js
vendored
41
ElectronClient/app/gui/IconButton.min.js
vendored
@ -1,41 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
||||
class IconButton extends React.Component {
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const iconStyle = {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize * 1.4
|
||||
};
|
||||
const icon = React.createElement('i', { style: iconStyle, className: "fa " + this.props.iconName });
|
||||
|
||||
const rootStyle = Object.assign({
|
||||
display: 'flex',
|
||||
textDecoration: 'none',
|
||||
padding: 10,
|
||||
width: theme.buttonMinHeight,
|
||||
height: theme.buttonMinHeight,
|
||||
boxSizing: 'border-box',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
cursor: 'default'
|
||||
}, style);
|
||||
|
||||
return React.createElement(
|
||||
'a',
|
||||
{ href: '#', style: rootStyle, className: 'icon-button', onClick: () => {
|
||||
if (this.props.onClick) this.props.onClick();
|
||||
} },
|
||||
icon
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { IconButton };
|
||||
|
143
ElectronClient/app/gui/ImportScreen.min.js
vendored
143
ElectronClient/app/gui/ImportScreen.min.js
vendored
@ -1,143 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { filename, basename } = require('lib/path-utils.js');
|
||||
const { importEnex } = require('lib/import-enex');
|
||||
|
||||
class ImportScreenComponent extends React.Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
doImport: true,
|
||||
filePath: this.props.filePath,
|
||||
messages: []
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.filePath) {
|
||||
this.setState({
|
||||
doImport: true,
|
||||
filePath: newProps.filePath,
|
||||
messages: []
|
||||
});
|
||||
|
||||
this.doImport();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (this.state.filePath && this.state.doImport) {
|
||||
this.doImport();
|
||||
}
|
||||
}
|
||||
|
||||
addMessage(key, text) {
|
||||
const messages = this.state.messages.slice();
|
||||
let found = false;
|
||||
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
if (messages[i].key === key) {
|
||||
messages[i].text = text;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) messages.push({ key: key, text: text });
|
||||
|
||||
this.setState({ messages: messages });
|
||||
}
|
||||
|
||||
async doImport() {
|
||||
const filePath = this.props.filePath;
|
||||
const folderTitle = await Folder.findUniqueFolderTitle(filename(filePath));
|
||||
const messages = this.state.messages.slice();
|
||||
|
||||
this.addMessage('start', _('New notebook "%s" will be created and file "%s" will be imported into it', folderTitle, basename(filePath)));
|
||||
|
||||
let lastProgress = '';
|
||||
let progressCount = 0;
|
||||
|
||||
const options = {
|
||||
onProgress: progressState => {
|
||||
let line = [];
|
||||
line.push(_('Found: %d.', progressState.loaded));
|
||||
line.push(_('Created: %d.', progressState.created));
|
||||
if (progressState.updated) line.push(_('Updated: %d.', progressState.updated));
|
||||
if (progressState.skipped) line.push(_('Skipped: %d.', progressState.skipped));
|
||||
if (progressState.resourcesCreated) line.push(_('Resources: %d.', progressState.resourcesCreated));
|
||||
if (progressState.notesTagged) line.push(_('Tagged: %d.', progressState.notesTagged));
|
||||
lastProgress = line.join(' ');
|
||||
this.addMessage('progress', lastProgress);
|
||||
},
|
||||
onError: error => {
|
||||
const messages = this.state.messages.slice();
|
||||
let s = error.trace ? error.trace : error.toString();
|
||||
messages.push({ key: 'error_' + progressCount++, text: s });
|
||||
this.addMessage('error_' + progressCount++, lastProgress);
|
||||
}
|
||||
};
|
||||
|
||||
const folder = await Folder.save({ title: folderTitle });
|
||||
|
||||
await importEnex(folder.id, filePath, options);
|
||||
|
||||
this.addMessage('done', _('The notes have been imported: %s', lastProgress));
|
||||
this.setState({ doImport: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
const messages = this.state.messages;
|
||||
|
||||
const messagesStyle = {
|
||||
padding: 10,
|
||||
fontSize: theme.fontSize,
|
||||
fontFamily: theme.fontFamily,
|
||||
backgroundColor: theme.backgroundColor
|
||||
};
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width
|
||||
};
|
||||
|
||||
const messageComps = [];
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
messageComps.push(React.createElement(
|
||||
'div',
|
||||
{ key: messages[i].key },
|
||||
messages[i].text
|
||||
));
|
||||
}
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: {} },
|
||||
React.createElement(Header, { style: headerStyle }),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: messagesStyle },
|
||||
messageComps
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme
|
||||
};
|
||||
};
|
||||
|
||||
const ImportScreen = connect(mapStateToProps)(ImportScreenComponent);
|
||||
|
||||
module.exports = { ImportScreen };
|
||||
|
77
ElectronClient/app/gui/ItemList.min.js
vendored
77
ElectronClient/app/gui/ItemList.min.js
vendored
@ -1,77 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
class ItemList extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.scrollTop_ = 0;
|
||||
}
|
||||
|
||||
updateStateItemIndexes(props) {
|
||||
if (typeof props === 'undefined') props = this.props;
|
||||
|
||||
const topItemIndex = Math.floor(this.scrollTop_ / props.itemHeight);
|
||||
const visibleItemCount = Math.ceil(props.style.height / props.itemHeight);
|
||||
|
||||
let bottomItemIndex = topItemIndex + visibleItemCount;
|
||||
if (bottomItemIndex >= props.items.length) bottomItemIndex = props.items.length - 1;
|
||||
|
||||
this.setState({
|
||||
topItemIndex: topItemIndex,
|
||||
bottomItemIndex: bottomItemIndex
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
onScroll(scrollTop) {
|
||||
this.scrollTop_ = scrollTop;
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
render() {
|
||||
const items = this.props.items;
|
||||
const style = Object.assign({}, this.props.style, {
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto'
|
||||
});
|
||||
|
||||
if (!this.props.itemHeight) throw new Error('itemHeight is required');
|
||||
|
||||
const blankItem = function (key, height) {
|
||||
return React.createElement('div', { key: key, style: { height: height } });
|
||||
};
|
||||
|
||||
let itemComps = [blankItem('top', this.state.topItemIndex * this.props.itemHeight)];
|
||||
|
||||
for (let i = this.state.topItemIndex; i <= this.state.bottomItemIndex; i++) {
|
||||
const itemComp = this.props.itemRenderer(items[i]);
|
||||
itemComps.push(itemComp);
|
||||
}
|
||||
|
||||
itemComps.push(blankItem('bottom', (items.length - this.state.bottomItemIndex - 1) * this.props.itemHeight));
|
||||
|
||||
let classes = ['item-list'];
|
||||
if (this.props.className) classes.push(this.props.className);
|
||||
|
||||
const that = this;
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: classes.join(' '), style: style, onScroll: event => {
|
||||
this.onScroll(event.target.scrollTop);
|
||||
} },
|
||||
itemComps
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ItemList };
|
||||
|
58
ElectronClient/app/gui/Main.min.js
vendored
58
ElectronClient/app/gui/Main.min.js
vendored
@ -1,58 +0,0 @@
|
||||
const React = require('react');
|
||||
const { render } = require('react-dom');
|
||||
const { createStore } = require('redux');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
|
||||
const { SideBar } = require('./SideBar.min.js');
|
||||
const { NoteList } = require('./NoteList.min.js');
|
||||
const { NoteText } = require('./NoteText.min.js');
|
||||
|
||||
const { app } = require('../app');
|
||||
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
class MainComponent extends React.Component {
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
|
||||
const noteListStyle = {
|
||||
width: Math.floor(style.width / 3),
|
||||
height: style.height,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top'
|
||||
};
|
||||
|
||||
const noteTextStyle = {
|
||||
width: noteListStyle.width,
|
||||
height: style.height,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top'
|
||||
};
|
||||
|
||||
const sideBarStyle = {
|
||||
width: style.width - (noteTextStyle.width + noteListStyle.width),
|
||||
height: style.height,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top'
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: style },
|
||||
React.createElement(SideBar, { style: sideBarStyle }),
|
||||
React.createElement(NoteList, { itemHeight: 40, style: noteListStyle }),
|
||||
React.createElement(NoteText, { style: noteTextStyle })
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const Main = connect(mapStateToProps)(MainComponent);
|
||||
|
||||
module.exports = { Main };
|
||||
|
256
ElectronClient/app/gui/MainScreen.min.js
vendored
256
ElectronClient/app/gui/MainScreen.min.js
vendored
@ -1,256 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { SideBar } = require('./SideBar.min.js');
|
||||
const { NoteList } = require('./NoteList.min.js');
|
||||
const { NoteText } = require('./NoteText.min.js');
|
||||
const { PromptDialog } = require('./PromptDialog.min.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const layoutUtils = require('lib/layout-utils.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
class MainScreenComponent extends React.Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
promptOptions: null
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.windowCommand) {
|
||||
this.doCommand(newProps.windowCommand);
|
||||
}
|
||||
}
|
||||
|
||||
toggleVisiblePanes() {
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_VISIBLE_PANES_TOGGLE'
|
||||
});
|
||||
}
|
||||
|
||||
async doCommand(command) {
|
||||
if (!command) return;
|
||||
|
||||
const createNewNote = async (title, isTodo) => {
|
||||
const folderId = Setting.value('activeFolderId');
|
||||
if (!folderId) return;
|
||||
|
||||
const note = await Note.save({
|
||||
title: title,
|
||||
parent_id: folderId,
|
||||
is_todo: isTodo ? 1 : 0
|
||||
});
|
||||
Note.updateGeolocation(note.id);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: note.id
|
||||
});
|
||||
};
|
||||
|
||||
let commandProcessed = true;
|
||||
|
||||
if (command.name === 'newNote') {
|
||||
if (!this.props.folders.length) {
|
||||
bridge().showErrorMessageBox(_('Please create a notebook first.'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
label: _('Note title:'),
|
||||
onClose: async answer => {
|
||||
if (answer) await createNewNote(answer, false);
|
||||
this.setState({ promptOptions: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (command.name === 'newTodo') {
|
||||
if (!this.props.folders.length) {
|
||||
bridge().showErrorMessageBox(_('Please create a notebook first'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
label: _('To-do title:'),
|
||||
onClose: async answer => {
|
||||
if (answer) await createNewNote(answer, true);
|
||||
this.setState({ promptOptions: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (command.name === 'newNotebook') {
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
label: _('Notebook title:'),
|
||||
onClose: async answer => {
|
||||
if (answer) {
|
||||
let folder = null;
|
||||
try {
|
||||
folder = await Folder.save({ title: answer }, { userSideValidation: true });
|
||||
} catch (error) {
|
||||
bridge().showErrorMessageBox(error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder.id
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({ promptOptions: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (command.name === 'setTags') {
|
||||
const tags = await Tag.tagsByNoteId(command.noteId);
|
||||
const tagTitles = tags.map(a => {
|
||||
return a.title;
|
||||
});
|
||||
|
||||
this.setState({
|
||||
promptOptions: {
|
||||
label: _('Add or remove tags:'),
|
||||
description: _('Separate each tag by a comma.'),
|
||||
value: tagTitles.join(', '),
|
||||
onClose: async answer => {
|
||||
if (answer !== null) {
|
||||
const tagTitles = answer.split(',').map(a => {
|
||||
return a.trim();
|
||||
});
|
||||
await Tag.setNoteTagsByTitles(command.noteId, tagTitles);
|
||||
}
|
||||
this.setState({ promptOptions: null });
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
commandProcessed = false;
|
||||
}
|
||||
|
||||
if (commandProcessed) {
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const promptOptions = this.state.promptOptions;
|
||||
const folders = this.props.folders;
|
||||
const notes = this.props.notes;
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width
|
||||
};
|
||||
|
||||
const rowHeight = style.height - theme.headerHeight;
|
||||
|
||||
const sideBarStyle = {
|
||||
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
|
||||
height: rowHeight,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top'
|
||||
};
|
||||
|
||||
const noteListStyle = {
|
||||
width: Math.floor(layoutUtils.size(style.width * .2, 150, 300)),
|
||||
height: rowHeight,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top'
|
||||
};
|
||||
|
||||
const noteTextStyle = {
|
||||
width: Math.floor(layoutUtils.size(style.width - sideBarStyle.width - noteListStyle.width, 0)),
|
||||
height: rowHeight,
|
||||
display: 'inline-block',
|
||||
verticalAlign: 'top'
|
||||
};
|
||||
|
||||
const promptStyle = {
|
||||
width: style.width,
|
||||
height: style.height
|
||||
};
|
||||
|
||||
const headerButtons = [];
|
||||
|
||||
headerButtons.push({
|
||||
title: _('New note'),
|
||||
iconName: 'fa-file-o',
|
||||
enabled: !!folders.length,
|
||||
onClick: () => {
|
||||
this.doCommand({ name: 'newNote' });
|
||||
}
|
||||
});
|
||||
|
||||
headerButtons.push({
|
||||
title: _('New to-do'),
|
||||
iconName: 'fa-check-square-o',
|
||||
enabled: !!folders.length,
|
||||
onClick: () => {
|
||||
this.doCommand({ name: 'newTodo' });
|
||||
}
|
||||
});
|
||||
|
||||
headerButtons.push({
|
||||
title: _('New notebook'),
|
||||
iconName: 'fa-folder-o',
|
||||
onClick: () => {
|
||||
this.doCommand({ name: 'newNotebook' });
|
||||
}
|
||||
});
|
||||
|
||||
headerButtons.push({
|
||||
title: _('Layout'),
|
||||
iconName: 'fa-columns',
|
||||
enabled: !!notes.length,
|
||||
onClick: () => {
|
||||
this.toggleVisiblePanes();
|
||||
}
|
||||
});
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: style },
|
||||
React.createElement(PromptDialog, {
|
||||
value: promptOptions && promptOptions.value ? promptOptions.value : '',
|
||||
theme: this.props.theme,
|
||||
style: promptStyle,
|
||||
onClose: answer => promptOptions.onClose(answer),
|
||||
label: promptOptions ? promptOptions.label : '',
|
||||
description: promptOptions ? promptOptions.description : null,
|
||||
visible: !!this.state.promptOptions }),
|
||||
React.createElement(Header, { style: headerStyle, showBackButton: false, buttons: headerButtons }),
|
||||
React.createElement(SideBar, { style: sideBarStyle }),
|
||||
React.createElement(NoteList, { style: noteListStyle }),
|
||||
React.createElement(NoteText, { style: noteTextStyle, visiblePanes: this.props.noteVisiblePanes })
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
windowCommand: state.windowCommand,
|
||||
noteVisiblePanes: state.noteVisiblePanes,
|
||||
folders: state.folders,
|
||||
notes: state.notes
|
||||
};
|
||||
};
|
||||
|
||||
const MainScreen = connect(mapStateToProps)(MainScreenComponent);
|
||||
|
||||
module.exports = { MainScreen };
|
||||
|
54
ElectronClient/app/gui/Navigator.min.js
vendored
54
ElectronClient/app/gui/Navigator.min.js
vendored
@ -1,54 +0,0 @@
|
||||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
||||
|
||||
const React = require('react');const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { app } = require('../app.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
class NavigatorComponent extends Component {
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if (newProps.route) {
|
||||
const screenInfo = this.props.screens[newProps.route.routeName];
|
||||
let windowTitle = ['Joplin'];
|
||||
if (screenInfo.title) {
|
||||
windowTitle.push(screenInfo.title());
|
||||
}
|
||||
this.updateWindowTitle(windowTitle.join(' - '));
|
||||
}
|
||||
}
|
||||
|
||||
updateWindowTitle(title) {
|
||||
bridge().window().setTitle(title);
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.route) throw new Error('Route must not be null');
|
||||
|
||||
const route = this.props.route;
|
||||
const screenProps = route.props ? route.props : {};
|
||||
const screenInfo = this.props.screens[route.routeName];
|
||||
const Screen = screenInfo.screen;
|
||||
|
||||
const screenStyle = {
|
||||
width: this.props.style.width,
|
||||
height: this.props.style.height
|
||||
};
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: this.props.style },
|
||||
React.createElement(Screen, _extends({ style: screenStyle }, screenProps))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const Navigator = connect(state => {
|
||||
return {
|
||||
route: state.route
|
||||
};
|
||||
})(NavigatorComponent);
|
||||
|
||||
module.exports = { Navigator };
|
||||
|
188
ElectronClient/app/gui/NoteList.min.js
vendored
188
ElectronClient/app/gui/NoteList.min.js
vendored
@ -1,188 +0,0 @@
|
||||
const { ItemList } = require('./ItemList.min.js');
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
|
||||
class NoteListComponent extends React.Component {
|
||||
|
||||
style() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const itemHeight = 34;
|
||||
|
||||
let style = {
|
||||
root: {
|
||||
backgroundColor: theme.backgroundColor
|
||||
},
|
||||
listItem: {
|
||||
height: itemHeight,
|
||||
boxSizing: 'border-box',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderBottom: '1px solid ' + theme.dividerColor
|
||||
},
|
||||
listItemSelected: {
|
||||
backgroundColor: theme.selectedColor
|
||||
},
|
||||
listItemTitle: {
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: theme.fontSize,
|
||||
textDecoration: 'none',
|
||||
color: theme.color,
|
||||
cursor: 'default',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
overflow: 'hidden'
|
||||
},
|
||||
listItemTitleCompleted: {
|
||||
opacity: 0.5,
|
||||
textDecoration: 'line-through'
|
||||
}
|
||||
};
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
const noteId = event.target.getAttribute('data-id');
|
||||
if (!noteId) throw new Error('No data-id on element');
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
menu.append(new MenuItem({ label: _('Add or remove tags'), click: async () => {
|
||||
this.props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'setTags',
|
||||
noteId: noteId
|
||||
});
|
||||
} }));
|
||||
|
||||
menu.append(new MenuItem({ label: _('Switch between note and to-do'), click: async () => {
|
||||
const note = await Note.load(noteId);
|
||||
await Note.save(Note.toggleIsTodo(note));
|
||||
} }));
|
||||
|
||||
menu.append(new MenuItem({ label: _('Delete'), click: async () => {
|
||||
const ok = bridge().showConfirmMessageBox(_('Delete note?'));
|
||||
if (!ok) return;
|
||||
await Note.delete(noteId);
|
||||
} }));
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
|
||||
itemRenderer(item, theme, width) {
|
||||
const onTitleClick = async (event, item) => {
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: item.id
|
||||
});
|
||||
};
|
||||
|
||||
const onCheckboxClick = async event => {
|
||||
const checked = event.target.checked;
|
||||
const newNote = {
|
||||
id: item.id,
|
||||
todo_completed: checked ? time.unixMs() : 0
|
||||
};
|
||||
await Note.save(newNote);
|
||||
};
|
||||
|
||||
const hPadding = 10;
|
||||
|
||||
let style = Object.assign({ width: width }, this.style().listItem);
|
||||
if (this.props.selectedNoteId === item.id) style = Object.assign(style, this.style().listItemSelected);
|
||||
|
||||
// Setting marginBottom = 1 because it makes the checkbox looks more centered, at least on Windows
|
||||
// but don't know how it will look in other OSes.
|
||||
const checkbox = item.is_todo ? React.createElement(
|
||||
'div',
|
||||
{ style: { display: 'flex', height: style.height, alignItems: 'center', paddingLeft: hPadding } },
|
||||
React.createElement('input', { style: { margin: 0, marginBottom: 1 }, type: 'checkbox', defaultChecked: !!item.todo_completed, onClick: event => {
|
||||
onCheckboxClick(event, item);
|
||||
} })
|
||||
) : null;
|
||||
|
||||
let listItemTitleStyle = Object.assign({}, this.style().listItemTitle);
|
||||
listItemTitleStyle.paddingLeft = !checkbox ? hPadding : 4;
|
||||
if (item.is_todo && !!item.todo_completed) listItemTitleStyle = Object.assign(listItemTitleStyle, this.style().listItemTitleCompleted);
|
||||
|
||||
// Need to include "todo_completed" in key so that checkbox is updated when
|
||||
// item is changed via sync.
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ key: item.id + '_' + item.todo_completed, style: style },
|
||||
checkbox,
|
||||
React.createElement(
|
||||
'a',
|
||||
{
|
||||
'data-id': item.id,
|
||||
className: 'list-item',
|
||||
onContextMenu: event => this.itemContextMenu(event),
|
||||
href: '#',
|
||||
style: listItemTitleStyle,
|
||||
onClick: event => {
|
||||
onTitleClick(event, item);
|
||||
}
|
||||
},
|
||||
item.title
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
|
||||
if (!this.props.notes.length) {
|
||||
const padding = 10;
|
||||
const emptyDivStyle = Object.assign({
|
||||
padding: padding + 'px',
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontFamily: theme.fontFamily
|
||||
}, style);
|
||||
emptyDivStyle.width = emptyDivStyle.width - padding * 2;
|
||||
emptyDivStyle.height = emptyDivStyle.height - padding * 2;
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: emptyDivStyle },
|
||||
_('No notes in here. Create one by clicking on "New note".')
|
||||
);
|
||||
}
|
||||
|
||||
return React.createElement(ItemList, {
|
||||
itemHeight: this.style().listItem.height,
|
||||
style: style,
|
||||
className: "note-list",
|
||||
items: this.props.notes,
|
||||
itemRenderer: item => {
|
||||
return this.itemRenderer(item, theme, style.width);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
notes: state.notes,
|
||||
selectedNoteId: state.selectedNoteId,
|
||||
theme: state.settings.theme
|
||||
// uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
};
|
||||
};
|
||||
|
||||
const NoteList = connect(mapStateToProps)(NoteListComponent);
|
||||
|
||||
module.exports = { NoteList };
|
||||
|
525
ElectronClient/app/gui/NoteText.min.js
vendored
525
ElectronClient/app/gui/NoteText.min.js
vendored
@ -1,525 +0,0 @@
|
||||
const React = require('react');
|
||||
const { Note } = require('lib/models/note.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { IconButton } = require('./IconButton.min.js');
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const MdToHtml = require('lib/MdToHtml');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const AceEditor = require('react-ace').default;
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
const { shim } = require('lib/shim.js');
|
||||
|
||||
require('brace/mode/markdown');
|
||||
// https://ace.c9.io/build/kitchen-sink.html
|
||||
// https://highlightjs.org/static/demo/
|
||||
require('brace/theme/chrome');
|
||||
|
||||
class NoteTextComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
note: null,
|
||||
noteMetadata: '',
|
||||
showNoteMetadata: false,
|
||||
folder: null,
|
||||
lastSavedNote: null,
|
||||
isLoading: true,
|
||||
webviewReady: false,
|
||||
scrollHeight: null,
|
||||
editorScrollTop: 0
|
||||
};
|
||||
|
||||
this.lastLoadedNoteId_ = null;
|
||||
|
||||
this.webviewListeners_ = null;
|
||||
this.ignoreNextEditorScroll_ = false;
|
||||
this.scheduleSaveTimeout_ = null;
|
||||
this.restoreScrollTop_ = null;
|
||||
|
||||
// Complicated but reliable method to get editor content height
|
||||
// https://github.com/ajaxorg/ace/issues/2046
|
||||
this.editorMaxScrollTop_ = 0;
|
||||
this.onAfterEditorRender_ = () => {
|
||||
const r = this.editor_.editor.renderer;
|
||||
this.editorMaxScrollTop_ = Math.max(0, r.layerConfig.maxHeight - r.$size.scrollerHeight);
|
||||
|
||||
if (this.restoreScrollTop_ !== null) {
|
||||
this.editorSetScrollTop(this.restoreScrollTop_);
|
||||
this.restoreScrollTop_ = null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mdToHtml() {
|
||||
if (this.mdToHtml_) return this.mdToHtml_;
|
||||
this.mdToHtml_ = new MdToHtml({ supportsResourceLinks: true });
|
||||
return this.mdToHtml_;
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
let note = null;
|
||||
if (this.props.noteId) {
|
||||
note = await Note.load(this.props.noteId);
|
||||
}
|
||||
|
||||
const folder = note ? Folder.byId(this.props.folders, note.parent_id) : null;
|
||||
|
||||
this.setState({
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
note: note,
|
||||
folder: folder,
|
||||
isLoading: false
|
||||
});
|
||||
|
||||
this.lastLoadedNoteId_ = note ? note.id : null;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.saveIfNeeded();
|
||||
|
||||
this.mdToHtml_ = null;
|
||||
this.destroyWebview();
|
||||
}
|
||||
|
||||
async saveIfNeeded() {
|
||||
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
|
||||
this.scheduleSaveTimeout_ = null;
|
||||
if (!shared.isModified(this)) return;
|
||||
await shared.saveNoteButton_press(this);
|
||||
}
|
||||
|
||||
async saveOneProperty(name, value) {
|
||||
await shared.saveOneProperty(this, name, value);
|
||||
}
|
||||
|
||||
scheduleSave() {
|
||||
if (this.scheduleSaveTimeout_) clearTimeout(this.scheduleSaveTimeout_);
|
||||
this.scheduleSaveTimeout_ = setTimeout(() => {
|
||||
this.saveIfNeeded();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async reloadNote(props) {
|
||||
this.mdToHtml_ = null;
|
||||
|
||||
const noteId = props.noteId;
|
||||
this.lastLoadedNoteId_ = noteId;
|
||||
const note = noteId ? await Note.load(noteId) : null;
|
||||
if (noteId !== this.lastLoadedNoteId_) return; // Race condition - current note was changed while this one was loading
|
||||
|
||||
// If we are loading nothing (noteId == null), make sure to
|
||||
// set webviewReady to false too because the webview component
|
||||
// is going to be removed in render().
|
||||
const webviewReady = this.webview_ && this.state.webviewReady && noteId;
|
||||
|
||||
this.editorMaxScrollTop_ = 0;
|
||||
|
||||
// HACK: To go around a bug in Ace editor, we first set the scroll position to 1
|
||||
// and then (in the renderer callback) to the value we actually need. The first
|
||||
// operation helps clear the scroll position cache. See:
|
||||
// https://github.com/ajaxorg/ace/issues/2195
|
||||
this.editorSetScrollTop(1);
|
||||
this.restoreScrollTop_ = 0;
|
||||
|
||||
this.setState({
|
||||
note: note,
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
webviewReady: webviewReady
|
||||
});
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||
await this.reloadNote(nextProps);
|
||||
}
|
||||
|
||||
if ('syncStarted' in nextProps && !nextProps.syncStarted && !this.isModified()) {
|
||||
await this.reloadNote(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
isModified() {
|
||||
return shared.isModified(this);
|
||||
}
|
||||
|
||||
refreshNoteMetadata(force = null) {
|
||||
return shared.refreshNoteMetadata(this, force);
|
||||
}
|
||||
|
||||
title_changeText(event) {
|
||||
shared.noteComponent_change(this, 'title', event.target.value);
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
toggleIsTodo_onPress() {
|
||||
shared.toggleIsTodo_onPress(this);
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
showMetadata_onPress() {
|
||||
shared.showMetadata_onPress(this);
|
||||
}
|
||||
|
||||
webview_ipcMessage(event) {
|
||||
const msg = event.channel ? event.channel : '';
|
||||
const args = event.args;
|
||||
const arg0 = args && args.length >= 1 ? args[0] : null;
|
||||
const arg1 = args && args.length >= 2 ? args[1] : null;
|
||||
|
||||
reg.logger().debug('Got ipc-message: ' + msg, args);
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
// Ugly hack because setting the body here will make the scrollbar
|
||||
// go to some random position. So we save the scrollTop here and it
|
||||
// will be restored after the editor ref has been reset, and the
|
||||
// "afterRender" event has been called.
|
||||
this.restoreScrollTop_ = this.editorScrollTop();
|
||||
|
||||
const newBody = this.mdToHtml_.handleCheckboxClick(msg, this.state.note.body);
|
||||
this.saveOneProperty('body', newBody);
|
||||
} else if (msg.toLowerCase().indexOf('http') === 0) {
|
||||
require('electron').shell.openExternal(msg);
|
||||
} else if (msg === 'percentScroll') {
|
||||
this.ignoreNextEditorScroll_ = true;
|
||||
this.setEditorPercentScroll(arg0);
|
||||
} else if (msg.indexOf('joplin://') === 0) {
|
||||
const resourceId = msg.substr('joplin://'.length);
|
||||
Resource.load(resourceId).then(resource => {
|
||||
const filePath = Resource.fullPath(resource);
|
||||
bridge().openItem(filePath);
|
||||
});
|
||||
} else {
|
||||
bridge().showMessageBox({
|
||||
type: 'error',
|
||||
message: _('Unsupported link or message: %s', msg)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
editorMaxScroll() {
|
||||
return this.editorMaxScrollTop_;
|
||||
}
|
||||
|
||||
editorScrollTop() {
|
||||
return this.editor_.editor.getSession().getScrollTop();
|
||||
}
|
||||
|
||||
editorSetScrollTop(v) {
|
||||
if (!this.editor_) return;
|
||||
this.editor_.editor.getSession().setScrollTop(v);
|
||||
}
|
||||
|
||||
setEditorPercentScroll(p) {
|
||||
this.editorSetScrollTop(p * this.editorMaxScroll());
|
||||
}
|
||||
|
||||
setViewerPercentScroll(p) {
|
||||
this.webview_.send('setPercentScroll', p);
|
||||
}
|
||||
|
||||
editor_scroll() {
|
||||
if (this.ignoreNextEditorScroll_) {
|
||||
this.ignoreNextEditorScroll_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const m = this.editorMaxScroll();
|
||||
this.setViewerPercentScroll(m ? this.editorScrollTop() / m : 0);
|
||||
}
|
||||
|
||||
webview_domReady() {
|
||||
if (!this.webview_) return;
|
||||
|
||||
this.setState({
|
||||
webviewReady: true
|
||||
});
|
||||
|
||||
// if (Setting.value('env') === 'dev') this.webview_.openDevTools();
|
||||
}
|
||||
|
||||
webview_ref(element) {
|
||||
if (this.webview_) {
|
||||
if (this.webview_ === element) return;
|
||||
this.destroyWebview();
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
this.destroyWebview();
|
||||
} else {
|
||||
this.initWebview(element);
|
||||
}
|
||||
}
|
||||
|
||||
editor_ref(element) {
|
||||
if (this.editor_ === element) return;
|
||||
|
||||
if (this.editor_) {
|
||||
this.editor_.editor.renderer.off('afterRender', this.onAfterEditorRender_);
|
||||
}
|
||||
|
||||
this.editor_ = element;
|
||||
|
||||
if (this.editor_) {
|
||||
this.editor_.editor.renderer.on('afterRender', this.onAfterEditorRender_);
|
||||
}
|
||||
}
|
||||
|
||||
initWebview(wv) {
|
||||
if (!this.webviewListeners_) {
|
||||
this.webviewListeners_ = {
|
||||
'dom-ready': this.webview_domReady.bind(this),
|
||||
'ipc-message': this.webview_ipcMessage.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
for (let n in this.webviewListeners_) {
|
||||
if (!this.webviewListeners_.hasOwnProperty(n)) continue;
|
||||
const fn = this.webviewListeners_[n];
|
||||
wv.addEventListener(n, fn);
|
||||
}
|
||||
|
||||
this.webview_ = wv;
|
||||
}
|
||||
|
||||
destroyWebview() {
|
||||
if (!this.webview_) return;
|
||||
|
||||
for (let n in this.webviewListeners_) {
|
||||
if (!this.webviewListeners_.hasOwnProperty(n)) continue;
|
||||
const fn = this.webviewListeners_[n];
|
||||
this.webview_.removeEventListener(n, fn);
|
||||
}
|
||||
|
||||
this.webview_ = null;
|
||||
}
|
||||
|
||||
aceEditor_change(body) {
|
||||
shared.noteComponent_change(this, 'body', body);
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
const noteId = this.props.noteId;
|
||||
if (!noteId) return;
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
menu.append(new MenuItem({ label: _('Attach file'), click: async () => {
|
||||
const filePaths = bridge().showOpenDialog({
|
||||
properties: ['openFile', 'createDirectory']
|
||||
});
|
||||
if (!filePaths || !filePaths.length) return;
|
||||
|
||||
await this.saveIfNeeded();
|
||||
const note = await Note.load(noteId);
|
||||
const newNote = await shim.attachFileToNote(note, filePaths[0]);
|
||||
|
||||
this.setState({
|
||||
note: newNote,
|
||||
lastSavedNote: Object.assign({}, newNote)
|
||||
});
|
||||
} }));
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const note = this.state.note;
|
||||
const body = note ? note.body : '';
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const visiblePanes = this.props.visiblePanes || ['editor', 'viewer'];
|
||||
|
||||
const borderWidth = 1;
|
||||
|
||||
const rootStyle = Object.assign({
|
||||
borderLeft: borderWidth + 'px solid ' + theme.dividerColor,
|
||||
boxSizing: 'border-box',
|
||||
paddingLeft: 10,
|
||||
paddingRight: 0
|
||||
}, style);
|
||||
|
||||
const innerWidth = rootStyle.width - rootStyle.paddingLeft - rootStyle.paddingRight - borderWidth;
|
||||
|
||||
if (!note) {
|
||||
const emptyDivStyle = Object.assign({
|
||||
backgroundColor: 'black',
|
||||
opacity: 0.1
|
||||
}, rootStyle);
|
||||
return React.createElement('div', { style: emptyDivStyle });
|
||||
}
|
||||
|
||||
const titleBarStyle = {
|
||||
width: innerWidth - rootStyle.paddingLeft,
|
||||
height: 30,
|
||||
boxSizing: 'border-box',
|
||||
marginTop: 10,
|
||||
marginBottom: 10,
|
||||
display: 'flex',
|
||||
flexDirection: 'row'
|
||||
};
|
||||
|
||||
const titleEditorStyle = {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
display: 'inline-block',
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
marginRight: rootStyle.paddingLeft
|
||||
};
|
||||
|
||||
const bottomRowHeight = rootStyle.height - titleBarStyle.height - titleBarStyle.marginBottom - titleBarStyle.marginTop;
|
||||
|
||||
const viewerStyle = {
|
||||
width: Math.floor(innerWidth / 2),
|
||||
height: bottomRowHeight,
|
||||
overflow: 'hidden',
|
||||
float: 'left',
|
||||
verticalAlign: 'top',
|
||||
boxSizing: 'border-box'
|
||||
};
|
||||
|
||||
const paddingTop = 14;
|
||||
|
||||
const editorStyle = {
|
||||
width: innerWidth - viewerStyle.width,
|
||||
height: bottomRowHeight - paddingTop,
|
||||
overflowY: 'hidden',
|
||||
float: 'left',
|
||||
verticalAlign: 'top',
|
||||
paddingTop: paddingTop + 'px',
|
||||
lineHeight: theme.textAreaLineHeight + 'px',
|
||||
fontSize: theme.fontSize + 'px'
|
||||
};
|
||||
|
||||
if (visiblePanes.indexOf('viewer') < 0) {
|
||||
// Note: setting webview.display to "none" is currently not supported due
|
||||
// to this bug: https://github.com/electron/electron/issues/8277
|
||||
// So instead setting the width 0.
|
||||
viewerStyle.width = 0;
|
||||
editorStyle.width = innerWidth;
|
||||
}
|
||||
|
||||
if (visiblePanes.indexOf('editor') < 0) {
|
||||
editorStyle.display = 'none';
|
||||
viewerStyle.width = innerWidth;
|
||||
}
|
||||
|
||||
if (visiblePanes.indexOf('viewer') >= 0 && visiblePanes.indexOf('editor') >= 0) {
|
||||
viewerStyle.borderLeft = '1px solid ' + theme.dividerColor;
|
||||
} else {
|
||||
viewerStyle.borderLeft = 'none';
|
||||
}
|
||||
|
||||
if (this.state.webviewReady) {
|
||||
const mdOptions = {
|
||||
onResourceLoaded: () => {
|
||||
this.forceUpdate();
|
||||
},
|
||||
postMessageSyntax: 'ipcRenderer.sendToHost'
|
||||
};
|
||||
const html = this.mdToHtml().render(body, theme, mdOptions);
|
||||
this.webview_.send('setHtml', html);
|
||||
}
|
||||
|
||||
const titleEditor = React.createElement('input', {
|
||||
type: 'text',
|
||||
style: titleEditorStyle,
|
||||
value: note ? note.title : '',
|
||||
onChange: event => {
|
||||
this.title_changeText(event);
|
||||
}
|
||||
});
|
||||
|
||||
const titleBarMenuButton = React.createElement(IconButton, { style: {
|
||||
display: 'flex'
|
||||
}, iconName: 'fa-caret-down', theme: this.props.theme, onClick: () => {
|
||||
this.itemContextMenu();
|
||||
} });
|
||||
|
||||
const viewer = React.createElement('webview', {
|
||||
style: viewerStyle,
|
||||
nodeintegration: '1',
|
||||
src: 'gui/note-viewer/index.html',
|
||||
ref: elem => {
|
||||
this.webview_ref(elem);
|
||||
}
|
||||
});
|
||||
|
||||
const editorRootStyle = Object.assign({}, editorStyle);
|
||||
delete editorRootStyle.width;
|
||||
delete editorRootStyle.height;
|
||||
delete editorRootStyle.fontSize;
|
||||
|
||||
const editor = React.createElement(AceEditor, {
|
||||
value: body,
|
||||
mode: 'markdown',
|
||||
theme: 'chrome',
|
||||
style: editorRootStyle,
|
||||
width: editorStyle.width + 'px',
|
||||
height: editorStyle.height + 'px',
|
||||
fontSize: editorStyle.fontSize,
|
||||
showGutter: false,
|
||||
name: 'note-editor',
|
||||
wrapEnabled: true,
|
||||
onScroll: event => {
|
||||
this.editor_scroll();
|
||||
},
|
||||
ref: elem => {
|
||||
this.editor_ref(elem);
|
||||
},
|
||||
onChange: body => {
|
||||
this.aceEditor_change(body);
|
||||
},
|
||||
showPrintMargin: false
|
||||
|
||||
// Disable warning: "Automatically scrolling cursor into view after
|
||||
// selection change this will be disabled in the next version set
|
||||
// editor.$blockScrolling = Infinity to disable this message"
|
||||
, editorProps: { $blockScrolling: true }
|
||||
|
||||
// This is buggy (gets outside the container)
|
||||
, highlightActiveLine: false
|
||||
});
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: rootStyle },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: titleBarStyle },
|
||||
titleEditor,
|
||||
titleBarMenuButton
|
||||
),
|
||||
editor,
|
||||
viewer
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
noteId: state.selectedNoteId,
|
||||
folderId: state.selectedFolderId,
|
||||
itemType: state.selectedItemType,
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
showAdvancedOptions: state.settings.showAdvancedOptions,
|
||||
syncStarted: state.syncStarted
|
||||
};
|
||||
};
|
||||
|
||||
const NoteText = connect(mapStateToProps)(NoteTextComponent);
|
||||
|
||||
module.exports = { NoteText };
|
||||
|
108
ElectronClient/app/gui/OneDriveAuthScreen.min.js
vendored
108
ElectronClient/app/gui/OneDriveAuthScreen.min.js
vendored
@ -1,108 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class OneDriveAuthScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.webview_ = null;
|
||||
this.authCode_ = null;
|
||||
}
|
||||
|
||||
refresh_click() {
|
||||
if (!this.webview_) return;
|
||||
this.webview_.src = this.startUrl();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
webviewUrl: this.startUrl(),
|
||||
webviewReady: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
|
||||
}
|
||||
|
||||
webview_domReady() {
|
||||
this.setState({ webviewReady: true });
|
||||
|
||||
this.webview_.addEventListener('did-navigate', async event => {
|
||||
const url = event.url;
|
||||
|
||||
if (this.authCode_) return;
|
||||
if (url.indexOf(this.redirectUrl() + '?code=') !== 0) return;
|
||||
|
||||
let code = url.split('?code=');
|
||||
this.authCode_ = code[1];
|
||||
|
||||
try {
|
||||
await reg.oneDriveApi().execTokenRequest(this.authCode_, this.redirectUrl(), true);
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
reg.scheduleSync(0);
|
||||
} catch (error) {
|
||||
bridge().showMessageBox({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
this.authCode_ = null;
|
||||
});
|
||||
}
|
||||
|
||||
startUrl() {
|
||||
return reg.oneDriveApi().authCodeUrl(this.redirectUrl());
|
||||
}
|
||||
|
||||
redirectUrl() {
|
||||
return reg.oneDriveApi().nativeClientRedirectUrl();
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width
|
||||
};
|
||||
|
||||
const webviewStyle = {
|
||||
width: this.props.style.width,
|
||||
height: this.props.style.height - theme.headerHeight,
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const headerButtons = [{
|
||||
title: _('Refresh'),
|
||||
onClick: () => this.refresh_click()
|
||||
}];
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
null,
|
||||
React.createElement(Header, { style: headerStyle, buttons: headerButtons }),
|
||||
React.createElement('webview', { src: this.startUrl(), style: webviewStyle, nodeintegration: '1', ref: elem => this.webview_ = elem })
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {};
|
||||
};
|
||||
|
||||
const OneDriveAuthScreen = connect(mapStateToProps)(OneDriveAuthScreenComponent);
|
||||
|
||||
module.exports = { OneDriveAuthScreen };
|
||||
|
111
ElectronClient/app/gui/OneDriveLoginScreen.min.js
vendored
111
ElectronClient/app/gui/OneDriveLoginScreen.min.js
vendored
@ -1,111 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class OneDriveLoginScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.webview_ = null;
|
||||
this.authCode_ = null;
|
||||
}
|
||||
|
||||
refresh_click() {
|
||||
if (!this.webview_) return;
|
||||
this.webview_.src = this.startUrl();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
webviewUrl: this.startUrl(),
|
||||
webviewReady: false
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this));
|
||||
}
|
||||
|
||||
webview_domReady() {
|
||||
this.setState({ webviewReady: true });
|
||||
|
||||
this.webview_.addEventListener('did-navigate', async event => {
|
||||
const url = event.url;
|
||||
|
||||
if (this.authCode_) return;
|
||||
if (url.indexOf(this.redirectUrl() + '?code=') !== 0) return;
|
||||
|
||||
let code = url.split('?code=');
|
||||
this.authCode_ = code[1];
|
||||
|
||||
try {
|
||||
await reg.oneDriveApi().execTokenRequest(this.authCode_, this.redirectUrl(), true);
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
reg.scheduleSync(0);
|
||||
} catch (error) {
|
||||
bridge().showMessageBox({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
this.authCode_ = null;
|
||||
});
|
||||
}
|
||||
|
||||
startUrl() {
|
||||
return reg.oneDriveApi().authCodeUrl(this.redirectUrl());
|
||||
}
|
||||
|
||||
redirectUrl() {
|
||||
return reg.oneDriveApi().nativeClientRedirectUrl();
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width
|
||||
};
|
||||
|
||||
const webviewStyle = {
|
||||
width: this.props.style.width,
|
||||
height: this.props.style.height - theme.headerHeight,
|
||||
overflow: 'hidden'
|
||||
};
|
||||
|
||||
const headerButtons = [{
|
||||
title: _('Refresh'),
|
||||
onClick: () => this.refresh_click(),
|
||||
iconName: 'fa-refresh'
|
||||
}];
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
null,
|
||||
React.createElement(Header, { style: headerStyle, buttons: headerButtons }),
|
||||
React.createElement('webview', { src: this.startUrl(), style: webviewStyle, nodeintegration: '1', ref: elem => this.webview_ = elem })
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
theme: state.settings.theme
|
||||
};
|
||||
};
|
||||
|
||||
const OneDriveLoginScreen = connect(mapStateToProps)(OneDriveLoginScreenComponent);
|
||||
|
||||
module.exports = { OneDriveLoginScreen };
|
||||
|
146
ElectronClient/app/gui/PromptDialog.min.js
vendored
146
ElectronClient/app/gui/PromptDialog.min.js
vendored
@ -1,146 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
||||
class PromptDialog extends React.Component {
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
visible: false,
|
||||
answer: this.props.value ? this.props.value : ''
|
||||
});
|
||||
this.focusInput_ = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
if ('visible' in newProps) {
|
||||
this.setState({ visible: newProps.visible });
|
||||
if (newProps.visible) this.focusInput_ = true;
|
||||
}
|
||||
|
||||
if ('value' in newProps) {
|
||||
this.setState({ answer: newProps.value });
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
if (this.focusInput_ && this.answerInput_) this.answerInput_.focus();
|
||||
this.focusInput_ = false;
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style;
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const modalLayerStyle = {
|
||||
zIndex: 9999,
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: style.width,
|
||||
height: style.height,
|
||||
backgroundColor: 'rgba(0,0,0,0.6)',
|
||||
display: this.state.visible ? 'flex' : 'none',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
};
|
||||
|
||||
const promptDialogStyle = {
|
||||
backgroundColor: 'white',
|
||||
padding: 16,
|
||||
display: 'inline-block',
|
||||
boxShadow: '6px 6px 20px rgba(0,0,0,0.5)'
|
||||
};
|
||||
|
||||
const buttonStyle = {
|
||||
minWidth: theme.buttonMinWidth,
|
||||
minHeight: theme.buttonMinHeight,
|
||||
marginLeft: 5
|
||||
};
|
||||
|
||||
const labelStyle = {
|
||||
marginRight: 5,
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
fontFamily: theme.fontFamily,
|
||||
verticalAlign: 'top'
|
||||
};
|
||||
|
||||
const inputStyle = {
|
||||
width: 0.5 * style.width,
|
||||
maxWidth: 400
|
||||
};
|
||||
|
||||
const descStyle = Object.assign({}, theme.textStyle, {
|
||||
marginTop: 10
|
||||
});
|
||||
|
||||
const onClose = accept => {
|
||||
if (this.props.onClose) this.props.onClose(accept ? this.state.answer : null);
|
||||
this.setState({ visible: false, answer: '' });
|
||||
};
|
||||
|
||||
const onChange = event => {
|
||||
this.setState({ answer: event.target.value });
|
||||
};
|
||||
|
||||
const onKeyDown = event => {
|
||||
if (event.key === 'Enter') {
|
||||
onClose(true);
|
||||
} else if (event.key === 'Escape') {
|
||||
onClose(false);
|
||||
}
|
||||
};
|
||||
|
||||
const descComp = this.props.description ? React.createElement(
|
||||
'div',
|
||||
{ style: descStyle },
|
||||
this.props.description
|
||||
) : null;
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: modalLayerStyle },
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: promptDialogStyle },
|
||||
React.createElement(
|
||||
'label',
|
||||
{ style: labelStyle },
|
||||
this.props.label ? this.props.label : ''
|
||||
),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: { display: 'inline-block' } },
|
||||
React.createElement('input', {
|
||||
style: inputStyle,
|
||||
ref: input => this.answerInput_ = input,
|
||||
value: this.state.answer,
|
||||
type: 'text',
|
||||
onChange: event => onChange(event),
|
||||
onKeyDown: event => onKeyDown(event) }),
|
||||
descComp
|
||||
),
|
||||
React.createElement(
|
||||
'div',
|
||||
{ style: { textAlign: 'right', marginTop: 10 } },
|
||||
React.createElement(
|
||||
'button',
|
||||
{ style: buttonStyle, onClick: () => onClose(true) },
|
||||
'OK'
|
||||
),
|
||||
React.createElement(
|
||||
'button',
|
||||
{ style: buttonStyle, onClick: () => onClose(false) },
|
||||
'Cancel'
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = { PromptDialog };
|
||||
|
92
ElectronClient/app/gui/Root.min.js
vendored
92
ElectronClient/app/gui/Root.min.js
vendored
@ -1,92 +0,0 @@
|
||||
const React = require('react');
|
||||
const { render } = require('react-dom');
|
||||
const { createStore } = require('redux');
|
||||
const { connect, Provider } = require('react-redux');
|
||||
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
|
||||
const { MainScreen } = require('./MainScreen.min.js');
|
||||
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||
const { Navigator } = require('./Navigator.min.js');
|
||||
|
||||
const { app } = require('../app');
|
||||
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
async function initialize(dispatch) {
|
||||
this.wcsTimeoutId_ = null;
|
||||
|
||||
bridge().window().on('resize', function () {
|
||||
if (this.wcsTimeoutId_) clearTimeout(this.wcsTimeoutId_);
|
||||
|
||||
this.wcsTimeoutId_ = setTimeout(() => {
|
||||
store.dispatch({
|
||||
type: 'WINDOW_CONTENT_SIZE_SET',
|
||||
size: bridge().windowContentSize()
|
||||
});
|
||||
this.wcsTimeoutId_ = null;
|
||||
}, 10);
|
||||
});
|
||||
|
||||
store.dispatch({
|
||||
type: 'NOTE_VISIBLE_PANES_SET',
|
||||
panes: Setting.value('noteVisiblePanes')
|
||||
});
|
||||
}
|
||||
|
||||
class RootComponent extends React.Component {
|
||||
|
||||
async componentDidMount() {
|
||||
if (this.props.appState == 'starting') {
|
||||
this.props.dispatch({
|
||||
type: 'APP_STATE_SET',
|
||||
state: 'initializing'
|
||||
});
|
||||
|
||||
await initialize(this.props.dispatch);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'APP_STATE_SET',
|
||||
state: 'ready'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const navigatorStyle = {
|
||||
width: this.props.size.width,
|
||||
height: this.props.size.height
|
||||
};
|
||||
|
||||
const screens = {
|
||||
Main: { screen: MainScreen },
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||
Config: { screen: ConfigScreen, title: () => _('Configuration') }
|
||||
};
|
||||
|
||||
return React.createElement(Navigator, { style: navigatorStyle, screens: screens });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
size: state.windowContentSize,
|
||||
appState: state.appState
|
||||
};
|
||||
};
|
||||
|
||||
const Root = connect(mapStateToProps)(RootComponent);
|
||||
|
||||
const store = app().store();
|
||||
|
||||
render(React.createElement(
|
||||
Provider,
|
||||
{ store: store },
|
||||
React.createElement(Root, null)
|
||||
), document.getElementById('react-root'));
|
||||
|
262
ElectronClient/app/gui/SideBar.min.js
vendored
262
ElectronClient/app/gui/SideBar.min.js
vendored
@ -1,262 +0,0 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const shared = require('lib/components/shared/side-menu-shared.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { BaseModel } = require('lib/base-model.js');
|
||||
const { Folder } = require('lib/models/folder.js');
|
||||
const { Tag } = require('lib/models/tag.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const Menu = bridge().Menu;
|
||||
const MenuItem = bridge().MenuItem;
|
||||
|
||||
class SideBarComponent extends React.Component {
|
||||
|
||||
style() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const itemHeight = 25;
|
||||
|
||||
let style = {
|
||||
root: {
|
||||
backgroundColor: theme.backgroundColor2
|
||||
},
|
||||
listItem: {
|
||||
height: itemHeight,
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: theme.fontSize,
|
||||
textDecoration: 'none',
|
||||
boxSizing: 'border-box',
|
||||
color: theme.color2,
|
||||
paddingLeft: 14,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
cursor: 'default',
|
||||
opacity: 0.8
|
||||
},
|
||||
listItemSelected: {
|
||||
backgroundColor: theme.selectedColor2
|
||||
},
|
||||
conflictFolder: {
|
||||
color: theme.colorError2,
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
header: {
|
||||
height: itemHeight * 1.8,
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: theme.fontSize * 1.3,
|
||||
textDecoration: 'none',
|
||||
boxSizing: 'border-box',
|
||||
color: theme.color2,
|
||||
paddingLeft: 8,
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
},
|
||||
button: {
|
||||
padding: 6,
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: theme.fontSize,
|
||||
textDecoration: 'none',
|
||||
boxSizing: 'border-box',
|
||||
color: theme.color2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: "1px solid rgba(255,255,255,0.2)",
|
||||
marginTop: 10,
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
cursor: 'default'
|
||||
},
|
||||
syncReport: {
|
||||
fontFamily: theme.fontFamily,
|
||||
fontSize: Math.round(theme.fontSize * .9),
|
||||
color: theme.color2,
|
||||
opacity: .5,
|
||||
display: 'flex',
|
||||
alignItems: 'left',
|
||||
justifyContent: 'top',
|
||||
flexDirection: 'column',
|
||||
marginTop: 10,
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
minHeight: 70
|
||||
}
|
||||
};
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
itemContextMenu(event) {
|
||||
const itemId = event.target.getAttribute('data-id');
|
||||
if (itemId === Folder.conflictFolderId()) return;
|
||||
|
||||
const itemType = Number(event.target.getAttribute('data-type'));
|
||||
if (!itemId || !itemType) throw new Error('No data on element');
|
||||
|
||||
let deleteMessage = '';
|
||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||
deleteMessage = _('Delete notebook?');
|
||||
} else if (itemType === BaseModel.TYPE_TAG) {
|
||||
deleteMessage = _('Remove this tag from all the notes?');
|
||||
}
|
||||
|
||||
const menu = new Menu();
|
||||
|
||||
menu.append(new MenuItem({ label: _('Delete'), click: async () => {
|
||||
const ok = bridge().showConfirmMessageBox(deleteMessage);
|
||||
if (!ok) return;
|
||||
|
||||
if (itemType === BaseModel.TYPE_FOLDER) {
|
||||
await Folder.delete(itemId);
|
||||
} else if (itemType === BaseModel.TYPE_TAG) {
|
||||
await Tag.untagAll(itemId);
|
||||
}
|
||||
} }));
|
||||
|
||||
menu.popup(bridge().window());
|
||||
}
|
||||
|
||||
folderItem_click(folder) {
|
||||
this.props.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder ? folder.id : null
|
||||
});
|
||||
}
|
||||
|
||||
tagItem_click(tag) {
|
||||
this.props.dispatch({
|
||||
type: 'TAG_SELECT',
|
||||
id: tag ? tag.id : null
|
||||
});
|
||||
}
|
||||
|
||||
async sync_click() {
|
||||
await shared.synchronize_press(this);
|
||||
}
|
||||
|
||||
folderItem(folder, selected) {
|
||||
let style = Object.assign({}, this.style().listItem);
|
||||
if (selected) style = Object.assign(style, this.style().listItemSelected);
|
||||
if (folder.id === Folder.conflictFolderId()) style = Object.assign(style, this.style().conflictFolder);
|
||||
return React.createElement(
|
||||
'a',
|
||||
{ className: 'list-item', href: '#', 'data-id': folder.id, 'data-type': BaseModel.TYPE_FOLDER, onContextMenu: event => this.itemContextMenu(event), key: folder.id, style: style, onClick: () => {
|
||||
this.folderItem_click(folder);
|
||||
} },
|
||||
folder.title
|
||||
);
|
||||
}
|
||||
|
||||
tagItem(tag, selected) {
|
||||
let style = Object.assign({}, this.style().listItem);
|
||||
if (selected) style = Object.assign(style, this.style().listItemSelected);
|
||||
return React.createElement(
|
||||
'a',
|
||||
{ className: 'list-item', href: '#', 'data-id': tag.id, 'data-type': BaseModel.TYPE_TAG, onContextMenu: event => this.itemContextMenu(event), key: tag.id, style: style, onClick: () => {
|
||||
this.tagItem_click(tag);
|
||||
} },
|
||||
tag.title
|
||||
);
|
||||
}
|
||||
|
||||
makeDivider(key) {
|
||||
return React.createElement('div', { style: { height: 2, backgroundColor: 'blue' }, key: key });
|
||||
}
|
||||
|
||||
makeHeader(key, label, iconName) {
|
||||
const style = this.style().header;
|
||||
const icon = React.createElement('i', { style: { fontSize: style.fontSize * 1.2, marginRight: 5 }, className: "fa " + iconName });
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ style: style, key: key },
|
||||
icon,
|
||||
label
|
||||
);
|
||||
}
|
||||
|
||||
synchronizeButton(label) {
|
||||
const style = this.style().button;
|
||||
return React.createElement(
|
||||
'a',
|
||||
{ className: 'synchronize-button', style: style, href: '#', key: 'sync_button', onClick: () => {
|
||||
this.sync_click();
|
||||
} },
|
||||
label
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = Object.assign({}, this.style().root, this.props.style, {
|
||||
overflowX: 'hidden',
|
||||
overflowY: 'auto'
|
||||
});
|
||||
|
||||
let items = [];
|
||||
|
||||
items.push(this.makeHeader('folderHeader', _('Notebooks'), 'fa-folder-o'));
|
||||
|
||||
if (this.props.folders.length) {
|
||||
const folderItems = shared.renderFolders(this.props, this.folderItem.bind(this));
|
||||
items = items.concat(folderItems);
|
||||
}
|
||||
|
||||
items.push(this.makeHeader('tagHeader', _('Tags'), 'fa-tags'));
|
||||
|
||||
if (this.props.tags.length) {
|
||||
const tagItems = shared.renderTags(this.props, this.tagItem.bind(this));
|
||||
|
||||
items.push(React.createElement(
|
||||
'div',
|
||||
{ className: 'tags', key: 'tag_items' },
|
||||
tagItems
|
||||
));
|
||||
}
|
||||
|
||||
let lines = Synchronizer.reportToLines(this.props.syncReport);
|
||||
const syncReportText = [];
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
syncReportText.push(React.createElement(
|
||||
'div',
|
||||
{ key: i },
|
||||
lines[i]
|
||||
));
|
||||
}
|
||||
|
||||
items.push(this.synchronizeButton(this.props.syncStarted ? _('Cancel') : _('Synchronise')));
|
||||
|
||||
items.push(React.createElement(
|
||||
'div',
|
||||
{ style: this.style().syncReport, key: 'sync_report' },
|
||||
syncReportText
|
||||
));
|
||||
|
||||
return React.createElement(
|
||||
'div',
|
||||
{ className: 'side-bar', style: style },
|
||||
items
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = state => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
tags: state.tags,
|
||||
syncStarted: state.syncStarted,
|
||||
syncReport: state.syncReport,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedTagId: state.selectedTagId,
|
||||
notesParentType: state.notesParentType,
|
||||
locale: state.settings.locale,
|
||||
theme: state.settings.theme
|
||||
};
|
||||
};
|
||||
|
||||
const SideBar = connect(mapStateToProps)(SideBarComponent);
|
||||
|
||||
module.exports = { SideBar };
|
||||
|
@ -7,7 +7,9 @@
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"pack": "node_modules/.bin/electron-builder --dir",
|
||||
"dist": "node_modules/.bin/electron-builder",
|
||||
"publish": "build -p always"
|
||||
"publish": "build -p always",
|
||||
"postinstall": "node compile-jsx.js",
|
||||
"compile": "node compile-jsx.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
Loading…
Reference in New Issue
Block a user