You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-08-13 22:12:50 +02:00
Merge branch 'master' of github.com:laurent22/joplin
This commit is contained in:
21
BUILD.md
21
BUILD.md
@@ -1,20 +1,21 @@
|
||||
# General information
|
||||
|
||||
- All the applications share the same library, which, for historical reasons, is in ReactNativeClient/lib. This library is copied to the relevant directories when builing each app.
|
||||
- The translations are built by running CliClient/build-translation.sh. For this reasons, it's generally better to get the CLI app to build first so that everything is setup correctly.
|
||||
- Note: building translations is no longer required to run the apps, so you can ignore all the below requirements about gettext.
|
||||
- The translations are built by running CliClient/build-translation.sh. You normally don't need to run this if you haven't updated the translation since the compiled files are on the repository.
|
||||
|
||||
## macOS dependencies
|
||||
|
||||
brew install yarn node xgettext
|
||||
npm install -g node-gyp
|
||||
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
brew install yarn node
|
||||
echo 'export PATH="/usr/local/opt/gettext/bin:$PATH"' >> ~/.bash_profile
|
||||
source ~/.bash_profile
|
||||
|
||||
If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
## Linux and Windows dependencies
|
||||
## 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/
|
||||
- If you get a node-gyp related error you might need to manually install it: `npm install -g node-gyp`
|
||||
|
||||
# Building the Electron application
|
||||
|
||||
@@ -37,4 +38,8 @@ From `/ReactNativeClient`, run `npm install`, then `react-native run-ios` or `re
|
||||
|
||||
# Building the Terminal application
|
||||
|
||||
From `/CliClient`, run `npm install` then run `run.sh`. If you get an error about `xgettext`, comment out the command `node build-translation.js --silent` in build.sh
|
||||
From `/CliClient`:
|
||||
- Run `npm install`
|
||||
- Then `build.sh`
|
||||
- Copy the translations to the build directory: `rsync -aP ../ReactNativeClient/locales/ build/locales/`
|
||||
- Run `run.sh` to start the application for testing.
|
||||
|
@@ -246,9 +246,13 @@ class Application extends BaseApplication {
|
||||
try {
|
||||
CommandClass = require(__dirname + '/command-' + name + '.js');
|
||||
} catch (error) {
|
||||
let e = new Error('No such command: ' + name);
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
if (error.message && error.message.indexOf('Cannot find module') >= 0) {
|
||||
let e = new Error(_('No such command: %s', name));
|
||||
e.type = 'notFound';
|
||||
throw e;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
let cmd = new CommandClass();
|
||||
|
1062
CliClient/locales/de_DE.po
Normal file
1062
CliClient/locales/de_DE.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -100,6 +100,10 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
@@ -508,6 +512,9 @@ msgstr ""
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
@@ -568,6 +575,12 @@ msgstr ""
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -605,6 +618,9 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr ""
|
||||
|
||||
@@ -812,6 +828,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
|
||||
|
1030
CliClient/locales/es_419.po
Normal file
1030
CliClient/locales/es_419.po
Normal file
File diff suppressed because it is too large
Load Diff
@@ -106,6 +106,10 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr "Cancelando sincronización en segundo plano... Por favor espere."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr "Comando inválido: \"%s\""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr "El comando \"%s\" solo está disponible en el modo gráfico (GUI)"
|
||||
@@ -553,6 +557,10 @@ msgstr "Buscar en todas la notas"
|
||||
msgid "Tools"
|
||||
msgstr "Herramientas"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Objetivo de sincronización"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Opciones"
|
||||
|
||||
@@ -616,6 +624,13 @@ msgstr "Establecer alarma"
|
||||
msgid "Layout"
|
||||
msgstr "Plantilla"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "No se puede iniciar la sincronización."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Añadir o eliminar etiquetas"
|
||||
|
||||
@@ -654,6 +669,10 @@ msgstr "Inicio de sesión de OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importar"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Objetivo de sincronización"
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr "¿Eliminar cuaderno?"
|
||||
|
||||
@@ -869,6 +888,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Valor inválido: \"%s\". Posibles valores: %s."
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
"Estado de sincronización (elementos sincronizados / total de elementos)"
|
||||
|
@@ -100,6 +100,10 @@ msgstr "o"
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr "Annulation de la synchronisation... Veuillez patienter."
|
||||
|
||||
#, fuzzy, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr "Commande invalide : \"%s\""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
@@ -558,6 +562,10 @@ msgstr "Chercher dans toutes les notes"
|
||||
msgid "Tools"
|
||||
msgstr "Outils"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation status"
|
||||
msgstr "Cible de la synchronisation"
|
||||
|
||||
msgid "Options"
|
||||
msgstr "Options"
|
||||
|
||||
@@ -621,6 +629,13 @@ msgstr "Définir ou modifier alarme"
|
||||
msgid "Layout"
|
||||
msgstr "Disposition"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr "Impossible d'initialiser la synchronisation."
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr "Gérer les étiquettes"
|
||||
|
||||
@@ -660,6 +675,10 @@ msgstr "Connexion OneDrive"
|
||||
msgid "Import"
|
||||
msgstr "Importer"
|
||||
|
||||
#, fuzzy
|
||||
msgid "Synchronisation Status"
|
||||
msgstr "Cible de la synchronisation"
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr "Supprimer le carnet ?"
|
||||
|
||||
@@ -875,6 +894,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr "Option invalide: \"%s\". Les valeurs possibles sont : %s."
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr "Status de la synchronisation (objets synchro. / total)"
|
||||
|
||||
@@ -1078,10 +1104,6 @@ msgstr "Bienvenue"
|
||||
#~ msgid "Show/Hide the console"
|
||||
#~ msgstr "Quitter le logiciel."
|
||||
|
||||
#, fuzzy
|
||||
#~ msgid "Last command: %s"
|
||||
#~ msgstr "Commande invalide : \"%s\""
|
||||
|
||||
#~ msgid "Done editing."
|
||||
#~ msgstr "Edition terminée."
|
||||
|
||||
|
@@ -100,6 +100,10 @@ msgstr ""
|
||||
msgid "Cancelling background synchronisation... Please wait."
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "No such command: %s"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "The command \"%s\" is only available in GUI mode"
|
||||
msgstr ""
|
||||
@@ -508,6 +512,9 @@ msgstr ""
|
||||
msgid "Tools"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Options"
|
||||
msgstr ""
|
||||
|
||||
@@ -568,6 +575,12 @@ msgstr ""
|
||||
msgid "Layout"
|
||||
msgstr ""
|
||||
|
||||
msgid "Some items cannot be synchronised."
|
||||
msgstr ""
|
||||
|
||||
msgid "View them now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add or remove tags"
|
||||
msgstr ""
|
||||
|
||||
@@ -605,6 +618,9 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
msgid "Synchronisation Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Delete notebook?"
|
||||
msgstr ""
|
||||
|
||||
@@ -812,6 +828,13 @@ msgstr ""
|
||||
msgid "Invalid option value: \"%s\". Possible values are: %s."
|
||||
msgstr ""
|
||||
|
||||
msgid "Items that cannot be synchronised"
|
||||
msgstr ""
|
||||
|
||||
#, javascript-format
|
||||
msgid "\"%s\": \"%s\""
|
||||
msgstr ""
|
||||
|
||||
msgid "Sync status (synced items / total items)"
|
||||
msgstr ""
|
||||
|
||||
|
2
CliClient/package-lock.json
generated
2
CliClient/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "0.10.77",
|
||||
"version": "0.10.78",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -18,7 +18,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "0.10.77",
|
||||
"version": "0.10.78",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
|
@@ -629,4 +629,32 @@ describe('Synchronizer', function() {
|
||||
done();
|
||||
});
|
||||
|
||||
it('items should skip items that cannot be synced', async (done) => {
|
||||
let folder1 = await Folder.save({ title: "folder1" });
|
||||
let note1 = await Note.save({ title: "un", is_todo: 1, parent_id: folder1.id });
|
||||
const noteId = note1.id;
|
||||
await synchronizer().start();
|
||||
let disabledItems = await BaseItem.syncDisabledItems();
|
||||
expect(disabledItems.length).toBe(0);
|
||||
await Note.save({ id: noteId, title: "un mod", });
|
||||
synchronizer().debugFlags_ = ['cannotSync'];
|
||||
await synchronizer().start();
|
||||
synchronizer().debugFlags_ = [];
|
||||
await synchronizer().start(); // Another sync to check that this item is now excluded from sync
|
||||
|
||||
await switchClient(2);
|
||||
|
||||
await synchronizer().start();
|
||||
let notes = await Note.all();
|
||||
expect(notes.length).toBe(1);
|
||||
expect(notes[0].title).toBe('un');
|
||||
|
||||
await switchClient(1);
|
||||
|
||||
disabledItems = await BaseItem.syncDisabledItems();
|
||||
expect(disabledItems.length).toBe(1);
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
@@ -43,8 +43,9 @@ const syncDir = __dirname + '/../tests/sync';
|
||||
const sleepTime = syncTargetId_ == SyncTargetRegistry.nameToId('filesystem') ? 1001 : 400;
|
||||
|
||||
const logger = new Logger();
|
||||
logger.addTarget('console');
|
||||
logger.addTarget('file', { path: logDir + '/log.txt' });
|
||||
logger.setLevel(Logger.LEVEL_DEBUG);
|
||||
logger.setLevel(Logger.LEVEL_WARN);
|
||||
|
||||
BaseItem.loadClass('Note', Note);
|
||||
BaseItem.loadClass('Folder', Folder);
|
||||
|
@@ -259,6 +259,14 @@ class Application extends BaseApplication {
|
||||
}, {
|
||||
label: _('Tools'),
|
||||
submenu: [{
|
||||
label: _('Synchronisation status'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Status',
|
||||
});
|
||||
}
|
||||
},{
|
||||
label: _('Options'),
|
||||
click: () => {
|
||||
this.dispatch({
|
||||
|
@@ -229,8 +229,8 @@ class MainScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
styles(themeId, width, height) {
|
||||
const styleKey = themeId + '_' + width + '_' + height;
|
||||
styles(themeId, width, height, messageBoxVisible) {
|
||||
const styleKey = themeId + '_' + width + '_' + height + '_' + messageBoxVisible;
|
||||
if (styleKey === this.styleKey_) return this.styles_;
|
||||
|
||||
const theme = themeStyle(themeId);
|
||||
@@ -239,12 +239,21 @@ class MainScreenComponent extends React.Component {
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
const rowHeight = height - theme.headerHeight;
|
||||
|
||||
this.styles_.header = {
|
||||
width: width,
|
||||
};
|
||||
|
||||
this.styles_.messageBox = {
|
||||
width: width,
|
||||
height: 30,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
paddingLeft: 10,
|
||||
backgroundColor: theme.warningBackgroundColor,
|
||||
}
|
||||
|
||||
const rowHeight = height - theme.headerHeight - (messageBoxVisible ? this.styles_.messageBox.height : 0);
|
||||
|
||||
this.styles_.sideBar = {
|
||||
width: Math.floor(layoutUtils.size(width * .2, 150, 300)),
|
||||
height: rowHeight,
|
||||
@@ -280,7 +289,8 @@ class MainScreenComponent extends React.Component {
|
||||
const folders = this.props.folders;
|
||||
const notes = this.props.notes;
|
||||
|
||||
const styles = this.styles(this.props.theme, style.width, style.height);
|
||||
const styles = this.styles(this.props.theme, style.width, style.height, true);
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const headerButtons = [];
|
||||
|
||||
@@ -325,6 +335,21 @@ class MainScreenComponent extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
const onViewDisabledItemsClick = () => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Status',
|
||||
});
|
||||
}
|
||||
|
||||
const messageComp = this.props.hasDisabledSyncItems ? (
|
||||
<div style={styles.messageBox}>
|
||||
<span style={theme.textStyle}>
|
||||
{_('Some items cannot be synchronised.')} <a href="#" onClick={() => { onViewDisabledItemsClick() }}>{_('View them now')}</a>
|
||||
</span>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<PromptDialog
|
||||
@@ -339,6 +364,7 @@ class MainScreenComponent extends React.Component {
|
||||
buttons={promptOptions && ('buttons' in promptOptions) ? promptOptions.buttons : null}
|
||||
inputType={promptOptions && ('inputType' in promptOptions) ? promptOptions.inputType : null} />
|
||||
<Header style={styles.header} showBackButton={false} buttons={headerButtons} />
|
||||
{messageComp}
|
||||
<SideBar style={styles.sideBar} />
|
||||
<NoteList style={styles.noteList} />
|
||||
<NoteText style={styles.noteText} visiblePanes={this.props.noteVisiblePanes} />
|
||||
@@ -355,6 +381,7 @@ const mapStateToProps = (state) => {
|
||||
noteVisiblePanes: state.noteVisiblePanes,
|
||||
folders: state.folders,
|
||||
notes: state.notes,
|
||||
hasDisabledSyncItems: state.hasDisabledSyncItems,
|
||||
};
|
||||
};
|
||||
|
||||
|
@@ -8,6 +8,7 @@ const { Setting } = require('lib/models/setting.js');
|
||||
|
||||
const { MainScreen } = require('./MainScreen.min.js');
|
||||
const { OneDriveLoginScreen } = require('./OneDriveLoginScreen.min.js');
|
||||
const { StatusScreen } = require('./StatusScreen.min.js');
|
||||
const { ImportScreen } = require('./ImportScreen.min.js');
|
||||
const { ConfigScreen } = require('./ConfigScreen.min.js');
|
||||
const { Navigator } = require('./Navigator.min.js');
|
||||
@@ -75,6 +76,7 @@ class RootComponent extends React.Component {
|
||||
OneDriveLogin: { screen: OneDriveLoginScreen, title: () => _('OneDrive Login') },
|
||||
Import: { screen: ImportScreen, title: () => _('Import') },
|
||||
Config: { screen: ConfigScreen, title: () => _('Options') },
|
||||
Status: { screen: StatusScreen, title: () => _('Synchronisation Status') },
|
||||
};
|
||||
|
||||
return (
|
||||
|
118
ElectronClient/app/gui/StatusScreen.jsx
Normal file
118
ElectronClient/app/gui/StatusScreen.jsx
Normal file
@@ -0,0 +1,118 @@
|
||||
const React = require('react');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { Setting } = require('lib/models/setting.js');
|
||||
const { bridge } = require('electron').remote.require('./bridge');
|
||||
const { Header } = require('./Header.min.js');
|
||||
const { themeStyle } = require('../theme.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
|
||||
class StatusScreenComponent extends React.Component {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
report: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.resfreshScreen();
|
||||
}
|
||||
|
||||
async resfreshScreen() {
|
||||
const service = new ReportService();
|
||||
const report = await service.status(Setting.value('sync.target'));
|
||||
this.setState({ report: report });
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const style = this.props.style;
|
||||
|
||||
const headerStyle = {
|
||||
width: style.width,
|
||||
};
|
||||
|
||||
const containerPadding = 10;
|
||||
|
||||
const containerStyle = {
|
||||
padding: containerPadding,
|
||||
overflowY: 'auto',
|
||||
height: style.height - theme.headerHeight - containerPadding * 2,
|
||||
};
|
||||
|
||||
function renderSectionTitleHtml(key, title) {
|
||||
return <h2 key={'section_' + key} style={theme.h2Style}>{title}</h2>
|
||||
}
|
||||
|
||||
function renderSectionHtml(key, section) {
|
||||
let itemsHtml = [];
|
||||
|
||||
itemsHtml.push(renderSectionTitleHtml(section.title, section.title));
|
||||
|
||||
for (let n in section.body) {
|
||||
if (!section.body.hasOwnProperty(n)) continue;
|
||||
itemsHtml.push(<div style={theme.textStyle} key={'item_' + n}>{section.body[n]}</div>);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={key}>
|
||||
{itemsHtml}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderBodyHtml(report) {
|
||||
let output = [];
|
||||
let baseStyle = {
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
flex: 0,
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
};
|
||||
|
||||
let sectionsHtml = [];
|
||||
|
||||
for (let i = 0; i < report.length; i++) {
|
||||
let section = report[i];
|
||||
if (!section.body.length) continue;
|
||||
sectionsHtml.push(renderSectionHtml(i, section));
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{sectionsHtml}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let body = renderBodyHtml(this.state.report);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<Header style={headerStyle} />
|
||||
<div style={containerStyle}>
|
||||
{body}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const mapStateToProps = (state) => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
settings: state.settings,
|
||||
locale: state.settings.locale,
|
||||
};
|
||||
};
|
||||
|
||||
const StatusScreen = connect(mapStateToProps)(StatusScreenComponent);
|
||||
|
||||
module.exports = { StatusScreen };
|
1
ElectronClient/app/locales/de_DE.json
Normal file
1
ElectronClient/app/locales/de_DE.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
var locales = {};
|
||||
locales['en_GB'] = require('./en_GB.json');
|
||||
locales['de_DE'] = require('./de_DE.json');
|
||||
locales['es_CR'] = require('./es_CR.json');
|
||||
locales['fr_FR'] = require('./fr_FR.json');
|
||||
module.exports = { locales: locales };
|
@@ -9,6 +9,19 @@ body, textarea {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table th {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
table td, table th {
|
||||
padding: .5em;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
/* By default, the Ice Editor displays invalid characters, such as non-breaking spaces
|
||||
as red boxes, but since those are actually valid characters and common in imported
|
||||
Evernote data, we hide them here. */
|
||||
|
@@ -25,6 +25,8 @@ const globalStyle = {
|
||||
selectedColor2: "#5A4D70",
|
||||
colorError2: "#ff6c6c",
|
||||
|
||||
warningBackgroundColor: "#FFD08D",
|
||||
|
||||
headerHeight: 35,
|
||||
headerButtonHPadding: 6,
|
||||
|
||||
@@ -69,6 +71,9 @@ globalStyle.textStyle2 = Object.assign({}, globalStyle.textStyle, {
|
||||
color: globalStyle.color2,
|
||||
});
|
||||
|
||||
globalStyle.h2Style = Object.assign({}, globalStyle.textStyle);
|
||||
globalStyle.h2Style.fontSize *= 1.3;
|
||||
|
||||
let themeCache_ = {};
|
||||
|
||||
function themeStyle(theme) {
|
||||
|
@@ -123,15 +123,27 @@ class FileApiDriverOneDrive {
|
||||
return this.makeItem_(item);
|
||||
}
|
||||
|
||||
put(path, content, options = null) {
|
||||
async put(path, content, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.source == 'file') {
|
||||
return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, null, options);
|
||||
} else {
|
||||
options.headers = { 'Content-Type': 'text/plain' };
|
||||
return this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options);
|
||||
let response = null;
|
||||
|
||||
try {
|
||||
if (options.source == 'file') {
|
||||
response = await this.api_.exec('PUT', this.makePath_(path) + ':/content', null, null, options);
|
||||
} else {
|
||||
options.headers = { 'Content-Type': 'text/plain' };
|
||||
response = await this.api_.exec('PUT', this.makePath_(path) + ':/content', null, content, options);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.code === 'BadRequest' && error.message === 'Maximum request length exceeded.') {
|
||||
error.code = 'cannotSync';
|
||||
error.message = 'Resource exceeds OneDrive max file size (4MB)';
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
delete(path) {
|
||||
|
@@ -194,11 +194,15 @@ function addResourceTag(lines, resource, alt = "") {
|
||||
|
||||
|
||||
function isBlockTag(n) {
|
||||
return n=="div" || n=="p" || n=="dl" || n=="dd" || n=="center";
|
||||
return n=="div" || n=="p" || n=="dl" || n=="dd" || n == 'dt' || n=="center";
|
||||
}
|
||||
|
||||
function isStrongTag(n) {
|
||||
return n == "strong" || n == "b";
|
||||
return n == "strong" || n == "b" || n == 'big';
|
||||
}
|
||||
|
||||
function isStrikeTag(n) {
|
||||
return n == "strike" || n == "s" || n == 'del';
|
||||
}
|
||||
|
||||
function isEmTag(n) {
|
||||
@@ -210,7 +214,7 @@ function isAnchor(n) {
|
||||
}
|
||||
|
||||
function isIgnoredEndTag(n) {
|
||||
return n=="en-note" || n=="en-todo" || n=="span" || n=="body" || n=="html" || n=="font" || n=="br" || n=='hr' || n=='s' || n == 'tbody' || n == 'sup';
|
||||
return n=="en-note" || n=="en-todo" || n=="span" || n=="body" || n=="html" || n=="font" || n=="br" || n=='hr' || n=='s' || n == 'tbody' || n == 'sup' || n == 'img' || n == 'abbr' || n == 'cite' || n == 'thead' || n == 'small' || n == 'tt' || n == 'sub';
|
||||
}
|
||||
|
||||
function isListTag(n) {
|
||||
@@ -219,7 +223,7 @@ function isListTag(n) {
|
||||
|
||||
// Elements that don't require any special treatment beside adding a newline character
|
||||
function isNewLineOnlyEndTag(n) {
|
||||
return n=="div" || n=="p" || n=="li" || n=="h1" || n=="h2" || n=="h3" || n=="h4" || n=="h5" || n=="dl" || n=="dd" || n=="center";
|
||||
return n=="div" || n=="p" || n=="li" || n=="h1" || n=="h2" || n=="h3" || n=="h4" || n=="h5" || n=='h6' || n=="dl" || n=="dd" || n == 'dt' || n=="center";
|
||||
}
|
||||
|
||||
function isCodeTag(n) {
|
||||
@@ -253,8 +257,27 @@ function xmlNodeText(xmlNode) {
|
||||
return xmlNode[0];
|
||||
}
|
||||
|
||||
function attributeToLowerCase(node) {
|
||||
if (!node.attributes) return {};
|
||||
let output = {};
|
||||
for (let n in node.attributes) {
|
||||
if (!node.attributes.hasOwnProperty(n)) continue;
|
||||
output[n.toLowerCase()] = node.attributes[n];
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function enexXmlToMdArray(stream, resources) {
|
||||
resources = resources.slice();
|
||||
let remainingResources = resources.slice();
|
||||
|
||||
const removeRemainingResource = (id) => {
|
||||
for (let i = 0; i < remainingResources.length; i++) {
|
||||
const r = remainingResources[i];
|
||||
if (r.id === id) {
|
||||
remainingResources.splice(i, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let state = {
|
||||
@@ -265,7 +288,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
};
|
||||
|
||||
let options = {};
|
||||
let strict = true;
|
||||
let strict = false;
|
||||
var saxStream = require('sax').createStream(strict, options)
|
||||
|
||||
let section = {
|
||||
@@ -275,14 +298,18 @@ function enexXmlToMdArray(stream, resources) {
|
||||
};
|
||||
|
||||
saxStream.on('error', function(e) {
|
||||
reject(e);
|
||||
console.warn(e);
|
||||
//reject(e);
|
||||
})
|
||||
|
||||
saxStream.on('text', function(text) {
|
||||
if (['table', 'tr', 'tbody'].indexOf(section.type) >= 0) return;
|
||||
section.lines = collapseWhiteSpaceAndAppend(section.lines, state, text);
|
||||
})
|
||||
|
||||
saxStream.on('opentag', function(node) {
|
||||
const nodeAttributes = attributeToLowerCase(node);
|
||||
|
||||
let n = node.name.toLowerCase();
|
||||
if (n == 'en-note') {
|
||||
// Start of note
|
||||
@@ -293,25 +320,51 @@ function enexXmlToMdArray(stream, resources) {
|
||||
type: 'table',
|
||||
lines: [],
|
||||
parent: section,
|
||||
toString: function() {
|
||||
let output = [];
|
||||
output.push(BLOCK_OPEN);
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
output = output.concat(this.lines[i].toMdLines());
|
||||
}
|
||||
output.push(BLOCK_CLOSE);
|
||||
return processMdArrayNewLines(output);
|
||||
},
|
||||
};
|
||||
section.lines.push(newSection);
|
||||
section = newSection;
|
||||
} else if (n == 'tbody') {
|
||||
} else if (n == 'tbody' || n == 'thead') {
|
||||
// Ignore it
|
||||
} else if (n == 'tr') {
|
||||
if (section.type != 'table') throw new Error('Found a <tr> tag outside of a table');
|
||||
if (section.type != 'table') {
|
||||
console.warn('Found a <tr> tag outside of a table');
|
||||
return;
|
||||
}
|
||||
|
||||
let newSection = {
|
||||
type: 'tr',
|
||||
lines: [],
|
||||
parent: section,
|
||||
isHeader: false,
|
||||
// Normally tables are rendered properly as markdown, but for table within table within table... we cannot
|
||||
// handle this in Markdown so simply render it as one cell per line.
|
||||
toMdLines: function() {
|
||||
let output = [];
|
||||
output.push(BLOCK_OPEN);
|
||||
for (let i = 0; i < this.lines.length; i++) {
|
||||
output.push(this.lines[i].toString());
|
||||
}
|
||||
output.push(BLOCK_CLOSE);
|
||||
return output;
|
||||
},
|
||||
}
|
||||
|
||||
section.lines.push(newSection);
|
||||
section = newSection;
|
||||
} else if (n == 'td' || n == 'th') {
|
||||
if (section.type != 'tr') throw new Error('Found a <td> tag outside of a <tr>');
|
||||
if (section.type != 'tr') {
|
||||
console.warn('Found a <td> tag outside of a <tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
if (n == 'th') section.isHeader = true;
|
||||
|
||||
@@ -319,6 +372,9 @@ function enexXmlToMdArray(stream, resources) {
|
||||
type: 'td',
|
||||
lines: [],
|
||||
parent: section,
|
||||
toString: function() {
|
||||
return processMdArrayNewLines(this.lines);
|
||||
},
|
||||
};
|
||||
|
||||
section.lines.push(newSection);
|
||||
@@ -342,17 +398,27 @@ function enexXmlToMdArray(stream, resources) {
|
||||
}
|
||||
} else if (isStrongTag(n)) {
|
||||
section.lines.push("**");
|
||||
} else if (n == 's') {
|
||||
// Not supported
|
||||
} else if (isStrikeTag(n)) {
|
||||
section.lines.push('(');
|
||||
} else if (n == 'samp') {
|
||||
section.lines.push('`');
|
||||
} else if (n == 'q') {
|
||||
section.lines.push('"');
|
||||
} else if (n == 'img') {
|
||||
// TODO: TEST IMAGE
|
||||
if (nodeAttributes.src) { // Many (most?) img tags don't have no source associated, especially when they were imported from HTML
|
||||
let s = '';
|
||||
section.lines.push(s);
|
||||
}
|
||||
} else if (isAnchor(n)) {
|
||||
state.anchorAttributes.push(node.attributes);
|
||||
state.anchorAttributes.push(nodeAttributes);
|
||||
section.lines.push('[');
|
||||
} else if (isEmTag(n)) {
|
||||
section.lines.push("*");
|
||||
} else if (n == "en-todo") {
|
||||
let x = node.attributes && node.attributes.checked && node.attributes.checked.toLowerCase() == 'true' ? 'X' : ' ';
|
||||
let x = nodeAttributes && nodeAttributes.checked && nodeAttributes.checked.toLowerCase() == 'true' ? 'X' : ' ';
|
||||
section.lines.push('- [' + x + '] ');
|
||||
} else if (n == "hr") {
|
||||
// Needs to be surrounded by new lines so that it's properly rendered as a line when converting to HTML
|
||||
@@ -375,20 +441,20 @@ function enexXmlToMdArray(stream, resources) {
|
||||
} else if (n == 'blockquote') {
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
state.inQuote = true;
|
||||
} else if (isCodeTag(n, node.attributes)) {
|
||||
} else if (isCodeTag(n, nodeAttributes)) {
|
||||
section.lines.push(BLOCK_OPEN);
|
||||
state.inCode = true;
|
||||
} else if (n == "br") {
|
||||
section.lines.push(NEWLINE);
|
||||
} else if (n == "en-media") {
|
||||
const hash = node.attributes.hash;
|
||||
const hash = nodeAttributes.hash;
|
||||
|
||||
let resource = null;
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
let r = resources[i];
|
||||
if (r.id == hash) {
|
||||
resource = r;
|
||||
resources.splice(i, 1);
|
||||
removeRemainingResource(r.id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -430,11 +496,11 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// </en-export>
|
||||
|
||||
let found = false;
|
||||
for (let i = 0; i < resources.length; i++) {
|
||||
let r = resources[i];
|
||||
for (let i = 0; i < remainingResources.length; i++) {
|
||||
let r = remainingResources[i];
|
||||
if (!r.id) {
|
||||
r.id = hash;
|
||||
resources[i] = r;
|
||||
remainingResources[i] = r;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
@@ -448,27 +514,29 @@ function enexXmlToMdArray(stream, resources) {
|
||||
// means it's an attachement. It will be appended along with the
|
||||
// other remaining resources at the bottom of the markdown text.
|
||||
if (!!resource.id) {
|
||||
section.lines = addResourceTag(section.lines, resource, node.attributes.alt);
|
||||
section.lines = addResourceTag(section.lines, resource, nodeAttributes.alt);
|
||||
}
|
||||
}
|
||||
} else if (n == "span" || n == "font" || n == 'sup') {
|
||||
// Ignore
|
||||
} else if (n == "span" || n == "font" || n == 'sup' || n == 'cite' || n == 'abbr' || n == 'small' || n == 'tt' || n == 'sub') {
|
||||
// Inline tags that can be ignored in Markdown
|
||||
} else {
|
||||
console.warn("Unsupported start tag: " + n);
|
||||
}
|
||||
})
|
||||
|
||||
saxStream.on('closetag', function(n) {
|
||||
n = n ? n.toLowerCase() : n;
|
||||
|
||||
if (n == 'en-note') {
|
||||
// End of note
|
||||
} else if (isNewLineOnlyEndTag(n)) {
|
||||
section.lines.push(BLOCK_CLOSE);
|
||||
} else if (n == 'td' || n == 'th') {
|
||||
section = section.parent;
|
||||
if (section && section.parent) section = section.parent;
|
||||
} else if (n == 'tr') {
|
||||
section = section.parent;
|
||||
if (section && section.parent) section = section.parent;
|
||||
} else if (n == 'table') {
|
||||
section = section.parent;
|
||||
if (section && section.parent) section = section.parent;
|
||||
} else if (isIgnoredEndTag(n)) {
|
||||
// Skip
|
||||
} else if (isListTag(n)) {
|
||||
@@ -476,6 +544,10 @@ function enexXmlToMdArray(stream, resources) {
|
||||
state.lists.pop();
|
||||
} else if (isStrongTag(n)) {
|
||||
section.lines.push("**");
|
||||
} else if (isStrikeTag(n)) {
|
||||
section.lines.push(')');
|
||||
} else if (n == 'samp') {
|
||||
section.lines.push('`');
|
||||
} else if (isEmTag(n)) {
|
||||
section.lines.push("*");
|
||||
} else if (n == 'q') {
|
||||
@@ -527,7 +599,7 @@ function enexXmlToMdArray(stream, resources) {
|
||||
saxStream.on('end', function() {
|
||||
resolve({
|
||||
content: section,
|
||||
resources: resources,
|
||||
resources: remainingResources,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -570,7 +642,7 @@ function colWidths(table) {
|
||||
const tr = table.lines[trIndex];
|
||||
for (let tdIndex = 0; tdIndex < tr.lines.length; tdIndex++) {
|
||||
const td = tr.lines[tdIndex];
|
||||
const w = cellWidth(td.content);
|
||||
const w = Math.min(cellWidth(td.content), 20); // Have to set a max width otherwise it can be extremely long for notes that import entire web pages (eg. Hacker News comment pages)
|
||||
if (output.length <= tdIndex) output.push(0);
|
||||
if (w > output[tdIndex]) output[tdIndex] = w;
|
||||
}
|
||||
|
@@ -212,51 +212,92 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
async function processNotes() {
|
||||
if (processingNotes) return false;
|
||||
|
||||
processingNotes = true;
|
||||
stream.pause();
|
||||
try {
|
||||
processingNotes = true;
|
||||
stream.pause();
|
||||
|
||||
let chain = [];
|
||||
while (notes.length) {
|
||||
let note = notes.shift();
|
||||
const contentStream = stringToStream(note.bodyXml);
|
||||
chain.push(() => {
|
||||
return enexXmlToMd(contentStream, note.resources).then((body) => {
|
||||
delete note.bodyXml;
|
||||
while (notes.length) {
|
||||
let note = notes.shift();
|
||||
const contentStream = stringToStream(note.bodyXml);
|
||||
const body = await enexXmlToMd(contentStream, note.resources);
|
||||
delete note.bodyXml;
|
||||
|
||||
// console.info('-----------------------------------------------------------');
|
||||
// console.info(body);
|
||||
// console.info('-----------------------------------------------------------');
|
||||
// console.info('-----------------------------------------------------------');
|
||||
// console.info(body);
|
||||
// console.info('-----------------------------------------------------------');
|
||||
|
||||
note.id = uuid.create();
|
||||
note.parent_id = parentFolderId;
|
||||
note.body = body;
|
||||
note.id = uuid.create();
|
||||
note.parent_id = parentFolderId;
|
||||
note.body = body;
|
||||
|
||||
// Notes in enex files always have a created timestamp but not always an
|
||||
// updated timestamp (it the note has never been modified). For sync
|
||||
// we require an updated_time property, so set it to create_time in that case
|
||||
if (!note.updated_time) note.updated_time = note.created_time;
|
||||
// Notes in enex files always have a created timestamp but not always an
|
||||
// updated timestamp (it the note has never been modified). For sync
|
||||
// we require an updated_time property, so set it to create_time in that case
|
||||
if (!note.updated_time) note.updated_time = note.created_time;
|
||||
|
||||
return saveNoteToStorage(note, importOptions.fuzzyMatching);
|
||||
}).then((result) => {
|
||||
if (result.noteUpdated) {
|
||||
progressState.updated++;
|
||||
} else if (result.noteCreated) {
|
||||
progressState.created++;
|
||||
} else if (result.noteSkipped) {
|
||||
progressState.skipped++;
|
||||
}
|
||||
progressState.resourcesCreated += result.resourcesCreated;
|
||||
progressState.notesTagged += result.notesTagged;
|
||||
importOptions.onProgress(progressState);
|
||||
});
|
||||
});
|
||||
const result = await saveNoteToStorage(note, importOptions.fuzzyMatching);
|
||||
|
||||
if (result.noteUpdated) {
|
||||
progressState.updated++;
|
||||
} else if (result.noteCreated) {
|
||||
progressState.created++;
|
||||
} else if (result.noteSkipped) {
|
||||
progressState.skipped++;
|
||||
}
|
||||
progressState.resourcesCreated += result.resourcesCreated;
|
||||
progressState.notesTagged += result.notesTagged;
|
||||
importOptions.onProgress(progressState);
|
||||
}
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return promiseChain(chain).then(() => {
|
||||
stream.resume();
|
||||
processingNotes = false;
|
||||
return true;
|
||||
});
|
||||
stream.resume();
|
||||
processingNotes = false;
|
||||
return true;
|
||||
|
||||
// let chain = [];
|
||||
// while (notes.length) {
|
||||
// let note = notes.shift();
|
||||
// const contentStream = stringToStream(note.bodyXml);
|
||||
// chain.push(() => {
|
||||
// return enexXmlToMd(contentStream, note.resources).then((body) => {
|
||||
// delete note.bodyXml;
|
||||
|
||||
// // console.info('-----------------------------------------------------------');
|
||||
// // console.info(body);
|
||||
// // console.info('-----------------------------------------------------------');
|
||||
|
||||
// note.id = uuid.create();
|
||||
// note.parent_id = parentFolderId;
|
||||
// note.body = body;
|
||||
|
||||
// // Notes in enex files always have a created timestamp but not always an
|
||||
// // updated timestamp (it the note has never been modified). For sync
|
||||
// // we require an updated_time property, so set it to create_time in that case
|
||||
// if (!note.updated_time) note.updated_time = note.created_time;
|
||||
|
||||
// return saveNoteToStorage(note, importOptions.fuzzyMatching);
|
||||
// }).then((result) => {
|
||||
// if (result.noteUpdated) {
|
||||
// progressState.updated++;
|
||||
// } else if (result.noteCreated) {
|
||||
// progressState.created++;
|
||||
// } else if (result.noteSkipped) {
|
||||
// progressState.skipped++;
|
||||
// }
|
||||
// progressState.resourcesCreated += result.resourcesCreated;
|
||||
// progressState.notesTagged += result.notesTagged;
|
||||
// importOptions.onProgress(progressState);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// return promiseChain(chain).then(() => {
|
||||
// stream.resume();
|
||||
// processingNotes = false;
|
||||
// return true;
|
||||
// });
|
||||
}
|
||||
|
||||
saxStream.on('error', (error) => {
|
||||
@@ -323,7 +364,11 @@ function importEnex(parentFolderId, filePath, importOptions = null) {
|
||||
noteResourceRecognition.objID = extractRecognitionObjId(data);
|
||||
} else if (note) {
|
||||
if (n == 'content') {
|
||||
note.bodyXml = data;
|
||||
if ('bodyXml' in note) {
|
||||
note.bodyXml += data;
|
||||
} else {
|
||||
note.bodyXml = data;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@@ -202,7 +202,7 @@ class JoplinDatabase extends Database {
|
||||
// default value and thus might cause problems. In that case, the default value
|
||||
// must be set in the synchronizer too.
|
||||
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7];
|
||||
const existingDatabaseVersions = [0, 1, 2, 3, 4, 5, 6, 7, 8];
|
||||
|
||||
let currentVersionIndex = existingDatabaseVersions.indexOf(fromVersion);
|
||||
// currentVersionIndex < 0 if for the case where an old version of Joplin used with a newer
|
||||
@@ -265,6 +265,11 @@ class JoplinDatabase extends Database {
|
||||
queries.push('ALTER TABLE resources ADD COLUMN file_extension TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
if (targetVersion == 8) {
|
||||
queries.push('ALTER TABLE sync_items ADD COLUMN sync_disabled INT NOT NULL DEFAULT "0"');
|
||||
queries.push('ALTER TABLE sync_items ADD COLUMN sync_disabled_reason TEXT NOT NULL DEFAULT ""');
|
||||
}
|
||||
|
||||
queries.push({ sql: 'UPDATE version SET version = ?', params: [targetVersion] });
|
||||
await this.transactionExecBatch(queries);
|
||||
|
||||
|
@@ -339,6 +339,7 @@ class BaseItem extends BaseModel {
|
||||
JOIN sync_items s ON s.item_id = items.id
|
||||
WHERE sync_target = %d
|
||||
AND s.sync_time < items.updated_time
|
||||
AND s.sync_disabled = 0
|
||||
%s
|
||||
LIMIT %d
|
||||
`,
|
||||
@@ -382,7 +383,21 @@ class BaseItem extends BaseModel {
|
||||
throw new Error('Invalid type: ' + type);
|
||||
}
|
||||
|
||||
static updateSyncTimeQueries(syncTarget, item, syncTime) {
|
||||
static async syncDisabledItems(syncTargetId) {
|
||||
const rows = await this.db().selectAll('SELECT * FROM sync_items WHERE sync_disabled = 1 AND sync_target = ?', [syncTargetId]);
|
||||
let output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const item = await this.loadItem(rows[i].item_type, rows[i].item_id);
|
||||
if (!item) continue; // The referenced item no longer exist
|
||||
output.push({
|
||||
syncInfo: rows[i],
|
||||
item: item,
|
||||
});
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static updateSyncTimeQueries(syncTarget, item, syncTime, syncDisabled = false, syncDisabledReason = '') {
|
||||
const itemType = item.type_;
|
||||
const itemId = item.id;
|
||||
if (!itemType || !itemId || syncTime === undefined) throw new Error('Invalid parameters in updateSyncTimeQueries()');
|
||||
@@ -393,8 +408,8 @@ class BaseItem extends BaseModel {
|
||||
params: [syncTarget, itemType, itemId],
|
||||
},
|
||||
{
|
||||
sql: 'INSERT INTO sync_items (sync_target, item_type, item_id, sync_time) VALUES (?, ?, ?, ?)',
|
||||
params: [syncTarget, itemType, itemId, syncTime],
|
||||
sql: 'INSERT INTO sync_items (sync_target, item_type, item_id, sync_time, sync_disabled, sync_disabled_reason) VALUES (?, ?, ?, ?, ?, ?)',
|
||||
params: [syncTarget, itemType, itemId, syncTime, syncDisabled ? 1 : 0, syncDisabledReason + ''],
|
||||
}
|
||||
];
|
||||
}
|
||||
@@ -404,6 +419,12 @@ class BaseItem extends BaseModel {
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
static async saveSyncDisabled(syncTargetId, item, syncDisabledReason) {
|
||||
const syncTime = 'sync_time' in item ? item.sync_time : 0;
|
||||
const queries = this.updateSyncTimeQueries(syncTargetId, item, syncTime, true, syncDisabledReason);
|
||||
return this.db().transactionExecBatch(queries);
|
||||
}
|
||||
|
||||
// When an item is deleted, its associated sync_items data is not immediately deleted for
|
||||
// performance reason. So this function is used to look for these remaining sync_items and
|
||||
// delete them.
|
||||
|
@@ -126,7 +126,11 @@ class Note extends BaseItem {
|
||||
let r = null;
|
||||
r = noteFieldComp(a.user_updated_time, b.user_updated_time); if (r) return r;
|
||||
r = noteFieldComp(a.user_created_time, b.user_created_time); if (r) return r;
|
||||
r = noteFieldComp(a.title.toLowerCase(), b.title.toLowerCase()); if (r) return r;
|
||||
|
||||
const titleA = a.title ? a.title.toLowerCase() : '';
|
||||
const titleB = b.title ? b.title.toLowerCase() : '';
|
||||
r = noteFieldComp(titleA, titleB); if (r) return r;
|
||||
|
||||
return noteFieldComp(a.id, b.id);
|
||||
}
|
||||
|
||||
|
@@ -25,7 +25,8 @@ const defaultState = {
|
||||
searchQuery: '',
|
||||
settings: {},
|
||||
appState: 'starting',
|
||||
windowContentSize: { width: 0, height: 0 },
|
||||
//windowContentSize: { width: 0, height: 0 },
|
||||
hasDisabledSyncItems: false,
|
||||
};
|
||||
|
||||
// When deleting a note, tag or folder
|
||||
@@ -395,6 +396,12 @@ const reducer = (state = defaultState, action) => {
|
||||
newState.appState = action.state;
|
||||
break;
|
||||
|
||||
case 'SYNC_HAS_DISABLED_SYNC_ITEMS':
|
||||
|
||||
newState = Object.assign({}, state);
|
||||
newState.hasDisabledSyncItems = true;
|
||||
break;
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action);
|
||||
|
@@ -109,8 +109,21 @@ class ReportService {
|
||||
async status(syncTarget) {
|
||||
let r = await this.syncStatus(syncTarget);
|
||||
let sections = [];
|
||||
let section = null;
|
||||
|
||||
let section = { title: _('Sync status (synced items / total items)'), body: [] };
|
||||
const disabledItems = await BaseItem.syncDisabledItems(syncTarget);
|
||||
|
||||
if (disabledItems.length) {
|
||||
section = { title: _('Items that cannot be synchronised'), body: [] };
|
||||
|
||||
for (let i = 0; i < disabledItems.length; i++) {
|
||||
const row = disabledItems[i];
|
||||
section.body.push(_('"%s": "%s"', row.item.title, row.syncInfo.sync_disabled_reason));
|
||||
}
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
section = { title: _('Sync status (synced items / total items)'), body: [] };
|
||||
|
||||
for (let n in r.items) {
|
||||
if (!r.items.hasOwnProperty(n)) continue;
|
||||
@@ -138,16 +151,19 @@ class ReportService {
|
||||
|
||||
sections.push(section);
|
||||
|
||||
section = { title: _('Coming alarms'), body: [] };
|
||||
|
||||
const alarms = await Alarm.allDue();
|
||||
for (let i = 0; i < alarms.length; i++) {
|
||||
const alarm = alarms[i];
|
||||
const note = await Note.load(alarm.note_id);
|
||||
section.body.push(_('On %s: %s', time.formatMsToLocal(alarm.trigger_time), note.title));
|
||||
}
|
||||
|
||||
sections.push(section);
|
||||
if (alarms.length) {
|
||||
section = { title: _('Coming alarms'), body: [] };
|
||||
|
||||
for (let i = 0; i < alarms.length; i++) {
|
||||
const alarm = alarms[i];
|
||||
const note = await Note.load(alarm.note_id);
|
||||
section.body.push(_('On %s: %s', time.formatMsToLocal(alarm.trigger_time), note.title));
|
||||
}
|
||||
|
||||
sections.push(section);
|
||||
}
|
||||
|
||||
return sections;
|
||||
}
|
||||
|
@@ -253,22 +253,36 @@ class Synchronizer {
|
||||
|
||||
this.logSyncOperation(action, local, remote, reason);
|
||||
|
||||
const handleCannotSyncItem = async (syncTargetId, item, cannotSyncReason) => {
|
||||
await ItemClass.saveSyncDisabled(syncTargetId, item, cannotSyncReason);
|
||||
this.dispatch({ type: 'SYNC_HAS_DISABLED_SYNC_ITEMS' });
|
||||
}
|
||||
|
||||
if (local.type_ == BaseModel.TYPE_RESOURCE && (action == 'createRemote' || (action == 'itemConflict' && remote))) {
|
||||
let remoteContentPath = this.resourceDirName_ + '/' + local.id;
|
||||
// TODO: handle node and mobile in the same way
|
||||
if (shim.isNode()) {
|
||||
let resourceContent = '';
|
||||
try {
|
||||
resourceContent = await Resource.content(local);
|
||||
} catch (error) {
|
||||
error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message;
|
||||
this.logger().error(error);
|
||||
this.progressReport_.errors.push(error);
|
||||
try {
|
||||
// TODO: handle node and mobile in the same way
|
||||
if (shim.isNode()) {
|
||||
let resourceContent = '';
|
||||
try {
|
||||
resourceContent = await Resource.content(local);
|
||||
} catch (error) {
|
||||
error.message = 'Cannot read resource content: ' + local.id + ': ' + error.message;
|
||||
this.logger().error(error);
|
||||
this.progressReport_.errors.push(error);
|
||||
}
|
||||
await this.api().put(remoteContentPath, resourceContent);
|
||||
} else {
|
||||
const localResourceContentPath = Resource.fullPath(local);
|
||||
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||
}
|
||||
} catch (error) {
|
||||
if (error && error.code === 'cannotSync') {
|
||||
await handleCannotSyncItem(syncTargetId, local, error.message);
|
||||
action = null;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
await this.api().put(remoteContentPath, resourceContent);
|
||||
} else {
|
||||
const localResourceContentPath = Resource.fullPath(local);
|
||||
await this.api().put(remoteContentPath, null, { path: localResourceContentPath, source: 'file' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,9 +299,27 @@ class Synchronizer {
|
||||
// await this.api().setTimestamp(tempPath, local.updated_time);
|
||||
// await this.api().move(tempPath, path);
|
||||
|
||||
await this.api().put(path, content);
|
||||
await this.api().setTimestamp(path, local.updated_time);
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs());
|
||||
let canSync = true;
|
||||
try {
|
||||
if (this.debugFlags_.indexOf('cannotSync') >= 0) {
|
||||
const error = new Error('Testing cannotSync');
|
||||
error.code = 'cannotSync';
|
||||
throw error;
|
||||
}
|
||||
await this.api().put(path, content);
|
||||
} catch (error) {
|
||||
if (error && error.code === 'cannotSync') {
|
||||
await handleCannotSyncItem(syncTargetId, local, error.message);
|
||||
canSync = false;
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (canSync) {
|
||||
await this.api().setTimestamp(path, local.updated_time);
|
||||
await ItemClass.saveSyncTime(syncTargetId, local, time.unixMs());
|
||||
}
|
||||
|
||||
} else if (action == 'itemConflict') {
|
||||
|
||||
|
1
ReactNativeClient/locales/de_DE.json
Normal file
1
ReactNativeClient/locales/de_DE.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
||||
var locales = {};
|
||||
locales['en_GB'] = require('./en_GB.json');
|
||||
locales['de_DE'] = require('./de_DE.json');
|
||||
locales['es_CR'] = require('./es_CR.json');
|
||||
locales['fr_FR'] = require('./fr_FR.json');
|
||||
module.exports = { locales: locales };
|
Reference in New Issue
Block a user