1
0
mirror of https://github.com/laurent22/joplin.git synced 2024-12-21 09:38:01 +02:00

Desktop: Add ability to share a note publicly using Nextcloud (#2173)

* Moved button row to separate component file and started Sharing dialog

* Adding Sharing dialog

* Applied "npx react-codemod rename-unsafe-lifecycles"

* More UI

* Tools: Improved TypeScript integration

* Improved share dialog

* Tools Added support for translation validation in CI, and added support for plural translations

* Improved UI and sharing workflow

* Share workflow

* Cleaned up and improved sharing config error handling

* Fixed build scripts and doc for TypeScript

* Run linter
This commit is contained in:
Laurent Cozic 2019-12-13 01:16:34 +00:00 committed by GitHub
parent 611be7c0fa
commit 34f0a2951a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 988 additions and 159 deletions

View File

@ -45,4 +45,8 @@ Server/docs/
Server/dist/
Server/bin/
Server/node_modules/
ElectronClient/app/packageInfo.js
ElectronClient/app/packageInfo.js
# Ignore files generated from TypeScript files
ElectronClient/app/gui/ShareNoteDialog.js
ReactNativeClient/lib/JoplinServerApi.js

5
.gitignore vendored
View File

@ -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

View File

@ -50,12 +50,17 @@ before_install:
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update || true
sudo apt-get install -y yarn
sudo apt-get install -y gettext
fi
script:
- |
# Copy lib
rsync -aP --delete ReactNativeClient/lib/ ElectronClient/app/lib/
# Install tools
npm install
npm run typescript-compile
cd Tools
npm install
cd ..
@ -84,6 +89,19 @@ script:
fi
fi
# Validate translations - this is needed as some users manually
# edit .po files (and often make mistakes) instead of using a proper
# tool like poedit. Doing it for Linux only is sufficient.
if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
if [ "$TRAVIS_OS_NAME" != "osx" ]; then
node Tools/validate-translation.js
testResult=$?
if [ $testResult -ne 0 ]; then
exit $testResult
fi
fi
fi
# Find out if we should run the build or not. Electron-builder gets stuck when
# builing PRs so we disable it in this case. The Linux build should provide
# enough info if the app builds or not.
@ -96,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

View File

@ -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.

View File

@ -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"

View File

@ -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 = [];
for (let i = 0; i < section.metadatas.length; i++) {
const md = section.metadatas[i];
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]);
output.push(settingComp);
}
return output;
};
const settingComp = this.settingToComponent(md.key, settings[md.key]);
settingComps.push(settingComp);
}
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>
&nbsp;&nbsp;
{showLogButton}
&nbsp;&nbsp;
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
{_('Check Status')}
</button>
&nbsp;&nbsp;
<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>
);
}

View 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;

View File

@ -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();
}

View File

@ -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);
}

View File

@ -71,7 +71,7 @@ class HeaderComponent extends React.Component {
};
}
async componentWillReceiveProps(nextProps) {
async UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.windowCommand) {
this.doCommand(nextProps.windowCommand);
}

View File

@ -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(
{

View File

@ -32,11 +32,11 @@ class ItemList extends React.Component {
});
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.updateStateItemIndexes();
}
componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
this.updateStateItemIndexes(newProps);
}

View File

@ -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} />

View File

@ -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'];

View File

@ -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(
{
editedKey: null,
editedValue: null,
},
() => {
resolve();
}
);
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>
);

View File

@ -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) {

View File

@ -18,7 +18,7 @@ class OneDriveLoginScreenComponent extends React.Component {
this.webview_.src = this.startUrl();
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({
webviewUrl: this.startUrl(),
webviewReady: false,

View File

@ -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;

View 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>
);
}

View File

@ -16,7 +16,7 @@ class StatusScreenComponent extends React.Component {
};
}
componentWillMount() {
UNSAFE_componentWillMount() {
this.resfreshScreen();
}

View File

@ -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();

View File

@ -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"

View File

@ -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",

View File

@ -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 };

View 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;
}
}

View File

@ -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;

View File

@ -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();

View File

@ -317,4 +317,9 @@ function _(s, ...args) {
}
}
module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };
function _n(singular, plural, n, ...args) {
if (n > 1) return _(plural, ...args);
return _(singular, ...args);
}
module.exports = { _, _n, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };

View File

@ -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.') },

View File

@ -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];

View File

@ -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));

View File

@ -88,6 +88,7 @@ async function createPotFile(potFilePath, sources) {
baseArgs.push('--package-name=Joplin-CLI');
baseArgs.push('--package-version=1.0.0');
baseArgs.push('--no-location');
baseArgs.push('--keyword=_n:1,2');
for (let i = 0; i < sources.length; i++) {
let args = baseArgs.slice();

View File

@ -0,0 +1,35 @@
'use strict';
// Dependencies:
//
// sudo apt install gettext
require('app-module-path').addPath(`${__dirname}/../ReactNativeClient`);
const rootDir = `${__dirname}/..`;
const fs = require('fs-extra');
const cliLocalesDir = `${rootDir}/CliClient/locales`;
const { execCommand } = require('./tool-utils.js');
async function main() {
const files = fs.readdirSync(cliLocalesDir);
let hasErrors = false;
for (const file of files) {
if (!file.endsWith('.po')) continue;
const fullPath = `${cliLocalesDir}/${file}`;
try {
await execCommand(`msgfmt -v "${fullPath}"`);
} catch (error) {
hasErrors = true;
console.error(error);
}
}
if (hasErrors) throw new Error('Some .po files could not be validated');
}
main().catch((error) => {
console.error(error);
process.exit(1);
});

View File

@ -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

View File

@ -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
View File

@ -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",

View File

@ -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
View 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

View File

@ -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",