mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-21 09:38:01 +02:00
Merge branch 'master' of github.com:laurent22/joplin
This commit is contained in:
commit
48ce788118
@ -46,3 +46,7 @@ Server/dist/
|
||||
Server/bin/
|
||||
Server/node_modules/
|
||||
ElectronClient/app/packageInfo.js
|
||||
|
||||
# Ignore files generated from TypeScript files
|
||||
ElectronClient/app/gui/ShareNoteDialog.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
|
5
.gitignore
vendored
5
.gitignore
vendored
@ -44,3 +44,8 @@ ElectronClient/app/gui/note-viewer/fonts/
|
||||
ElectronClient/app/gui/note-viewer/lib.js
|
||||
Tools/commit_hook.txt
|
||||
.vscode/*
|
||||
*.map
|
||||
|
||||
# Ignore files generated from TypeScript files
|
||||
ElectronClient/app/gui/ShareNoteDialog.js
|
||||
ReactNativeClient/lib/JoplinServerApi.js
|
||||
|
@ -55,8 +55,12 @@ before_install:
|
||||
|
||||
script:
|
||||
- |
|
||||
# Copy lib
|
||||
rsync -aP --delete ReactNativeClient/lib/ ElectronClient/app/lib/
|
||||
|
||||
# Install tools
|
||||
npm install
|
||||
npm run typescript-compile
|
||||
cd Tools
|
||||
npm install
|
||||
cd ..
|
||||
@ -110,5 +114,4 @@ script:
|
||||
|
||||
# Prepare the Electron app and build it
|
||||
cd ElectronClient/app
|
||||
rsync -aP --delete ../../ReactNativeClient/lib/ lib/
|
||||
npm install && USE_HARD_LINKS=false yarn dist
|
||||
|
31
BUILD.md
31
BUILD.md
@ -5,6 +5,14 @@
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when building each app.
|
||||
- In general, most of the backend (anything to do with the database, synchronisation, data import or export, etc.) is shared across all the apps, so when making a change please consider how it will affect all the apps.
|
||||
|
||||
# TypeScript
|
||||
|
||||
Most of the application is written in JavaScript, however new classes and files should generally be written in [TypeScript](https://www.typescriptlang.org/). Even if you don't write TypeScript code, you will need to build the existing .ts and .tsx files. This is done from the root of the project, by running `npm run typescript-compile`.
|
||||
|
||||
If you are modifying TypeScript code, the best is to have the compiler watch for changes from a terminal. To do so, run `npm run typescript-watch`.
|
||||
|
||||
All TypeScript files are generated next to the .ts or .tsx file. So for example, if there's a file "lib/MyClass.ts", there will be a generated "lib/MyClass.js" next to it. If you create a new TypeScript file, make sure you add the generated .js file to .gitignore. It is implemented that way as it requires minimal changes to integrate TypeScript in the existing JavaScript code base.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
brew install yarn node
|
||||
@ -14,7 +22,7 @@
|
||||
## Linux and Windows (WSL) dependencies
|
||||
|
||||
- Install yarn - https://yarnpkg.com/lang/en/docs/install/
|
||||
- Install node v8.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- Install node v10.x (check with `node --version`) - https://nodejs.org/en/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the tools
|
||||
@ -28,8 +36,9 @@ npm install && cd Tools && npm install
|
||||
# Building the Electron application
|
||||
|
||||
```
|
||||
rsync --delete -a ReactNativeClient/lib/ ElectronClient/app/lib/
|
||||
npm run typescript-compile
|
||||
cd ElectronClient/app
|
||||
rsync --delete -a ../../ReactNativeClient/lib/ lib/
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
@ -47,10 +56,9 @@ From `/ElectronClient` you can also run `run.sh` to run the app for testing.
|
||||
## Building Electron application on Windows
|
||||
|
||||
```
|
||||
cd Tools
|
||||
npm install
|
||||
cd ..\ElectronClient\app
|
||||
xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
|
||||
xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
|
||||
npm run typescript-compile
|
||||
cd ElectronClient\app
|
||||
npm install
|
||||
yarn dist
|
||||
```
|
||||
@ -67,7 +75,15 @@ The [building\_win32\_tips on this page](./readme/building_win32_tips.md) might
|
||||
|
||||
First you need to setup React Native to build projects with native code. For this, follow the instructions on the [Get Started](https://facebook.github.io/react-native/docs/getting-started.html) tutorial, in the "React Native CLI Quickstart" tab.
|
||||
|
||||
Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `react-native run-android`.
|
||||
Then:
|
||||
|
||||
```
|
||||
npm run typescript-compile
|
||||
cd ReactNativeClient
|
||||
npm install
|
||||
react-native run-ios
|
||||
# Or: react-native run-android
|
||||
```
|
||||
|
||||
# Building the Terminal application
|
||||
|
||||
@ -75,7 +91,6 @@ Then, from `/ReactNativeClient`, run `npm install`, then `react-native run-ios`
|
||||
cd CliClient
|
||||
npm install
|
||||
./build.sh
|
||||
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
|
||||
```
|
||||
|
||||
Run `run.sh` to start the application for testing.
|
||||
|
@ -7,4 +7,9 @@ rsync -a --exclude "node_modules/" "$ROOT_DIR/app/" "$BUILD_DIR/"
|
||||
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/lib/" "$BUILD_DIR/lib/"
|
||||
rsync -a --delete "$ROOT_DIR/../ReactNativeClient/locales/" "$BUILD_DIR/locales/"
|
||||
cp "$ROOT_DIR/package.json" "$BUILD_DIR"
|
||||
|
||||
cd $ROOT_DIR/..
|
||||
npm run typescript-compile
|
||||
cd $ROOT_DIR
|
||||
|
||||
chmod 755 "$BUILD_DIR/main.js"
|
@ -24,6 +24,19 @@ class ConfigScreenComponent extends React.Component {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
};
|
||||
|
||||
this.checkNextcloudAppButton_click = async () => {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
await shared.checkNextcloudApp(this, this.state.settings);
|
||||
};
|
||||
|
||||
this.showLogButton_click = () => {
|
||||
this.setState({ showNextcloudAppLog: true });
|
||||
};
|
||||
|
||||
this.nextcloudAppHelpLink_click = () => {
|
||||
bridge().openExternal('https://joplinapp.org/nextcloud_app');
|
||||
};
|
||||
|
||||
this.rowStyle_ = {
|
||||
marginBottom: 10,
|
||||
};
|
||||
@ -31,7 +44,7 @@ class ConfigScreenComponent extends React.Component {
|
||||
this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
@ -93,14 +106,21 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
sectionToComponent(key, section, settings, selected) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const settingComps = [];
|
||||
// const settingComps = [];
|
||||
|
||||
const createSettingComponents = (advanced) => {
|
||||
const output = [];
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
|
||||
if (!!md.advanced !== advanced) continue;
|
||||
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
||||
settingComps.push(settingComp);
|
||||
output.push(settingComp);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const settingComps = createSettingComponents(false);
|
||||
const advancedSettingComps = createSettingComponents(true);
|
||||
|
||||
const sectionStyle = {
|
||||
marginTop: 20,
|
||||
@ -117,10 +137,10 @@ class ConfigScreenComponent extends React.Component {
|
||||
|
||||
if (section.name === 'sync') {
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusStyle = Object.assign({}, theme.textStyle, { marginTop: 10 });
|
||||
const statusComp = !messages.length ? null : (
|
||||
<div style={statusStyle}>
|
||||
{messages[0]}
|
||||
@ -137,12 +157,69 @@ class ConfigScreenComponent extends React.Component {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (syncTargetMd.name === 'nextcloud') {
|
||||
const syncTarget = settings['sync.5.syncTargets'][settings['sync.5.path']];
|
||||
|
||||
let status = _('Unknown');
|
||||
let errorMessage = null;
|
||||
|
||||
if (this.state.checkNextcloudAppResult === 'checking') {
|
||||
status = _('Checking...');
|
||||
} else if (syncTarget) {
|
||||
if (syncTarget.uuid) status = _('OK');
|
||||
if (syncTarget.error) {
|
||||
status = _('Error');
|
||||
errorMessage = syncTarget.error;
|
||||
}
|
||||
}
|
||||
|
||||
const statusComp = !errorMessage || this.state.checkNextcloudAppResult === 'checking' || !this.state.showNextcloudAppLog ? null : (
|
||||
<div style={statusStyle}>
|
||||
<p style={theme.textStyle}>{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}</p>
|
||||
<pre>{errorMessage}</pre>
|
||||
</div>
|
||||
);
|
||||
|
||||
const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
|
||||
<a style={theme.urlStyle} href="#" onClick={this.showLogButton_click}>[{_('Show Log')}]</a>
|
||||
);
|
||||
|
||||
const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });
|
||||
|
||||
settingComps.push(
|
||||
<div key="nextcloud_app_check" style={this.rowStyle_}>
|
||||
<span style={theme.textStyle}>Beta: {_('Joplin Nextcloud App status:')} </span><span style={appStatusStyle}>{status}</span>
|
||||
|
||||
{showLogButton}
|
||||
|
||||
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
|
||||
{_('Check Status')}
|
||||
</button>
|
||||
|
||||
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
|
||||
{statusComp}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let advancedSettingsButton = null;
|
||||
let advancedSettingsSectionStyle = { display: 'none' };
|
||||
|
||||
if (advancedSettingComps.length) {
|
||||
const iconName = this.state.showAdvancedSettings ? 'fa fa-toggle-up' : 'fa fa-toggle-down';
|
||||
const advancedSettingsButtonStyle = Object.assign({}, theme.buttonStyle, { marginBottom: 10 });
|
||||
advancedSettingsButton = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{fontSize: 14}} className={iconName}></i> {_('Show Advanced Settings')}</button>;
|
||||
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key} style={sectionStyle}>
|
||||
{noteComp}
|
||||
<div>{settingComps}</div>
|
||||
{advancedSettingsButton}
|
||||
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
45
ElectronClient/app/gui/DialogButtonRow.jsx
Normal file
45
ElectronClient/app/gui/DialogButtonRow.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
|
||||
function DialogButtonRow(props) {
|
||||
const theme = themeStyle(props.theme);
|
||||
|
||||
const okButton_click = () => {
|
||||
if (props.onClick) props.onClick({ buttonName: 'ok' });
|
||||
};
|
||||
|
||||
const cancelButton_click = () => {
|
||||
if (props.onClick) props.onClick({ buttonName: 'cancel' });
|
||||
};
|
||||
|
||||
const onKeyDown = (event) => {
|
||||
if (event.keyCode === 13) {
|
||||
okButton_click();
|
||||
} else if (event.keyCode === 27) {
|
||||
cancelButton_click();
|
||||
}
|
||||
};
|
||||
|
||||
const buttonComps = [];
|
||||
|
||||
if (props.okButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button key="ok" style={theme.buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
|
||||
{_('OK')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.cancelButtonShow !== false) {
|
||||
buttonComps.push(
|
||||
<button key="cancel" style={Object.assign({}, theme.buttonStyle, { marginLeft: 10 })} onClick={cancelButton_click}>
|
||||
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
return <div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>;
|
||||
}
|
||||
|
||||
module.exports = DialogButtonRow;
|
@ -13,7 +13,7 @@ class DropboxLoginScreenComponent extends React.Component {
|
||||
this.shared_ = new Shared(this, msg => bridge().showInfoMessageBox(msg), msg => bridge().showErrorMessageBox(msg));
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.shared_.refreshUrl();
|
||||
}
|
||||
|
||||
|
@ -31,11 +31,11 @@ class EncryptionConfigScreenComponent extends React.Component {
|
||||
return shared.refreshStats(this);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.initState(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.initState(nextProps);
|
||||
}
|
||||
|
||||
|
@ -71,7 +71,7 @@ class HeaderComponent extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.windowCommand) {
|
||||
this.doCommand(nextProps.windowCommand);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ const { filename, basename } = require('lib/path-utils.js');
|
||||
const { importEnex } = require('lib/import-enex');
|
||||
|
||||
class ImportScreenComponent extends React.Component {
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
doImport: true,
|
||||
filePath: this.props.filePath,
|
||||
@ -16,7 +16,7 @@ class ImportScreenComponent extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.filePath) {
|
||||
this.setState(
|
||||
{
|
||||
|
@ -32,11 +32,11 @@ class ItemList extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,7 @@ const { NoteList } = require('./NoteList.min.js');
|
||||
const { NoteText } = require('./NoteText.min.js');
|
||||
const { PromptDialog } = require('./PromptDialog.min.js');
|
||||
const NotePropertiesDialog = require('./NotePropertiesDialog.min.js');
|
||||
const ShareNoteDialog = require('./ShareNoteDialog.js').default;
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
@ -24,6 +25,7 @@ class MainScreenComponent extends React.Component {
|
||||
super();
|
||||
|
||||
this.notePropertiesDialog_close = this.notePropertiesDialog_close.bind(this);
|
||||
this.shareNoteDialog_close = this.shareNoteDialog_close.bind(this);
|
||||
this.sidebar_onDrag = this.sidebar_onDrag.bind(this);
|
||||
this.noteList_onDrag = this.noteList_onDrag.bind(this);
|
||||
}
|
||||
@ -40,7 +42,11 @@ class MainScreenComponent extends React.Component {
|
||||
this.setState({ notePropertiesDialogOptions: {} });
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
shareNoteDialog_close() {
|
||||
this.setState({ shareNoteDialogOptions: {} });
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
promptOptions: null,
|
||||
modalLayer: {
|
||||
@ -48,10 +54,11 @@ class MainScreenComponent extends React.Component {
|
||||
message: '',
|
||||
},
|
||||
notePropertiesDialogOptions: {},
|
||||
shareNoteDialogOptions: {},
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.windowCommand) {
|
||||
this.doCommand(newProps.windowCommand);
|
||||
}
|
||||
@ -247,6 +254,13 @@ class MainScreenComponent extends React.Component {
|
||||
onRevisionLinkClick: command.onRevisionLinkClick,
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'commandShareNoteDialog') {
|
||||
this.setState({
|
||||
shareNoteDialogOptions: {
|
||||
noteIds: command.noteIds,
|
||||
visible: true,
|
||||
},
|
||||
});
|
||||
} else if (command.name === 'toggleVisiblePanes') {
|
||||
this.toggleVisiblePanes();
|
||||
} else if (command.name === 'toggleSidebar') {
|
||||
@ -573,6 +587,7 @@ class MainScreenComponent extends React.Component {
|
||||
const modalLayerStyle = Object.assign({}, styles.modalLayer, { display: this.state.modalLayer.visible ? 'block' : 'none' });
|
||||
|
||||
const notePropertiesDialogOptions = this.state.notePropertiesDialogOptions;
|
||||
const shareNoteDialogOptions = this.state.shareNoteDialogOptions;
|
||||
const keyboardMode = Setting.value('editor.keyboardMode');
|
||||
|
||||
return (
|
||||
@ -580,6 +595,7 @@ class MainScreenComponent extends React.Component {
|
||||
<div style={modalLayerStyle}>{this.state.modalLayer.message}</div>
|
||||
|
||||
{notePropertiesDialogOptions.visible && <NotePropertiesDialog theme={this.props.theme} noteId={notePropertiesDialogOptions.noteId} onClose={this.notePropertiesDialog_close} onRevisionLinkClick={notePropertiesDialogOptions.onRevisionLinkClick} />}
|
||||
{shareNoteDialogOptions.visible && <ShareNoteDialog theme={this.props.theme} noteIds={shareNoteDialogOptions.noteIds} onClose={this.shareNoteDialog_close} />}
|
||||
|
||||
<PromptDialog autocomplete={promptOptions && 'autocomplete' in promptOptions ? promptOptions.autocomplete : null} defaultValue={promptOptions && promptOptions.value ? promptOptions.value : ''} theme={this.props.theme} style={styles.prompt} onClose={this.promptOnClose_} label={promptOptions ? promptOptions.label : ''} description={promptOptions ? promptOptions.description : null} visible={!!this.state.promptOptions} buttons={promptOptions && 'buttons' in promptOptions ? promptOptions.buttons : null} inputType={promptOptions && 'inputType' in promptOptions ? promptOptions.inputType : null} />
|
||||
|
||||
|
@ -4,7 +4,7 @@ const { connect } = require('react-redux');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
|
||||
class NavigatorComponent extends Component {
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.route) {
|
||||
const screenInfo = this.props.screens[newProps.route.routeName];
|
||||
let windowTitle = ['Joplin'];
|
||||
|
@ -2,6 +2,7 @@ const React = require('react');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const DialogButtonRow = require('./DialogButtonRow.min');
|
||||
const Datetime = require('react-datetime');
|
||||
const Note = require('lib/models/Note');
|
||||
const formatcoords = require('formatcoords');
|
||||
@ -11,10 +12,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.okButton_click = this.okButton_click.bind(this);
|
||||
this.cancelButton_click = this.cancelButton_click.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
this.revisionsLink_click = this.revisionsLink_click.bind(this);
|
||||
this.buttonRow_click = this.buttonRow_click.bind(this);
|
||||
this.okButton = React.createRef();
|
||||
|
||||
this.state = {
|
||||
@ -153,12 +152,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
okButton_click() {
|
||||
this.closeDialog(true);
|
||||
}
|
||||
|
||||
cancelButton_click() {
|
||||
this.closeDialog(false);
|
||||
buttonRow_click(event) {
|
||||
this.closeDialog(event.buttonName === 'ok');
|
||||
}
|
||||
|
||||
revisionsLink_click() {
|
||||
@ -166,14 +161,6 @@ class NotePropertiesDialog extends React.Component {
|
||||
if (this.props.onRevisionLinkClick) this.props.onRevisionLinkClick();
|
||||
}
|
||||
|
||||
onKeyDown(event) {
|
||||
if (event.keyCode === 13) {
|
||||
this.closeDialog(true);
|
||||
} else if (event.keyCode === 27) {
|
||||
this.closeDialog(false);
|
||||
}
|
||||
}
|
||||
|
||||
editPropertyButtonClick(key, initialValue) {
|
||||
this.setState({
|
||||
editedKey: key,
|
||||
@ -218,15 +205,12 @@ class NotePropertiesDialog extends React.Component {
|
||||
async cancelProperty() {
|
||||
return new Promise((resolve) => {
|
||||
this.okButton.current.focus();
|
||||
this.setState(
|
||||
{
|
||||
this.setState({
|
||||
editedKey: null,
|
||||
editedValue: null,
|
||||
},
|
||||
() => {
|
||||
}, () => {
|
||||
resolve();
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -363,21 +347,8 @@ class NotePropertiesDialog extends React.Component {
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const styles = this.styles(this.props.theme);
|
||||
const formNote = this.state.formNote;
|
||||
|
||||
const buttonComps = [];
|
||||
buttonComps.push(
|
||||
<button key="ok" style={styles.button} onClick={this.okButton_click} ref={this.okButton} onKeyDown={this.onKeyDown}>
|
||||
{_('Apply')}
|
||||
</button>
|
||||
);
|
||||
buttonComps.push(
|
||||
<button key="cancel" style={styles.button} onClick={this.cancelButton_click}>
|
||||
{_('Cancel')}
|
||||
</button>
|
||||
);
|
||||
|
||||
const noteComps = [];
|
||||
|
||||
if (formNote) {
|
||||
@ -393,7 +364,7 @@ class NotePropertiesDialog extends React.Component {
|
||||
<div style={theme.dialogBox}>
|
||||
<div style={theme.dialogTitle}>{_('Note properties')}</div>
|
||||
<div>{noteComps}</div>
|
||||
<div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>
|
||||
<DialogButtonRow theme={this.props.theme} okButtonRef={this.okButton} onClick={this.buttonRow_click}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -406,7 +406,7 @@ class NoteTextComponent extends React.Component {
|
||||
return this.markupToHtml_;
|
||||
}
|
||||
|
||||
async componentWillMount() {
|
||||
async UNSAFE_componentWillMount() {
|
||||
let note = null;
|
||||
let noteTags = [];
|
||||
if (this.props.newNote) {
|
||||
@ -685,7 +685,7 @@ class NoteTextComponent extends React.Component {
|
||||
defer();
|
||||
}
|
||||
|
||||
async componentWillReceiveProps(nextProps) {
|
||||
async UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
if (this.props.newNote !== nextProps.newNote && nextProps.newNote) {
|
||||
await this.scheduleReloadNote(nextProps);
|
||||
} else if ('noteId' in nextProps && nextProps.noteId !== this.props.noteId) {
|
||||
|
@ -18,7 +18,7 @@ class OneDriveLoginScreenComponent extends React.Component {
|
||||
this.webview_.src = this.startUrl();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
webviewUrl: this.startUrl(),
|
||||
webviewReady: false,
|
||||
|
@ -14,7 +14,7 @@ class PromptDialog extends React.Component {
|
||||
this.answerInput_ = React.createRef();
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
visible: false,
|
||||
answer: this.props.defaultValue ? this.props.defaultValue : '',
|
||||
@ -22,7 +22,7 @@ class PromptDialog extends React.Component {
|
||||
this.focusInput_ = true;
|
||||
}
|
||||
|
||||
componentWillReceiveProps(newProps) {
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if ('visible' in newProps && newProps.visible !== this.props.visible) {
|
||||
this.setState({ visible: newProps.visible });
|
||||
if (newProps.visible) this.focusInput_ = true;
|
||||
|
200
ElectronClient/app/gui/ShareNoteDialog.tsx
Normal file
200
ElectronClient/app/gui/ShareNoteDialog.tsx
Normal file
@ -0,0 +1,200 @@
|
||||
// const React = require('react');
|
||||
// const { useState, useEffect } = React;
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import JoplinServerApi from '../lib/JoplinServerApi';
|
||||
|
||||
const { _, _n } = require('lib/locale.js');
|
||||
const { themeStyle, buildStyle } = require('../theme.js');
|
||||
const DialogButtonRow = require('./DialogButtonRow.min');
|
||||
const Note = require('lib/models/Note');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { clipboard } = require('electron');
|
||||
|
||||
interface ShareNoteDialogProps {
|
||||
theme: number,
|
||||
noteIds: Array<string>,
|
||||
onClose: Function,
|
||||
}
|
||||
|
||||
interface SharesMap {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function styles_(props:ShareNoteDialogProps) {
|
||||
return buildStyle('ShareNoteDialog', props.theme, (theme:any) => {
|
||||
return {
|
||||
noteList: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
note: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
border: '1px solid',
|
||||
borderColor: theme.dividerColor,
|
||||
padding: '0.5em',
|
||||
marginBottom: 5,
|
||||
},
|
||||
noteTitle: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
color: theme.color,
|
||||
},
|
||||
noteRemoveButton: {
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
},
|
||||
noteRemoveButtonIcon: {
|
||||
color: theme.color,
|
||||
fontSize: '1.4em',
|
||||
},
|
||||
copyShareLinkButton: {
|
||||
...theme.buttonStyle,
|
||||
marginBottom: 10,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export default function ShareNoteDialog(props:ShareNoteDialogProps) {
|
||||
console.info('Render ShareNoteDialog');
|
||||
|
||||
const [notes, setNotes] = useState<any[]>([]);
|
||||
const [sharesState, setSharesState] = useState<string>('unknown');
|
||||
const [shares, setShares] = useState<SharesMap>({});
|
||||
|
||||
const noteCount = notes.length;
|
||||
const theme = themeStyle(props.theme);
|
||||
const styles = styles_(props);
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchNotes() {
|
||||
const result = [];
|
||||
for (let noteId of props.noteIds) {
|
||||
result.push(await Note.load(noteId));
|
||||
}
|
||||
setNotes(result);
|
||||
}
|
||||
|
||||
fetchNotes();
|
||||
}, [props.noteIds]);
|
||||
|
||||
const appApi = async () => {
|
||||
return reg.syncTargetNextcloud().appApi();
|
||||
};
|
||||
|
||||
const buttonRow_click = () => {
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
const copyLinksToClipboard = (shares:SharesMap) => {
|
||||
const links = [];
|
||||
for (const n in shares) links.push(shares[n]._url);
|
||||
clipboard.writeText(links.join('\n'));
|
||||
};
|
||||
|
||||
const shareLinkButton_click = async () => {
|
||||
let hasSynced = false;
|
||||
let tryToSync = false;
|
||||
while (true) {
|
||||
try {
|
||||
setSharesState('creating');
|
||||
|
||||
if (tryToSync) {
|
||||
const synchronizer = await reg.syncTarget().synchronizer();
|
||||
await synchronizer.waitForSyncToFinish();
|
||||
await reg.scheduleSync(0);
|
||||
tryToSync = false;
|
||||
hasSynced = true;
|
||||
}
|
||||
|
||||
const api = await appApi();
|
||||
const syncTargetId = api.syncTargetId(Setting.toPlainObject());
|
||||
const newShares = Object.assign({}, shares);
|
||||
|
||||
for (const note of notes) {
|
||||
const result = await api.exec('POST', 'shares', {
|
||||
syncTargetId: syncTargetId,
|
||||
noteId: note.id,
|
||||
});
|
||||
newShares[note.id] = result;
|
||||
}
|
||||
|
||||
setShares(newShares);
|
||||
|
||||
copyLinksToClipboard(newShares);
|
||||
|
||||
setSharesState('created');
|
||||
} catch (error) {
|
||||
if (error.code === 404 && !hasSynced) {
|
||||
reg.logger().info('ShareNoteDialog: Note does not exist on server - trying to sync it.', error);
|
||||
tryToSync = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
reg.logger().error('ShareNoteDialog: Cannot share note:', error);
|
||||
|
||||
setSharesState('idle');
|
||||
alert(JoplinServerApi.connectionErrorMessage(error));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const removeNoteButton_click = (event:any) => {
|
||||
const newNotes = [];
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const n = notes[i];
|
||||
if (n.id === event.noteId) continue;
|
||||
newNotes.push(n);
|
||||
}
|
||||
setNotes(newNotes);
|
||||
};
|
||||
|
||||
const renderNote = (note:any) => {
|
||||
const removeButton = notes.length <= 1 ? null : (
|
||||
<button onClick={() => removeNoteButton_click({ noteId: note.id })} style={styles.noteRemoveButton}>
|
||||
<i style={styles.noteRemoveButtonIcon} className={'fa fa-times'}></i>
|
||||
</button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div key={note.id} style={styles.note}>
|
||||
<span style={styles.noteTitle}>{note.title}</span>{removeButton}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderNoteList = (notes:any) => {
|
||||
const noteComps = [];
|
||||
for (let noteId of Object.keys(notes)) {
|
||||
noteComps.push(renderNote(notes[noteId]));
|
||||
}
|
||||
return <div style={styles.noteList}>{noteComps}</div>;
|
||||
};
|
||||
|
||||
const statusMessage = (sharesState:string):string => {
|
||||
if (sharesState === 'creating') return _n('Generating link...', 'Generating links...', noteCount);
|
||||
if (sharesState === 'created') return _n('Link has been copied to clipboard!', 'Links have been copied to clipboard!', noteCount);
|
||||
return '';
|
||||
};
|
||||
|
||||
const rootStyle = Object.assign({}, theme.dialogBox);
|
||||
rootStyle.width = '50%';
|
||||
|
||||
return (
|
||||
<div style={theme.dialogModalLayer}>
|
||||
<div style={rootStyle}>
|
||||
<div style={theme.dialogTitle}>{_('Share Notes')}</div>
|
||||
{renderNoteList(notes)}
|
||||
<button disabled={sharesState === 'creating'} style={styles.copyShareLinkButton} onClick={shareLinkButton_click}>{_n('Copy Shareable Link', 'Copy Shareable Links', noteCount)}</button>
|
||||
<div style={theme.textStyle}>{statusMessage(sharesState)}</div>
|
||||
<DialogButtonRow theme={props.theme} onClick={buttonRow_click} okButtonShow={false} cancelButtonLabel={_('Close')}/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -16,7 +16,7 @@ class StatusScreenComponent extends React.Component {
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
UNSAFE_componentWillMount() {
|
||||
this.resfreshScreen();
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,20 @@ class NoteListUtils {
|
||||
})
|
||||
);
|
||||
|
||||
menu.append(
|
||||
new MenuItem({
|
||||
label: _('Share note...'),
|
||||
click: async () => {
|
||||
console.info('NOTE IDS', noteIds);
|
||||
props.dispatch({
|
||||
type: 'WINDOW_COMMAND',
|
||||
name: 'commandShareNoteDialog',
|
||||
noteIds: noteIds.slice(),
|
||||
});
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const exportMenu = new Menu();
|
||||
|
||||
const ioService = new InteropService();
|
||||
|
84
ElectronClient/app/package-lock.json
generated
84
ElectronClient/app/package-lock.json
generated
@ -143,6 +143,22 @@
|
||||
"integrity": "sha512-Ja7d4s0qyGFxjGeDq5S7Si25OFibSAHUi6i17UWnwNnpitADN7hah9q0Tl25gxuV5R1u2Bx+np6w4LHXfHyj/g==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/prop-types": {
|
||||
"version": "15.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz",
|
||||
"integrity": "sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.16.tgz",
|
||||
"integrity": "sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"abab": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/abab/-/abab-2.0.2.tgz",
|
||||
@ -596,12 +612,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -616,12 +634,14 @@
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
@ -743,7 +763,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.3",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
@ -755,6 +776,7 @@
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
@ -769,6 +791,7 @@
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -776,7 +799,8 @@
|
||||
"minimist": {
|
||||
"version": "0.0.8",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.3.5",
|
||||
@ -880,7 +904,8 @@
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
@ -5548,14 +5573,13 @@
|
||||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.8.1.tgz",
|
||||
"integrity": "sha512-wLw5CFGPdo7p/AgteFz7GblI2JPOos0+biSoxf1FPsGxWQZdN/pj6oToJs1crn61DL3Ln7mN86uZ4j74p31ELQ==",
|
||||
"version": "16.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
|
||||
"integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.1"
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
@ -5611,14 +5635,36 @@
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.4.0.tgz",
|
||||
"integrity": "sha512-bbLd+HYpBEnYoNyxDe9XpSG2t9wypMohwQPvKw8Hov3nF7SJiJIgK56b46zHpBUpHb06a1iEuw7G3rbrsnNL6w==",
|
||||
"version": "16.12.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.12.0.tgz",
|
||||
"integrity": "sha512-LMxFfAGrcS3kETtQaCkTKjMiifahaMySFDn71fZUNpPHZQEzmk/GiAeIT8JSOrHB23fnuCOMruL2a8NYlw+8Gw==",
|
||||
"requires": {
|
||||
"fbjs": "^0.8.16",
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.0"
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.18.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"prop-types": {
|
||||
"version": "15.7.2",
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz",
|
||||
"integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"requires": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-input-autosize": {
|
||||
@ -6073,9 +6119,9 @@
|
||||
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.13.6",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
|
||||
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.18.0.tgz",
|
||||
"integrity": "sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
|
@ -9,7 +9,8 @@
|
||||
"dist": "node_modules/.bin/electron-builder",
|
||||
"publish": "build -p always",
|
||||
"postinstall": "node compile.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts && node electronRebuild.js",
|
||||
"compile": "node compile.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts"
|
||||
"compile": "node compile.js && node compile-package-info.js && node ../../Tools/copycss.js --copy-fonts",
|
||||
"install-141": "npm install --toolset=v141"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -133,10 +134,10 @@
|
||||
"node-notifier": "^6.0.0",
|
||||
"promise": "^8.0.1",
|
||||
"query-string": "^5.1.1",
|
||||
"react": "^16.4.0",
|
||||
"react": "^16.12.0",
|
||||
"react-ace": "^6.1.4",
|
||||
"react-datetime": "^2.14.0",
|
||||
"react-dom": "^16.4.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"react-redux": "^5.0.7",
|
||||
"react-select": "^2.4.3",
|
||||
"react-tooltip": "^3.10.0",
|
||||
|
@ -63,7 +63,7 @@ globalStyle.buttonStyle = {
|
||||
border: '1px solid',
|
||||
minHeight: 26,
|
||||
minWidth: 80,
|
||||
maxWidth: 160,
|
||||
maxWidth: 220,
|
||||
paddingLeft: 12,
|
||||
paddingRight: 12,
|
||||
paddingTop: 6,
|
||||
@ -471,4 +471,19 @@ function themeStyle(theme) {
|
||||
return themeCache_[cacheKey];
|
||||
}
|
||||
|
||||
module.exports = { themeStyle };
|
||||
const cachedStyles_ = {};
|
||||
|
||||
function buildStyle(cacheKey, themeId, callback) {
|
||||
if (cachedStyles_[cacheKey]) cachedStyles_[cacheKey].style;
|
||||
|
||||
const s = callback(themeStyle(themeId));
|
||||
|
||||
cachedStyles_[cacheKey] = {
|
||||
style: s,
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
return cachedStyles_[cacheKey].style;
|
||||
}
|
||||
|
||||
module.exports = { themeStyle, buildStyle };
|
||||
|
158
ReactNativeClient/lib/JoplinServerApi.ts
Normal file
158
ReactNativeClient/lib/JoplinServerApi.ts
Normal file
@ -0,0 +1,158 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const { rtrimSlashes } = require('lib/path-utils.js');
|
||||
const base64 = require('base-64');
|
||||
const {_ } = require('lib/locale');
|
||||
|
||||
interface JoplinServerApiOptions {
|
||||
username: Function,
|
||||
password: Function,
|
||||
baseUrl: Function,
|
||||
}
|
||||
|
||||
export default class JoplinServerApi {
|
||||
|
||||
logger_:any;
|
||||
options_:JoplinServerApiOptions;
|
||||
kvStore_:any;
|
||||
|
||||
constructor(options:JoplinServerApiOptions) {
|
||||
this.logger_ = new Logger();
|
||||
this.options_ = options;
|
||||
this.kvStore_ = null;
|
||||
}
|
||||
|
||||
setLogger(l:any) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger():any {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
setKvStore(v:any) {
|
||||
this.kvStore_ = v;
|
||||
}
|
||||
|
||||
kvStore() {
|
||||
if (!this.kvStore_) throw new Error('JoplinServerApi.kvStore_ is not set!!');
|
||||
return this.kvStore_;
|
||||
}
|
||||
|
||||
authToken():string {
|
||||
if (!this.options_.username() || !this.options_.password()) return null;
|
||||
try {
|
||||
// Note: Non-ASCII passwords will throw an error about Latin1 characters - https://github.com/laurent22/joplin/issues/246
|
||||
// Tried various things like the below, but it didn't work on React Native:
|
||||
// return base64.encode(utf8.encode(this.options_.username() + ':' + this.options_.password()));
|
||||
return base64.encode(`${this.options_.username()}:${this.options_.password()}`);
|
||||
} catch (error) {
|
||||
error.message = `Cannot encode username/password: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
baseUrl():string {
|
||||
return rtrimSlashes(this.options_.baseUrl());
|
||||
}
|
||||
|
||||
static baseUrlFromNextcloudWebDavUrl(webDavUrl:string) {
|
||||
// http://nextcloud.local/remote.php/webdav/Joplin
|
||||
// http://nextcloud.local/index.php/apps/joplin/api
|
||||
const splitted = webDavUrl.split('/remote.php/webdav');
|
||||
if (splitted.length !== 2) throw new Error(`Unsupported WebDAV URL format: ${webDavUrl}`);
|
||||
return `${splitted[0]}/index.php/apps/joplin/api`;
|
||||
}
|
||||
|
||||
syncTargetId(settings:any) {
|
||||
const s = settings['sync.5.syncTargets'][settings['sync.5.path']];
|
||||
if (!s) throw new Error(`Joplin Nextcloud app not configured for URL: ${this.baseUrl()}`);
|
||||
return s.uuid;
|
||||
}
|
||||
|
||||
static connectionErrorMessage(error:any) {
|
||||
const msg = error && error.message ? error.message : 'Unknown error';
|
||||
return _('Could not connect to the Joplin Nextcloud app. Please check the configuration in the Synchronisation config screen. Full error was:\n\n%s', msg);
|
||||
}
|
||||
|
||||
async setupSyncTarget(webDavUrl:string) {
|
||||
return this.exec('POST', 'sync_targets', {
|
||||
webDavUrl: webDavUrl,
|
||||
});
|
||||
}
|
||||
|
||||
requestToCurl_(url:string, options:any) {
|
||||
let output = [];
|
||||
output.push('curl');
|
||||
output.push('-v');
|
||||
if (options.method) output.push(`-X ${options.method}`);
|
||||
if (options.headers) {
|
||||
for (let n in options.headers) {
|
||||
if (!options.headers.hasOwnProperty(n)) continue;
|
||||
output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
|
||||
}
|
||||
}
|
||||
if (options.body) output.push(`${'--data ' + '\''}${options.body}'`);
|
||||
output.push(url);
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
async exec(method:string, path:string = '', body:any = null, headers:any = null, options:any = null):Promise<any> {
|
||||
if (headers === null) headers = {};
|
||||
if (options === null) options = {};
|
||||
|
||||
const authToken = this.authToken();
|
||||
|
||||
if (authToken) headers['Authorization'] = `Basic ${authToken}`;
|
||||
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
if (typeof body === 'object' && body !== null) body = JSON.stringify(body);
|
||||
|
||||
const fetchOptions:any = {};
|
||||
fetchOptions.headers = headers;
|
||||
fetchOptions.method = method;
|
||||
if (options.path) fetchOptions.path = options.path;
|
||||
if (body) fetchOptions.body = body;
|
||||
|
||||
const url = `${this.baseUrl()}/${path}`;
|
||||
|
||||
let response = null;
|
||||
|
||||
// console.info('WebDAV Call', method + ' ' + url, headers, options);
|
||||
console.info(this.requestToCurl_(url, fetchOptions));
|
||||
|
||||
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
let responseJson_:any = null;
|
||||
const loadResponseJson = async () => {
|
||||
if (!responseText) return null;
|
||||
if (responseJson_) return responseJson_;
|
||||
return JSON.parse(responseText);
|
||||
};
|
||||
|
||||
const newError = (message:string, code:number = 0) => {
|
||||
return new JoplinError(`${method} ${path}: ${message} (${code})`, code);
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
let json = null;
|
||||
try {
|
||||
json = await loadResponseJson();
|
||||
} catch (error) {
|
||||
throw newError(`Unknown error: ${responseText.substr(0, 4096)}`, response.status);
|
||||
}
|
||||
|
||||
const trace = json.stacktrace ? `\n${json.stacktrace}` : '';
|
||||
throw newError(json.error + trace, response.status);
|
||||
}
|
||||
|
||||
const output = await loadResponseJson();
|
||||
return output;
|
||||
}
|
||||
}
|
@ -6,8 +6,10 @@ const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV');
|
||||
const JoplinServerApi = require('lib/JoplinServerApi.js').default;
|
||||
|
||||
class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
|
||||
static id() {
|
||||
return 5;
|
||||
}
|
||||
@ -47,6 +49,21 @@ class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
async initSynchronizer() {
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
|
||||
async appApi() {
|
||||
if (!this.appApi_) {
|
||||
this.appApi_ = new JoplinServerApi({
|
||||
baseUrl: () => JoplinServerApi.baseUrlFromNextcloudWebDavUrl(Setting.value('sync.5.path')),
|
||||
username: () => Setting.value('sync.5.username'),
|
||||
password: () => Setting.value('sync.5.password'),
|
||||
});
|
||||
|
||||
this.appApi_.setLogger(this.logger());
|
||||
}
|
||||
|
||||
return this.appApi_;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
module.exports = SyncTargetNextcloud;
|
||||
|
@ -3,14 +3,24 @@ const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const ObjectUtils = require('lib/ObjectUtils');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { createSelector } = require('reselect');
|
||||
const { reg } = require('lib/registry');
|
||||
|
||||
const shared = {};
|
||||
|
||||
shared.init = function(comp) {
|
||||
if (!comp.state) comp.state = {};
|
||||
comp.state.checkSyncConfigResult = null;
|
||||
comp.state.checkNextcloudAppResult = null;
|
||||
comp.state.settings = {};
|
||||
comp.state.changedSettingKeys = [];
|
||||
comp.state.showNextcloudAppLog = false;
|
||||
comp.state.showAdvancedSettings = false;
|
||||
};
|
||||
|
||||
shared.advancedSettingsButton_click = (comp) => {
|
||||
comp.setState(state => {
|
||||
return { showAdvancedSettings: !state.showAdvancedSettings };
|
||||
});
|
||||
};
|
||||
|
||||
shared.checkSyncConfig = async function(comp, settings) {
|
||||
@ -20,6 +30,12 @@ shared.checkSyncConfig = async function(comp, settings) {
|
||||
comp.setState({ checkSyncConfigResult: 'checking' });
|
||||
const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options));
|
||||
comp.setState({ checkSyncConfigResult: result });
|
||||
|
||||
if (result.ok) {
|
||||
await shared.checkNextcloudApp(comp, settings);
|
||||
// Users often expect config to be auto-saved at this point, if the config check was successful
|
||||
shared.saveSettings(comp);
|
||||
}
|
||||
};
|
||||
|
||||
shared.checkSyncConfigMessages = function(comp) {
|
||||
@ -38,6 +54,28 @@ shared.checkSyncConfigMessages = function(comp) {
|
||||
return output;
|
||||
};
|
||||
|
||||
shared.checkNextcloudApp = async function(comp, settings) {
|
||||
comp.setState({ checkNextcloudAppResult: 'checking' });
|
||||
let result = null;
|
||||
const appApi = await reg.syncTargetNextcloud().appApi();
|
||||
|
||||
try {
|
||||
result = await appApi.setupSyncTarget(settings['sync.5.path']);
|
||||
} catch (error) {
|
||||
reg.logger().error('Could not setup sync target:', error);
|
||||
result = { error: error.message };
|
||||
}
|
||||
|
||||
const newSyncTargets = Object.assign({}, settings['sync.5.syncTargets']);
|
||||
newSyncTargets[settings['sync.5.path']] = result;
|
||||
shared.updateSettingValue(comp, 'sync.5.syncTargets', newSyncTargets);
|
||||
|
||||
// Also immediately save the result as this is most likely what the user would expect
|
||||
Setting.setValue('sync.5.syncTargets', newSyncTargets);
|
||||
|
||||
comp.setState({ checkNextcloudAppResult: 'done' });
|
||||
};
|
||||
|
||||
shared.updateSettingValue = function(comp, key, value) {
|
||||
const settings = Object.assign({}, comp.state.settings);
|
||||
const changedSettingKeys = comp.state.changedSettingKeys.slice();
|
||||
|
@ -162,11 +162,14 @@ class Setting extends BaseModel {
|
||||
'sync.6.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
'sync.7.context': { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
|
||||
'sync.5.syncTargets': { value: {}, type: Setting.TYPE_OBJECT, public: false },
|
||||
|
||||
'sync.resourceDownloadMode': {
|
||||
value: 'always',
|
||||
type: Setting.TYPE_STRING,
|
||||
section: 'sync',
|
||||
public: true,
|
||||
advanced: true,
|
||||
isEnum: true,
|
||||
appTypes: ['mobile', 'desktop'],
|
||||
label: () => _('Attachment download behaviour'),
|
||||
@ -180,7 +183,7 @@ class Setting extends BaseModel {
|
||||
},
|
||||
},
|
||||
|
||||
'sync.maxConcurrentConnections': { value: 5, type: Setting.TYPE_INT, public: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||
'sync.maxConcurrentConnections': { value: 5, type: Setting.TYPE_INT, public: true, advanced: true, section: 'sync', label: () => _('Max concurrent connections'), minimum: 1, maximum: 20, step: 1 },
|
||||
|
||||
activeFolderId: { value: '', type: Setting.TYPE_STRING, public: false },
|
||||
firstStart: { value: true, type: Setting.TYPE_BOOL, public: false },
|
||||
@ -497,6 +500,7 @@ class Setting extends BaseModel {
|
||||
value: '',
|
||||
type: Setting.TYPE_STRING,
|
||||
section: 'sync',
|
||||
advanced: true,
|
||||
show: settings => {
|
||||
return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0;
|
||||
},
|
||||
@ -508,6 +512,7 @@ class Setting extends BaseModel {
|
||||
'net.ignoreTlsErrors': {
|
||||
value: false,
|
||||
type: Setting.TYPE_BOOL,
|
||||
advanced: true,
|
||||
section: 'sync',
|
||||
show: settings => {
|
||||
return [SyncTargetRegistry.nameToId('nextcloud'), SyncTargetRegistry.nameToId('webdav')].indexOf(settings['sync.target']) >= 0;
|
||||
@ -517,7 +522,7 @@ class Setting extends BaseModel {
|
||||
label: () => _('Ignore TLS certificate errors'),
|
||||
},
|
||||
|
||||
'sync.wipeOutFailSafe': { value: true, type: Setting.TYPE_BOOL, public: true, section: 'sync', label: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)') },
|
||||
'sync.wipeOutFailSafe': { value: true, type: Setting.TYPE_BOOL, advanced: true, public: true, section: 'sync', label: () => _('Fail-safe: Do not wipe out local data when sync target is empty (often the result of a misconfiguration or bug)') },
|
||||
|
||||
'api.token': { value: null, type: Setting.TYPE_STRING, public: false },
|
||||
'api.port': { value: null, type: Setting.TYPE_INT, public: true, appTypes: ['cli'], description: () => _('Specify the port that should be used by the API server. If not set, a default will be used.') },
|
||||
|
@ -35,6 +35,10 @@ reg.resetSyncTarget = (syncTargetId = null) => {
|
||||
delete reg.syncTargets_[syncTargetId];
|
||||
};
|
||||
|
||||
reg.syncTargetNextcloud = () => {
|
||||
return reg.syncTarget(SyncTargetRegistry.nameToId('nextcloud'));
|
||||
};
|
||||
|
||||
reg.syncTarget = (syncTargetId = null) => {
|
||||
if (syncTargetId === null) syncTargetId = Setting.value('sync.target');
|
||||
if (reg.syncTargets_[syncTargetId]) return reg.syncTargets_[syncTargetId];
|
||||
|
@ -80,6 +80,15 @@ class Synchronizer {
|
||||
return this.encryptionService_;
|
||||
}
|
||||
|
||||
async waitForSyncToFinish() {
|
||||
if (this.state() === 'idle') return;
|
||||
|
||||
while (true) {
|
||||
await time.sleep(1);
|
||||
if (this.state() === 'idle') return;
|
||||
}
|
||||
}
|
||||
|
||||
static reportToLines(report) {
|
||||
let lines = [];
|
||||
if (report.createLocal) lines.push(_('Created local items: %d.', report.createLocal));
|
||||
|
@ -13,10 +13,12 @@ install:
|
||||
- yarn
|
||||
|
||||
build_script:
|
||||
- ps: xcopy /C /I /H /R /Y /S ReactNativeClient\lib ElectronClient\app\lib
|
||||
- npm install
|
||||
- npm run typescript-compile
|
||||
- ps: cd Tools
|
||||
- npm install
|
||||
- ps: cd ..\ElectronClient\app
|
||||
- ps: xcopy /C /I /H /R /Y /S ..\..\ReactNativeClient\lib lib
|
||||
- npm install
|
||||
- yarn dist
|
||||
|
||||
|
@ -1,7 +1,11 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "Joplin",
|
||||
"path": ".",
|
||||
}, {
|
||||
"name": "Joplin Nextcloud App",
|
||||
"path": "D:/Web/www/nextcloud/apps/joplin",
|
||||
},
|
||||
],
|
||||
"settings": {
|
||||
|
198
package-lock.json
generated
198
package-lock.json
generated
@ -62,7 +62,8 @@
|
||||
"@types/eslint-visitor-keys": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
||||
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag=="
|
||||
"integrity": "sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/events": {
|
||||
"version": "3.0.0",
|
||||
@ -84,7 +85,8 @@
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.3.tgz",
|
||||
"integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A=="
|
||||
"integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/minimatch": {
|
||||
"version": "3.0.3",
|
||||
@ -111,9 +113,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "16.9.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.15.tgz",
|
||||
"integrity": "sha512-WsmM1b6xQn1tG3X2Hx4F3bZwc2E82pJXt5OPs2YJgg71IzvUoKOSSSYOvLXYCg1ttipM+UuA4Lj3sfvqjVxyZw==",
|
||||
"version": "16.9.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.16.tgz",
|
||||
"integrity": "sha512-dQ3wlehuBbYlfvRXfF5G+5TbZF3xqgkikK7DWAsQXe2KnzV+kjD4W2ea+ThCrKASZn9h98bjjPzoTYzfRqyBkw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
@ -130,64 +132,147 @@
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/eslint-plugin": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.2.0.tgz",
|
||||
"integrity": "sha512-rOodtI+IvaO8USa6ValYOrdWm9eQBgqwsY+B0PPiB+aSiK6p6Z4l9jLn/jI3z3WM4mkABAhKIqvGIBl0AFRaLQ==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz",
|
||||
"integrity": "sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@typescript-eslint/experimental-utils": "2.2.0",
|
||||
"eslint-utils": "^1.4.2",
|
||||
"@typescript-eslint/experimental-utils": "2.10.0",
|
||||
"eslint-utils": "^1.4.3",
|
||||
"functional-red-black-tree": "^1.0.1",
|
||||
"regexpp": "^2.0.1",
|
||||
"regexpp": "^3.0.0",
|
||||
"tsutils": "^3.17.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-utils": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
|
||||
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz",
|
||||
"integrity": "sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-visitor-keys": "^1.0.0"
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"@typescript-eslint/typescript-estree": "2.10.0",
|
||||
"eslint-scope": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz",
|
||||
"integrity": "sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"debug": "^4.1.1",
|
||||
"eslint-visitor-keys": "^1.1.0",
|
||||
"glob": "^7.1.6",
|
||||
"is-glob": "^4.0.1",
|
||||
"lodash.unescape": "4.0.1",
|
||||
"semver": "^6.3.0",
|
||||
"tsutils": "^3.17.1"
|
||||
}
|
||||
},
|
||||
"eslint-utils": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz",
|
||||
"integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eslint-visitor-keys": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
|
||||
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
|
||||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"regexpp": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
|
||||
"integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/experimental-utils": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.2.0.tgz",
|
||||
"integrity": "sha512-IMhbewFs27Frd/ICHBRfIcsUCK213B8MsEUqvKFK14SDPjPR5JF6jgOGPlroybFTrGWpMvN5tMZdXAf+xcmxsA==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz",
|
||||
"integrity": "sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.3",
|
||||
"@typescript-eslint/typescript-estree": "2.2.0",
|
||||
"@typescript-eslint/typescript-estree": "2.10.0",
|
||||
"eslint-scope": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/parser": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.2.0.tgz",
|
||||
"integrity": "sha512-0mf893kj9L65O5sA7wP6EoYvTybefuRFavUNhT7w9kjhkdZodoViwVS+k3D+ZxKhvtL7xGtP/y/cNMJX9S8W4A==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-2.10.0.tgz",
|
||||
"integrity": "sha512-wQNiBokcP5ZsTuB+i4BlmVWq6o+oAhd8en2eSm/EE9m7BgZUIfEeYFd6z3S+T7bgNuloeiHA1/cevvbBDLr98g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/eslint-visitor-keys": "^1.0.0",
|
||||
"@typescript-eslint/experimental-utils": "2.2.0",
|
||||
"@typescript-eslint/typescript-estree": "2.2.0",
|
||||
"@typescript-eslint/experimental-utils": "2.10.0",
|
||||
"@typescript-eslint/typescript-estree": "2.10.0",
|
||||
"eslint-visitor-keys": "^1.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
|
||||
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A=="
|
||||
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"@typescript-eslint/typescript-estree": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.2.0.tgz",
|
||||
"integrity": "sha512-9/6x23A3HwWWRjEQbuR24on5XIfVmV96cDpGR9671eJv1ebFKHj2sGVVAwkAVXR2UNuhY1NeKS2QMv5P8kQb2Q==",
|
||||
"version": "2.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz",
|
||||
"integrity": "sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.4",
|
||||
"debug": "^4.1.1",
|
||||
"eslint-visitor-keys": "^1.1.0",
|
||||
"glob": "^7.1.6",
|
||||
"is-glob": "^4.0.1",
|
||||
"lodash.unescape": "4.0.1",
|
||||
"semver": "^6.3.0"
|
||||
"semver": "^6.3.0",
|
||||
"tsutils": "^3.17.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz",
|
||||
"integrity": "sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==",
|
||||
"dev": true
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
|
||||
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"acorn": {
|
||||
@ -275,12 +360,14 @@
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
|
||||
"dev": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
@ -448,7 +535,8 @@
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
|
||||
"dev": true
|
||||
},
|
||||
"cosmiconfig": {
|
||||
"version": "5.2.1",
|
||||
@ -712,6 +800,7 @@
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
|
||||
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"esrecurse": "^4.1.0",
|
||||
"estraverse": "^4.1.1"
|
||||
@ -729,7 +818,8 @@
|
||||
"eslint-visitor-keys": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
|
||||
"integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ=="
|
||||
"integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==",
|
||||
"dev": true
|
||||
},
|
||||
"espree": {
|
||||
"version": "6.0.0",
|
||||
@ -761,6 +851,7 @@
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
|
||||
"integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"estraverse": "^4.1.0"
|
||||
}
|
||||
@ -768,7 +859,8 @@
|
||||
"estraverse": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
|
||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
|
||||
"integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=",
|
||||
"dev": true
|
||||
},
|
||||
"esutils": {
|
||||
"version": "2.0.2",
|
||||
@ -900,7 +992,8 @@
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
|
||||
"dev": true
|
||||
},
|
||||
"function-bind": {
|
||||
"version": "1.1.1",
|
||||
@ -911,7 +1004,8 @@
|
||||
"functional-red-black-tree": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
|
||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
|
||||
"integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=",
|
||||
"dev": true
|
||||
},
|
||||
"get-own-enumerable-property-symbols": {
|
||||
"version": "3.0.0",
|
||||
@ -932,6 +1026,7 @@
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
|
||||
"integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
@ -1092,6 +1187,7 @@
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
@ -1100,7 +1196,8 @@
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"inquirer": {
|
||||
"version": "6.5.0",
|
||||
@ -1159,7 +1256,8 @@
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI="
|
||||
"integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
|
||||
"dev": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "2.0.0",
|
||||
@ -1171,6 +1269,7 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
|
||||
"integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
@ -1543,7 +1642,8 @@
|
||||
"lodash.unescape": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.unescape/-/lodash.unescape-4.0.1.tgz",
|
||||
"integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw="
|
||||
"integrity": "sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=",
|
||||
"dev": true
|
||||
},
|
||||
"log-symbols": {
|
||||
"version": "3.0.0",
|
||||
@ -1606,6 +1706,7 @@
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
@ -1748,6 +1849,7 @@
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
@ -1851,7 +1953,8 @@
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
|
||||
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
|
||||
"dev": true
|
||||
},
|
||||
"path-is-inside": {
|
||||
"version": "1.0.2",
|
||||
@ -1975,7 +2078,8 @@
|
||||
"regexpp": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
|
||||
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw=="
|
||||
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
|
||||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.11.1",
|
||||
@ -2056,7 +2160,8 @@
|
||||
"semver": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw=="
|
||||
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
|
||||
"dev": true
|
||||
},
|
||||
"semver-compare": {
|
||||
"version": "1.0.0",
|
||||
@ -2286,12 +2391,14 @@
|
||||
"tslib": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==",
|
||||
"dev": true
|
||||
},
|
||||
"tsutils": {
|
||||
"version": "3.17.1",
|
||||
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.17.1.tgz",
|
||||
"integrity": "sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.8.1"
|
||||
}
|
||||
@ -2381,7 +2488,8 @@
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
|
||||
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
|
||||
"dev": true
|
||||
},
|
||||
"write": {
|
||||
"version": "1.0.3",
|
||||
|
16
package.json
16
package.json
@ -3,8 +3,10 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Joplin root package for linting",
|
||||
"scripts": {
|
||||
"linter": "./node_modules/.bin/eslint --fix --ext .js --ext .jsx",
|
||||
"linter-ci": "./node_modules/.bin/eslint --ext .js --ext .jsx"
|
||||
"linter": "./node_modules/.bin/eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"linter-ci": "./node_modules/.bin/eslint --ext .js --ext .jsx --ext .ts --ext .tsx",
|
||||
"typescript-compile": "tsc",
|
||||
"typescript-watch": "tsc --watch"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -12,7 +14,7 @@
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx}": [
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"npm run linter",
|
||||
"git add"
|
||||
]
|
||||
@ -23,16 +25,14 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@types/react": "^16.9.15",
|
||||
"@types/react": "^16.9.16",
|
||||
"@types/react-dom": "^16.9.4",
|
||||
"@typescript-eslint/eslint-plugin": "^2.10.0",
|
||||
"@typescript-eslint/parser": "^2.10.0",
|
||||
"eslint": "^6.1.0",
|
||||
"eslint-plugin-react": "^7.14.3",
|
||||
"husky": "^3.0.2",
|
||||
"lint-staged": "^9.2.1",
|
||||
"typescript": "^3.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^2.2.0",
|
||||
"@typescript-eslint/parser": "^2.2.0"
|
||||
}
|
||||
}
|
||||
|
10
readme/nextcloud_app.md
Normal file
10
readme/nextcloud_app.md
Normal file
@ -0,0 +1,10 @@
|
||||
# Nextcloud App
|
||||
|
||||
**This is a beta feature, not yet completed. More info coming soon!**
|
||||
|
||||
The Joplin Nextcloud App is a helper application that enables certain features that are not possible otherwise. In particular:
|
||||
|
||||
- [Done] Sharing a note publicly
|
||||
- [Not done] Sharing a note with another Joplin user (who uses the same Nextcloud instance)
|
||||
- [Not done] Collaborating on a note
|
||||
- [Not done] Sharing a notebook
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es2018",
|
||||
"target": "es2015",
|
||||
"alwaysStrict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"listEmittedFiles": true,
|
||||
@ -16,10 +16,12 @@
|
||||
"jsx": "react",
|
||||
},
|
||||
"include": [
|
||||
"ReactNativeClient/lib/**/*.ts",
|
||||
"ReactNativeClient/lib/**/*.tsx",
|
||||
"ReactNativeClient/**/*.ts",
|
||||
"ReactNativeClient/**/*.tsx",
|
||||
"ElectronClient/**/*.ts",
|
||||
"ElectronClient/**/*.tsx",
|
||||
"CliClient/**/*.ts",
|
||||
"CliClient/**/*.tsx",
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules",
|
||||
|
Loading…
Reference in New Issue
Block a user