1
0
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:
Laurent Cozic
2017-12-07 13:33:26 +00:00
37 changed files with 2748 additions and 133 deletions

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "joplin",
"version": "0.10.77",
"version": "0.10.78",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@@ -18,7 +18,7 @@
],
"owner": "Laurent Cozic"
},
"version": "0.10.77",
"version": "0.10.78",
"bin": {
"joplin": "./main.js"
},

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 = '![';
if (nodeAttributes.alt) s += nodeAttributes.alt;
s += '](' + nodeAttributes.src + ')';
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;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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