You've already forked joplin
mirror of
https://github.com/laurent22/joplin.git
synced 2025-09-02 20:46:21 +02:00
Compare commits
7 Commits
cli-v2.1.1
...
android-v2
Author | SHA1 | Date | |
---|---|---|---|
|
e08a909bdb | ||
|
94ea177b76 | ||
|
07ea79bb46 | ||
|
f792fbb5fc | ||
|
7ed1a453e5 | ||
|
55248ed08b | ||
|
1476cdf467 |
@@ -36,7 +36,7 @@ Linux | <a href='https://github.com/laurent22/joplin/releases/download/v2.0.11/J
|
||||
|
||||
Operating System | Download | Alt. Download
|
||||
---|---|---
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.1.3/joplin-v2.1.3.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.1.3/joplin-v2.1.3-32bit.apk)
|
||||
Android | <a href='https://play.google.com/store/apps/details?id=net.cozic.joplin&utm_source=GitHub&utm_campaign=README&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1'><img alt='Get it on Google Play' height="40px" src='https://joplinapp.org/images/BadgeAndroid.png'/></a> | or download the APK file: [64-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.1.4/joplin-v2.1.4.apk) [32-bit](https://github.com/laurent22/joplin-android/releases/download/android-v2.1.4/joplin-v2.1.4-32bit.apk)
|
||||
iOS | <a href='https://itunes.apple.com/us/app/joplin/id1315599797'><img alt='Get it on the App Store' height="40px" src='https://joplinapp.org/images/BadgeIOS.png'/></a> | -
|
||||
|
||||
## Terminal application
|
||||
|
2
packages/app-cli/package-lock.json
generated
2
packages/app-cli/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "joplin",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -32,7 +32,7 @@
|
||||
],
|
||||
"owner": "Laurent Cozic"
|
||||
},
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"bin": {
|
||||
"joplin": "./main.js"
|
||||
},
|
||||
@@ -40,8 +40,8 @@
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@joplin/lib": "2.0",
|
||||
"@joplin/renderer": "2.0",
|
||||
"@joplin/lib": "2.1",
|
||||
"@joplin/renderer": "2.1",
|
||||
"aws-sdk": "^2.588.0",
|
||||
"chalk": "^4.1.0",
|
||||
"compare-version": "^0.1.2",
|
||||
@@ -65,7 +65,7 @@
|
||||
"yargs-parser": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@joplin/tools": "2.0",
|
||||
"@joplin/tools": "2.1",
|
||||
"@types/fs-extra": "^9.0.6",
|
||||
"@types/jest": "^26.0.15",
|
||||
"@types/node": "^14.14.6",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"manifest_version": 2,
|
||||
"name": "Joplin Web Clipper [DEV]",
|
||||
"version": "2.1.1",
|
||||
"version": "2.1.2",
|
||||
"description": "Capture and save web pages and screenshots from your browser to Joplin.",
|
||||
"homepage_url": "https://joplinapp.org",
|
||||
"content_security_policy": "script-src 'self'; object-src 'self'",
|
||||
|
@@ -237,12 +237,18 @@ class AppComponent extends Component {
|
||||
renderStartupScreen() {
|
||||
const messages = {
|
||||
serverFoundState: {
|
||||
// We need to display the "Connecting to the Joplin
|
||||
// application..." message because if the app doesn't currently
|
||||
// allow access to the clipper API, the clipper tries several
|
||||
// ports and it takes time before failing. So if we don't
|
||||
// display any message, it looks like it's not doing anything
|
||||
// when clicking on the extension button.
|
||||
'searching': 'Connecting to the Joplin application...',
|
||||
'not_found': 'Error: Could not connect to the Joplin application. Please ensure that it is started and that the clipper service is enabled in the configuration.',
|
||||
},
|
||||
authState: {
|
||||
'starting': 'Starting...',
|
||||
'waiting': 'The Joplin Web Clipper requires your authorisation in order to access your data. To do, please open the Joplin desktop application and grant permission. Note: Joplin 2.1+ is needed to use this version of the Web Clipper.',
|
||||
'waiting': 'The Joplin Web Clipper requires your authorisation in order to access your data. To proceed, please open the Joplin desktop application and grant permission. Note: Joplin 2.1+ is needed to use this version of the Web Clipper.',
|
||||
'rejected': 'Permission to access your data was not granted. To try again please close this popup and open it again.',
|
||||
},
|
||||
};
|
||||
|
@@ -85,7 +85,13 @@ class Bridge {
|
||||
type: 'ENV_SET',
|
||||
env: this.env(),
|
||||
});
|
||||
}
|
||||
|
||||
token() {
|
||||
return this.token_;
|
||||
}
|
||||
|
||||
async onReactAppStarts() {
|
||||
await this.findClipperServerPort();
|
||||
|
||||
if (this.clipperServerPortStatus_ !== 'found') {
|
||||
@@ -94,13 +100,7 @@ class Bridge {
|
||||
}
|
||||
|
||||
await this.restoreState();
|
||||
}
|
||||
|
||||
token() {
|
||||
return this.token_;
|
||||
}
|
||||
|
||||
async onReactAppStarts() {
|
||||
await this.checkAuth();
|
||||
if (!this.token_) return; // Didn't get a token
|
||||
|
||||
@@ -138,29 +138,59 @@ class Bridge {
|
||||
|
||||
this.dispatch({ type: 'AUTH_STATE_SET', value: 'waiting' });
|
||||
|
||||
const response = await this.clipperApiExec('POST', 'auth');
|
||||
const authToken = response.auth_token;
|
||||
// Note that Firefox and Chrome works differently for this:
|
||||
//
|
||||
// - In Chrome, the popup stays open, even when the user leaves the
|
||||
// browser to grant permission in the application.
|
||||
//
|
||||
// - In Firefox, as soon as the browser loses focus, the popup closes.
|
||||
//
|
||||
// It means we can't rely on local state to get this working - instead
|
||||
// we request the auth token, and cache it to local storage (along
|
||||
// with a timestamp). Then next time the user opens the popup (after
|
||||
// it was automatically closed by Firefox), that cached auth token is
|
||||
// re-used and the auth process continues.
|
||||
//
|
||||
// https://github.com/laurent22/joplin/issues/5125#issuecomment-869547421
|
||||
|
||||
const existingAuthInfo = await this.storageGet(['authToken', 'authTokenTimestamp']);
|
||||
|
||||
let authToken = null;
|
||||
if (existingAuthInfo.authToken && Date.now() - existingAuthInfo.authTokenTimestamp < 5 * 60 * 1000) {
|
||||
console.info('checkAuth: we already have an auth token - reusing it');
|
||||
authToken = existingAuthInfo.authToken;
|
||||
} else {
|
||||
console.info('checkAuth: we do not have an auth token - requesting it...');
|
||||
const response = await this.clipperApiExec('POST', 'auth');
|
||||
authToken = response.auth_token;
|
||||
|
||||
await this.storageSet({ authToken: authToken, authTokenTimestamp: Date.now() });
|
||||
}
|
||||
|
||||
console.info('checkAuth: we do not have a token - requesting one using auth_token: ', authToken);
|
||||
|
||||
while (true) {
|
||||
const response = await this.clipperApiExec('GET', 'auth/check', { auth_token: authToken });
|
||||
try {
|
||||
while (true) {
|
||||
const response = await this.clipperApiExec('GET', 'auth/check', { auth_token: authToken });
|
||||
|
||||
if (response.status === 'rejected') {
|
||||
console.info('checkAuth: Auth request was not accepted', response);
|
||||
this.dispatch({ type: 'AUTH_STATE_SET', value: 'rejected' });
|
||||
break;
|
||||
} else if (response.status === 'accepted') {
|
||||
console.info('checkAuth: Auth request was accepted', response);
|
||||
this.dispatch({ type: 'AUTH_STATE_SET', value: 'accepted' });
|
||||
this.token_ = response.token;
|
||||
await this.storageSet({ token: this.token_ });
|
||||
break;
|
||||
} else if (response.status === 'waiting') {
|
||||
await msleep(1000);
|
||||
} else {
|
||||
throw new Error(`Unknown auth/check status: ${response.status}`);
|
||||
if (response.status === 'rejected') {
|
||||
console.info('checkAuth: Auth request was not accepted', response);
|
||||
this.dispatch({ type: 'AUTH_STATE_SET', value: 'rejected' });
|
||||
break;
|
||||
} else if (response.status === 'accepted') {
|
||||
console.info('checkAuth: Auth request was accepted', response);
|
||||
this.dispatch({ type: 'AUTH_STATE_SET', value: 'accepted' });
|
||||
this.token_ = response.token;
|
||||
await this.storageSet({ token: this.token_ });
|
||||
break;
|
||||
} else if (response.status === 'waiting') {
|
||||
await msleep(1000);
|
||||
} else {
|
||||
throw new Error(`Unknown auth/check status: ${response.status}`);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await this.storageSet({ authToken: '', authTokenTimestamp: 0 });
|
||||
}
|
||||
}
|
||||
|
||||
|
2
packages/app-desktop/package-lock.json
generated
2
packages/app-desktop/package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.1.7",
|
||||
"version": "2.1.8",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@joplin/app-desktop",
|
||||
"version": "2.1.7",
|
||||
"version": "2.1.8",
|
||||
"description": "Joplin for Desktop",
|
||||
"main": "main.js",
|
||||
"private": true,
|
||||
|
@@ -141,8 +141,8 @@ android {
|
||||
applicationId "net.cozic.joplin"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 2097638
|
||||
versionName "2.1.3"
|
||||
versionCode 2097639
|
||||
versionName "2.1.4"
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { setupDatabaseAndSynchronizer, switchClient, createFolderTree, supportDir } from '../testing/test-utils';
|
||||
import { setupDatabaseAndSynchronizer, switchClient, createFolderTree, supportDir, msleep } from '../testing/test-utils';
|
||||
import Folder from '../models/Folder';
|
||||
import { allNotesFolders } from '../testing/test-utils-synchronizer';
|
||||
import Note from '../models/Note';
|
||||
@@ -356,6 +356,8 @@ describe('models_Folder.sharing', function() {
|
||||
|
||||
const resourceService = new ResourceService();
|
||||
|
||||
const folder1: FolderEntity = await Folder.loadByTitle('folder 1');
|
||||
const folder2: FolderEntity = await Folder.loadByTitle('folder 2');
|
||||
let note1: NoteEntity = await Note.loadByTitle('note 1');
|
||||
let note2: NoteEntity = await Note.loadByTitle('note 2');
|
||||
note1 = await shim.attachFileToNote(note1, testImagePath);
|
||||
@@ -369,6 +371,13 @@ describe('models_Folder.sharing', function() {
|
||||
|
||||
await Folder.updateNoLongerSharedItems(['1']);
|
||||
|
||||
// Since `updateNoLongerSharedItems` sets the parent_id too,
|
||||
// double-check that it's not actually modified.
|
||||
expect((await Note.loadByTitle('note 1')).parent_id).toBe(folder1.id);
|
||||
expect((await Note.loadByTitle('note 2')).parent_id).toBe(folder2.id);
|
||||
expect((await Folder.loadByTitle('folder 1')).parent_id).toBe(folder1.parent_id);
|
||||
expect((await Folder.loadByTitle('folder 2')).parent_id).toBe(folder2.parent_id);
|
||||
|
||||
// At this point, all items associated with share 2 should have their
|
||||
// share_id cleared, because the share no longer exists. We also
|
||||
// double-check that share 1 hasn't been cleared.
|
||||
@@ -385,6 +394,28 @@ describe('models_Folder.sharing', function() {
|
||||
expect((await Note.loadByTitle('note 1')).share_id).toBe('');
|
||||
expect((await Folder.loadByTitle('folder 1')).share_id).toBe('');
|
||||
expect((await Resource.load(resourceId1)).share_id).toBe('');
|
||||
|
||||
{
|
||||
// If we run it again, it should not update the notes since the share_id
|
||||
// has already been cleared.
|
||||
const resource1 = await Resource.load(resourceId1);
|
||||
const resource2 = await Resource.load(resourceId2);
|
||||
const note1 = await Note.loadByTitle('note 1');
|
||||
const note2 = await Note.loadByTitle('note 2');
|
||||
const folder1 = await Folder.loadByTitle('folder 1');
|
||||
const folder2 = await Folder.loadByTitle('folder 2');
|
||||
|
||||
await msleep(1);
|
||||
|
||||
await Folder.updateNoLongerSharedItems(['1']);
|
||||
|
||||
expect((await Resource.load(resourceId1)).updated_time).toBe(resource1.updated_time);
|
||||
expect((await Resource.load(resourceId2)).updated_time).toBe(resource2.updated_time);
|
||||
expect((await Note.loadByTitle('note 1')).updated_time).toBe(note1.updated_time);
|
||||
expect((await Note.loadByTitle('note 2')).updated_time).toBe(note2.updated_time);
|
||||
expect((await Folder.loadByTitle('folder 1')).updated_time).toBe(folder1.updated_time);
|
||||
expect((await Folder.loadByTitle('folder 2')).updated_time).toBe(folder2.updated_time);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -7,8 +7,11 @@ import Database from '../database';
|
||||
import BaseItem from './BaseItem';
|
||||
import Resource from './Resource';
|
||||
import { isRootSharedFolder } from '../services/share/reducer';
|
||||
import Logger from '../Logger';
|
||||
const { substrWithEllipsis } = require('../string-utils.js');
|
||||
|
||||
const logger = Logger.create('models/Folder');
|
||||
|
||||
interface FolderEntityWithChildren extends FolderEntity {
|
||||
children?: FolderEntity[];
|
||||
}
|
||||
@@ -288,9 +291,16 @@ export default class Folder extends BaseItem {
|
||||
|
||||
let sharedFolderIds: string[] = [];
|
||||
|
||||
const report = {
|
||||
shareUpdateCount: 0,
|
||||
unshareUpdateCount: 0,
|
||||
};
|
||||
|
||||
for (const rootFolder of rootFolders) {
|
||||
const children = await this.allChildrenFolders(rootFolder.id);
|
||||
|
||||
report.shareUpdateCount += children.length;
|
||||
|
||||
for (const child of children) {
|
||||
if (child.share_id !== rootFolder.share_id) {
|
||||
await this.save({
|
||||
@@ -310,19 +320,25 @@ export default class Folder extends BaseItem {
|
||||
// if they've been moved out of a shared folder.
|
||||
// await this.unshareItems(ModelType.Folder, sharedFolderIds);
|
||||
|
||||
const sql = ['SELECT id FROM folders WHERE share_id != ""'];
|
||||
const sql = ['SELECT id, parent_id FROM folders WHERE share_id != ""'];
|
||||
if (sharedFolderIds.length) {
|
||||
sql.push(` AND id NOT IN ("${sharedFolderIds.join('","')}")`);
|
||||
}
|
||||
|
||||
const foldersToUnshare = await this.db().selectAll(sql.join(' '));
|
||||
const foldersToUnshare: FolderEntity[] = await this.db().selectAll(sql.join(' '));
|
||||
|
||||
report.unshareUpdateCount += foldersToUnshare.length;
|
||||
|
||||
for (const item of foldersToUnshare) {
|
||||
await this.save({
|
||||
id: item.id,
|
||||
share_id: '',
|
||||
updated_time: Date.now(),
|
||||
parent_id: item.parent_id,
|
||||
}, { autoTimestamp: false });
|
||||
}
|
||||
|
||||
logger.debug('updateFolderShareIds:', report);
|
||||
}
|
||||
|
||||
public static async updateNoteShareIds() {
|
||||
@@ -335,6 +351,8 @@ export default class Folder extends BaseItem {
|
||||
WHERE notes.share_id != folders.share_id
|
||||
`);
|
||||
|
||||
logger.debug('updateNoteShareIds: notes to update:', rows.length);
|
||||
|
||||
for (const row of rows) {
|
||||
await Note.save({
|
||||
id: row.id,
|
||||
@@ -359,6 +377,8 @@ export default class Folder extends BaseItem {
|
||||
OR n.is_shared != r.is_shared
|
||||
`);
|
||||
|
||||
logger.debug('updateResourceShareIds: resources to update:', rows.length);
|
||||
|
||||
for (const row of rows) {
|
||||
await Resource.save({
|
||||
id: row.id,
|
||||
@@ -384,27 +404,41 @@ export default class Folder extends BaseItem {
|
||||
'resources': Resource,
|
||||
};
|
||||
|
||||
const report: any = {};
|
||||
|
||||
for (const tableName of ['folders', 'notes', 'resources']) {
|
||||
const ItemClass = tableNameToClasses[tableName];
|
||||
const hasParentId = tableName !== 'resources';
|
||||
|
||||
const fields = ['id'];
|
||||
if (hasParentId) fields.push('parent_id');
|
||||
|
||||
const query = activeShareIds.length ? `
|
||||
SELECT id FROM ${tableName}
|
||||
WHERE share_id NOT IN ("${activeShareIds.join('","')}")
|
||||
SELECT ${this.db().escapeFields(fields)} FROM ${tableName}
|
||||
WHERE share_id != "" AND share_id NOT IN ("${activeShareIds.join('","')}")
|
||||
` : `
|
||||
SELECT id FROM ${tableName}
|
||||
SELECT ${this.db().escapeFields(fields)} FROM ${tableName}
|
||||
WHERE share_id != ''
|
||||
`;
|
||||
|
||||
const rows = await this.db().selectAll(query);
|
||||
|
||||
report[tableName] = rows.length;
|
||||
|
||||
for (const row of rows) {
|
||||
await ItemClass.save({
|
||||
const toSave: any = {
|
||||
id: row.id,
|
||||
share_id: '',
|
||||
updated_time: Date.now(),
|
||||
}, { autoTimestamp: false });
|
||||
};
|
||||
|
||||
if (hasParentId) toSave.parent_id = row.parent_id;
|
||||
|
||||
await ItemClass.save(toSave, { autoTimestamp: false });
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug('updateNoLongerSharedItems:', report);
|
||||
}
|
||||
|
||||
static async allAsTree(folders: FolderEntity[] = null, options: any = null) {
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Joplin terminal app changelog
|
||||
|
||||
## [cli-v2.1.1](https://github.com/laurent22/joplin/releases/tag/cli-v2.1.1) - 2021-06-27T14:08:02Z
|
||||
## [cli-v2.1.2](https://github.com/laurent22/joplin/releases/tag/cli-v2.1.2) - 2021-06-27T15:51:36Z
|
||||
|
||||
- New: Add support for X-API-MIN-VERSION header (51f3c00)
|
||||
- New: Added flag to disable local lock when synchronising (7aff6d2)
|
||||
|
Reference in New Issue
Block a user