mirror of
https://github.com/laurent22/joplin.git
synced 2025-01-11 18:24:43 +02:00
Removed invalid files
This commit is contained in:
parent
f0f6e7c856
commit
b873e706ca
Binary file not shown.
109
Server/dist/app/app.js
vendored
109
Server/dist/app/app.js
vendored
File diff suppressed because one or more lines are too long
68
Server/dist/app/controllers/BaseController.js
vendored
68
Server/dist/app/controllers/BaseController.js
vendored
@ -1,68 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var errors_1 = require("../utils/errors");
|
||||
var SessionModel_1 = require("../models/SessionModel");
|
||||
var BaseController = /** @class */ (function () {
|
||||
function BaseController() {
|
||||
}
|
||||
BaseController.prototype.initSession = function (sessionId, mustBeAdmin) {
|
||||
if (mustBeAdmin === void 0) { mustBeAdmin = false; }
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var sessionModel, user;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
sessionModel = new SessionModel_1.default();
|
||||
return [4 /*yield*/, sessionModel.sessionUser(sessionId)];
|
||||
case 1:
|
||||
user = _a.sent();
|
||||
if (!user)
|
||||
throw new errors_1.ErrorForbidden("Invalid session ID: " + sessionId);
|
||||
if (!user.is_admin && mustBeAdmin)
|
||||
throw new errors_1.ErrorForbidden('Non-admin user is not allowed');
|
||||
return [2 /*return*/, user];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return BaseController;
|
||||
}());
|
||||
exports.default = BaseController;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIkJhc2VDb250cm9sbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQ0EsMENBQWlEO0FBQ2pELHVEQUFrRDtBQUVsRDtJQUFBO0lBVUEsQ0FBQztJQVJNLG9DQUFXLEdBQWpCLFVBQWtCLFNBQWdCLEVBQUUsV0FBMkI7UUFBM0IsNEJBQUEsRUFBQSxtQkFBMkI7Ozs7Ozt3QkFDeEQsWUFBWSxHQUFHLElBQUksc0JBQVksRUFBRSxDQUFDO3dCQUN0QixxQkFBTSxZQUFZLENBQUMsV0FBVyxDQUFDLFNBQVMsQ0FBQyxFQUFBOzt3QkFBckQsSUFBSSxHQUFRLFNBQXlDO3dCQUMzRCxJQUFJLENBQUMsSUFBSTs0QkFBRSxNQUFNLElBQUksdUJBQWMsQ0FBQyx5QkFBdUIsU0FBVyxDQUFDLENBQUM7d0JBQ3hFLElBQUksQ0FBQyxJQUFJLENBQUMsUUFBUSxJQUFJLFdBQVc7NEJBQUUsTUFBTSxJQUFJLHVCQUFjLENBQUMsK0JBQStCLENBQUMsQ0FBQzt3QkFDN0Ysc0JBQU8sSUFBSSxFQUFDOzs7O0tBQ1o7SUFFRixxQkFBQztBQUFELENBVkEsQUFVQyxJQUFBIiwiZmlsZSI6IkJhc2VDb250cm9sbGVyLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgVXNlciB9IGZyb20gJy4uL2RiJztcbmltcG9ydCB7IEVycm9yRm9yYmlkZGVuIH0gZnJvbSAnLi4vdXRpbHMvZXJyb3JzJztcbmltcG9ydCBTZXNzaW9uTW9kZWwgZnJvbSAnLi4vbW9kZWxzL1Nlc3Npb25Nb2RlbCc7XG5cbmV4cG9ydCBkZWZhdWx0IGFic3RyYWN0IGNsYXNzIEJhc2VDb250cm9sbGVyIHtcblxuXHRhc3luYyBpbml0U2Vzc2lvbihzZXNzaW9uSWQ6c3RyaW5nLCBtdXN0QmVBZG1pbjpib29sZWFuID0gZmFsc2UpOlByb21pc2U8VXNlcj4ge1xuXHRcdGNvbnN0IHNlc3Npb25Nb2RlbCA9IG5ldyBTZXNzaW9uTW9kZWwoKTtcblx0XHRjb25zdCB1c2VyOlVzZXIgPSBhd2FpdCBzZXNzaW9uTW9kZWwuc2Vzc2lvblVzZXIoc2Vzc2lvbklkKTtcblx0XHRpZiAoIXVzZXIpIHRocm93IG5ldyBFcnJvckZvcmJpZGRlbihgSW52YWxpZCBzZXNzaW9uIElEOiAke3Nlc3Npb25JZH1gKTtcblx0XHRpZiAoIXVzZXIuaXNfYWRtaW4gJiYgbXVzdEJlQWRtaW4pIHRocm93IG5ldyBFcnJvckZvcmJpZGRlbignTm9uLWFkbWluIHVzZXIgaXMgbm90IGFsbG93ZWQnKTtcblx0XHRyZXR1cm4gdXNlcjtcblx0fVxuXG59XG4iXX0=
|
200
Server/dist/app/controllers/FileController.js
vendored
200
Server/dist/app/controllers/FileController.js
vendored
File diff suppressed because one or more lines are too long
72
Server/dist/app/controllers/SessionController.js
vendored
72
Server/dist/app/controllers/SessionController.js
vendored
@ -1,72 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var auth_1 = require("../utils/auth");
|
||||
var errors_1 = require("../utils/errors");
|
||||
var SessionModel_1 = require("../models/SessionModel");
|
||||
var UserModel_1 = require("../models/UserModel");
|
||||
var uuidgen_1 = require("../utils/uuidgen");
|
||||
var SessionController = /** @class */ (function () {
|
||||
function SessionController() {
|
||||
}
|
||||
SessionController.prototype.authenticate = function (email, password) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var userModel, user, session, sessionModel;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
userModel = new UserModel_1.default();
|
||||
return [4 /*yield*/, userModel.loadByEmail(email)];
|
||||
case 1:
|
||||
user = _a.sent();
|
||||
if (!user)
|
||||
throw new errors_1.ErrorForbidden('Invalid username or password');
|
||||
if (!auth_1.checkPassword(password, user.password))
|
||||
throw new errors_1.ErrorForbidden('Invalid username or password');
|
||||
session = { id: uuidgen_1.default(), user_id: user.id };
|
||||
sessionModel = new SessionModel_1.default();
|
||||
return [2 /*return*/, sessionModel.save(session, { isNew: true })];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return SessionController;
|
||||
}());
|
||||
exports.default = SessionController;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlNlc3Npb25Db250cm9sbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQ0Esc0NBQThDO0FBQzlDLDBDQUFpRDtBQUNqRCx1REFBa0Q7QUFDbEQsaURBQTRDO0FBQzVDLDRDQUF1QztBQUV2QztJQUFBO0lBWUEsQ0FBQztJQVZNLHdDQUFZLEdBQWxCLFVBQW1CLEtBQWEsRUFBRSxRQUFnQjs7Ozs7O3dCQUMzQyxTQUFTLEdBQUcsSUFBSSxtQkFBUyxFQUFFLENBQUM7d0JBQ2hCLHFCQUFNLFNBQVMsQ0FBQyxXQUFXLENBQUMsS0FBSyxDQUFDLEVBQUE7O3dCQUE5QyxJQUFJLEdBQVEsU0FBa0M7d0JBQ3BELElBQUksQ0FBQyxJQUFJOzRCQUFFLE1BQU0sSUFBSSx1QkFBYyxDQUFDLDhCQUE4QixDQUFDLENBQUM7d0JBQ3BFLElBQUksQ0FBQyxvQkFBYSxDQUFDLFFBQVEsRUFBRSxJQUFJLENBQUMsUUFBUSxDQUFDOzRCQUFFLE1BQU0sSUFBSSx1QkFBYyxDQUFDLDhCQUE4QixDQUFDLENBQUM7d0JBQ2hHLE9BQU8sR0FBVyxFQUFFLEVBQUUsRUFBRSxpQkFBTyxFQUFFLEVBQUUsT0FBTyxFQUFFLElBQUksQ0FBQyxFQUFFLEVBQUUsQ0FBQzt3QkFDdEQsWUFBWSxHQUFHLElBQUksc0JBQVksRUFBRSxDQUFDO3dCQUN4QyxzQkFBTyxZQUFZLENBQUMsSUFBSSxDQUFDLE9BQU8sRUFBRSxFQUFFLEtBQUssRUFBRSxJQUFJLEVBQUUsQ0FBQyxFQUFDOzs7O0tBQ25EO0lBRUYsd0JBQUM7QUFBRCxDQVpBLEFBWUMsSUFBQSIsImZpbGUiOiJTZXNzaW9uQ29udHJvbGxlci5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IFNlc3Npb24sIFVzZXIgfSBmcm9tICcuLi9kYic7XG5pbXBvcnQgeyBjaGVja1Bhc3N3b3JkIH0gZnJvbSAnLi4vdXRpbHMvYXV0aCc7XG5pbXBvcnQgeyBFcnJvckZvcmJpZGRlbiB9IGZyb20gJy4uL3V0aWxzL2Vycm9ycyc7XG5pbXBvcnQgU2Vzc2lvbk1vZGVsIGZyb20gJy4uL21vZGVscy9TZXNzaW9uTW9kZWwnO1xuaW1wb3J0IFVzZXJNb2RlbCBmcm9tICcuLi9tb2RlbHMvVXNlck1vZGVsJztcbmltcG9ydCB1dWlkZ2VuIGZyb20gJy4uL3V0aWxzL3V1aWRnZW4nO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTZXNzaW9uQ29udHJvbGxlciB7XG5cblx0YXN5bmMgYXV0aGVudGljYXRlKGVtYWlsOiBzdHJpbmcsIHBhc3N3b3JkOiBzdHJpbmcpOlByb21pc2U8U2Vzc2lvbj4ge1xuXHRcdGNvbnN0IHVzZXJNb2RlbCA9IG5ldyBVc2VyTW9kZWwoKTtcblx0XHRjb25zdCB1c2VyOlVzZXIgPSBhd2FpdCB1c2VyTW9kZWwubG9hZEJ5RW1haWwoZW1haWwpO1xuXHRcdGlmICghdXNlcikgdGhyb3cgbmV3IEVycm9yRm9yYmlkZGVuKCdJbnZhbGlkIHVzZXJuYW1lIG9yIHBhc3N3b3JkJyk7XG5cdFx0aWYgKCFjaGVja1Bhc3N3b3JkKHBhc3N3b3JkLCB1c2VyLnBhc3N3b3JkKSkgdGhyb3cgbmV3IEVycm9yRm9yYmlkZGVuKCdJbnZhbGlkIHVzZXJuYW1lIG9yIHBhc3N3b3JkJyk7XG5cdFx0Y29uc3Qgc2Vzc2lvbjpTZXNzaW9uID0geyBpZDogdXVpZGdlbigpLCB1c2VyX2lkOiB1c2VyLmlkIH07XG5cdFx0Y29uc3Qgc2Vzc2lvbk1vZGVsID0gbmV3IFNlc3Npb25Nb2RlbCgpO1xuXHRcdHJldHVybiBzZXNzaW9uTW9kZWwuc2F2ZShzZXNzaW9uLCB7IGlzTmV3OiB0cnVlIH0pO1xuXHR9XG5cbn1cbiJdfQ==
|
136
Server/dist/app/controllers/UserController.js
vendored
136
Server/dist/app/controllers/UserController.js
vendored
@ -1,136 +0,0 @@
|
||||
"use strict";
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var UserModel_1 = require("../models/UserModel");
|
||||
var BaseController_1 = require("./BaseController");
|
||||
var UserController = /** @class */ (function (_super) {
|
||||
__extends(UserController, _super);
|
||||
function UserController() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
UserController.prototype.createUser = function (sessionId, user) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var owner, userModel, newUser;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.initSession(sessionId, true)];
|
||||
case 1:
|
||||
owner = _a.sent();
|
||||
userModel = new UserModel_1.default({ userId: owner.id });
|
||||
return [4 /*yield*/, userModel.fromApiInput(user)];
|
||||
case 2:
|
||||
newUser = _a.sent();
|
||||
return [4 /*yield*/, userModel.save(newUser)];
|
||||
case 3:
|
||||
newUser = _a.sent();
|
||||
return [2 /*return*/, userModel.toApiOutput(newUser)];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
UserController.prototype.getUser = function (sessionId, userId) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var owner, userModel, _a, _b;
|
||||
return __generator(this, function (_c) {
|
||||
switch (_c.label) {
|
||||
case 0: return [4 /*yield*/, this.initSession(sessionId)];
|
||||
case 1:
|
||||
owner = _c.sent();
|
||||
userModel = new UserModel_1.default({ userId: owner.id });
|
||||
_b = (_a = userModel).toApiOutput;
|
||||
return [4 /*yield*/, userModel.load(userId)];
|
||||
case 2: return [2 /*return*/, _b.apply(_a, [_c.sent()])];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
UserController.prototype.updateUser = function (sessionId, user) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var owner, userModel, newUser;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.initSession(sessionId)];
|
||||
case 1:
|
||||
owner = _a.sent();
|
||||
userModel = new UserModel_1.default({ userId: owner.id });
|
||||
return [4 /*yield*/, userModel.fromApiInput(user)];
|
||||
case 2:
|
||||
newUser = _a.sent();
|
||||
return [4 /*yield*/, userModel.save(newUser, { isNew: false })];
|
||||
case 3:
|
||||
_a.sent();
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
UserController.prototype.deleteUser = function (sessionId, userId) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var user, userModel;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.initSession(sessionId)];
|
||||
case 1:
|
||||
user = _a.sent();
|
||||
userModel = new UserModel_1.default({ userId: user.id });
|
||||
return [4 /*yield*/, userModel.delete(userId)];
|
||||
case 2:
|
||||
_a.sent();
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return UserController;
|
||||
}(BaseController_1.default));
|
||||
exports.default = UserController;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlVzZXJDb250cm9sbGVyLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7OztBQUNBLGlEQUE0QztBQUM1QyxtREFBOEM7QUFFOUM7SUFBNEMsa0NBQWM7SUFBMUQ7O0lBNkJBLENBQUM7SUEzQk0sbUNBQVUsR0FBaEIsVUFBaUIsU0FBZ0IsRUFBRSxJQUFTOzs7Ozs0QkFDN0IscUJBQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQyxTQUFTLEVBQUUsSUFBSSxDQUFDLEVBQUE7O3dCQUEvQyxLQUFLLEdBQUcsU0FBdUM7d0JBQy9DLFNBQVMsR0FBRyxJQUFJLG1CQUFTLENBQUMsRUFBRSxNQUFNLEVBQUUsS0FBSyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUM7d0JBQ3hDLHFCQUFNLFNBQVMsQ0FBQyxZQUFZLENBQUMsSUFBSSxDQUFDLEVBQUE7O3dCQUE1QyxPQUFPLEdBQUcsU0FBa0M7d0JBQ3RDLHFCQUFNLFNBQVMsQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLEVBQUE7O3dCQUF2QyxPQUFPLEdBQUcsU0FBNkIsQ0FBQzt3QkFDeEMsc0JBQU8sU0FBUyxDQUFDLFdBQVcsQ0FBQyxPQUFPLENBQUMsRUFBQzs7OztLQUN0QztJQUVLLGdDQUFPLEdBQWIsVUFBYyxTQUFnQixFQUFFLE1BQWE7Ozs7OzRCQUM5QixxQkFBTSxJQUFJLENBQUMsV0FBVyxDQUFDLFNBQVMsQ0FBQyxFQUFBOzt3QkFBekMsS0FBSyxHQUFHLFNBQWlDO3dCQUN6QyxTQUFTLEdBQUcsSUFBSSxtQkFBUyxDQUFDLEVBQUUsTUFBTSxFQUFFLEtBQUssQ0FBQyxFQUFFLEVBQUUsQ0FBQyxDQUFDO3dCQUMvQyxLQUFBLENBQUEsS0FBQSxTQUFTLENBQUEsQ0FBQyxXQUFXLENBQUE7d0JBQUMscUJBQU0sU0FBUyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBQTs0QkFBekQsc0JBQU8sY0FBc0IsU0FBNEIsRUFBQyxFQUFDOzs7O0tBQzNEO0lBRUssbUNBQVUsR0FBaEIsVUFBaUIsU0FBZ0IsRUFBRSxJQUFTOzs7Ozs0QkFDN0IscUJBQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQyxTQUFTLENBQUMsRUFBQTs7d0JBQXpDLEtBQUssR0FBRyxTQUFpQzt3QkFDekMsU0FBUyxHQUFHLElBQUksbUJBQVMsQ0FBQyxFQUFFLE1BQU0sRUFBRSxLQUFLLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQzt3QkFDdEMscUJBQU0sU0FBUyxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsRUFBQTs7d0JBQTVDLE9BQU8sR0FBRyxTQUFrQzt3QkFDbEQscUJBQU0sU0FBUyxDQUFDLElBQUksQ0FBQyxPQUFPLEVBQUUsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLENBQUMsRUFBQTs7d0JBQS9DLFNBQStDLENBQUM7Ozs7O0tBQ2hEO0lBRUssbUNBQVUsR0FBaEIsVUFBaUIsU0FBZ0IsRUFBRSxNQUFhOzs7Ozs0QkFDbEMscUJBQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQyxTQUFTLENBQUMsRUFBQTs7d0JBQXhDLElBQUksR0FBRyxTQUFpQzt3QkFDeEMsU0FBUyxHQUFHLElBQUksbUJBQVMsQ0FBQyxFQUFFLE1BQU0sRUFBRSxJQUFJLENBQUMsRUFBRSxFQUFFLENBQUMsQ0FBQzt3QkFDckQscUJBQU0sU0FBUyxDQUFDLE1BQU0sQ0FBQyxNQUFNLENBQUMsRUFBQTs7d0JBQTlCLFNBQThCLENBQUM7Ozs7O0tBQy9CO0lBRUYscUJBQUM7QUFBRCxDQTdCQSxBQTZCQyxDQTdCMkMsd0JBQWMsR0E2QnpEIiwiZmlsZSI6IlVzZXJDb250cm9sbGVyLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgVXNlciB9IGZyb20gJy4uL2RiJztcbmltcG9ydCBVc2VyTW9kZWwgZnJvbSAnLi4vbW9kZWxzL1VzZXJNb2RlbCc7XG5pbXBvcnQgQmFzZUNvbnRyb2xsZXIgZnJvbSAnLi9CYXNlQ29udHJvbGxlcic7XG5cbmV4cG9ydCBkZWZhdWx0IGNsYXNzIFVzZXJDb250cm9sbGVyIGV4dGVuZHMgQmFzZUNvbnRyb2xsZXIge1xuXG5cdGFzeW5jIGNyZWF0ZVVzZXIoc2Vzc2lvbklkOnN0cmluZywgdXNlcjpVc2VyKTpQcm9taXNlPFVzZXI+IHtcblx0XHRjb25zdCBvd25lciA9IGF3YWl0IHRoaXMuaW5pdFNlc3Npb24oc2Vzc2lvbklkLCB0cnVlKTtcblx0XHRjb25zdCB1c2VyTW9kZWwgPSBuZXcgVXNlck1vZGVsKHsgdXNlcklkOiBvd25lci5pZCB9KTtcblx0XHRsZXQgbmV3VXNlciA9IGF3YWl0IHVzZXJNb2RlbC5mcm9tQXBpSW5wdXQodXNlcik7XG5cdFx0bmV3VXNlciA9IGF3YWl0IHVzZXJNb2RlbC5zYXZlKG5ld1VzZXIpO1xuXHRcdHJldHVybiB1c2VyTW9kZWwudG9BcGlPdXRwdXQobmV3VXNlcik7XG5cdH1cblxuXHRhc3luYyBnZXRVc2VyKHNlc3Npb25JZDpzdHJpbmcsIHVzZXJJZDpzdHJpbmcpOlByb21pc2U8VXNlcj4ge1xuXHRcdGNvbnN0IG93bmVyID0gYXdhaXQgdGhpcy5pbml0U2Vzc2lvbihzZXNzaW9uSWQpO1xuXHRcdGNvbnN0IHVzZXJNb2RlbCA9IG5ldyBVc2VyTW9kZWwoeyB1c2VySWQ6IG93bmVyLmlkIH0pO1xuXHRcdHJldHVybiB1c2VyTW9kZWwudG9BcGlPdXRwdXQoYXdhaXQgdXNlck1vZGVsLmxvYWQodXNlcklkKSk7XG5cdH1cblxuXHRhc3luYyB1cGRhdGVVc2VyKHNlc3Npb25JZDpzdHJpbmcsIHVzZXI6VXNlcik6UHJvbWlzZTx2b2lkPiB7XG5cdFx0Y29uc3Qgb3duZXIgPSBhd2FpdCB0aGlzLmluaXRTZXNzaW9uKHNlc3Npb25JZCk7XG5cdFx0Y29uc3QgdXNlck1vZGVsID0gbmV3IFVzZXJNb2RlbCh7IHVzZXJJZDogb3duZXIuaWQgfSk7XG5cdFx0Y29uc3QgbmV3VXNlciA9IGF3YWl0IHVzZXJNb2RlbC5mcm9tQXBpSW5wdXQodXNlcik7XG5cdFx0YXdhaXQgdXNlck1vZGVsLnNhdmUobmV3VXNlciwgeyBpc05ldzogZmFsc2UgfSk7XG5cdH1cblxuXHRhc3luYyBkZWxldGVVc2VyKHNlc3Npb25JZDpzdHJpbmcsIHVzZXJJZDpzdHJpbmcpOlByb21pc2U8dm9pZD4ge1xuXHRcdGNvbnN0IHVzZXIgPSBhd2FpdCB0aGlzLmluaXRTZXNzaW9uKHNlc3Npb25JZCk7XG5cdFx0Y29uc3QgdXNlck1vZGVsID0gbmV3IFVzZXJNb2RlbCh7IHVzZXJJZDogdXNlci5pZCB9KTtcblx0XHRhd2FpdCB1c2VyTW9kZWwuZGVsZXRlKHVzZXJJZCk7XG5cdH1cblxufVxuIl19
|
66
Server/dist/app/db.js
vendored
66
Server/dist/app/db.js
vendored
File diff suppressed because one or more lines are too long
275
Server/dist/app/models/BaseModel.js
vendored
275
Server/dist/app/models/BaseModel.js
vendored
File diff suppressed because one or more lines are too long
504
Server/dist/app/models/FileModel.js
vendored
504
Server/dist/app/models/FileModel.js
vendored
File diff suppressed because one or more lines are too long
147
Server/dist/app/models/PermissionModel.js
vendored
147
Server/dist/app/models/PermissionModel.js
vendored
@ -1,147 +0,0 @@
|
||||
"use strict";
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var BaseModel_1 = require("./BaseModel");
|
||||
var db_1 = require("../db");
|
||||
var UserModel_1 = require("./UserModel");
|
||||
var ReadOrWriteKeys;
|
||||
(function (ReadOrWriteKeys) {
|
||||
ReadOrWriteKeys["CanRead"] = "can_read";
|
||||
ReadOrWriteKeys["CanWrite"] = "can_write";
|
||||
})(ReadOrWriteKeys || (ReadOrWriteKeys = {}));
|
||||
var PermissionModel = /** @class */ (function (_super) {
|
||||
__extends(PermissionModel, _super);
|
||||
function PermissionModel() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
Object.defineProperty(PermissionModel.prototype, "tableName", {
|
||||
get: function () {
|
||||
return 'permissions';
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
PermissionModel.prototype.filePermissions = function (fileId, userId) {
|
||||
if (userId === void 0) { userId = null; }
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var p;
|
||||
return __generator(this, function (_a) {
|
||||
p = {
|
||||
item_type: db_1.ItemType.File,
|
||||
item_id: fileId,
|
||||
};
|
||||
if (userId)
|
||||
p.user_id = userId;
|
||||
return [2 /*return*/, this.db(this.tableName).where(p).select()];
|
||||
});
|
||||
});
|
||||
};
|
||||
PermissionModel.prototype.canReadOrWrite = function (fileId, userId, method) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var permissions, _i, permissions_1, p, userModel, owner;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!userId || !fileId)
|
||||
return [2 /*return*/, false];
|
||||
return [4 /*yield*/, this.filePermissions(fileId, userId)];
|
||||
case 1:
|
||||
permissions = _a.sent();
|
||||
for (_i = 0, permissions_1 = permissions; _i < permissions_1.length; _i++) {
|
||||
p = permissions_1[_i];
|
||||
if (p[method] || p.is_owner)
|
||||
return [2 /*return*/, true];
|
||||
}
|
||||
userModel = new UserModel_1.default({ userId: userId });
|
||||
return [4 /*yield*/, userModel.load(userId)];
|
||||
case 2:
|
||||
owner = _a.sent();
|
||||
if (owner.is_admin)
|
||||
return [2 /*return*/, true];
|
||||
return [2 /*return*/, false];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
PermissionModel.prototype.canRead = function (fileId, userId) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/, this.canReadOrWrite(fileId, userId, ReadOrWriteKeys.CanRead)];
|
||||
});
|
||||
});
|
||||
};
|
||||
PermissionModel.prototype.canWrite = function (fileId, userId) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/, this.canReadOrWrite(fileId, userId, ReadOrWriteKeys.CanWrite)];
|
||||
});
|
||||
});
|
||||
};
|
||||
PermissionModel.prototype.deleteByFileId = function (fileId) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var permissions, ids;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.filePermissions(fileId)];
|
||||
case 1:
|
||||
permissions = _a.sent();
|
||||
ids = permissions.map(function (m) { return m.id; });
|
||||
_super.prototype.delete.call(this, ids);
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return PermissionModel;
|
||||
}(BaseModel_1.default));
|
||||
exports.default = PermissionModel;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlBlcm1pc3Npb25Nb2RlbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSx5Q0FBb0M7QUFDcEMsNEJBQW1EO0FBQ25ELHlDQUFvQztBQUVwQyxJQUFLLGVBR0o7QUFIRCxXQUFLLGVBQWU7SUFDbkIsdUNBQW9CLENBQUE7SUFDcEIseUNBQXNCLENBQUE7QUFDdkIsQ0FBQyxFQUhJLGVBQWUsS0FBZixlQUFlLFFBR25CO0FBRUQ7SUFBNkMsbUNBQVM7SUFBdEQ7O0lBNkNBLENBQUM7SUEzQ0Esc0JBQUksc0NBQVM7YUFBYjtZQUNDLE9BQU8sYUFBYSxDQUFDO1FBQ3RCLENBQUM7OztPQUFBO0lBRUsseUNBQWUsR0FBckIsVUFBc0IsTUFBYSxFQUFFLE1BQW9CO1FBQXBCLHVCQUFBLEVBQUEsYUFBb0I7Ozs7Z0JBQ2xELENBQUMsR0FBYztvQkFDcEIsU0FBUyxFQUFFLGFBQVEsQ0FBQyxJQUFJO29CQUN4QixPQUFPLEVBQUUsTUFBTTtpQkFDZixDQUFDO2dCQUVGLElBQUksTUFBTTtvQkFBRSxDQUFDLENBQUMsT0FBTyxHQUFHLE1BQU0sQ0FBQztnQkFFL0Isc0JBQU8sSUFBSSxDQUFDLEVBQUUsQ0FBYSxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sRUFBRSxFQUFDOzs7S0FDN0Q7SUFFYSx3Q0FBYyxHQUE1QixVQUE2QixNQUFhLEVBQUUsTUFBYSxFQUFFLE1BQXNCOzs7Ozs7d0JBQ2hGLElBQUksQ0FBQyxNQUFNLElBQUksQ0FBQyxNQUFNOzRCQUFFLHNCQUFPLEtBQUssRUFBQzt3QkFDakIscUJBQU0sSUFBSSxDQUFDLGVBQWUsQ0FBQyxNQUFNLEVBQUUsTUFBTSxDQUFDLEVBQUE7O3dCQUF4RCxXQUFXLEdBQUcsU0FBMEM7d0JBQzlELFdBQTJCLEVBQVgsMkJBQVcsRUFBWCx5QkFBVyxFQUFYLElBQVcsRUFBRTs0QkFBbEIsQ0FBQzs0QkFDWCxJQUFJLENBQUMsQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsUUFBUTtnQ0FBRSxzQkFBTyxJQUFJLEVBQUM7eUJBQ3pDO3dCQUVLLFNBQVMsR0FBRyxJQUFJLG1CQUFTLENBQUMsRUFBRSxNQUFNLEVBQUUsTUFBTSxFQUFFLENBQUMsQ0FBQzt3QkFDakMscUJBQU0sU0FBUyxDQUFDLElBQUksQ0FBQyxNQUFNLENBQUMsRUFBQTs7d0JBQXpDLEtBQUssR0FBUSxTQUE0Qjt3QkFDL0MsSUFBSSxLQUFLLENBQUMsUUFBUTs0QkFBRSxzQkFBTyxJQUFJLEVBQUM7d0JBRWhDLHNCQUFPLEtBQUssRUFBQzs7OztLQUNiO0lBRUssaUNBQU8sR0FBYixVQUFjLE1BQWEsRUFBRSxNQUFhOzs7Z0JBQ3pDLHNCQUFPLElBQUksQ0FBQyxjQUFjLENBQUMsTUFBTSxFQUFFLE1BQU0sRUFBRSxlQUFlLENBQUMsT0FBTyxDQUFDLEVBQUM7OztLQUNwRTtJQUVLLGtDQUFRLEdBQWQsVUFBZSxNQUFhLEVBQUUsTUFBYTs7O2dCQUMxQyxzQkFBTyxJQUFJLENBQUMsY0FBYyxDQUFDLE1BQU0sRUFBRSxNQUFNLEVBQUUsZUFBZSxDQUFDLFFBQVEsQ0FBQyxFQUFDOzs7S0FDckU7SUFFSyx3Q0FBYyxHQUFwQixVQUFxQixNQUFhOzs7Ozs0QkFDYixxQkFBTSxJQUFJLENBQUMsZUFBZSxDQUFDLE1BQU0sQ0FBQyxFQUFBOzt3QkFBaEQsV0FBVyxHQUFHLFNBQWtDO3dCQUNoRCxHQUFHLEdBQUcsV0FBVyxDQUFDLEdBQUcsQ0FBQyxVQUFBLENBQUMsSUFBSSxPQUFBLENBQUMsQ0FBQyxFQUFFLEVBQUosQ0FBSSxDQUFDLENBQUM7d0JBQ3ZDLGlCQUFNLE1BQU0sWUFBQyxHQUFHLENBQUMsQ0FBQzs7Ozs7S0FDbEI7SUFFRixzQkFBQztBQUFELENBN0NBLEFBNkNDLENBN0M0QyxtQkFBUyxHQTZDckQiLCJmaWxlIjoiUGVybWlzc2lvbk1vZGVsLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IEJhc2VNb2RlbCBmcm9tICcuL0Jhc2VNb2RlbCc7XG5pbXBvcnQgeyBQZXJtaXNzaW9uLCBJdGVtVHlwZSwgVXNlciB9IGZyb20gJy4uL2RiJztcbmltcG9ydCBVc2VyTW9kZWwgZnJvbSAnLi9Vc2VyTW9kZWwnO1xuXG5lbnVtIFJlYWRPcldyaXRlS2V5cyB7XG5cdENhblJlYWQgPSAnY2FuX3JlYWQnLFxuXHRDYW5Xcml0ZSA9ICdjYW5fd3JpdGUnLFxufVxuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBQZXJtaXNzaW9uTW9kZWwgZXh0ZW5kcyBCYXNlTW9kZWwge1xuXG5cdGdldCB0YWJsZU5hbWUoKTpzdHJpbmcge1xuXHRcdHJldHVybiAncGVybWlzc2lvbnMnO1xuXHR9XG5cblx0YXN5bmMgZmlsZVBlcm1pc3Npb25zKGZpbGVJZDpzdHJpbmcsIHVzZXJJZDpzdHJpbmcgPSBudWxsKTpQcm9taXNlPEFycmF5PFBlcm1pc3Npb24+PiB7XG5cdFx0Y29uc3QgcDpQZXJtaXNzaW9uID0ge1xuXHRcdFx0aXRlbV90eXBlOiBJdGVtVHlwZS5GaWxlLFxuXHRcdFx0aXRlbV9pZDogZmlsZUlkLFxuXHRcdH07XG5cblx0XHRpZiAodXNlcklkKSBwLnVzZXJfaWQgPSB1c2VySWQ7XG5cblx0XHRyZXR1cm4gdGhpcy5kYjxQZXJtaXNzaW9uPih0aGlzLnRhYmxlTmFtZSkud2hlcmUocCkuc2VsZWN0KCk7XG5cdH1cblxuXHRwcml2YXRlIGFzeW5jIGNhblJlYWRPcldyaXRlKGZpbGVJZDpzdHJpbmcsIHVzZXJJZDpzdHJpbmcsIG1ldGhvZDpSZWFkT3JXcml0ZUtleXMpOlByb21pc2U8Ym9vbGVhbj4ge1xuXHRcdGlmICghdXNlcklkIHx8ICFmaWxlSWQpIHJldHVybiBmYWxzZTtcblx0XHRjb25zdCBwZXJtaXNzaW9ucyA9IGF3YWl0IHRoaXMuZmlsZVBlcm1pc3Npb25zKGZpbGVJZCwgdXNlcklkKTtcblx0XHRmb3IgKGNvbnN0IHAgb2YgcGVybWlzc2lvbnMpIHtcblx0XHRcdGlmIChwW21ldGhvZF0gfHwgcC5pc19vd25lcikgcmV0dXJuIHRydWU7XG5cdFx0fVxuXG5cdFx0Y29uc3QgdXNlck1vZGVsID0gbmV3IFVzZXJNb2RlbCh7IHVzZXJJZDogdXNlcklkIH0pO1xuXHRcdGNvbnN0IG93bmVyOlVzZXIgPSBhd2FpdCB1c2VyTW9kZWwubG9hZCh1c2VySWQpO1xuXHRcdGlmIChvd25lci5pc19hZG1pbikgcmV0dXJuIHRydWU7XG5cblx0XHRyZXR1cm4gZmFsc2U7XG5cdH1cblxuXHRhc3luYyBjYW5SZWFkKGZpbGVJZDpzdHJpbmcsIHVzZXJJZDpzdHJpbmcpOlByb21pc2U8Ym9vbGVhbj4ge1xuXHRcdHJldHVybiB0aGlzLmNhblJlYWRPcldyaXRlKGZpbGVJZCwgdXNlcklkLCBSZWFkT3JXcml0ZUtleXMuQ2FuUmVhZCk7XG5cdH1cblxuXHRhc3luYyBjYW5Xcml0ZShmaWxlSWQ6c3RyaW5nLCB1c2VySWQ6c3RyaW5nKTpQcm9taXNlPGJvb2xlYW4+IHtcblx0XHRyZXR1cm4gdGhpcy5jYW5SZWFkT3JXcml0ZShmaWxlSWQsIHVzZXJJZCwgUmVhZE9yV3JpdGVLZXlzLkNhbldyaXRlKTtcblx0fVxuXG5cdGFzeW5jIGRlbGV0ZUJ5RmlsZUlkKGZpbGVJZDpzdHJpbmcpOlByb21pc2U8dm9pZD4ge1xuXHRcdGNvbnN0IHBlcm1pc3Npb25zID0gYXdhaXQgdGhpcy5maWxlUGVybWlzc2lvbnMoZmlsZUlkKTtcblx0XHRjb25zdCBpZHMgPSBwZXJtaXNzaW9ucy5tYXAobSA9PiBtLmlkKTtcblx0XHRzdXBlci5kZWxldGUoaWRzKTtcblx0fVxuXG59XG4iXX0=
|
86
Server/dist/app/models/SessionModel.js
vendored
86
Server/dist/app/models/SessionModel.js
vendored
@ -1,86 +0,0 @@
|
||||
"use strict";
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var BaseModel_1 = require("./BaseModel");
|
||||
var UserModel_1 = require("./UserModel");
|
||||
var SessionModel = /** @class */ (function (_super) {
|
||||
__extends(SessionModel, _super);
|
||||
function SessionModel() {
|
||||
return _super !== null && _super.apply(this, arguments) || this;
|
||||
}
|
||||
Object.defineProperty(SessionModel.prototype, "tableName", {
|
||||
get: function () {
|
||||
return 'sessions';
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
});
|
||||
SessionModel.prototype.sessionUser = function (sessionId) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var session, userModel;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, this.load(sessionId)];
|
||||
case 1:
|
||||
session = _a.sent();
|
||||
if (!session)
|
||||
return [2 /*return*/, null];
|
||||
userModel = new UserModel_1.default({ userId: session.user_id });
|
||||
return [2 /*return*/, userModel.load(session.user_id)];
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
return SessionModel;
|
||||
}(BaseModel_1.default));
|
||||
exports.default = SessionModel;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIlNlc3Npb25Nb2RlbC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSx5Q0FBb0M7QUFDcEMseUNBQW9DO0FBR3BDO0lBQTBDLGdDQUFTO0lBQW5EOztJQWFBLENBQUM7SUFYQSxzQkFBSSxtQ0FBUzthQUFiO1lBQ0MsT0FBTyxVQUFVLENBQUM7UUFDbkIsQ0FBQzs7O09BQUE7SUFFSyxrQ0FBVyxHQUFqQixVQUFrQixTQUFnQjs7Ozs7NEJBQ1QscUJBQU0sSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUMsRUFBQTs7d0JBQTVDLE9BQU8sR0FBVyxTQUEwQjt3QkFDbEQsSUFBSSxDQUFDLE9BQU87NEJBQUUsc0JBQU8sSUFBSSxFQUFDO3dCQUNwQixTQUFTLEdBQUcsSUFBSSxtQkFBUyxDQUFDLEVBQUUsTUFBTSxFQUFFLE9BQU8sQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDO3dCQUM3RCxzQkFBTyxTQUFTLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsRUFBQzs7OztLQUN2QztJQUVGLG1CQUFDO0FBQUQsQ0FiQSxBQWFDLENBYnlDLG1CQUFTLEdBYWxEIiwiZmlsZSI6IlNlc3Npb25Nb2RlbC5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCBCYXNlTW9kZWwgZnJvbSAnLi9CYXNlTW9kZWwnO1xuaW1wb3J0IFVzZXJNb2RlbCBmcm9tICcuL1VzZXJNb2RlbCc7XG5pbXBvcnQgeyBVc2VyLCBTZXNzaW9uIH0gZnJvbSAnLi4vZGInO1xuXG5leHBvcnQgZGVmYXVsdCBjbGFzcyBTZXNzaW9uTW9kZWwgZXh0ZW5kcyBCYXNlTW9kZWwge1xuXG5cdGdldCB0YWJsZU5hbWUoKTpzdHJpbmcge1xuXHRcdHJldHVybiAnc2Vzc2lvbnMnO1xuXHR9XG5cblx0YXN5bmMgc2Vzc2lvblVzZXIoc2Vzc2lvbklkOnN0cmluZyk6UHJvbWlzZTxVc2VyPiB7XG5cdFx0Y29uc3Qgc2Vzc2lvbjpTZXNzaW9uID0gYXdhaXQgdGhpcy5sb2FkKHNlc3Npb25JZCk7XG5cdFx0aWYgKCFzZXNzaW9uKSByZXR1cm4gbnVsbDtcblx0XHRjb25zdCB1c2VyTW9kZWwgPSBuZXcgVXNlck1vZGVsKHsgdXNlcklkOiBzZXNzaW9uLnVzZXJfaWQgfSk7XG5cdFx0cmV0dXJuIHVzZXJNb2RlbC5sb2FkKHNlc3Npb24udXNlcl9pZCk7XG5cdH1cblxufVxuIl19
|
269
Server/dist/app/models/UserModel.js
vendored
269
Server/dist/app/models/UserModel.js
vendored
File diff suppressed because one or more lines are too long
121
Server/dist/app/routes/api/files.js
vendored
121
Server/dist/app/routes/api/files.js
vendored
File diff suppressed because one or more lines are too long
50
Server/dist/app/routes/api/index.js
vendored
50
Server/dist/app/routes/api/index.js
vendored
@ -1,50 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var route = {
|
||||
exec: function () {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/, { status: 'ok', message: 'Joplin Server is running' }];
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
exports.default = route;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImluZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBRUEsSUFBTSxLQUFLLEdBQVM7SUFFbkIsSUFBSSxFQUFFOzs7Z0JBQ0wsc0JBQU8sRUFBRSxNQUFNLEVBQUUsSUFBSSxFQUFFLE9BQU8sRUFBRSwwQkFBMEIsRUFBRSxFQUFDOzs7S0FDN0Q7Q0FFRCxDQUFDO0FBRUYsa0JBQWUsS0FBSyxDQUFDIiwiZmlsZSI6ImluZGV4LmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgUm91dGUgfSBmcm9tICcuLi8uLi91dGlscy9yb3V0ZVV0aWxzJztcblxuY29uc3Qgcm91dGU6Um91dGUgPSB7XG5cblx0ZXhlYzogYXN5bmMgZnVuY3Rpb24oKSB7XG5cdFx0cmV0dXJuIHsgc3RhdHVzOiAnb2snLCBtZXNzYWdlOiAnSm9wbGluIFNlcnZlciBpcyBydW5uaW5nJyB9O1xuXHR9LFxuXG59O1xuXG5leHBvcnQgZGVmYXVsdCByb3V0ZTtcbiJdfQ==
|
50
Server/dist/app/routes/api/ping.js
vendored
50
Server/dist/app/routes/api/ping.js
vendored
@ -1,50 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var route = {
|
||||
exec: function () {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/, { status: 'ok', message: 'Joplin Server is running' }];
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
exports.default = route;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInBpbmcudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFFQSxJQUFNLEtBQUssR0FBUztJQUVuQixJQUFJLEVBQUU7OztnQkFDTCxzQkFBTyxFQUFFLE1BQU0sRUFBRSxJQUFJLEVBQUUsT0FBTyxFQUFFLDBCQUEwQixFQUFFLEVBQUM7OztLQUM3RDtDQUVELENBQUM7QUFFRixrQkFBZSxLQUFLLENBQUMiLCJmaWxlIjoicGluZy5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IFJvdXRlIH0gZnJvbSAnLi4vLi4vdXRpbHMvcm91dGVVdGlscyc7XG5cbmNvbnN0IHJvdXRlOlJvdXRlID0ge1xuXG5cdGV4ZWM6IGFzeW5jIGZ1bmN0aW9uKCkge1xuXHRcdHJldHVybiB7IHN0YXR1czogJ29rJywgbWVzc2FnZTogJ0pvcGxpbiBTZXJ2ZXIgaXMgcnVubmluZycgfTtcblx0fSxcblxufTtcblxuZXhwb3J0IGRlZmF1bHQgcm91dGU7XG4iXX0=
|
67
Server/dist/app/routes/api/sessions.js
vendored
67
Server/dist/app/routes/api/sessions.js
vendored
@ -1,67 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var parse = require("co-body");
|
||||
var SessionController_1 = require("../../controllers/SessionController");
|
||||
var errors_1 = require("../../utils/errors");
|
||||
var route = {
|
||||
exec: function (path, ctx) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var user, sessionController, session;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!!path.link) return [3 /*break*/, 3];
|
||||
if (!(ctx.method === 'POST')) return [3 /*break*/, 3];
|
||||
return [4 /*yield*/, parse.json(ctx)];
|
||||
case 1:
|
||||
user = _a.sent();
|
||||
sessionController = new SessionController_1.default();
|
||||
return [4 /*yield*/, sessionController.authenticate(user.email, user.password)];
|
||||
case 2:
|
||||
session = _a.sent();
|
||||
return [2 /*return*/, { id: session.id }];
|
||||
case 3: throw new errors_1.ErrorNotFound("Invalid link: " + path.link);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
exports.default = route;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNlc3Npb25zLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBQ0EsK0JBQWlDO0FBQ2pDLHlFQUFvRTtBQUVwRSw2Q0FBbUQ7QUFFbkQsSUFBTSxLQUFLLEdBQVM7SUFFbkIsSUFBSSxFQUFFLFVBQWUsSUFBWSxFQUFFLEdBQWU7Ozs7Ozs2QkFDN0MsQ0FBQyxJQUFJLENBQUMsSUFBSSxFQUFWLHdCQUFVOzZCQUNULENBQUEsR0FBRyxDQUFDLE1BQU0sS0FBSyxNQUFNLENBQUEsRUFBckIsd0JBQXFCO3dCQUNYLHFCQUFNLEtBQUssQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDLEVBQUE7O3dCQUE1QixJQUFJLEdBQUcsU0FBcUI7d0JBQzVCLGlCQUFpQixHQUFHLElBQUksMkJBQWlCLEVBQUUsQ0FBQzt3QkFDbEMscUJBQU0saUJBQWlCLENBQUMsWUFBWSxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUUsSUFBSSxDQUFDLFFBQVEsQ0FBQyxFQUFBOzt3QkFBekUsT0FBTyxHQUFHLFNBQStEO3dCQUMvRSxzQkFBTyxFQUFFLEVBQUUsRUFBRSxPQUFPLENBQUMsRUFBRSxFQUFFLEVBQUM7NEJBSTVCLE1BQU0sSUFBSSxzQkFBYSxDQUFDLG1CQUFpQixJQUFJLENBQUMsSUFBTSxDQUFDLENBQUM7Ozs7S0FDdEQ7Q0FFRCxDQUFDO0FBRUYsa0JBQWUsS0FBSyxDQUFDIiwiZmlsZSI6InNlc3Npb25zLmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgS29hIGZyb20gJ2tvYSc7XG5pbXBvcnQgKiBhcyBwYXJzZSBmcm9tICdjby1ib2R5JztcbmltcG9ydCBTZXNzaW9uQ29udHJvbGxlciBmcm9tICcuLi8uLi9jb250cm9sbGVycy9TZXNzaW9uQ29udHJvbGxlcic7XG5pbXBvcnQgeyBTdWJQYXRoLCBSb3V0ZSB9IGZyb20gJy4uLy4uL3V0aWxzL3JvdXRlVXRpbHMnO1xuaW1wb3J0IHsgRXJyb3JOb3RGb3VuZCB9IGZyb20gJy4uLy4uL3V0aWxzL2Vycm9ycyc7XG5cbmNvbnN0IHJvdXRlOlJvdXRlID0ge1xuXG5cdGV4ZWM6IGFzeW5jIGZ1bmN0aW9uKHBhdGg6U3ViUGF0aCwgY3R4OktvYS5Db250ZXh0KSB7XG5cdFx0aWYgKCFwYXRoLmxpbmspIHtcblx0XHRcdGlmIChjdHgubWV0aG9kID09PSAnUE9TVCcpIHtcblx0XHRcdFx0Y29uc3QgdXNlciA9IGF3YWl0IHBhcnNlLmpzb24oY3R4KTtcblx0XHRcdFx0Y29uc3Qgc2Vzc2lvbkNvbnRyb2xsZXIgPSBuZXcgU2Vzc2lvbkNvbnRyb2xsZXIoKTtcblx0XHRcdFx0Y29uc3Qgc2Vzc2lvbiA9IGF3YWl0IHNlc3Npb25Db250cm9sbGVyLmF1dGhlbnRpY2F0ZSh1c2VyLmVtYWlsLCB1c2VyLnBhc3N3b3JkKTtcblx0XHRcdFx0cmV0dXJuIHsgaWQ6IHNlc3Npb24uaWQgfTtcblx0XHRcdH1cblx0XHR9XG5cblx0XHR0aHJvdyBuZXcgRXJyb3JOb3RGb3VuZChgSW52YWxpZCBsaW5rOiAke3BhdGgubGlua31gKTtcblx0fSxcblxufTtcblxuZXhwb3J0IGRlZmF1bHQgcm91dGU7XG4iXX0=
|
65
Server/dist/app/uploadTest.js
vendored
65
Server/dist/app/uploadTest.js
vendored
@ -1,65 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var Koa = require("koa");
|
||||
var getRawBody = require("raw-body");
|
||||
var fs = require("fs-extra");
|
||||
var port = 3222;
|
||||
var app = new Koa();
|
||||
// app.use(koaBody({
|
||||
// multipart: true,
|
||||
// // includeUnparsed: true,
|
||||
// });
|
||||
app.use(function (ctx) { return __awaiter(void 0, void 0, void 0, function () {
|
||||
var body;
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0: return [4 /*yield*/, getRawBody(ctx.req)];
|
||||
case 1:
|
||||
body = _a.sent();
|
||||
return [4 /*yield*/, fs.writeFile('/mnt/c/Users/laurent/src/joplin/testingpost.jpg', body)];
|
||||
case 2:
|
||||
_a.sent();
|
||||
return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); });
|
||||
console.info('Starting server on port ' + port);
|
||||
app.listen(port);
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInVwbG9hZFRlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSx5QkFBMkI7QUFFM0IscUNBQXVDO0FBRXZDLDZCQUErQjtBQUUvQixJQUFNLElBQUksR0FBRyxJQUFJLENBQUM7QUFFbEIsSUFBTSxHQUFHLEdBQUcsSUFBSSxHQUFHLEVBQUUsQ0FBQztBQUV0QixvQkFBb0I7QUFDcEIsb0JBQW9CO0FBQ3BCLDZCQUE2QjtBQUM3QixNQUFNO0FBRU4sR0FBRyxDQUFDLEdBQUcsQ0FBQyxVQUFPLEdBQWU7Ozs7b0JBSWhCLHFCQUFNLFVBQVUsQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLEVBQUE7O2dCQUFoQyxJQUFJLEdBQUcsU0FBeUI7Z0JBRXRDLHFCQUFNLEVBQUUsQ0FBQyxTQUFTLENBQUMsaURBQWlELEVBQUUsSUFBSSxDQUFDLEVBQUE7O2dCQUEzRSxTQUEyRSxDQUFDOzs7O0tBRTVFLENBQUMsQ0FBQztBQUVILE9BQU8sQ0FBQyxJQUFJLENBQUMsMEJBQTBCLEdBQUcsSUFBSSxDQUFDLENBQUM7QUFFaEQsR0FBRyxDQUFDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyIsImZpbGUiOiJ1cGxvYWRUZXN0LmpzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0ICogYXMgS29hIGZyb20gJ2tvYSc7XG5pbXBvcnQgKiBhcyBrb2FCb2R5IGZyb20gJ2tvYS1ib2R5JztcbmltcG9ydCAqIGFzIGdldFJhd0JvZHkgZnJvbSAncmF3LWJvZHknO1xuXG5pbXBvcnQgKiBhcyBmcyBmcm9tICdmcy1leHRyYSc7XG5cbmNvbnN0IHBvcnQgPSAzMjIyO1xuXG5jb25zdCBhcHAgPSBuZXcgS29hKCk7XG5cbi8vIGFwcC51c2Uoa29hQm9keSh7XG4vLyBcdG11bHRpcGFydDogdHJ1ZSxcbi8vIFx0Ly8gaW5jbHVkZVVucGFyc2VkOiB0cnVlLFxuLy8gfSk7XG5cbmFwcC51c2UoYXN5bmMgKGN0eDpLb2EuQ29udGV4dCkgPT4ge1xuXHQvL2NvbnNvbGUuaW5mbyhcIkZJTEVTXCIsIGN0eC5yZXF1ZXN0LmZpbGVzKTtcblx0Ly8gY29uc29sZS5pbmZvKFwiQk9EWVwiLCBjdHgucmVxdWVzdC5ib2R5KTtcblxuXHRjb25zdCBib2R5ID0gYXdhaXQgZ2V0UmF3Qm9keShjdHgucmVxKVxuXG5cdGF3YWl0IGZzLndyaXRlRmlsZSgnL21udC9jL1VzZXJzL2xhdXJlbnQvc3JjL2pvcGxpbi90ZXN0aW5ncG9zdC5qcGcnLCBib2R5KTtcblxufSk7XG5cbmNvbnNvbGUuaW5mbygnU3RhcnRpbmcgc2VydmVyIG9uIHBvcnQgJyArIHBvcnQpO1xuXG5hcHAubGlzdGVuKHBvcnQpO1xuIl19
|
14
Server/dist/app/utils/appLogger.js
vendored
14
Server/dist/app/utils/appLogger.js
vendored
@ -1,14 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var fs = require("fs-extra");
|
||||
var FsDriverNode = require('lib/fs-driver-node').FsDriverNode;
|
||||
var Logger = require('lib/logger').Logger;
|
||||
var logDir = __dirname + "/../../../logs";
|
||||
fs.mkdirpSync(logDir);
|
||||
Logger.fsDriver_ = new FsDriverNode();
|
||||
var logger = new Logger();
|
||||
logger.addTarget('file', { path: logDir + "/app.txt" });
|
||||
logger.addTarget('console');
|
||||
exports.default = logger;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImFwcExvZ2dlci50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLDZCQUErQjtBQUN2QixJQUFBLHlEQUFZLENBQW1DO0FBQy9DLElBQUEscUNBQU0sQ0FBMkI7QUFFekMsSUFBTSxNQUFNLEdBQU0sU0FBUyxtQkFBZ0IsQ0FBQztBQUM1QyxFQUFFLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDO0FBRXRCLE1BQU0sQ0FBQyxTQUFTLEdBQUcsSUFBSSxZQUFZLEVBQUUsQ0FBQztBQUN0QyxJQUFNLE1BQU0sR0FBRyxJQUFJLE1BQU0sRUFBRSxDQUFDO0FBQzVCLE1BQU0sQ0FBQyxTQUFTLENBQUMsTUFBTSxFQUFFLEVBQUUsSUFBSSxFQUFLLE1BQU0sYUFBVSxFQUFFLENBQUMsQ0FBQztBQUN4RCxNQUFNLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0FBRTVCLGtCQUFlLE1BQU0sQ0FBQyIsImZpbGUiOiJhcHBMb2dnZXIuanMiLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQgKiBhcyBmcyBmcm9tICdmcy1leHRyYSc7XG5jb25zdCB7IEZzRHJpdmVyTm9kZSB9ID0gcmVxdWlyZSgnbGliL2ZzLWRyaXZlci1ub2RlJyk7XG5jb25zdCB7IExvZ2dlciB9ID0gcmVxdWlyZSgnbGliL2xvZ2dlcicpO1xuXG5jb25zdCBsb2dEaXIgPSBgJHtfX2Rpcm5hbWV9Ly4uLy4uLy4uL2xvZ3NgO1xuZnMubWtkaXJwU3luYyhsb2dEaXIpO1xuXG5Mb2dnZXIuZnNEcml2ZXJfID0gbmV3IEZzRHJpdmVyTm9kZSgpO1xuY29uc3QgbG9nZ2VyID0gbmV3IExvZ2dlcigpO1xubG9nZ2VyLmFkZFRhcmdldCgnZmlsZScsIHsgcGF0aDogYCR7bG9nRGlyfS9hcHAudHh0YCB9KTtcbmxvZ2dlci5hZGRUYXJnZXQoJ2NvbnNvbGUnKTtcblxuZXhwb3J0IGRlZmF1bHQgbG9nZ2VyO1xuIl19
|
14
Server/dist/app/utils/auth.js
vendored
14
Server/dist/app/utils/auth.js
vendored
@ -1,14 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var bcrypt = require('bcryptjs');
|
||||
function hashPassword(password) {
|
||||
var salt = bcrypt.genSaltSync(10);
|
||||
return bcrypt.hashSync(password, salt);
|
||||
}
|
||||
exports.hashPassword = hashPassword;
|
||||
function checkPassword(password, hash) {
|
||||
return bcrypt.compareSync(password, hash);
|
||||
}
|
||||
exports.checkPassword = checkPassword;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImF1dGgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxJQUFNLE1BQU0sR0FBRyxPQUFPLENBQUMsVUFBVSxDQUFDLENBQUM7QUFFbkMsU0FBZ0IsWUFBWSxDQUFDLFFBQWU7SUFDM0MsSUFBTSxJQUFJLEdBQUcsTUFBTSxDQUFDLFdBQVcsQ0FBQyxFQUFFLENBQUMsQ0FBQztJQUNwQyxPQUFPLE1BQU0sQ0FBQyxRQUFRLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxDQUFDO0FBQ3hDLENBQUM7QUFIRCxvQ0FHQztBQUVELFNBQWdCLGFBQWEsQ0FBQyxRQUFlLEVBQUUsSUFBVztJQUN6RCxPQUFPLE1BQU0sQ0FBQyxXQUFXLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxDQUFDO0FBQzNDLENBQUM7QUFGRCxzQ0FFQyIsImZpbGUiOiJhdXRoLmpzIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgYmNyeXB0ID0gcmVxdWlyZSgnYmNyeXB0anMnKTtcblxuZXhwb3J0IGZ1bmN0aW9uIGhhc2hQYXNzd29yZChwYXNzd29yZDpzdHJpbmcpOnN0cmluZyB7XG5cdGNvbnN0IHNhbHQgPSBiY3J5cHQuZ2VuU2FsdFN5bmMoMTApO1xuXHRyZXR1cm4gYmNyeXB0Lmhhc2hTeW5jKHBhc3N3b3JkLCBzYWx0KTtcbn1cblxuZXhwb3J0IGZ1bmN0aW9uIGNoZWNrUGFzc3dvcmQocGFzc3dvcmQ6c3RyaW5nLCBoYXNoOnN0cmluZyk6Ym9vbGVhbiB7XG5cdHJldHVybiBiY3J5cHQuY29tcGFyZVN5bmMocGFzc3dvcmQsIGhhc2gpO1xufVxuIl19
|
103
Server/dist/app/utils/cache.js
vendored
103
Server/dist/app/utils/cache.js
vendored
@ -1,103 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var Cache = /** @class */ (function () {
|
||||
function Cache() {
|
||||
this.cache = {};
|
||||
}
|
||||
Cache.prototype.setAny = function (key, o) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
this.cache[key] = {
|
||||
object: JSON.stringify(o),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
return [2 /*return*/];
|
||||
});
|
||||
});
|
||||
};
|
||||
Cache.prototype.setObject = function (key, object) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/, this.setAny(key, object)];
|
||||
});
|
||||
});
|
||||
};
|
||||
Cache.prototype.getAny = function (key) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
if (!this.cache[key])
|
||||
return [2 /*return*/, null];
|
||||
return [2 /*return*/, JSON.parse(this.cache[key].object)];
|
||||
});
|
||||
});
|
||||
};
|
||||
Cache.prototype.object = function (key) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
return [2 /*return*/, this.getAny(key)];
|
||||
});
|
||||
});
|
||||
};
|
||||
Cache.prototype.delete = function (key) {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
var keys, _i, keys_1, k;
|
||||
return __generator(this, function (_a) {
|
||||
keys = typeof key === 'string' ? [key] : key;
|
||||
for (_i = 0, keys_1 = keys; _i < keys_1.length; _i++) {
|
||||
k = keys_1[_i];
|
||||
delete this.cache[k];
|
||||
}
|
||||
return [2 /*return*/];
|
||||
});
|
||||
});
|
||||
};
|
||||
Cache.prototype.clearAll = function () {
|
||||
return __awaiter(this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
this.cache = {};
|
||||
return [2 /*return*/];
|
||||
});
|
||||
});
|
||||
};
|
||||
return Cache;
|
||||
}());
|
||||
var cache = new Cache();
|
||||
exports.default = cache;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImNhY2hlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBU0E7SUFBQTtRQUVDLFVBQUssR0FBZ0IsRUFBRSxDQUFDO0lBK0J6QixDQUFDO0lBN0JjLHNCQUFNLEdBQXBCLFVBQXFCLEdBQVUsRUFBRSxDQUFLOzs7Z0JBQ3JDLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLEdBQUc7b0JBQ2pCLE1BQU0sRUFBRSxJQUFJLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQztvQkFDekIsU0FBUyxFQUFFLElBQUksQ0FBQyxHQUFHLEVBQUU7aUJBQ3JCLENBQUM7Ozs7S0FDRjtJQUVLLHlCQUFTLEdBQWYsVUFBZ0IsR0FBVSxFQUFFLE1BQWE7OztnQkFDeEMsc0JBQU8sSUFBSSxDQUFDLE1BQU0sQ0FBQyxHQUFHLEVBQUUsTUFBTSxDQUFDLEVBQUM7OztLQUNoQztJQUVhLHNCQUFNLEdBQXBCLFVBQXFCLEdBQVU7OztnQkFDOUIsSUFBSSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDO29CQUFFLHNCQUFPLElBQUksRUFBQztnQkFDbEMsc0JBQU8sSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxFQUFDOzs7S0FDMUM7SUFFSyxzQkFBTSxHQUFaLFVBQWEsR0FBVTs7O2dCQUN0QixzQkFBTyxJQUFJLENBQUMsTUFBTSxDQUFDLEdBQUcsQ0FBVyxFQUFDOzs7S0FDbEM7SUFFSyxzQkFBTSxHQUFaLFVBQWEsR0FBcUI7Ozs7Z0JBQzNCLElBQUksR0FBRyxPQUFPLEdBQUcsS0FBSyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQztnQkFDbkQsV0FBb0IsRUFBSixhQUFJLEVBQUosa0JBQUksRUFBSixJQUFJO29CQUFULENBQUM7b0JBQVUsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQyxDQUFDO2lCQUFBOzs7O0tBQzNDO0lBRUssd0JBQVEsR0FBZDs7O2dCQUNDLElBQUksQ0FBQyxLQUFLLEdBQUcsRUFBRSxDQUFDOzs7O0tBQ2hCO0lBRUYsWUFBQztBQUFELENBakNBLEFBaUNDLElBQUE7QUFFRCxJQUFNLEtBQUssR0FBUyxJQUFJLEtBQUssRUFBRSxDQUFDO0FBRWhDLGtCQUFlLEtBQUssQ0FBQyIsImZpbGUiOiJjYWNoZS5qcyIsInNvdXJjZXNDb250ZW50IjpbImludGVyZmFjZSBDYWNoZUVudHJ5IHtcblx0b2JqZWN0OiBhbnksXG5cdHRpbWVzdGFtcDogbnVtYmVyLFxufVxuXG5pbnRlcmZhY2UgQ2FjaGVFbnRyaWVzIHtcblx0W2tleTogc3RyaW5nXTogQ2FjaGVFbnRyeSxcbn1cblxuY2xhc3MgQ2FjaGUge1xuXG5cdGNhY2hlOkNhY2hlRW50cmllcyA9IHt9O1xuXG5cdHByaXZhdGUgYXN5bmMgc2V0QW55KGtleTpzdHJpbmcsIG86YW55KTpQcm9taXNlPHZvaWQ+IHtcblx0XHR0aGlzLmNhY2hlW2tleV0gPSB7XG5cdFx0XHRvYmplY3Q6IEpTT04uc3RyaW5naWZ5KG8pLFxuXHRcdFx0dGltZXN0YW1wOiBEYXRlLm5vdygpLFxuXHRcdH07XG5cdH1cblxuXHRhc3luYyBzZXRPYmplY3Qoa2V5OnN0cmluZywgb2JqZWN0Ok9iamVjdCk6UHJvbWlzZTx2b2lkPiB7XG5cdFx0cmV0dXJuIHRoaXMuc2V0QW55KGtleSwgb2JqZWN0KTtcblx0fVxuXG5cdHByaXZhdGUgYXN5bmMgZ2V0QW55KGtleTpzdHJpbmcpOlByb21pc2U8YW55PiB7XG5cdFx0aWYgKCF0aGlzLmNhY2hlW2tleV0pIHJldHVybiBudWxsO1xuXHRcdHJldHVybiBKU09OLnBhcnNlKHRoaXMuY2FjaGVba2V5XS5vYmplY3QpO1xuXHR9XG5cblx0YXN5bmMgb2JqZWN0KGtleTpzdHJpbmcpOlByb21pc2U8b2JqZWN0PiB7XG5cdFx0cmV0dXJuIHRoaXMuZ2V0QW55KGtleSkgYXMgb2JqZWN0O1xuXHR9XG5cblx0YXN5bmMgZGVsZXRlKGtleTpzdHJpbmcgfCBzdHJpbmdbXSk6UHJvbWlzZTx2b2lkPiB7XG5cdFx0Y29uc3Qga2V5cyA9IHR5cGVvZiBrZXkgPT09ICdzdHJpbmcnID8gW2tleV0gOiBrZXk7XG5cdFx0Zm9yIChjb25zdCBrIG9mIGtleXMpIGRlbGV0ZSB0aGlzLmNhY2hlW2tdO1xuXHR9XG5cblx0YXN5bmMgY2xlYXJBbGwoKTpQcm9taXNlPHZvaWQ+IHtcblx0XHR0aGlzLmNhY2hlID0ge307XG5cdH1cblxufVxuXG5jb25zdCBjYWNoZTpDYWNoZSA9IG5ldyBDYWNoZSgpO1xuXG5leHBvcnQgZGVmYXVsdCBjYWNoZTtcbiJdfQ==
|
140
Server/dist/app/utils/dbUtils.js
vendored
140
Server/dist/app/utils/dbUtils.js
vendored
File diff suppressed because one or more lines are too long
85
Server/dist/app/utils/errors.js
vendored
85
Server/dist/app/utils/errors.js
vendored
@ -1,85 +0,0 @@
|
||||
"use strict";
|
||||
// For explanation of the setPrototypeOf call, see:
|
||||
// https://github.com/Microsoft/TypeScript-wiki/blob/master/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||
var __extends = (this && this.__extends) || (function () {
|
||||
var extendStatics = function (d, b) {
|
||||
extendStatics = Object.setPrototypeOf ||
|
||||
({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
|
||||
function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
|
||||
return extendStatics(d, b);
|
||||
};
|
||||
return function (d, b) {
|
||||
extendStatics(d, b);
|
||||
function __() { this.constructor = d; }
|
||||
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
|
||||
};
|
||||
})();
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var ApiError = /** @class */ (function (_super) {
|
||||
__extends(ApiError, _super);
|
||||
function ApiError(message, httpCode) {
|
||||
if (httpCode === void 0) { httpCode = 400; }
|
||||
var _this = _super.call(this, message) || this;
|
||||
_this.httpCode = httpCode;
|
||||
Object.setPrototypeOf(_this, ApiError.prototype);
|
||||
return _this;
|
||||
}
|
||||
return ApiError;
|
||||
}(Error));
|
||||
var ErrorMethodNotAllowed = /** @class */ (function (_super) {
|
||||
__extends(ErrorMethodNotAllowed, _super);
|
||||
function ErrorMethodNotAllowed(message) {
|
||||
if (message === void 0) { message = 'Method Not Allowed'; }
|
||||
var _this = _super.call(this, message, 405) || this;
|
||||
Object.setPrototypeOf(_this, ErrorMethodNotAllowed.prototype);
|
||||
return _this;
|
||||
}
|
||||
return ErrorMethodNotAllowed;
|
||||
}(ApiError));
|
||||
exports.ErrorMethodNotAllowed = ErrorMethodNotAllowed;
|
||||
var ErrorNotFound = /** @class */ (function (_super) {
|
||||
__extends(ErrorNotFound, _super);
|
||||
function ErrorNotFound(message) {
|
||||
if (message === void 0) { message = 'Not Found'; }
|
||||
var _this = _super.call(this, message, 404) || this;
|
||||
Object.setPrototypeOf(_this, ErrorNotFound.prototype);
|
||||
return _this;
|
||||
}
|
||||
return ErrorNotFound;
|
||||
}(ApiError));
|
||||
exports.ErrorNotFound = ErrorNotFound;
|
||||
var ErrorForbidden = /** @class */ (function (_super) {
|
||||
__extends(ErrorForbidden, _super);
|
||||
function ErrorForbidden(message) {
|
||||
if (message === void 0) { message = 'Forbidden'; }
|
||||
var _this = _super.call(this, message, 403) || this;
|
||||
Object.setPrototypeOf(_this, ErrorForbidden.prototype);
|
||||
return _this;
|
||||
}
|
||||
return ErrorForbidden;
|
||||
}(ApiError));
|
||||
exports.ErrorForbidden = ErrorForbidden;
|
||||
var ErrorBadRequest = /** @class */ (function (_super) {
|
||||
__extends(ErrorBadRequest, _super);
|
||||
function ErrorBadRequest(message) {
|
||||
if (message === void 0) { message = 'Bad Request'; }
|
||||
var _this = _super.call(this, message, 400) || this;
|
||||
Object.setPrototypeOf(_this, ErrorBadRequest.prototype);
|
||||
return _this;
|
||||
}
|
||||
return ErrorBadRequest;
|
||||
}(ApiError));
|
||||
exports.ErrorBadRequest = ErrorBadRequest;
|
||||
var ErrorUnprocessableEntity = /** @class */ (function (_super) {
|
||||
__extends(ErrorUnprocessableEntity, _super);
|
||||
function ErrorUnprocessableEntity(message) {
|
||||
if (message === void 0) { message = 'Unprocessable Entity'; }
|
||||
var _this = _super.call(this, message, 422) || this;
|
||||
Object.setPrototypeOf(_this, ErrorUnprocessableEntity.prototype);
|
||||
return _this;
|
||||
}
|
||||
return ErrorUnprocessableEntity;
|
||||
}(ApiError));
|
||||
exports.ErrorUnprocessableEntity = ErrorUnprocessableEntity;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImVycm9ycy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO0FBQUEsbURBQW1EO0FBQ25ELCtJQUErSTs7Ozs7Ozs7Ozs7Ozs7O0FBRS9JO0lBQXVCLDRCQUFLO0lBRTNCLGtCQUFZLE9BQWMsRUFBRSxRQUFxQjtRQUFyQix5QkFBQSxFQUFBLGNBQXFCO1FBQWpELFlBQ0Msa0JBQU0sT0FBTyxDQUFDLFNBR2Q7UUFGQSxLQUFJLENBQUMsUUFBUSxHQUFHLFFBQVEsQ0FBQztRQUN6QixNQUFNLENBQUMsY0FBYyxDQUFDLEtBQUksRUFBRSxRQUFRLENBQUMsU0FBUyxDQUFDLENBQUM7O0lBQ2pELENBQUM7SUFDRixlQUFDO0FBQUQsQ0FQQSxBQU9DLENBUHNCLEtBQUssR0FPM0I7QUFFRDtJQUEyQyx5Q0FBUTtJQUNsRCwrQkFBWSxPQUFxQztRQUFyQyx3QkFBQSxFQUFBLDhCQUFxQztRQUFqRCxZQUNDLGtCQUFNLE9BQU8sRUFBRSxHQUFHLENBQUMsU0FFbkI7UUFEQSxNQUFNLENBQUMsY0FBYyxDQUFDLEtBQUksRUFBRSxxQkFBcUIsQ0FBQyxTQUFTLENBQUMsQ0FBQzs7SUFDOUQsQ0FBQztJQUNGLDRCQUFDO0FBQUQsQ0FMQSxBQUtDLENBTDBDLFFBQVEsR0FLbEQ7QUFMWSxzREFBcUI7QUFPbEM7SUFBbUMsaUNBQVE7SUFDMUMsdUJBQVksT0FBNEI7UUFBNUIsd0JBQUEsRUFBQSxxQkFBNEI7UUFBeEMsWUFDQyxrQkFBTSxPQUFPLEVBQUUsR0FBRyxDQUFDLFNBRW5CO1FBREEsTUFBTSxDQUFDLGNBQWMsQ0FBQyxLQUFJLEVBQUUsYUFBYSxDQUFDLFNBQVMsQ0FBQyxDQUFDOztJQUN0RCxDQUFDO0lBQ0Ysb0JBQUM7QUFBRCxDQUxBLEFBS0MsQ0FMa0MsUUFBUSxHQUsxQztBQUxZLHNDQUFhO0FBTzFCO0lBQW9DLGtDQUFRO0lBQzNDLHdCQUFZLE9BQTRCO1FBQTVCLHdCQUFBLEVBQUEscUJBQTRCO1FBQXhDLFlBQ0Msa0JBQU0sT0FBTyxFQUFFLEdBQUcsQ0FBQyxTQUVuQjtRQURBLE1BQU0sQ0FBQyxjQUFjLENBQUMsS0FBSSxFQUFFLGNBQWMsQ0FBQyxTQUFTLENBQUMsQ0FBQzs7SUFDdkQsQ0FBQztJQUNGLHFCQUFDO0FBQUQsQ0FMQSxBQUtDLENBTG1DLFFBQVEsR0FLM0M7QUFMWSx3Q0FBYztBQU8zQjtJQUFxQyxtQ0FBUTtJQUM1Qyx5QkFBWSxPQUE4QjtRQUE5Qix3QkFBQSxFQUFBLHVCQUE4QjtRQUExQyxZQUNDLGtCQUFNLE9BQU8sRUFBRSxHQUFHLENBQUMsU0FFbkI7UUFEQSxNQUFNLENBQUMsY0FBYyxDQUFDLEtBQUksRUFBRSxlQUFlLENBQUMsU0FBUyxDQUFDLENBQUM7O0lBQ3hELENBQUM7SUFFRixzQkFBQztBQUFELENBTkEsQUFNQyxDQU5vQyxRQUFRLEdBTTVDO0FBTlksMENBQWU7QUFRNUI7SUFBOEMsNENBQVE7SUFDckQsa0NBQVksT0FBdUM7UUFBdkMsd0JBQUEsRUFBQSxnQ0FBdUM7UUFBbkQsWUFDQyxrQkFBTSxPQUFPLEVBQUUsR0FBRyxDQUFDLFNBRW5CO1FBREEsTUFBTSxDQUFDLGNBQWMsQ0FBQyxLQUFJLEVBQUUsd0JBQXdCLENBQUMsU0FBUyxDQUFDLENBQUM7O0lBQ2pFLENBQUM7SUFDRiwrQkFBQztBQUFELENBTEEsQUFLQyxDQUw2QyxRQUFRLEdBS3JEO0FBTFksNERBQXdCIiwiZmlsZSI6ImVycm9ycy5qcyIsInNvdXJjZXNDb250ZW50IjpbIi8vIEZvciBleHBsYW5hdGlvbiBvZiB0aGUgc2V0UHJvdG90eXBlT2YgY2FsbCwgc2VlOlxuLy8gaHR0cHM6Ly9naXRodWIuY29tL01pY3Jvc29mdC9UeXBlU2NyaXB0LXdpa2kvYmxvYi9tYXN0ZXIvQnJlYWtpbmctQ2hhbmdlcy5tZCNleHRlbmRpbmctYnVpbHQtaW5zLWxpa2UtZXJyb3ItYXJyYXktYW5kLW1hcC1tYXktbm8tbG9uZ2VyLXdvcmtcblxuY2xhc3MgQXBpRXJyb3IgZXh0ZW5kcyBFcnJvciB7XG5cdGh0dHBDb2RlOiBudW1iZXJcblx0Y29uc3RydWN0b3IobWVzc2FnZTpzdHJpbmcsIGh0dHBDb2RlOm51bWJlciA9IDQwMCkge1xuXHRcdHN1cGVyKG1lc3NhZ2UpO1xuXHRcdHRoaXMuaHR0cENvZGUgPSBodHRwQ29kZTtcblx0XHRPYmplY3Quc2V0UHJvdG90eXBlT2YodGhpcywgQXBpRXJyb3IucHJvdG90eXBlKTtcblx0fVxufVxuXG5leHBvcnQgY2xhc3MgRXJyb3JNZXRob2ROb3RBbGxvd2VkIGV4dGVuZHMgQXBpRXJyb3Ige1xuXHRjb25zdHJ1Y3RvcihtZXNzYWdlOnN0cmluZyA9ICdNZXRob2QgTm90IEFsbG93ZWQnKSB7XG5cdFx0c3VwZXIobWVzc2FnZSwgNDA1KTtcblx0XHRPYmplY3Quc2V0UHJvdG90eXBlT2YodGhpcywgRXJyb3JNZXRob2ROb3RBbGxvd2VkLnByb3RvdHlwZSk7XG5cdH1cbn1cblxuZXhwb3J0IGNsYXNzIEVycm9yTm90Rm91bmQgZXh0ZW5kcyBBcGlFcnJvciB7XG5cdGNvbnN0cnVjdG9yKG1lc3NhZ2U6c3RyaW5nID0gJ05vdCBGb3VuZCcpIHtcblx0XHRzdXBlcihtZXNzYWdlLCA0MDQpO1xuXHRcdE9iamVjdC5zZXRQcm90b3R5cGVPZih0aGlzLCBFcnJvck5vdEZvdW5kLnByb3RvdHlwZSk7XG5cdH1cbn1cblxuZXhwb3J0IGNsYXNzIEVycm9yRm9yYmlkZGVuIGV4dGVuZHMgQXBpRXJyb3Ige1xuXHRjb25zdHJ1Y3RvcihtZXNzYWdlOnN0cmluZyA9ICdGb3JiaWRkZW4nKSB7XG5cdFx0c3VwZXIobWVzc2FnZSwgNDAzKTtcblx0XHRPYmplY3Quc2V0UHJvdG90eXBlT2YodGhpcywgRXJyb3JGb3JiaWRkZW4ucHJvdG90eXBlKTtcblx0fVxufVxuXG5leHBvcnQgY2xhc3MgRXJyb3JCYWRSZXF1ZXN0IGV4dGVuZHMgQXBpRXJyb3Ige1xuXHRjb25zdHJ1Y3RvcihtZXNzYWdlOnN0cmluZyA9ICdCYWQgUmVxdWVzdCcpIHtcblx0XHRzdXBlcihtZXNzYWdlLCA0MDApO1xuXHRcdE9iamVjdC5zZXRQcm90b3R5cGVPZih0aGlzLCBFcnJvckJhZFJlcXVlc3QucHJvdG90eXBlKTtcblx0fVxuXG59XG5cbmV4cG9ydCBjbGFzcyBFcnJvclVucHJvY2Vzc2FibGVFbnRpdHkgZXh0ZW5kcyBBcGlFcnJvciB7XG5cdGNvbnN0cnVjdG9yKG1lc3NhZ2U6c3RyaW5nID0gJ1VucHJvY2Vzc2FibGUgRW50aXR5Jykge1xuXHRcdHN1cGVyKG1lc3NhZ2UsIDQyMik7XG5cdFx0T2JqZWN0LnNldFByb3RvdHlwZU9mKHRoaXMsIEVycm9yVW5wcm9jZXNzYWJsZUVudGl0eS5wcm90b3R5cGUpO1xuXHR9XG59XG4iXX0=
|
68
Server/dist/app/utils/koaIf.js
vendored
68
Server/dist/app/utils/koaIf.js
vendored
@ -1,68 +0,0 @@
|
||||
"use strict";
|
||||
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
||||
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
||||
return new (P || (P = Promise))(function (resolve, reject) {
|
||||
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
||||
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
||||
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
||||
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
||||
});
|
||||
};
|
||||
var __generator = (this && this.__generator) || function (thisArg, body) {
|
||||
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
||||
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
||||
function verb(n) { return function (v) { return step([n, v]); }; }
|
||||
function step(op) {
|
||||
if (f) throw new TypeError("Generator is already executing.");
|
||||
while (_) try {
|
||||
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
||||
if (y = 0, t) op = [op[0] & 2, t.value];
|
||||
switch (op[0]) {
|
||||
case 0: case 1: t = op; break;
|
||||
case 4: _.label++; return { value: op[1], done: false };
|
||||
case 5: _.label++; y = op[1]; op = [0]; continue;
|
||||
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
||||
default:
|
||||
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
||||
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
||||
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
||||
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
||||
if (t[2]) _.ops.pop();
|
||||
_.trys.pop(); continue;
|
||||
}
|
||||
op = body.call(thisArg, _);
|
||||
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
||||
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
||||
}
|
||||
};
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function koaIf(middleware, condition) {
|
||||
var _this = this;
|
||||
if (condition === void 0) { condition = null; }
|
||||
return function (ctx, next) { return __awaiter(_this, void 0, void 0, function () {
|
||||
return __generator(this, function (_a) {
|
||||
switch (_a.label) {
|
||||
case 0:
|
||||
if (!(typeof condition === 'function' && condition(ctx))) return [3 /*break*/, 2];
|
||||
return [4 /*yield*/, middleware(ctx, next)];
|
||||
case 1:
|
||||
_a.sent();
|
||||
return [3 /*break*/, 6];
|
||||
case 2:
|
||||
if (!(typeof condition === 'boolean' && condition)) return [3 /*break*/, 4];
|
||||
return [4 /*yield*/, middleware(ctx, next)];
|
||||
case 3:
|
||||
_a.sent();
|
||||
return [3 /*break*/, 6];
|
||||
case 4: return [4 /*yield*/, next()];
|
||||
case 5:
|
||||
_a.sent();
|
||||
_a.label = 6;
|
||||
case 6: return [2 /*return*/];
|
||||
}
|
||||
});
|
||||
}); };
|
||||
}
|
||||
exports.default = koaIf;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImtvYUlmLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBRUEsU0FBd0IsS0FBSyxDQUFDLFVBQW1CLEVBQUUsU0FBa0I7SUFBckUsaUJBVUM7SUFWa0QsMEJBQUEsRUFBQSxnQkFBa0I7SUFDcEUsT0FBTyxVQUFPLEdBQVcsRUFBRSxJQUFhOzs7O3lCQUNuQyxDQUFBLE9BQU8sU0FBUyxLQUFLLFVBQVUsSUFBSSxTQUFTLENBQUMsR0FBRyxDQUFDLENBQUEsRUFBakQsd0JBQWlEO29CQUNwRCxxQkFBTSxVQUFVLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxFQUFBOztvQkFBM0IsU0FBMkIsQ0FBQzs7O3lCQUNsQixDQUFBLE9BQU8sU0FBUyxLQUFLLFNBQVMsSUFBSSxTQUFTLENBQUEsRUFBM0Msd0JBQTJDO29CQUNyRCxxQkFBTSxVQUFVLENBQUMsR0FBRyxFQUFFLElBQUksQ0FBQyxFQUFBOztvQkFBM0IsU0FBMkIsQ0FBQzs7d0JBRTVCLHFCQUFNLElBQUksRUFBRSxFQUFBOztvQkFBWixTQUFZLENBQUM7Ozs7O1NBRWQsQ0FBQztBQUNILENBQUM7QUFWRCx3QkFVQyIsImZpbGUiOiJrb2FJZi5qcyIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IENvbnRleHQgfSBmcm9tICdrb2EnO1xuXG5leHBvcnQgZGVmYXVsdCBmdW5jdGlvbiBrb2FJZihtaWRkbGV3YXJlOkZ1bmN0aW9uLCBjb25kaXRpb246YW55PW51bGwpIHtcblx0cmV0dXJuIGFzeW5jIChjdHg6Q29udGV4dCwgbmV4dDpGdW5jdGlvbikgPT4ge1xuXHRcdGlmICh0eXBlb2YgY29uZGl0aW9uID09PSAnZnVuY3Rpb24nICYmIGNvbmRpdGlvbihjdHgpKSB7XG5cdFx0XHRhd2FpdCBtaWRkbGV3YXJlKGN0eCwgbmV4dCk7XG5cdFx0fSBlbHNlIGlmICh0eXBlb2YgY29uZGl0aW9uID09PSAnYm9vbGVhbicgJiYgY29uZGl0aW9uKSB7XG5cdFx0XHRhd2FpdCBtaWRkbGV3YXJlKGN0eCwgbmV4dCk7XG5cdFx0fSBlbHNlIHtcblx0XHRcdGF3YWl0IG5leHQoKTtcblx0XHR9XG5cdH07XG59XG4iXX0=
|
8
Server/dist/app/utils/requestUtils.js
vendored
8
Server/dist/app/utils/requestUtils.js
vendored
@ -1,8 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
function sessionIdFromHeaders(headers) {
|
||||
return headers['x-api-auth'] ? headers['x-api-auth'] : '';
|
||||
}
|
||||
exports.sessionIdFromHeaders = sessionIdFromHeaders;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInJlcXVlc3RVdGlscy50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOztBQUFBLFNBQWdCLG9CQUFvQixDQUFDLE9BQVc7SUFDL0MsT0FBTyxPQUFPLENBQUMsWUFBWSxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxZQUFZLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDO0FBQzNELENBQUM7QUFGRCxvREFFQyIsImZpbGUiOiJyZXF1ZXN0VXRpbHMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgZnVuY3Rpb24gc2Vzc2lvbklkRnJvbUhlYWRlcnMoaGVhZGVyczphbnkpOnN0cmluZyB7XG5cdHJldHVybiBoZWFkZXJzWyd4LWFwaS1hdXRoJ10gPyBoZWFkZXJzWyd4LWFwaS1hdXRoJ10gOiAnJztcbn1cbiJdfQ==
|
97
Server/dist/app/utils/routeUtils.js
vendored
97
Server/dist/app/utils/routeUtils.js
vendored
File diff suppressed because one or more lines are too long
9
Server/dist/app/utils/uuidgen.js
vendored
9
Server/dist/app/utils/uuidgen.js
vendored
@ -1,9 +0,0 @@
|
||||
"use strict";
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
var generate = require('nanoid/generate');
|
||||
function uuidgen() {
|
||||
return generate('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', 22);
|
||||
}
|
||||
exports.default = uuidgen;
|
||||
|
||||
//# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInV1aWRnZW4udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxJQUFNLFFBQVEsR0FBRyxPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBQztBQUU1QyxTQUF3QixPQUFPO0lBQzlCLE9BQU8sUUFBUSxDQUFDLGdFQUFnRSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3ZGLENBQUM7QUFGRCwwQkFFQyIsImZpbGUiOiJ1dWlkZ2VuLmpzIiwic291cmNlc0NvbnRlbnQiOlsiY29uc3QgZ2VuZXJhdGUgPSByZXF1aXJlKCduYW5vaWQvZ2VuZXJhdGUnKTtcblxuZXhwb3J0IGRlZmF1bHQgZnVuY3Rpb24gdXVpZGdlbigpOnN0cmluZyB7XG5cdHJldHVybiBnZW5lcmF0ZSgnMDEyMzQ1Njc4OUFCQ0RFRkdISUpLTE1OT1BRUlNUVVZXWFlaYWJjZGVmZ2hpamtsbW5vcHFyc3R1dnd4eXonLCAyMik7XG59XG4iXX0=
|
60
Server/dist/lib/ArrayUtils.js
vendored
60
Server/dist/lib/ArrayUtils.js
vendored
@ -1,60 +0,0 @@
|
||||
const ArrayUtils = {};
|
||||
|
||||
ArrayUtils.unique = function(array) {
|
||||
return array.filter(function(elem, index, self) {
|
||||
return index === self.indexOf(elem);
|
||||
});
|
||||
};
|
||||
|
||||
ArrayUtils.removeElement = function(array, element) {
|
||||
const index = array.indexOf(element);
|
||||
if (index < 0) return array;
|
||||
array.splice(index, 1);
|
||||
return array;
|
||||
};
|
||||
|
||||
// https://stackoverflow.com/a/10264318/561309
|
||||
ArrayUtils.binarySearch = function(items, value) {
|
||||
var startIndex = 0,
|
||||
stopIndex = items.length - 1,
|
||||
middle = Math.floor((stopIndex + startIndex) / 2);
|
||||
|
||||
while (items[middle] != value && startIndex < stopIndex) {
|
||||
//adjust search area
|
||||
if (value < items[middle]) {
|
||||
stopIndex = middle - 1;
|
||||
} else if (value > items[middle]) {
|
||||
startIndex = middle + 1;
|
||||
}
|
||||
|
||||
//recalculate middle
|
||||
middle = Math.floor((stopIndex + startIndex) / 2);
|
||||
}
|
||||
|
||||
//make sure it's the right value
|
||||
return items[middle] != value ? -1 : middle;
|
||||
};
|
||||
|
||||
ArrayUtils.findByKey = function(array, key, value) {
|
||||
for (let i = 0; i < array.length; i++) {
|
||||
const o = array[i];
|
||||
if (typeof o !== 'object') continue;
|
||||
if (o[key] === value) return o;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
ArrayUtils.contentEquals = function(array1, array2) {
|
||||
if (array1 === array2) return true;
|
||||
if (!array1.length && !array2.length) return true;
|
||||
if (array1.length !== array2.length) return false;
|
||||
|
||||
for (let i = 0; i < array1.length; i++) {
|
||||
const a1 = array1[i];
|
||||
if (array2.indexOf(a1) < 0) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
module.exports = ArrayUtils;
|
665
Server/dist/lib/BaseApplication.js
vendored
665
Server/dist/lib/BaseApplication.js
vendored
@ -1,665 +0,0 @@
|
||||
const { createStore, applyMiddleware } = require('redux');
|
||||
const { reducer, defaultState, stateUtils } = require('lib/reducer.js');
|
||||
const { JoplinDatabase } = require('lib/joplin-database.js');
|
||||
const { FoldersScreenUtils } = require('lib/folders-screen-utils.js');
|
||||
const { DatabaseDriverNode } = require('lib/database-driver-node.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { splitCommandString } = require('lib/string-utils.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const { _, setLocale } = require('lib/locale.js');
|
||||
const reduxSharedMiddleware = require('lib/components/shared/reduxSharedMiddleware');
|
||||
const os = require('os');
|
||||
const fs = require('fs-extra');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const EventEmitter = require('events');
|
||||
const syswidecas = require('syswide-cas');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry.js');
|
||||
const SyncTargetFilesystem = require('lib/SyncTargetFilesystem.js');
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const SyncTargetOneDriveDev = require('lib/SyncTargetOneDriveDev.js');
|
||||
const SyncTargetNextcloud = require('lib/SyncTargetNextcloud.js');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV.js');
|
||||
const SyncTargetDropbox = require('lib/SyncTargetDropbox.js');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const SearchEngineUtils = require('lib/services/SearchEngineUtils');
|
||||
const RevisionService = require('lib/services/RevisionService');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker');
|
||||
const BaseService = require('lib/services/BaseService');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
const KvStore = require('lib/services/KvStore');
|
||||
const MigrationService = require('lib/services/MigrationService');
|
||||
|
||||
SyncTargetRegistry.addClass(SyncTargetFilesystem);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDrive);
|
||||
SyncTargetRegistry.addClass(SyncTargetOneDriveDev);
|
||||
SyncTargetRegistry.addClass(SyncTargetNextcloud);
|
||||
SyncTargetRegistry.addClass(SyncTargetWebDAV);
|
||||
SyncTargetRegistry.addClass(SyncTargetDropbox);
|
||||
|
||||
class BaseApplication {
|
||||
constructor() {
|
||||
this.logger_ = new Logger();
|
||||
this.dbLogger_ = new Logger();
|
||||
this.eventEmitter_ = new EventEmitter();
|
||||
|
||||
// Note: this is basically a cache of state.selectedFolderId. It should *only*
|
||||
// be derived from the state and not set directly since that would make the
|
||||
// state and UI out of sync.
|
||||
this.currentFolder_ = null;
|
||||
|
||||
this.decryptionWorker_resourceMetadataButNotBlobDecrypted = this.decryptionWorker_resourceMetadataButNotBlobDecrypted.bind(this);
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
store() {
|
||||
return this.store_;
|
||||
}
|
||||
|
||||
currentFolder() {
|
||||
return this.currentFolder_;
|
||||
}
|
||||
|
||||
async refreshCurrentFolder() {
|
||||
let newFolder = null;
|
||||
|
||||
if (this.currentFolder_) newFolder = await Folder.load(this.currentFolder_.id);
|
||||
if (!newFolder) newFolder = await Folder.defaultFolder();
|
||||
|
||||
this.switchCurrentFolder(newFolder);
|
||||
}
|
||||
|
||||
switchCurrentFolder(folder) {
|
||||
if (!this.hasGui()) {
|
||||
this.currentFolder_ = Object.assign({}, folder);
|
||||
Setting.setValue('activeFolderId', folder ? folder.id : '');
|
||||
} else {
|
||||
this.dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: folder ? folder.id : '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Handles the initial flags passed to main script and
|
||||
// returns the remaining args.
|
||||
async handleStartFlags_(argv, setDefaults = true) {
|
||||
let matched = {};
|
||||
argv = argv.slice(0);
|
||||
argv.splice(0, 2); // First arguments are the node executable, and the node JS file
|
||||
|
||||
while (argv.length) {
|
||||
let arg = argv[0];
|
||||
let nextArg = argv.length >= 2 ? argv[1] : null;
|
||||
|
||||
if (arg == '--profile') {
|
||||
if (!nextArg) throw new JoplinError(_('Usage: %s', '--profile <dir-path>'), 'flagError');
|
||||
matched.profileDir = nextArg;
|
||||
argv.splice(0, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--no-welcome') {
|
||||
matched.welcomeDisabled = true;
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--env') {
|
||||
if (!nextArg) throw new JoplinError(_('Usage: %s', '--env <dev|prod>'), 'flagError');
|
||||
matched.env = nextArg;
|
||||
argv.splice(0, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--is-demo') {
|
||||
Setting.setConstant('isDemo', true);
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--open-dev-tools') {
|
||||
Setting.setConstant('openDevTools', true);
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--update-geolocation-disabled') {
|
||||
Note.updateGeolocationEnabled_ = false;
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--stack-trace-enabled') {
|
||||
this.showStackTraces_ = true;
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg == '--log-level') {
|
||||
if (!nextArg) throw new JoplinError(_('Usage: %s', '--log-level <none|error|warn|info|debug>'), 'flagError');
|
||||
matched.logLevel = Logger.levelStringToId(nextArg);
|
||||
argv.splice(0, 2);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.indexOf('-psn') === 0) {
|
||||
// Some weird flag passed by macOS - can be ignored.
|
||||
// https://github.com/laurent22/joplin/issues/480
|
||||
// https://stackoverflow.com/questions/10242115
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg === '--enable-logging') {
|
||||
// Electron-specific flag used for debugging - ignore it
|
||||
argv.splice(0, 1);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (arg.length && arg[0] == '-') {
|
||||
throw new JoplinError(_('Unknown flag: %s', arg), 'flagError');
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (setDefaults) {
|
||||
if (!matched.logLevel) matched.logLevel = Logger.LEVEL_INFO;
|
||||
if (!matched.env) matched.env = 'prod';
|
||||
}
|
||||
|
||||
return {
|
||||
matched: matched,
|
||||
argv: argv,
|
||||
};
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
return this.eventEmitter_.on(eventName, callback);
|
||||
}
|
||||
|
||||
async exit(code = 0) {
|
||||
await Setting.saveAll();
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
async refreshNotes(state, useSelectedNoteId = false, noteHash = '') {
|
||||
let parentType = state.notesParentType;
|
||||
let parentId = null;
|
||||
|
||||
if (parentType === 'Folder') {
|
||||
parentId = state.selectedFolderId;
|
||||
parentType = BaseModel.TYPE_FOLDER;
|
||||
} else if (parentType === 'Tag') {
|
||||
parentId = state.selectedTagId;
|
||||
parentType = BaseModel.TYPE_TAG;
|
||||
} else if (parentType === 'Search') {
|
||||
parentId = state.selectedSearchId;
|
||||
parentType = BaseModel.TYPE_SEARCH;
|
||||
}
|
||||
|
||||
this.logger().debug('Refreshing notes:', parentType, parentId);
|
||||
|
||||
let options = {
|
||||
order: stateUtils.notesOrder(state.settings),
|
||||
uncompletedTodosOnTop: Setting.value('uncompletedTodosOnTop'),
|
||||
showCompletedTodos: Setting.value('showCompletedTodos'),
|
||||
caseInsensitive: true,
|
||||
};
|
||||
|
||||
const source = JSON.stringify({
|
||||
options: options,
|
||||
parentId: parentId,
|
||||
});
|
||||
|
||||
let notes = [];
|
||||
|
||||
if (parentId) {
|
||||
if (parentType === Folder.modelType()) {
|
||||
notes = await Note.previews(parentId, options);
|
||||
} else if (parentType === Tag.modelType()) {
|
||||
notes = await Tag.notes(parentId, options);
|
||||
} else if (parentType === BaseModel.TYPE_SEARCH) {
|
||||
const search = BaseModel.byId(state.searches, parentId);
|
||||
notes = await SearchEngineUtils.notesForQuery(search.query_pattern);
|
||||
}
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_UPDATE_ALL',
|
||||
notes: notes,
|
||||
notesSource: source,
|
||||
});
|
||||
|
||||
if (useSelectedNoteId) {
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: state.selectedNoteIds && state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
hash: noteHash,
|
||||
});
|
||||
} else {
|
||||
const lastSelectedNoteIds = stateUtils.lastSelectedNoteIds(state);
|
||||
const foundIds = [];
|
||||
for (let i = 0; i < lastSelectedNoteIds.length; i++) {
|
||||
const noteId = lastSelectedNoteIds[i];
|
||||
let found = false;
|
||||
for (let j = 0; j < notes.length; j++) {
|
||||
if (notes[j].id === noteId) {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (found) foundIds.push(noteId);
|
||||
}
|
||||
|
||||
let selectedNoteId = null;
|
||||
if (foundIds.length) {
|
||||
selectedNoteId = foundIds[0];
|
||||
} else {
|
||||
selectedNoteId = notes.length ? notes[0].id : null;
|
||||
}
|
||||
|
||||
this.store().dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: selectedNoteId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resourceFetcher_downloadComplete(event) {
|
||||
if (event.encrypted) {
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
}
|
||||
}
|
||||
|
||||
async decryptionWorker_resourceMetadataButNotBlobDecrypted() {
|
||||
this.scheduleAutoAddResources();
|
||||
}
|
||||
|
||||
scheduleAutoAddResources() {
|
||||
if (this.scheduleAutoAddResourcesIID_) return;
|
||||
|
||||
this.scheduleAutoAddResourcesIID_ = setTimeout(() => {
|
||||
this.scheduleAutoAddResourcesIID_ = null;
|
||||
ResourceFetcher.instance().autoAddResources();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
reducerActionToString(action) {
|
||||
let o = [action.type];
|
||||
if ('id' in action) o.push(action.id);
|
||||
if ('noteId' in action) o.push(action.noteId);
|
||||
if ('folderId' in action) o.push(action.folderId);
|
||||
if ('tagId' in action) o.push(action.tagId);
|
||||
if ('tag' in action) o.push(action.tag.id);
|
||||
if ('folder' in action) o.push(action.folder.id);
|
||||
if ('notesSource' in action) o.push(JSON.stringify(action.notesSource));
|
||||
return o.join(', ');
|
||||
}
|
||||
|
||||
hasGui() {
|
||||
return false;
|
||||
}
|
||||
|
||||
uiType() {
|
||||
return this.hasGui() ? 'gui' : 'cli';
|
||||
}
|
||||
|
||||
generalMiddlewareFn() {
|
||||
const middleware = store => next => action => {
|
||||
return this.generalMiddleware(store, next, action);
|
||||
};
|
||||
|
||||
return middleware;
|
||||
}
|
||||
|
||||
async applySettingsSideEffects(action = null) {
|
||||
const sideEffects = {
|
||||
'dateFormat': async () => {
|
||||
time.setLocale(Setting.value('locale'));
|
||||
time.setDateFormat(Setting.value('dateFormat'));
|
||||
time.setTimeFormat(Setting.value('timeFormat'));
|
||||
},
|
||||
'net.ignoreTlsErrors': async () => {
|
||||
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = Setting.value('net.ignoreTlsErrors') ? '0' : '1';
|
||||
},
|
||||
'net.customCertificates': async () => {
|
||||
const caPaths = Setting.value('net.customCertificates').split(',');
|
||||
for (let i = 0; i < caPaths.length; i++) {
|
||||
const f = caPaths[i].trim();
|
||||
if (!f) continue;
|
||||
syswidecas.addCAs(f);
|
||||
}
|
||||
},
|
||||
'encryption.enabled': async () => {
|
||||
if (this.hasGui()) {
|
||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
const loadedMasterKeyIds = EncryptionService.instance().loadedMasterKeyIds();
|
||||
|
||||
this.dispatch({
|
||||
type: 'MASTERKEY_REMOVE_NOT_LOADED',
|
||||
ids: loadedMasterKeyIds,
|
||||
});
|
||||
|
||||
// Schedule a sync operation so that items that need to be encrypted
|
||||
// are sent to sync target.
|
||||
reg.scheduleSync();
|
||||
}
|
||||
},
|
||||
'sync.interval': async () => {
|
||||
if (this.hasGui()) reg.setupRecurrentSync();
|
||||
},
|
||||
};
|
||||
|
||||
sideEffects['timeFormat'] = sideEffects['dateFormat'];
|
||||
sideEffects['locale'] = sideEffects['dateFormat'];
|
||||
sideEffects['encryption.activeMasterKeyId'] = sideEffects['encryption.enabled'];
|
||||
sideEffects['encryption.passwordCache'] = sideEffects['encryption.enabled'];
|
||||
|
||||
if (action) {
|
||||
const effect = sideEffects[action.key];
|
||||
if (effect) await effect();
|
||||
} else {
|
||||
for (const key in sideEffects) {
|
||||
await sideEffects[key]();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async generalMiddleware(store, next, action) {
|
||||
// this.logger().debug('Reducer action', this.reducerActionToString(action));
|
||||
|
||||
const result = next(action);
|
||||
const newState = store.getState();
|
||||
let refreshNotes = false;
|
||||
let refreshFolders = false;
|
||||
// let refreshTags = false;
|
||||
let refreshNotesUseSelectedNoteId = false;
|
||||
let refreshNotesHash = '';
|
||||
|
||||
await reduxSharedMiddleware(store, next, action);
|
||||
|
||||
if (this.hasGui() && ['NOTE_UPDATE_ONE', 'NOTE_DELETE', 'FOLDER_UPDATE_ONE', 'FOLDER_DELETE'].indexOf(action.type) >= 0) {
|
||||
if (!(await reg.syncTarget().syncStarted())) reg.scheduleSync(30 * 1000, { syncSteps: ['update_remote', 'delete_remote'] });
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
}
|
||||
|
||||
// Don't add FOLDER_UPDATE_ALL as refreshFolders() is calling it too, which
|
||||
// would cause the sidebar to refresh all the time.
|
||||
if (this.hasGui() && ['FOLDER_UPDATE_ONE'].indexOf(action.type) >= 0) {
|
||||
refreshFolders = true;
|
||||
}
|
||||
|
||||
if (action.type == 'FOLDER_SELECT' || action.type === 'FOLDER_DELETE' || action.type === 'FOLDER_AND_NOTE_SELECT' || (action.type === 'SEARCH_UPDATE' && newState.notesParentType === 'Folder')) {
|
||||
Setting.setValue('activeFolderId', newState.selectedFolderId);
|
||||
this.currentFolder_ = newState.selectedFolderId ? await Folder.load(newState.selectedFolderId) : null;
|
||||
refreshNotes = true;
|
||||
|
||||
if (action.type === 'FOLDER_AND_NOTE_SELECT') {
|
||||
refreshNotesUseSelectedNoteId = true;
|
||||
refreshNotesHash = action.hash;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'uncompletedTodosOnTop') || action.type == 'SETTING_UPDATE_ALL')) {
|
||||
refreshNotes = true;
|
||||
}
|
||||
|
||||
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key == 'showCompletedTodos') || action.type == 'SETTING_UPDATE_ALL')) {
|
||||
refreshNotes = true;
|
||||
}
|
||||
|
||||
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('notes.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
|
||||
refreshNotes = true;
|
||||
}
|
||||
|
||||
if (action.type == 'TAG_SELECT' || action.type === 'TAG_DELETE') {
|
||||
refreshNotes = true;
|
||||
}
|
||||
|
||||
if (action.type == 'SEARCH_SELECT' || action.type === 'SEARCH_DELETE') {
|
||||
refreshNotes = true;
|
||||
}
|
||||
|
||||
if (refreshNotes) {
|
||||
await this.refreshNotes(newState, refreshNotesUseSelectedNoteId, refreshNotesHash);
|
||||
}
|
||||
|
||||
if (action.type === 'NOTE_UPDATE_ONE') {
|
||||
refreshFolders = true;
|
||||
}
|
||||
|
||||
if (this.hasGui() && ((action.type == 'SETTING_UPDATE_ONE' && action.key.indexOf('folders.sortOrder') === 0) || action.type == 'SETTING_UPDATE_ALL')) {
|
||||
refreshFolders = 'now';
|
||||
}
|
||||
|
||||
if (this.hasGui() && action.type === 'SYNC_GOT_ENCRYPTED_ITEM') {
|
||||
DecryptionWorker.instance().scheduleStart();
|
||||
}
|
||||
|
||||
if (this.hasGui() && action.type === 'SYNC_CREATED_RESOURCE') {
|
||||
ResourceFetcher.instance().autoAddResources();
|
||||
}
|
||||
|
||||
if (action.type == 'SETTING_UPDATE_ONE') {
|
||||
await this.applySettingsSideEffects(action);
|
||||
} else if (action.type == 'SETTING_UPDATE_ALL') {
|
||||
await this.applySettingsSideEffects();
|
||||
}
|
||||
|
||||
if (refreshFolders) {
|
||||
if (refreshFolders === 'now') {
|
||||
await FoldersScreenUtils.refreshFolders();
|
||||
} else {
|
||||
await FoldersScreenUtils.scheduleRefreshFolders();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
dispatch(action) {
|
||||
if (this.store()) return this.store().dispatch(action);
|
||||
}
|
||||
|
||||
reducer(state = defaultState, action) {
|
||||
return reducer(state, action);
|
||||
}
|
||||
|
||||
initRedux() {
|
||||
this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddlewareFn()));
|
||||
BaseModel.dispatch = this.store().dispatch;
|
||||
FoldersScreenUtils.dispatch = this.store().dispatch;
|
||||
reg.dispatch = this.store().dispatch;
|
||||
BaseSyncTarget.dispatch = this.store().dispatch;
|
||||
DecryptionWorker.instance().dispatch = this.store().dispatch;
|
||||
ResourceFetcher.instance().dispatch = this.store().dispatch;
|
||||
}
|
||||
|
||||
async readFlagsFromFile(flagPath) {
|
||||
if (!fs.existsSync(flagPath)) return {};
|
||||
let flagContent = fs.readFileSync(flagPath, 'utf8');
|
||||
if (!flagContent) return {};
|
||||
|
||||
flagContent = flagContent.trim();
|
||||
|
||||
let flags = splitCommandString(flagContent);
|
||||
flags.splice(0, 0, 'cmd');
|
||||
flags.splice(0, 0, 'node');
|
||||
|
||||
flags = await this.handleStartFlags_(flags, false);
|
||||
|
||||
return flags.matched;
|
||||
}
|
||||
|
||||
determineProfileDir(initArgs) {
|
||||
if (initArgs.profileDir) return initArgs.profileDir;
|
||||
|
||||
if (process && process.env && process.env.PORTABLE_EXECUTABLE_DIR) return `${process.env.PORTABLE_EXECUTABLE_DIR}/JoplinProfile`;
|
||||
|
||||
return `${os.homedir()}/.config/${Setting.value('appName')}`;
|
||||
}
|
||||
|
||||
async testing() {
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
const ClipperServer = require('lib/ClipperServer');
|
||||
const server = new ClipperServer();
|
||||
const HtmlToMd = require('lib/HtmlToMd');
|
||||
const service = new HtmlToMd();
|
||||
const html = await shim.fsDriver().readFile('/mnt/d/test.html');
|
||||
let markdown = service.parse(html, { baseUrl: 'https://duckduckgo.com/' });
|
||||
console.info(markdown);
|
||||
console.info('--------------------------------------------------');
|
||||
|
||||
const imageUrls = markdownUtils.extractImageUrls(markdown);
|
||||
let result = await server.downloadImages_(imageUrls);
|
||||
result = await server.createResourcesFromPaths_(result);
|
||||
console.info(result);
|
||||
markdown = server.replaceImageUrlsByResources_(markdown, result);
|
||||
console.info('--------------------------------------------------');
|
||||
console.info(markdown);
|
||||
console.info('--------------------------------------------------');
|
||||
}
|
||||
|
||||
async start(argv) {
|
||||
let startFlags = await this.handleStartFlags_(argv);
|
||||
|
||||
argv = startFlags.argv;
|
||||
let initArgs = startFlags.matched;
|
||||
if (argv.length) this.showPromptString_ = false;
|
||||
|
||||
let appName = initArgs.env == 'dev' ? 'joplindev' : 'joplin';
|
||||
if (Setting.value('appId').indexOf('-desktop') >= 0) appName += '-desktop';
|
||||
Setting.setConstant('appName', appName);
|
||||
|
||||
const profileDir = this.determineProfileDir(initArgs);
|
||||
const resourceDirName = 'resources';
|
||||
const resourceDir = `${profileDir}/${resourceDirName}`;
|
||||
const tempDir = `${profileDir}/tmp`;
|
||||
|
||||
Setting.setConstant('env', initArgs.env);
|
||||
Setting.setConstant('profileDir', profileDir);
|
||||
Setting.setConstant('templateDir', `${profileDir}/templates`);
|
||||
Setting.setConstant('resourceDirName', resourceDirName);
|
||||
Setting.setConstant('resourceDir', resourceDir);
|
||||
Setting.setConstant('tempDir', tempDir);
|
||||
|
||||
await shim.fsDriver().remove(tempDir);
|
||||
|
||||
await fs.mkdirp(profileDir, 0o755);
|
||||
await fs.mkdirp(resourceDir, 0o755);
|
||||
await fs.mkdirp(tempDir, 0o755);
|
||||
|
||||
// Clean up any remaining watched files (they start with "edit-")
|
||||
await shim.fsDriver().removeAllThatStartWith(profileDir, 'edit-');
|
||||
|
||||
const extraFlags = await this.readFlagsFromFile(`${profileDir}/flags.txt`);
|
||||
initArgs = Object.assign(initArgs, extraFlags);
|
||||
|
||||
this.logger_.addTarget('file', { path: `${profileDir}/log.txt` });
|
||||
if (Setting.value('env') === 'dev') this.logger_.addTarget('console', { level: Logger.LEVEL_WARN });
|
||||
this.logger_.setLevel(initArgs.logLevel);
|
||||
|
||||
reg.setLogger(this.logger_);
|
||||
reg.dispatch = () => {};
|
||||
|
||||
this.dbLogger_.addTarget('file', { path: `${profileDir}/log-database.txt` });
|
||||
this.dbLogger_.setLevel(initArgs.logLevel);
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
this.dbLogger_.setLevel(Logger.LEVEL_INFO);
|
||||
}
|
||||
|
||||
this.logger_.info(`Profile directory: ${profileDir}`);
|
||||
|
||||
this.database_ = new JoplinDatabase(new DatabaseDriverNode());
|
||||
this.database_.setLogExcludedQueryTypes(['SELECT']);
|
||||
this.database_.setLogger(this.dbLogger_);
|
||||
await this.database_.open({ name: `${profileDir}/database.sqlite` });
|
||||
|
||||
// if (Setting.value('env') === 'dev') await this.database_.clearForTesting();
|
||||
|
||||
reg.setDb(this.database_);
|
||||
BaseModel.db_ = this.database_;
|
||||
|
||||
await Setting.load();
|
||||
|
||||
if (Setting.value('firstStart')) {
|
||||
const locale = shim.detectAndSetLocale(Setting);
|
||||
reg.logger().info(`First start: detected locale as ${locale}`);
|
||||
|
||||
if (Setting.value('env') === 'dev') {
|
||||
Setting.setValue('showTrayIcon', 0);
|
||||
Setting.setValue('autoUpdateEnabled', 0);
|
||||
Setting.setValue('sync.interval', 3600);
|
||||
}
|
||||
|
||||
Setting.setValue('firstStart', 0);
|
||||
} else {
|
||||
setLocale(Setting.value('locale'));
|
||||
}
|
||||
|
||||
if ('welcomeDisabled' in initArgs) Setting.setValue('welcome.enabled', !initArgs.welcomeDisabled);
|
||||
|
||||
if (!Setting.value('api.token')) {
|
||||
EncryptionService.instance()
|
||||
.randomHexString(64)
|
||||
.then(token => {
|
||||
Setting.setValue('api.token', token);
|
||||
});
|
||||
}
|
||||
|
||||
time.setDateFormat(Setting.value('dateFormat'));
|
||||
time.setTimeFormat(Setting.value('timeFormat'));
|
||||
|
||||
BaseItem.revisionService_ = RevisionService.instance();
|
||||
|
||||
KvStore.instance().setDb(reg.db());
|
||||
|
||||
BaseService.logger_ = this.logger_;
|
||||
EncryptionService.instance().setLogger(this.logger_);
|
||||
BaseItem.encryptionService_ = EncryptionService.instance();
|
||||
DecryptionWorker.instance().setLogger(this.logger_);
|
||||
DecryptionWorker.instance().setEncryptionService(EncryptionService.instance());
|
||||
DecryptionWorker.instance().setKvStore(KvStore.instance());
|
||||
await EncryptionService.instance().loadMasterKeysFromSettings();
|
||||
DecryptionWorker.instance().on('resourceMetadataButNotBlobDecrypted', this.decryptionWorker_resourceMetadataButNotBlobDecrypted);
|
||||
|
||||
ResourceFetcher.instance().setFileApi(() => {
|
||||
return reg.syncTarget().fileApi();
|
||||
});
|
||||
ResourceFetcher.instance().setLogger(this.logger_);
|
||||
ResourceFetcher.instance().on('downloadComplete', this.resourceFetcher_downloadComplete);
|
||||
ResourceFetcher.instance().start();
|
||||
|
||||
SearchEngine.instance().setDb(reg.db());
|
||||
SearchEngine.instance().setLogger(reg.logger());
|
||||
SearchEngine.instance().scheduleSyncTables();
|
||||
|
||||
let currentFolderId = Setting.value('activeFolderId');
|
||||
let currentFolder = null;
|
||||
if (currentFolderId) currentFolder = await Folder.load(currentFolderId);
|
||||
if (!currentFolder) currentFolder = await Folder.defaultFolder();
|
||||
Setting.setValue('activeFolderId', currentFolder ? currentFolder.id : '');
|
||||
|
||||
await MigrationService.instance().run();
|
||||
|
||||
return argv;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { BaseApplication };
|
549
Server/dist/lib/BaseModel.js
vendored
549
Server/dist/lib/BaseModel.js
vendored
@ -1,549 +0,0 @@
|
||||
const { Database } = require('lib/database.js');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
class BaseModel {
|
||||
static modelType() {
|
||||
throw new Error('Must be overriden');
|
||||
}
|
||||
|
||||
static tableName() {
|
||||
throw new Error('Must be overriden');
|
||||
}
|
||||
|
||||
static addModelMd(model) {
|
||||
if (!model) return model;
|
||||
|
||||
if (Array.isArray(model)) {
|
||||
let output = [];
|
||||
for (let i = 0; i < model.length; i++) {
|
||||
output.push(this.addModelMd(model[i]));
|
||||
}
|
||||
return output;
|
||||
} else {
|
||||
model = Object.assign({}, model);
|
||||
model.type_ = this.modelType();
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
static logger() {
|
||||
return this.db().logger();
|
||||
}
|
||||
|
||||
static useUuid() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static byId(items, id) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id == id) return items[i];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static modelIndexById(items, id) {
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (items[i].id == id) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
static modelsByIds(items, ids) {
|
||||
const output = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (ids.indexOf(items[i].id) >= 0) {
|
||||
output.push(items[i]);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// Prefer the use of this function to compare IDs as it handles the case where
|
||||
// one ID is null and the other is "", in which case they are actually considered to be the same.
|
||||
static idsEqual(id1, id2) {
|
||||
if (!id1 && !id2) return true;
|
||||
if (!id1 && !!id2) return false;
|
||||
if (!!id1 && !id2) return false;
|
||||
return id1 === id2;
|
||||
}
|
||||
|
||||
static modelTypeToName(type) {
|
||||
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
|
||||
const e = BaseModel.typeEnum_[i];
|
||||
if (e[1] === type) return e[0].substr(5).toLowerCase();
|
||||
}
|
||||
throw new Error(`Unknown model type: ${type}`);
|
||||
}
|
||||
|
||||
static hasField(name) {
|
||||
let fields = this.fieldNames();
|
||||
return fields.indexOf(name) >= 0;
|
||||
}
|
||||
|
||||
static fieldNames(withPrefix = false) {
|
||||
let output = this.db().tableFieldNames(this.tableName());
|
||||
if (!withPrefix) return output;
|
||||
|
||||
let p = withPrefix === true ? this.tableName() : withPrefix;
|
||||
let temp = [];
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
temp.push(`${p}.${output[i]}`);
|
||||
}
|
||||
|
||||
return temp;
|
||||
}
|
||||
|
||||
static fieldType(name, defaultValue = null) {
|
||||
let fields = this.fields();
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
if (fields[i].name == name) return fields[i].type;
|
||||
}
|
||||
if (defaultValue !== null) return defaultValue;
|
||||
throw new Error(`Unknown field: ${name}`);
|
||||
}
|
||||
|
||||
static fields() {
|
||||
return this.db().tableFields(this.tableName());
|
||||
}
|
||||
|
||||
static removeUnknownFields(model) {
|
||||
const newModel = {};
|
||||
for (let n in model) {
|
||||
if (!model.hasOwnProperty(n)) continue;
|
||||
if (!this.hasField(n) && n !== 'type_') continue;
|
||||
newModel[n] = model[n];
|
||||
}
|
||||
return newModel;
|
||||
}
|
||||
|
||||
static new() {
|
||||
let fields = this.fields();
|
||||
let output = {};
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
let f = fields[i];
|
||||
output[f.name] = f.default;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static modOptions(options) {
|
||||
if (!options) {
|
||||
options = {};
|
||||
} else {
|
||||
options = Object.assign({}, options);
|
||||
}
|
||||
if (!('isNew' in options)) options.isNew = 'auto';
|
||||
if (!('autoTimestamp' in options)) options.autoTimestamp = true;
|
||||
return options;
|
||||
}
|
||||
|
||||
static count(options = null) {
|
||||
if (!options) options = {};
|
||||
let sql = `SELECT count(*) as total FROM \`${this.tableName()}\``;
|
||||
if (options.where) sql += ` WHERE ${options.where}`;
|
||||
return this.db()
|
||||
.selectOne(sql)
|
||||
.then(r => {
|
||||
return r ? r['total'] : 0;
|
||||
});
|
||||
}
|
||||
|
||||
static load(id) {
|
||||
return this.loadByField('id', id);
|
||||
}
|
||||
|
||||
static shortId(id) {
|
||||
return id.substr(0, 5);
|
||||
}
|
||||
|
||||
static loadByPartialId(partialId) {
|
||||
return this.modelSelectAll(`SELECT * FROM \`${this.tableName()}\` WHERE \`id\` LIKE ?`, [`${partialId}%`]);
|
||||
}
|
||||
|
||||
static applySqlOptions(options, sql, params = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.order && options.order.length) {
|
||||
let items = [];
|
||||
for (let i = 0; i < options.order.length; i++) {
|
||||
const o = options.order[i];
|
||||
let item = o.by;
|
||||
if (options.caseInsensitive === true) item += ' COLLATE NOCASE';
|
||||
if (o.dir) item += ` ${o.dir}`;
|
||||
items.push(item);
|
||||
}
|
||||
sql += ` ORDER BY ${items.join(', ')}`;
|
||||
}
|
||||
|
||||
if (options.limit) sql += ` LIMIT ${options.limit}`;
|
||||
|
||||
return { sql: sql, params: params };
|
||||
}
|
||||
|
||||
static async allIds(options = null) {
|
||||
let q = this.applySqlOptions(options, `SELECT id FROM \`${this.tableName()}\``);
|
||||
const rows = await this.db().selectAll(q.sql, q.params);
|
||||
return rows.map(r => r.id);
|
||||
}
|
||||
|
||||
static async all(options = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.fields) options.fields = '*';
|
||||
|
||||
let q = this.applySqlOptions(options, `SELECT ${this.db().escapeFields(options.fields)} FROM \`${this.tableName()}\``);
|
||||
return this.modelSelectAll(q.sql);
|
||||
}
|
||||
|
||||
static async byIds(ids, options = null) {
|
||||
if (!ids.length) return [];
|
||||
if (!options) options = {};
|
||||
if (!options.fields) options.fields = '*';
|
||||
|
||||
let sql = `SELECT ${this.db().escapeFields(options.fields)} FROM \`${this.tableName()}\``;
|
||||
sql += ` WHERE id IN ("${ids.join('","')}")`;
|
||||
let q = this.applySqlOptions(options, sql);
|
||||
return this.modelSelectAll(q.sql);
|
||||
}
|
||||
|
||||
static async search(options = null) {
|
||||
if (!options) options = {};
|
||||
if (!options.fields) options.fields = '*';
|
||||
|
||||
let conditions = options.conditions ? options.conditions.slice(0) : [];
|
||||
let params = options.conditionsParams ? options.conditionsParams.slice(0) : [];
|
||||
|
||||
if (options.titlePattern) {
|
||||
let pattern = options.titlePattern.replace(/\*/g, '%');
|
||||
conditions.push('title LIKE ?');
|
||||
params.push(pattern);
|
||||
}
|
||||
|
||||
if ('limit' in options && options.limit <= 0) return [];
|
||||
|
||||
let sql = `SELECT ${this.db().escapeFields(options.fields)} FROM \`${this.tableName()}\``;
|
||||
if (conditions.length) sql += ` WHERE ${conditions.join(' AND ')}`;
|
||||
|
||||
let query = this.applySqlOptions(options, sql, params);
|
||||
return this.modelSelectAll(query.sql, query.params);
|
||||
}
|
||||
|
||||
static modelSelectOne(sql, params = null) {
|
||||
if (params === null) params = [];
|
||||
return this.db()
|
||||
.selectOne(sql, params)
|
||||
.then(model => {
|
||||
return this.filter(this.addModelMd(model));
|
||||
});
|
||||
}
|
||||
|
||||
static modelSelectAll(sql, params = null) {
|
||||
if (params === null) params = [];
|
||||
return this.db()
|
||||
.selectAll(sql, params)
|
||||
.then(models => {
|
||||
return this.filterArray(this.addModelMd(models));
|
||||
});
|
||||
}
|
||||
|
||||
static loadByField(fieldName, fieldValue, options = null) {
|
||||
if (!options) options = {};
|
||||
if (!('caseInsensitive' in options)) options.caseInsensitive = false;
|
||||
let sql = `SELECT * FROM \`${this.tableName()}\` WHERE \`${fieldName}\` = ?`;
|
||||
if (options.caseInsensitive) sql += ' COLLATE NOCASE';
|
||||
return this.modelSelectOne(sql, [fieldValue]);
|
||||
}
|
||||
|
||||
static loadByTitle(fieldValue) {
|
||||
return this.modelSelectOne(`SELECT * FROM \`${this.tableName()}\` WHERE \`title\` = ?`, [fieldValue]);
|
||||
}
|
||||
|
||||
static diffObjects(oldModel, newModel) {
|
||||
let output = {};
|
||||
const fields = this.diffObjectsFields(oldModel, newModel);
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
output[fields[i]] = newModel[fields[i]];
|
||||
}
|
||||
if ('type_' in newModel) output.type_ = newModel.type_;
|
||||
return output;
|
||||
}
|
||||
|
||||
static diffObjectsFields(oldModel, newModel) {
|
||||
let output = [];
|
||||
for (let n in newModel) {
|
||||
if (!newModel.hasOwnProperty(n)) continue;
|
||||
if (n == 'type_') continue;
|
||||
if (!(n in oldModel) || newModel[n] !== oldModel[n]) {
|
||||
output.push(n);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static modelsAreSame(oldModel, newModel) {
|
||||
const diff = this.diffObjects(oldModel, newModel);
|
||||
delete diff.type_;
|
||||
return !Object.getOwnPropertyNames(diff).length;
|
||||
}
|
||||
|
||||
static saveMutex(modelOrId) {
|
||||
const noLockMutex = {
|
||||
acquire: function() {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
|
||||
if (!modelOrId) return noLockMutex;
|
||||
|
||||
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
|
||||
|
||||
if (!modelId) return noLockMutex;
|
||||
|
||||
let mutex = BaseModel.saveMutexes_[modelId];
|
||||
if (mutex) return mutex;
|
||||
|
||||
mutex = new Mutex();
|
||||
BaseModel.saveMutexes_[modelId] = mutex;
|
||||
return mutex;
|
||||
}
|
||||
|
||||
static releaseSaveMutex(modelOrId, release) {
|
||||
if (!release) return;
|
||||
if (!modelOrId) return release();
|
||||
|
||||
let modelId = typeof modelOrId === 'string' ? modelOrId : modelOrId.id;
|
||||
|
||||
if (!modelId) return release();
|
||||
|
||||
let mutex = BaseModel.saveMutexes_[modelId];
|
||||
if (!mutex) return release();
|
||||
|
||||
delete BaseModel.saveMutexes_[modelId];
|
||||
release();
|
||||
}
|
||||
|
||||
static saveQuery(o, options) {
|
||||
let temp = {};
|
||||
let fieldNames = this.fieldNames();
|
||||
for (let i = 0; i < fieldNames.length; i++) {
|
||||
let n = fieldNames[i];
|
||||
if (n in o) temp[n] = o[n];
|
||||
}
|
||||
|
||||
// Remove fields that are not in the `fields` list, if provided.
|
||||
// Note that things like update_time, user_updated_time will still
|
||||
// be part of the final list of fields if autoTimestamp is on.
|
||||
// id also will stay.
|
||||
if (!options.isNew && options.fields) {
|
||||
const filtered = {};
|
||||
for (let k in temp) {
|
||||
if (!temp.hasOwnProperty(k)) continue;
|
||||
if (k !== 'id' && options.fields.indexOf(k) < 0) continue;
|
||||
filtered[k] = temp[k];
|
||||
}
|
||||
temp = filtered;
|
||||
}
|
||||
|
||||
o = temp;
|
||||
|
||||
let modelId = temp.id;
|
||||
let query = {};
|
||||
|
||||
const timeNow = time.unixMs();
|
||||
|
||||
if (options.autoTimestamp && this.hasField('updated_time')) {
|
||||
o.updated_time = timeNow;
|
||||
}
|
||||
|
||||
// The purpose of user_updated_time is to allow the user to manually set the time of a note (in which case
|
||||
// options.autoTimestamp will be `false`). However note that if the item is later changed, this timestamp
|
||||
// will be set again to the current time.
|
||||
//
|
||||
// The technique to modify user_updated_time while keeping updated_time current (so that sync can happen) is to
|
||||
// manually set updated_time when saving and to set autoTimestamp to false, for example:
|
||||
// Note.save({ id: "...", updated_time: Date.now(), user_updated_time: 1436342618000 }, { autoTimestamp: false })
|
||||
if (options.autoTimestamp && this.hasField('user_updated_time')) {
|
||||
o.user_updated_time = timeNow;
|
||||
}
|
||||
|
||||
if (options.isNew) {
|
||||
if (this.useUuid() && !o.id) {
|
||||
modelId = uuid.create();
|
||||
o.id = modelId;
|
||||
}
|
||||
|
||||
if (!o.created_time && this.hasField('created_time')) {
|
||||
o.created_time = timeNow;
|
||||
}
|
||||
|
||||
if (!o.user_created_time && this.hasField('user_created_time')) {
|
||||
o.user_created_time = o.created_time ? o.created_time : timeNow;
|
||||
}
|
||||
|
||||
if (!o.user_updated_time && this.hasField('user_updated_time')) {
|
||||
o.user_updated_time = o.updated_time ? o.updated_time : timeNow;
|
||||
}
|
||||
|
||||
query = Database.insertQuery(this.tableName(), o);
|
||||
} else {
|
||||
let where = { id: o.id };
|
||||
let temp = Object.assign({}, o);
|
||||
delete temp.id;
|
||||
|
||||
query = Database.updateQuery(this.tableName(), temp, where);
|
||||
}
|
||||
|
||||
query.id = modelId;
|
||||
query.modObject = o;
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
static async save(o, options = null) {
|
||||
// When saving, there's a mutex per model ID. This is because the model returned from this function
|
||||
// is basically its input `o` (instead of being read from the database, for performance reasons).
|
||||
// This works well in general except if that model is saved simultaneously in two places. In that
|
||||
// case, the output won't be up-to-date and would cause for example display issues with out-dated
|
||||
// notes being displayed. This was an issue when notes were being synchronised while being decrypted
|
||||
// at the same time.
|
||||
|
||||
const mutexRelease = await this.saveMutex(o).acquire();
|
||||
|
||||
options = this.modOptions(options);
|
||||
options.isNew = this.isNew(o, options);
|
||||
|
||||
// Diff saving is an optimisation which takes a new version of the item and an old one,
|
||||
// do a diff and save only this diff. IMPORTANT: When using this make sure that both
|
||||
// models have been normalised using ItemClass.filter()
|
||||
const isDiffSaving = options && options.oldItem && !options.isNew;
|
||||
|
||||
if (isDiffSaving) {
|
||||
const newObject = BaseModel.diffObjects(options.oldItem, o);
|
||||
newObject.type_ = o.type_;
|
||||
newObject.id = o.id;
|
||||
o = newObject;
|
||||
}
|
||||
|
||||
o = this.filter(o);
|
||||
|
||||
let queries = [];
|
||||
let saveQuery = this.saveQuery(o, options);
|
||||
let modelId = saveQuery.id;
|
||||
|
||||
queries.push(saveQuery);
|
||||
|
||||
if (options.nextQueries && options.nextQueries.length) {
|
||||
queries = queries.concat(options.nextQueries);
|
||||
}
|
||||
|
||||
let output = null;
|
||||
|
||||
try {
|
||||
await this.db().transactionExecBatch(queries);
|
||||
|
||||
o = Object.assign({}, o);
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
if (modelId) o.id = modelId;
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
if ('updated_time' in saveQuery.modObject) o.updated_time = saveQuery.modObject.updated_time;
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
if ('created_time' in saveQuery.modObject) o.created_time = saveQuery.modObject.created_time;
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
if ('user_updated_time' in saveQuery.modObject) o.user_updated_time = saveQuery.modObject.user_updated_time;
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
if ('user_created_time' in saveQuery.modObject) o.user_created_time = saveQuery.modObject.user_created_time;
|
||||
o = this.addModelMd(o);
|
||||
|
||||
if (isDiffSaving) {
|
||||
for (let n in options.oldItem) {
|
||||
if (!options.oldItem.hasOwnProperty(n)) continue;
|
||||
if (n in o) continue;
|
||||
o[n] = options.oldItem[n];
|
||||
}
|
||||
}
|
||||
|
||||
output = this.filter(o);
|
||||
} finally {
|
||||
this.releaseSaveMutex(o, mutexRelease);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static isNew(object, options) {
|
||||
if (options && 'isNew' in options) {
|
||||
// options.isNew can be "auto" too
|
||||
if (options.isNew === true) return true;
|
||||
if (options.isNew === false) return false;
|
||||
}
|
||||
|
||||
return !object.id;
|
||||
}
|
||||
|
||||
static filterArray(models) {
|
||||
let output = [];
|
||||
for (let i = 0; i < models.length; i++) {
|
||||
output.push(this.filter(models[i]));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
static filter(model) {
|
||||
if (!model) return model;
|
||||
|
||||
let output = Object.assign({}, model);
|
||||
for (let n in output) {
|
||||
if (!output.hasOwnProperty(n)) continue;
|
||||
|
||||
// The SQLite database doesn't have booleans so cast everything to int
|
||||
if (output[n] === true) {
|
||||
output[n] = 1;
|
||||
} else if (output[n] === false) {
|
||||
output[n] = 0;
|
||||
} else {
|
||||
const t = this.fieldType(n, Database.TYPE_UNKNOWN);
|
||||
if (t === Database.TYPE_INT) {
|
||||
output[n] = !n ? 0 : parseInt(output[n], 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static delete(id) {
|
||||
if (!id) throw new Error('Cannot delete object without an ID');
|
||||
return this.db().exec(`DELETE FROM ${this.tableName()} WHERE id = ?`, [id]);
|
||||
}
|
||||
|
||||
static batchDelete(ids, options = null) {
|
||||
if (!ids.length) return;
|
||||
options = this.modOptions(options);
|
||||
const idFieldName = options.idFieldName ? options.idFieldName : 'id';
|
||||
const sql = `DELETE FROM ${this.tableName()} WHERE ${idFieldName} IN ("${ids.join('","')}")`;
|
||||
return this.db().exec(sql);
|
||||
}
|
||||
|
||||
static db() {
|
||||
if (!this.db_) throw new Error('Accessing database before it has been initialised');
|
||||
return this.db_;
|
||||
}
|
||||
|
||||
static isReady() {
|
||||
return !!this.db_;
|
||||
}
|
||||
}
|
||||
|
||||
BaseModel.typeEnum_ = [['TYPE_NOTE', 1], ['TYPE_FOLDER', 2], ['TYPE_SETTING', 3], ['TYPE_RESOURCE', 4], ['TYPE_TAG', 5], ['TYPE_NOTE_TAG', 6], ['TYPE_SEARCH', 7], ['TYPE_ALARM', 8], ['TYPE_MASTER_KEY', 9], ['TYPE_ITEM_CHANGE', 10], ['TYPE_NOTE_RESOURCE', 11], ['TYPE_RESOURCE_LOCAL_STATE', 12], ['TYPE_REVISION', 13], ['TYPE_MIGRATION', 14]];
|
||||
|
||||
for (let i = 0; i < BaseModel.typeEnum_.length; i++) {
|
||||
const e = BaseModel.typeEnum_[i];
|
||||
BaseModel[e[0]] = e[1];
|
||||
}
|
||||
|
||||
BaseModel.db_ = null;
|
||||
BaseModel.dispatch = function() {};
|
||||
BaseModel.saveMutexes_ = {};
|
||||
|
||||
module.exports = BaseModel;
|
127
Server/dist/lib/BaseSyncTarget.js
vendored
127
Server/dist/lib/BaseSyncTarget.js
vendored
@ -1,127 +0,0 @@
|
||||
const EncryptionService = require('lib/services/EncryptionService.js');
|
||||
|
||||
class BaseSyncTarget {
|
||||
constructor(db, options = null) {
|
||||
this.db_ = db;
|
||||
this.synchronizer_ = null;
|
||||
this.initState_ = null;
|
||||
this.logger_ = null;
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
static supportsConfigCheck() {
|
||||
return false;
|
||||
}
|
||||
|
||||
static resourceDirName() {
|
||||
return '.resource';
|
||||
}
|
||||
|
||||
option(name, defaultValue = null) {
|
||||
return this.options_ && name in this.options_ ? this.options_[name] : defaultValue;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
setLogger(v) {
|
||||
this.logger_ = v;
|
||||
}
|
||||
|
||||
db() {
|
||||
return this.db_;
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return false;
|
||||
}
|
||||
|
||||
authRouteName() {
|
||||
return null;
|
||||
}
|
||||
|
||||
static id() {
|
||||
throw new Error('id() not implemented');
|
||||
}
|
||||
|
||||
// Note: it cannot be called just "name()" because that's a reserved keyword and
|
||||
// it would throw an obscure error in React Native.
|
||||
static targetName() {
|
||||
throw new Error('targetName() not implemented');
|
||||
}
|
||||
|
||||
static label() {
|
||||
throw new Error('label() not implemented');
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
throw new Error('initSynchronizer() not implemented');
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
throw new Error('initFileApi() not implemented');
|
||||
}
|
||||
|
||||
async fileApi() {
|
||||
if (this.fileApi_) return this.fileApi_;
|
||||
this.fileApi_ = await this.initFileApi();
|
||||
return this.fileApi_;
|
||||
}
|
||||
|
||||
fileApiSync() {
|
||||
return this.fileApi_;
|
||||
}
|
||||
|
||||
// Usually each sync target should create and setup its own file API via initFileApi()
|
||||
// but for testing purposes it might be convenient to provide it here so that multiple
|
||||
// clients can share and sync to the same file api (see test-utils.js)
|
||||
setFileApi(v) {
|
||||
this.fileApi_ = v;
|
||||
}
|
||||
|
||||
async synchronizer() {
|
||||
if (this.synchronizer_) return this.synchronizer_;
|
||||
|
||||
if (this.initState_ == 'started') {
|
||||
// Synchronizer is already being initialized, so wait here till it's done.
|
||||
return new Promise((resolve, reject) => {
|
||||
const iid = setInterval(() => {
|
||||
if (this.initState_ == 'ready') {
|
||||
clearInterval(iid);
|
||||
resolve(this.synchronizer_);
|
||||
}
|
||||
if (this.initState_ == 'error') {
|
||||
clearInterval(iid);
|
||||
reject(new Error('Could not initialise synchroniser'));
|
||||
}
|
||||
}, 1000);
|
||||
});
|
||||
} else {
|
||||
this.initState_ = 'started';
|
||||
|
||||
try {
|
||||
this.synchronizer_ = await this.initSynchronizer();
|
||||
this.synchronizer_.setLogger(this.logger());
|
||||
this.synchronizer_.setEncryptionService(EncryptionService.instance());
|
||||
this.synchronizer_.dispatch = BaseSyncTarget.dispatch;
|
||||
this.initState_ = 'ready';
|
||||
return this.synchronizer_;
|
||||
} catch (error) {
|
||||
this.initState_ = 'error';
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async syncStarted() {
|
||||
if (!this.synchronizer_) return false;
|
||||
if (!(await this.isAuthenticated())) return false;
|
||||
const sync = await this.synchronizer();
|
||||
return sync.state() != 'idle';
|
||||
}
|
||||
}
|
||||
|
||||
BaseSyncTarget.dispatch = () => {};
|
||||
|
||||
module.exports = BaseSyncTarget;
|
33
Server/dist/lib/Cache.js
vendored
33
Server/dist/lib/Cache.js
vendored
@ -1,33 +0,0 @@
|
||||
class Cache {
|
||||
async getItem(name) {
|
||||
let output = null;
|
||||
try {
|
||||
const storage = await Cache.storage();
|
||||
output = await storage.getItem(name);
|
||||
} catch (error) {
|
||||
console.info(error);
|
||||
// Defaults to returning null
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async setItem(name, value, ttl = null) {
|
||||
try {
|
||||
const storage = await Cache.storage();
|
||||
const options = {};
|
||||
if (ttl !== null) options.ttl = ttl;
|
||||
await storage.setItem(name, value, options);
|
||||
} catch (error) {
|
||||
// Defaults to not saving to cache
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cache.storage = async function() {
|
||||
if (Cache.storage_) return Cache.storage_;
|
||||
Cache.storage_ = require('node-persist');
|
||||
await Cache.storage_.init({ dir: `${require('os').tmpdir()}/joplin-cache`, ttl: 1000 * 60 });
|
||||
return Cache.storage_;
|
||||
};
|
||||
|
||||
module.exports = Cache;
|
226
Server/dist/lib/ClipperServer.js
vendored
226
Server/dist/lib/ClipperServer.js
vendored
@ -1,226 +0,0 @@
|
||||
const urlParser = require('url');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { randomClipperPort, startPort } = require('lib/randomClipperPort');
|
||||
const enableServerDestroy = require('server-destroy');
|
||||
const Api = require('lib/services/rest/Api');
|
||||
const ApiResponse = require('lib/services/rest/ApiResponse');
|
||||
const multiparty = require('multiparty');
|
||||
|
||||
class ClipperServer {
|
||||
constructor() {
|
||||
this.logger_ = new Logger();
|
||||
this.startState_ = 'idle';
|
||||
this.server_ = null;
|
||||
this.port_ = null;
|
||||
this.api_ = new Api(() => {
|
||||
return Setting.value('api.token');
|
||||
});
|
||||
}
|
||||
|
||||
static instance() {
|
||||
if (this.instance_) return this.instance_;
|
||||
this.instance_ = new ClipperServer();
|
||||
return this.instance_;
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
this.api_.setLogger(l);
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
setDispatch(d) {
|
||||
this.dispatch_ = d;
|
||||
}
|
||||
|
||||
dispatch(action) {
|
||||
if (!this.dispatch_) throw new Error('dispatch not set!');
|
||||
this.dispatch_(action);
|
||||
}
|
||||
|
||||
setStartState(v) {
|
||||
if (this.startState_ === v) return;
|
||||
this.startState_ = v;
|
||||
this.dispatch({
|
||||
type: 'CLIPPER_SERVER_SET',
|
||||
startState: v,
|
||||
});
|
||||
}
|
||||
|
||||
setPort(v) {
|
||||
if (this.port_ === v) return;
|
||||
this.port_ = v;
|
||||
this.dispatch({
|
||||
type: 'CLIPPER_SERVER_SET',
|
||||
port: v,
|
||||
});
|
||||
}
|
||||
|
||||
async findAvailablePort() {
|
||||
const tcpPortUsed = require('tcp-port-used');
|
||||
|
||||
let state = null;
|
||||
for (let i = 0; i < 10000; i++) {
|
||||
state = randomClipperPort(state, Setting.value('env'));
|
||||
const inUse = await tcpPortUsed.check(state.port);
|
||||
if (!inUse) return state.port;
|
||||
}
|
||||
|
||||
throw new Error('All potential ports are in use or not available.');
|
||||
}
|
||||
|
||||
async isRunning() {
|
||||
const tcpPortUsed = require('tcp-port-used');
|
||||
const port = Setting.value('api.port') ? Setting.value('api.port') : startPort(Setting.value('env'));
|
||||
const inUse = await tcpPortUsed.check(port);
|
||||
return inUse ? port : 0;
|
||||
}
|
||||
|
||||
async start() {
|
||||
this.setPort(null);
|
||||
|
||||
this.setStartState('starting');
|
||||
|
||||
const settingPort = Setting.value('api.port');
|
||||
|
||||
try {
|
||||
const p = settingPort ? settingPort : await this.findAvailablePort();
|
||||
this.setPort(p);
|
||||
} catch (error) {
|
||||
this.setStartState('idle');
|
||||
this.logger().error(error);
|
||||
return;
|
||||
}
|
||||
|
||||
this.server_ = require('http').createServer();
|
||||
|
||||
this.server_.on('request', async (request, response) => {
|
||||
const writeCorsHeaders = (code, contentType = 'application/json', additionalHeaders = null) => {
|
||||
const headers = Object.assign(
|
||||
{},
|
||||
{
|
||||
'Content-Type': contentType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS, PUT, PATCH, DELETE',
|
||||
'Access-Control-Allow-Headers': 'X-Requested-With,content-type',
|
||||
},
|
||||
additionalHeaders ? additionalHeaders : {}
|
||||
);
|
||||
response.writeHead(code, headers);
|
||||
};
|
||||
|
||||
const writeResponseJson = (code, object) => {
|
||||
writeCorsHeaders(code);
|
||||
response.write(JSON.stringify(object));
|
||||
response.end();
|
||||
};
|
||||
|
||||
const writeResponseText = (code, text) => {
|
||||
writeCorsHeaders(code, 'text/plain');
|
||||
response.write(text);
|
||||
response.end();
|
||||
};
|
||||
|
||||
const writeResponseInstance = (code, instance) => {
|
||||
if (instance.type === 'attachment') {
|
||||
const filename = instance.attachmentFilename ? instance.attachmentFilename : 'file';
|
||||
writeCorsHeaders(code, instance.contentType ? instance.contentType : 'application/octet-stream', {
|
||||
'Content-disposition': `attachment; filename=${filename}`,
|
||||
'Content-Length': instance.body.length,
|
||||
});
|
||||
response.end(instance.body);
|
||||
} else {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
};
|
||||
|
||||
const writeResponse = (code, response) => {
|
||||
if (response instanceof ApiResponse) {
|
||||
writeResponseInstance(code, response);
|
||||
} else if (typeof response === 'string') {
|
||||
writeResponseText(code, response);
|
||||
} else {
|
||||
writeResponseJson(code, response);
|
||||
}
|
||||
};
|
||||
|
||||
this.logger().info(`Request: ${request.method} ${request.url}`);
|
||||
|
||||
const url = urlParser.parse(request.url, true);
|
||||
|
||||
const execRequest = async (request, body = '', files = []) => {
|
||||
try {
|
||||
const response = await this.api_.route(request.method, url.pathname, url.query, body, files);
|
||||
writeResponse(200, response ? response : '');
|
||||
} catch (error) {
|
||||
this.logger().error(error);
|
||||
const httpCode = error.httpCode ? error.httpCode : 500;
|
||||
const msg = [];
|
||||
if (httpCode >= 500) msg.push('Internal Server Error');
|
||||
if (error.message) msg.push(error.message);
|
||||
|
||||
writeResponse(httpCode, { error: msg.join(': ') });
|
||||
}
|
||||
};
|
||||
|
||||
const contentType = request.headers['content-type'] ? request.headers['content-type'] : '';
|
||||
|
||||
if (request.method === 'OPTIONS') {
|
||||
writeCorsHeaders(200);
|
||||
response.end();
|
||||
} else {
|
||||
if (contentType.indexOf('multipart/form-data') === 0) {
|
||||
const form = new multiparty.Form();
|
||||
|
||||
form.parse(request, function(error, fields, files) {
|
||||
if (error) {
|
||||
writeResponse(error.httpCode ? error.httpCode : 500, error.message);
|
||||
return;
|
||||
} else {
|
||||
execRequest(request, fields && fields.props && fields.props.length ? fields.props[0] : '', files && files.data ? files.data : []);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (request.method === 'POST' || request.method === 'PUT') {
|
||||
let body = '';
|
||||
|
||||
request.on('data', data => {
|
||||
body += data;
|
||||
});
|
||||
|
||||
request.on('end', async () => {
|
||||
execRequest(request, body);
|
||||
});
|
||||
} else {
|
||||
execRequest(request);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
enableServerDestroy(this.server_);
|
||||
|
||||
this.logger().info(`Starting Clipper server on port ${this.port_}`);
|
||||
|
||||
this.server_.listen(this.port_, '127.0.0.1');
|
||||
|
||||
this.setStartState('started');
|
||||
|
||||
// We return an empty promise that never resolves so that it's possible to `await` the server indefinitely.
|
||||
// This is used only in command-server.js
|
||||
return new Promise(() => {});
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.server_.destroy();
|
||||
this.server_ = null;
|
||||
this.setStartState('idle');
|
||||
this.setPort(null);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ClipperServer;
|
213
Server/dist/lib/DropboxApi.js
vendored
213
Server/dist/lib/DropboxApi.js
vendored
@ -1,213 +0,0 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const { time } = require('lib/time-utils');
|
||||
const EventDispatcher = require('lib/EventDispatcher');
|
||||
|
||||
class DropboxApi {
|
||||
constructor(options) {
|
||||
this.logger_ = new Logger();
|
||||
this.options_ = options;
|
||||
this.authToken_ = null;
|
||||
this.dispatcher_ = new EventDispatcher();
|
||||
}
|
||||
|
||||
clientId() {
|
||||
return this.options_.id;
|
||||
}
|
||||
|
||||
clientSecret() {
|
||||
return this.options_.secret;
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
authToken() {
|
||||
return this.authToken_; // Without the "Bearer " prefix
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
return this.dispatcher_.on(eventName, callback);
|
||||
}
|
||||
|
||||
setAuthToken(v) {
|
||||
this.authToken_ = v;
|
||||
this.dispatcher_.dispatch('authRefreshed', this.authToken());
|
||||
}
|
||||
|
||||
loginUrl() {
|
||||
return `https://www.dropbox.com/oauth2/authorize?response_type=code&client_id=${this.clientId()}`;
|
||||
}
|
||||
|
||||
baseUrl(endPointFormat) {
|
||||
if (['content', 'api'].indexOf(endPointFormat) < 0) throw new Error(`Invalid end point format: ${endPointFormat}`);
|
||||
return `https://${endPointFormat}.dropboxapi.com/2`;
|
||||
}
|
||||
|
||||
requestToCurl_(url, options) {
|
||||
let output = [];
|
||||
output.push('curl');
|
||||
if (options.method) output.push(`-X ${options.method}`);
|
||||
if (options.headers) {
|
||||
for (let n in options.headers) {
|
||||
if (!options.headers.hasOwnProperty(n)) continue;
|
||||
output.push(`${'-H ' + '\''}${n}: ${options.headers[n]}'`);
|
||||
}
|
||||
}
|
||||
if (options.body) output.push(`${'--data ' + '"'}${options.body}"`);
|
||||
output.push(url);
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
async execAuthToken(authCode) {
|
||||
const postData = {
|
||||
code: authCode,
|
||||
grant_type: 'authorization_code',
|
||||
client_id: this.clientId(),
|
||||
client_secret: this.clientSecret(),
|
||||
};
|
||||
|
||||
var formBody = [];
|
||||
for (var property in postData) {
|
||||
var encodedKey = encodeURIComponent(property);
|
||||
var encodedValue = encodeURIComponent(postData[property]);
|
||||
formBody.push(`${encodedKey}=${encodedValue}`);
|
||||
}
|
||||
formBody = formBody.join('&');
|
||||
|
||||
const response = await shim.fetch('https://api.dropboxapi.com/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||
},
|
||||
body: formBody,
|
||||
});
|
||||
|
||||
const responseText = await response.text();
|
||||
if (!response.ok) throw new Error(responseText);
|
||||
return JSON.parse(responseText);
|
||||
}
|
||||
|
||||
isTokenError(status, responseText) {
|
||||
if (status === 401) return true;
|
||||
if (responseText.indexOf('OAuth 2 access token is malformed') >= 0) return true;
|
||||
// eg. Error: POST files/create_folder_v2: Error (400): Error in call to API function "files/create_folder_v2": Must provide HTTP header "Authorization" or URL parameter "authorization".
|
||||
if (responseText.indexOf('Must provide HTTP header "Authorization"') >= 0) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async exec(method, path = '', body = null, headers = null, options = null) {
|
||||
if (headers === null) headers = {};
|
||||
if (options === null) options = {};
|
||||
if (!options.target) options.target = 'string';
|
||||
|
||||
const authToken = this.authToken();
|
||||
|
||||
if (authToken) headers['Authorization'] = `Bearer ${authToken}`;
|
||||
|
||||
const endPointFormat = ['files/upload', 'files/download'].indexOf(path) >= 0 ? 'content' : 'api';
|
||||
|
||||
if (endPointFormat === 'api') {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
if (body && typeof body === 'object') body = JSON.stringify(body);
|
||||
} else {
|
||||
headers['Content-Type'] = 'application/octet-stream';
|
||||
}
|
||||
|
||||
const fetchOptions = {};
|
||||
fetchOptions.headers = headers;
|
||||
fetchOptions.method = method;
|
||||
if (options.path) fetchOptions.path = options.path;
|
||||
if (body) fetchOptions.body = body;
|
||||
|
||||
const url = path.indexOf('https://') === 0 ? path : `${this.baseUrl(endPointFormat)}/${path}`;
|
||||
|
||||
let tryCount = 0;
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
let response = null;
|
||||
|
||||
// console.info(this.requestToCurl_(url, fetchOptions));
|
||||
|
||||
// console.info(method + ' ' + url);
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
response = await shim.uploadBlob(url, fetchOptions);
|
||||
} else if (options.target == 'string') {
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
} else {
|
||||
// file
|
||||
response = await shim.fetchBlob(url, fetchOptions);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
// console.info('Response: ' + responseText);
|
||||
|
||||
let responseJson_ = null;
|
||||
const loadResponseJson = () => {
|
||||
if (!responseText) return null;
|
||||
if (responseJson_) return responseJson_;
|
||||
try {
|
||||
responseJson_ = JSON.parse(responseText);
|
||||
} catch (error) {
|
||||
return { error: responseText };
|
||||
}
|
||||
return responseJson_;
|
||||
};
|
||||
|
||||
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
const newError = message => {
|
||||
const json = loadResponseJson();
|
||||
let code = '';
|
||||
if (json && json.error_summary) {
|
||||
code = json.error_summary;
|
||||
}
|
||||
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = (`${responseText}`).substr(0, 1024);
|
||||
const error = new JoplinError(`${method} ${path}: ${message} (${response.status}): ${shortResponseText}`, code);
|
||||
error.httpStatus = response.status;
|
||||
return error;
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
if (this.isTokenError(response.status, responseText)) {
|
||||
this.setAuthToken(null);
|
||||
}
|
||||
|
||||
// When using fetchBlob we only get a string (not xml or json) back
|
||||
if (options.target === 'file') throw newError('fetchBlob error');
|
||||
|
||||
throw newError('Error');
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
||||
return loadResponseJson();
|
||||
} catch (error) {
|
||||
tryCount++;
|
||||
if (error && typeof error.code === 'string' && error.code.indexOf('too_many_write_operations') >= 0) {
|
||||
this.logger().warn(`too_many_write_operations ${tryCount}`);
|
||||
if (tryCount >= 3) {
|
||||
throw error;
|
||||
}
|
||||
await time.sleep(tryCount * 2);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DropboxApi;
|
33
Server/dist/lib/EventDispatcher.js
vendored
33
Server/dist/lib/EventDispatcher.js
vendored
@ -1,33 +0,0 @@
|
||||
class EventDispatcher {
|
||||
constructor() {
|
||||
this.listeners_ = [];
|
||||
}
|
||||
|
||||
dispatch(eventName, event = null) {
|
||||
if (!this.listeners_[eventName]) return;
|
||||
|
||||
let ls = this.listeners_[eventName];
|
||||
for (let i = 0; i < ls.length; i++) {
|
||||
ls[i](event);
|
||||
}
|
||||
}
|
||||
|
||||
on(eventName, callback) {
|
||||
if (!this.listeners_[eventName]) this.listeners_[eventName] = [];
|
||||
this.listeners_[eventName].push(callback);
|
||||
}
|
||||
|
||||
off(eventName, callback) {
|
||||
if (!this.listeners_[eventName]) return;
|
||||
|
||||
let ls = this.listeners_[eventName];
|
||||
for (let i = 0; i < ls.length; i++) {
|
||||
if (ls[i] === callback) {
|
||||
ls.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = EventDispatcher;
|
21
Server/dist/lib/HtmlToMd.js
vendored
21
Server/dist/lib/HtmlToMd.js
vendored
@ -1,21 +0,0 @@
|
||||
const TurndownService = require('joplin-turndown');
|
||||
const markdownUtils = require('lib/markdownUtils');
|
||||
|
||||
class HtmlToMd {
|
||||
parse(html, options = {}) {
|
||||
const turndownPluginGfm = require('joplin-turndown-plugin-gfm').gfm;
|
||||
const turndown = new TurndownService({
|
||||
headingStyle: 'atx',
|
||||
anchorNames: options.anchorNames ? options.anchorNames.map(n => n.trim().toLowerCase()) : [],
|
||||
codeBlockStyle: 'fenced',
|
||||
});
|
||||
turndown.use(turndownPluginGfm);
|
||||
turndown.remove('script');
|
||||
turndown.remove('style');
|
||||
let md = turndown.turndown(html);
|
||||
if (options.baseUrl) md = markdownUtils.prependBaseUrl(md, options.baseUrl);
|
||||
return md;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = HtmlToMd;
|
8
Server/dist/lib/JoplinError.js
vendored
8
Server/dist/lib/JoplinError.js
vendored
@ -1,8 +0,0 @@
|
||||
class JoplinError extends Error {
|
||||
constructor(message, code = null) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = JoplinError;
|
184
Server/dist/lib/JoplinServerApi.js
vendored
184
Server/dist/lib/JoplinServerApi.js
vendored
File diff suppressed because one or more lines are too long
150
Server/dist/lib/JoplinServerApi.ts
vendored
150
Server/dist/lib/JoplinServerApi.ts
vendored
@ -1,150 +0,0 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { rtrimSlashes } = require('lib/path-utils.js');
|
||||
const { shim } = require('lib/shim');
|
||||
|
||||
export interface JoplinServerApiOption {
|
||||
baseUrl: Function
|
||||
}
|
||||
|
||||
export default class JoplinServerApi {
|
||||
|
||||
logger_:any = null;
|
||||
options_:any = null;
|
||||
sessionId_:string = null;
|
||||
|
||||
constructor(options:any) {
|
||||
this.logger_ = new Logger();
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
setLogger(l:any) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
baseUrl() {
|
||||
return rtrimSlashes(this.options_.baseUrl());
|
||||
}
|
||||
|
||||
setSessionId(v:string) {
|
||||
this.sessionId_ = v;
|
||||
}
|
||||
|
||||
sessionId() {
|
||||
return this.sessionId_;
|
||||
}
|
||||
|
||||
requestToCurl_(url:string, options:any) {
|
||||
let output = [];
|
||||
output.push('curl');
|
||||
output.push('-v');
|
||||
if (options.method) output.push(`-X ${options.method}`);
|
||||
if (options.headers) {
|
||||
for (let n in options.headers) {
|
||||
if (!options.headers.hasOwnProperty(n)) continue;
|
||||
output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
|
||||
}
|
||||
}
|
||||
if (options.body) output.push(`${'--data ' + '\''}${options.body}'`);
|
||||
output.push(url);
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
async exec(method:string, path:string = '', body:any = null, headers:any = null, options:any = null):Promise<any> {
|
||||
if (headers === null) headers = {};
|
||||
if (options === null) options = {};
|
||||
if (!options.responseFormat) options.responseFormat = 'json';
|
||||
if (!options.target) options.target = 'string';
|
||||
|
||||
if (this.sessionId()) headers['X-API-AUTH'] = this.sessionId();
|
||||
|
||||
const fetchOptions:any = {};
|
||||
fetchOptions.headers = headers;
|
||||
fetchOptions.method = method;
|
||||
if (options.path) fetchOptions.path = options.path;
|
||||
if (body) fetchOptions.body = body;
|
||||
|
||||
const url:string = `${this.baseUrl()}/${path}`;
|
||||
|
||||
let response:any = null;
|
||||
|
||||
console.info('Joplin API Call', method + ' ' + url, headers, options);
|
||||
console.info(this.requestToCurl_(url, fetchOptions));
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
// if (fetchOptions.path) {
|
||||
// const fileStat = await shim.fsDriver().stat(fetchOptions.path);
|
||||
// if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
|
||||
// }
|
||||
// response = await shim.uploadBlob(url, fetchOptions);
|
||||
} else if (options.target == 'string') {
|
||||
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
} else {
|
||||
// file
|
||||
response = await shim.fetchBlob(url, fetchOptions);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
console.info('Joplin API Response', responseText);
|
||||
|
||||
// // Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
// const newError = (message, code = 0) => {
|
||||
// // Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// // JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
// const shortResponseText = (`${responseText}`).substr(0, 1024);
|
||||
// return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
|
||||
// };
|
||||
|
||||
// let responseJson_ = null;
|
||||
// const loadResponseJson = async () => {
|
||||
// if (!responseText) return null;
|
||||
// if (responseJson_) return responseJson_;
|
||||
// // eslint-disable-next-line require-atomic-updates
|
||||
// responseJson_ = await this.xmlToJson(responseText);
|
||||
// if (!responseJson_) throw newError('Cannot parse XML response', response.status);
|
||||
// return responseJson_;
|
||||
// };
|
||||
|
||||
// if (!response.ok) {
|
||||
// // When using fetchBlob we only get a string (not xml or json) back
|
||||
// if (options.target === 'file') throw newError('fetchBlob error', response.status);
|
||||
|
||||
// let json = null;
|
||||
// try {
|
||||
// json = await loadResponseJson();
|
||||
// } catch (error) {
|
||||
// // Just send back the plain text in newErro()
|
||||
// }
|
||||
|
||||
// if (json && json['d:error']) {
|
||||
// const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
|
||||
// const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join('\n') : 'Unknown error 1';
|
||||
// throw newError(`${message} (Exception ${code})`, response.status);
|
||||
// }
|
||||
|
||||
// throw newError('Unknown error 2', response.status);
|
||||
// }
|
||||
|
||||
// if (options.responseFormat === 'text') return responseText;
|
||||
|
||||
// // The following methods may have a response depending on the server but it's not
|
||||
// // standard (some return a plain string, other XML, etc.) and we don't check the
|
||||
// // response anyway since we rely on the HTTP status code so return null.
|
||||
// if (['MKCOL', 'DELETE', 'PUT', 'MOVE'].indexOf(method) >= 0) return null;
|
||||
|
||||
// const output = await loadResponseJson();
|
||||
// this.handleNginxHack_(output, newError);
|
||||
|
||||
// // Check that we didn't get for example an HTML page (as an error) instead of the JSON response
|
||||
// // null responses are possible, for example for DELETE calls
|
||||
// if (output !== null && typeof output === 'object' && !('d:multistatus' in output)) throw newError('Not a valid WebDAV response');
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
37
Server/dist/lib/ModelCache.js
vendored
37
Server/dist/lib/ModelCache.js
vendored
@ -1,37 +0,0 @@
|
||||
class ModelCache {
|
||||
constructor(maxSize) {
|
||||
this.cache_ = [];
|
||||
this.maxSize_ = maxSize;
|
||||
}
|
||||
|
||||
fromCache(ModelClass, id) {
|
||||
for (let i = 0; i < this.cache_.length; i++) {
|
||||
const c = this.cache_[i];
|
||||
if (c.id === id && c.modelType === ModelClass.modelType()) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
cache(ModelClass, id, model) {
|
||||
if (this.fromCache(ModelClass, model.id)) return;
|
||||
this.cache_.push({
|
||||
id: id,
|
||||
model: model,
|
||||
modelType: ModelClass.modelType(),
|
||||
});
|
||||
|
||||
if (this.cache_.length > this.maxSize_) {
|
||||
this.cache_.splice(0, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async load(ModelClass, id) {
|
||||
const cached = this.fromCache(ModelClass, id);
|
||||
if (cached) return cached.model;
|
||||
const output = await ModelClass.load(id);
|
||||
this.cache(ModelClass, id, output);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ModelCache;
|
63
Server/dist/lib/ObjectUtils.js
vendored
63
Server/dist/lib/ObjectUtils.js
vendored
@ -1,63 +0,0 @@
|
||||
const ObjectUtils = {};
|
||||
|
||||
ObjectUtils.sortByValue = function(object) {
|
||||
const temp = [];
|
||||
for (let k in object) {
|
||||
if (!object.hasOwnProperty(k)) continue;
|
||||
temp.push({
|
||||
key: k,
|
||||
value: object[k],
|
||||
});
|
||||
}
|
||||
|
||||
temp.sort(function(a, b) {
|
||||
let v1 = a.value;
|
||||
let v2 = b.value;
|
||||
if (typeof v1 === 'string') v1 = v1.toLowerCase();
|
||||
if (typeof v2 === 'string') v2 = v2.toLowerCase();
|
||||
if (v1 === v2) return 0;
|
||||
return v1 < v2 ? -1 : +1;
|
||||
});
|
||||
|
||||
const output = {};
|
||||
for (let i = 0; i < temp.length; i++) {
|
||||
const item = temp[i];
|
||||
output[item.key] = item.value;
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
ObjectUtils.fieldsEqual = function(o1, o2) {
|
||||
if ((!o1 || !o2) && o1 !== o2) return false;
|
||||
|
||||
for (let k in o1) {
|
||||
if (!o1.hasOwnProperty(k)) continue;
|
||||
if (o1[k] !== o2[k]) return false;
|
||||
}
|
||||
|
||||
const c1 = Object.getOwnPropertyNames(o1);
|
||||
const c2 = Object.getOwnPropertyNames(o2);
|
||||
|
||||
if (c1.length !== c2.length) return false;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
ObjectUtils.convertValuesToFunctions = function(o) {
|
||||
const output = {};
|
||||
for (let n in o) {
|
||||
if (!o.hasOwnProperty(n)) continue;
|
||||
output[n] = () => {
|
||||
return typeof o[n] === 'function' ? o[n]() : o[n];
|
||||
};
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
ObjectUtils.isEmpty = function(o) {
|
||||
if (!o) return true;
|
||||
return Object.keys(o).length === 0 && o.constructor === Object;
|
||||
};
|
||||
|
||||
module.exports = ObjectUtils;
|
74
Server/dist/lib/SyncTargetDropbox.js
vendored
74
Server/dist/lib/SyncTargetDropbox.js
vendored
@ -1,74 +0,0 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const DropboxApi = require('lib/DropboxApi');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { parameters } = require('lib/parameters.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { FileApiDriverDropbox } = require('lib/file-api-driver-dropbox.js');
|
||||
|
||||
class SyncTargetDropbox extends BaseSyncTarget {
|
||||
static id() {
|
||||
return 7;
|
||||
}
|
||||
|
||||
constructor(db, options = null) {
|
||||
super(db, options);
|
||||
this.api_ = null;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'dropbox';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('Dropbox');
|
||||
}
|
||||
|
||||
authRouteName() {
|
||||
return 'DropboxLogin';
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
const f = await this.fileApi();
|
||||
return !!f
|
||||
.driver()
|
||||
.api()
|
||||
.authToken();
|
||||
}
|
||||
|
||||
async api() {
|
||||
const fileApi = await this.fileApi();
|
||||
return fileApi.driver().api();
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const params = parameters().dropbox;
|
||||
|
||||
const api = new DropboxApi({
|
||||
id: params.id,
|
||||
secret: params.secret,
|
||||
});
|
||||
|
||||
api.on('authRefreshed', auth => {
|
||||
this.logger().info('Saving updated Dropbox auth.');
|
||||
Setting.setValue(`sync.${SyncTargetDropbox.id()}.auth`, auth ? auth : null);
|
||||
});
|
||||
|
||||
const authToken = Setting.value(`sync.${SyncTargetDropbox.id()}.auth`);
|
||||
api.setAuthToken(authToken);
|
||||
|
||||
const appDir = '';
|
||||
const fileApi = new FileApi(appDir, new FileApiDriverDropbox(api));
|
||||
fileApi.setSyncTargetId(SyncTargetDropbox.id());
|
||||
fileApi.setLogger(this.logger());
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
if (!(await this.isAuthenticated())) throw new Error('User is not authentified');
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetDropbox;
|
40
Server/dist/lib/SyncTargetFilesystem.js
vendored
40
Server/dist/lib/SyncTargetFilesystem.js
vendored
@ -1,40 +0,0 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { FileApiDriverLocal } = require('lib/file-api-driver-local.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
|
||||
class SyncTargetFilesystem extends BaseSyncTarget {
|
||||
static id() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'filesystem';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('File system');
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const syncPath = Setting.value('sync.2.path');
|
||||
const driver = new FileApiDriverLocal();
|
||||
const fileApi = new FileApi(syncPath, driver);
|
||||
fileApi.setLogger(this.logger());
|
||||
fileApi.setSyncTargetId(SyncTargetFilesystem.id());
|
||||
await driver.mkdir(syncPath);
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetFilesystem;
|
165
Server/dist/lib/SyncTargetJoplinServer.js
vendored
165
Server/dist/lib/SyncTargetJoplinServer.js
vendored
File diff suppressed because one or more lines are too long
85
Server/dist/lib/SyncTargetJoplinServer.ts
vendored
85
Server/dist/lib/SyncTargetJoplinServer.ts
vendored
@ -1,85 +0,0 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
// const { FileApi } = require('lib/file-api.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
// const WebDavApi = require('lib/WebDavApi');
|
||||
// const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
|
||||
|
||||
class SyncTargetJoplinServer extends BaseSyncTarget {
|
||||
|
||||
static id() {
|
||||
return 8;
|
||||
}
|
||||
|
||||
static supportsConfigCheck() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'joplinServer';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('Joplin Server');
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// static async newFileApi_(syncTargetId, options) {
|
||||
// const apiOptions = {
|
||||
// baseUrl: () => options.path(),
|
||||
// username: () => options.username(),
|
||||
// password: () => options.password(),
|
||||
// };
|
||||
|
||||
// return null;
|
||||
|
||||
// // const api = new WebDavApi(apiOptions);
|
||||
// // const driver = new FileApiDriverWebDav(api);
|
||||
// // const fileApi = new FileApi('', driver);
|
||||
// // fileApi.setSyncTargetId(syncTargetId);
|
||||
// // return fileApi;
|
||||
// }
|
||||
|
||||
static async checkConfig(options) {
|
||||
const fileApi = await SyncTargetJoplinServer.newFileApi_(SyncTargetJoplinServer.id(), options);
|
||||
fileApi.requestRepeatCount_ = 0;
|
||||
|
||||
const output = {
|
||||
ok: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await fileApi.stat('');
|
||||
if (!result) throw new Error(`Sync directory not found: ${options.path()}`);
|
||||
output.ok = true;
|
||||
} catch (error) {
|
||||
output.errorMessage = error.message;
|
||||
if (error.code) output.errorMessage += ` (Code ${error.code})`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const fileApi = await SyncTargetJoplinServer.newFileApi_(SyncTargetJoplinServer.id(), {
|
||||
path: () => Setting.value('sync.8.path'),
|
||||
username: () => Setting.value('sync.8.username'),
|
||||
password: () => Setting.value('sync.8.password'),
|
||||
});
|
||||
|
||||
fileApi.setLogger(this.logger());
|
||||
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetJoplinServer;
|
36
Server/dist/lib/SyncTargetMemory.js
vendored
36
Server/dist/lib/SyncTargetMemory.js
vendored
@ -1,36 +0,0 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { FileApiDriverMemory } = require('lib/file-api-driver-memory.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
|
||||
class SyncTargetMemory extends BaseSyncTarget {
|
||||
static id() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'memory';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return 'Memory';
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
initFileApi() {
|
||||
const fileApi = new FileApi('/root', new FileApiDriverMemory());
|
||||
fileApi.setLogger(this.logger());
|
||||
fileApi.setSyncTargetId(SyncTargetMemory.id());
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetMemory;
|
52
Server/dist/lib/SyncTargetNextcloud.js
vendored
52
Server/dist/lib/SyncTargetNextcloud.js
vendored
@ -1,52 +0,0 @@
|
||||
// The Nextcloud sync target is essentially a wrapper over the WebDAV sync target,
|
||||
// thus all the calls to SyncTargetWebDAV to avoid duplicate code.
|
||||
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const SyncTargetWebDAV = require('lib/SyncTargetWebDAV');
|
||||
|
||||
class SyncTargetNextcloud extends BaseSyncTarget {
|
||||
static id() {
|
||||
return 5;
|
||||
}
|
||||
|
||||
static supportsConfigCheck() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'nextcloud';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('Nextcloud');
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async checkConfig(options) {
|
||||
return SyncTargetWebDAV.checkConfig(options);
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const fileApi = await SyncTargetWebDAV.newFileApi_(SyncTargetNextcloud.id(), {
|
||||
path: () => Setting.value('sync.5.path'),
|
||||
username: () => Setting.value('sync.5.username'),
|
||||
password: () => Setting.value('sync.5.password'),
|
||||
});
|
||||
|
||||
fileApi.setLogger(this.logger());
|
||||
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetNextcloud;
|
87
Server/dist/lib/SyncTargetOneDrive.js
vendored
87
Server/dist/lib/SyncTargetOneDrive.js
vendored
@ -1,87 +0,0 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { OneDriveApi } = require('lib/onedrive-api.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { parameters } = require('lib/parameters.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const { FileApiDriverOneDrive } = require('lib/file-api-driver-onedrive.js');
|
||||
|
||||
class SyncTargetOneDrive extends BaseSyncTarget {
|
||||
static id() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
constructor(db, options = null) {
|
||||
super(db, options);
|
||||
this.api_ = null;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'onedrive';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('OneDrive');
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return this.api().auth();
|
||||
}
|
||||
|
||||
syncTargetId() {
|
||||
return SyncTargetOneDrive.id();
|
||||
}
|
||||
|
||||
oneDriveParameters() {
|
||||
return parameters().oneDrive;
|
||||
}
|
||||
|
||||
authRouteName() {
|
||||
return 'OneDriveLogin';
|
||||
}
|
||||
|
||||
api() {
|
||||
if (this.api_) return this.api_;
|
||||
|
||||
const isPublic = Setting.value('appType') != 'cli';
|
||||
|
||||
this.api_ = new OneDriveApi(this.oneDriveParameters().id, this.oneDriveParameters().secret, isPublic);
|
||||
this.api_.setLogger(this.logger());
|
||||
|
||||
this.api_.on('authRefreshed', a => {
|
||||
this.logger().info('Saving updated OneDrive auth.');
|
||||
Setting.setValue(`sync.${this.syncTargetId()}.auth`, a ? JSON.stringify(a) : null);
|
||||
});
|
||||
|
||||
let auth = Setting.value(`sync.${this.syncTargetId()}.auth`);
|
||||
if (auth) {
|
||||
try {
|
||||
auth = JSON.parse(auth);
|
||||
} catch (error) {
|
||||
this.logger().warn('Could not parse OneDrive auth token');
|
||||
this.logger().warn(error);
|
||||
auth = null;
|
||||
}
|
||||
|
||||
this.api_.setAuth(auth);
|
||||
}
|
||||
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const appDir = await this.api().appDirectory();
|
||||
const fileApi = new FileApi(appDir, new FileApiDriverOneDrive(this.api()));
|
||||
fileApi.setSyncTargetId(this.syncTargetId());
|
||||
fileApi.setLogger(this.logger());
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
if (!(await this.isAuthenticated())) throw new Error('User is not authentified');
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetOneDrive;
|
27
Server/dist/lib/SyncTargetOneDriveDev.js
vendored
27
Server/dist/lib/SyncTargetOneDriveDev.js
vendored
@ -1,27 +0,0 @@
|
||||
const SyncTargetOneDrive = require('lib/SyncTargetOneDrive.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { parameters } = require('lib/parameters.js');
|
||||
|
||||
class SyncTargetOneDriveDev extends SyncTargetOneDrive {
|
||||
static id() {
|
||||
return 4;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'onedrive_dev';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('OneDrive Dev (For testing only)');
|
||||
}
|
||||
|
||||
syncTargetId() {
|
||||
return SyncTargetOneDriveDev.id();
|
||||
}
|
||||
|
||||
oneDriveParameters() {
|
||||
return parameters('dev').oneDrive;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetOneDriveDev;
|
50
Server/dist/lib/SyncTargetRegistry.js
vendored
50
Server/dist/lib/SyncTargetRegistry.js
vendored
@ -1,50 +0,0 @@
|
||||
class SyncTargetRegistry {
|
||||
static classById(syncTargetId) {
|
||||
const info = SyncTargetRegistry.reg_[syncTargetId];
|
||||
if (!info) throw new Error(`Invalid id: ${syncTargetId}`);
|
||||
return info.classRef;
|
||||
}
|
||||
|
||||
static addClass(SyncTargetClass) {
|
||||
this.reg_[SyncTargetClass.id()] = {
|
||||
id: SyncTargetClass.id(),
|
||||
name: SyncTargetClass.targetName(),
|
||||
label: SyncTargetClass.label(),
|
||||
classRef: SyncTargetClass,
|
||||
supportsConfigCheck: SyncTargetClass.supportsConfigCheck(),
|
||||
};
|
||||
}
|
||||
|
||||
static nameToId(name) {
|
||||
for (let n in this.reg_) {
|
||||
if (!this.reg_.hasOwnProperty(n)) continue;
|
||||
if (this.reg_[n].name === name) return this.reg_[n].id;
|
||||
}
|
||||
throw new Error(`Name not found: ${name}`);
|
||||
}
|
||||
|
||||
static idToMetadata(id) {
|
||||
for (let n in this.reg_) {
|
||||
if (!this.reg_.hasOwnProperty(n)) continue;
|
||||
if (this.reg_[n].id === id) return this.reg_[n];
|
||||
}
|
||||
throw new Error(`ID not found: ${id}`);
|
||||
}
|
||||
|
||||
static idToName(id) {
|
||||
return this.idToMetadata(id).name;
|
||||
}
|
||||
|
||||
static idAndLabelPlainObject() {
|
||||
let output = {};
|
||||
for (let n in this.reg_) {
|
||||
if (!this.reg_.hasOwnProperty(n)) continue;
|
||||
output[n] = this.reg_[n].label;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
SyncTargetRegistry.reg_ = {};
|
||||
|
||||
module.exports = SyncTargetRegistry;
|
82
Server/dist/lib/SyncTargetWebDAV.js
vendored
82
Server/dist/lib/SyncTargetWebDAV.js
vendored
@ -1,82 +0,0 @@
|
||||
const BaseSyncTarget = require('lib/BaseSyncTarget.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { FileApi } = require('lib/file-api.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const WebDavApi = require('lib/WebDavApi');
|
||||
const { FileApiDriverWebDav } = require('lib/file-api-driver-webdav');
|
||||
|
||||
class SyncTargetWebDAV extends BaseSyncTarget {
|
||||
static id() {
|
||||
return 6;
|
||||
}
|
||||
|
||||
static supportsConfigCheck() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static targetName() {
|
||||
return 'webdav';
|
||||
}
|
||||
|
||||
static label() {
|
||||
return _('WebDAV');
|
||||
}
|
||||
|
||||
async isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
static async newFileApi_(syncTargetId, options) {
|
||||
const apiOptions = {
|
||||
baseUrl: () => options.path(),
|
||||
username: () => options.username(),
|
||||
password: () => options.password(),
|
||||
};
|
||||
|
||||
const api = new WebDavApi(apiOptions);
|
||||
const driver = new FileApiDriverWebDav(api);
|
||||
const fileApi = new FileApi('', driver);
|
||||
fileApi.setSyncTargetId(syncTargetId);
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
static async checkConfig(options) {
|
||||
const fileApi = await SyncTargetWebDAV.newFileApi_(SyncTargetWebDAV.id(), options);
|
||||
fileApi.requestRepeatCount_ = 0;
|
||||
|
||||
const output = {
|
||||
ok: false,
|
||||
errorMessage: '',
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await fileApi.stat('');
|
||||
if (!result) throw new Error(`WebDAV directory not found: ${options.path()}`);
|
||||
output.ok = true;
|
||||
} catch (error) {
|
||||
output.errorMessage = error.message;
|
||||
if (error.code) output.errorMessage += ` (Code ${error.code})`;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async initFileApi() {
|
||||
const fileApi = await SyncTargetWebDAV.newFileApi_(SyncTargetWebDAV.id(), {
|
||||
path: () => Setting.value('sync.6.path'),
|
||||
username: () => Setting.value('sync.6.username'),
|
||||
password: () => Setting.value('sync.6.password'),
|
||||
});
|
||||
|
||||
fileApi.setLogger(this.logger());
|
||||
|
||||
return fileApi;
|
||||
}
|
||||
|
||||
async initSynchronizer() {
|
||||
return new Synchronizer(this.db(), await this.fileApi(), Setting.value('appType'));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SyncTargetWebDAV;
|
126
Server/dist/lib/TaskQueue.js
vendored
126
Server/dist/lib/TaskQueue.js
vendored
@ -1,126 +0,0 @@
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
|
||||
class TaskQueue {
|
||||
constructor(name) {
|
||||
this.waitingTasks_ = [];
|
||||
this.processingTasks_ = {};
|
||||
this.processingQueue_ = false;
|
||||
this.stopping_ = false;
|
||||
this.results_ = {};
|
||||
this.name_ = name;
|
||||
this.logger_ = new Logger();
|
||||
}
|
||||
|
||||
concurrency() {
|
||||
return Setting.value('sync.maxConcurrentConnections');
|
||||
}
|
||||
|
||||
push(id, callback) {
|
||||
if (this.stopping_) throw new Error('Cannot push task when queue is stopping');
|
||||
|
||||
this.waitingTasks_.push({
|
||||
id: id,
|
||||
callback: callback,
|
||||
});
|
||||
this.processQueue_();
|
||||
}
|
||||
|
||||
processQueue_() {
|
||||
if (this.processingQueue_ || this.stopping_) return;
|
||||
|
||||
this.processingQueue_ = true;
|
||||
|
||||
const completeTask = (task, result, error) => {
|
||||
delete this.processingTasks_[task.id];
|
||||
|
||||
const r = {
|
||||
id: task.id,
|
||||
result: result,
|
||||
};
|
||||
|
||||
if (error) r.error = error;
|
||||
|
||||
this.results_[task.id] = r;
|
||||
|
||||
this.processQueue_();
|
||||
};
|
||||
|
||||
while (this.waitingTasks_.length > 0 && Object.keys(this.processingTasks_).length < this.concurrency()) {
|
||||
if (this.stopping_) break;
|
||||
|
||||
const task = this.waitingTasks_.splice(0, 1)[0];
|
||||
this.processingTasks_[task.id] = task;
|
||||
|
||||
task
|
||||
.callback()
|
||||
.then(result => {
|
||||
completeTask(task, result, null);
|
||||
})
|
||||
.catch(error => {
|
||||
if (!error) error = new Error('Unknown error');
|
||||
completeTask(task, null, error);
|
||||
});
|
||||
}
|
||||
|
||||
this.processingQueue_ = false;
|
||||
}
|
||||
|
||||
isWaiting(taskId) {
|
||||
return this.waitingTasks_.find(task => task.id === taskId);
|
||||
}
|
||||
|
||||
isProcessing(taskId) {
|
||||
return taskId in this.processingTasks_;
|
||||
}
|
||||
|
||||
isDone(taskId) {
|
||||
return taskId in this.results_;
|
||||
}
|
||||
|
||||
async waitForResult(taskId) {
|
||||
if (!this.isWaiting(taskId) && !this.isProcessing(taskId) && !this.isDone(taskId)) throw new Error(`No such task: ${taskId}`);
|
||||
|
||||
while (true) {
|
||||
// if (this.stopping_) {
|
||||
// return {
|
||||
// id: taskId,
|
||||
// error: new JoplinError('Queue has been destroyed', 'destroyedQueue'),
|
||||
// };
|
||||
// }
|
||||
|
||||
const task = this.results_[taskId];
|
||||
if (task) return task;
|
||||
await time.sleep(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.stopping_ = true;
|
||||
|
||||
this.logger_.info(`TaskQueue.stop: ${this.name_}: waiting for tasks to complete: ${Object.keys(this.processingTasks_).length}`);
|
||||
|
||||
// In general it's not a big issue if some tasks are still running because
|
||||
// it won't call anything unexpected in caller code, since the caller has
|
||||
// to explicitely retrieve the results
|
||||
const startTime = Date.now();
|
||||
while (Object.keys(this.processingTasks_).length) {
|
||||
await time.sleep(0.1);
|
||||
if (Date.now() - startTime >= 30000) {
|
||||
this.logger_.warn(`TaskQueue.stop: ${this.name_}: timed out waiting for task to complete`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger_.info(`TaskQueue.stop: ${this.name_}: Done, waited for ${Date.now() - startTime}`);
|
||||
}
|
||||
|
||||
isStopping() {
|
||||
return this.stopping_;
|
||||
}
|
||||
}
|
||||
|
||||
TaskQueue.CONCURRENCY = 5;
|
||||
|
||||
module.exports = TaskQueue;
|
68
Server/dist/lib/TemplateUtils.js
vendored
68
Server/dist/lib/TemplateUtils.js
vendored
@ -1,68 +0,0 @@
|
||||
const { shim } = require('lib/shim.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const Mustache = require('mustache');
|
||||
|
||||
const TemplateUtils = {};
|
||||
|
||||
|
||||
// Mustache escapes strings (including /) with the html code by default
|
||||
// This isn't useful for markdown so it's disabled
|
||||
Mustache.escape = text => {
|
||||
return text;
|
||||
};
|
||||
|
||||
TemplateUtils.render = function(input) {
|
||||
// new template variables can be added here
|
||||
// If there are too many, this should be moved to a new file
|
||||
// view needs to be set in this function so that the formats reflect settings
|
||||
const view = {
|
||||
date: time.formatMsToLocal(new Date().getTime(), time.dateFormat()),
|
||||
time: time.formatMsToLocal(new Date().getTime(), time.timeFormat()),
|
||||
datetime: time.formatMsToLocal(new Date().getTime()),
|
||||
custom_datetime: () => {
|
||||
return (text, render) => {
|
||||
return render(time.formatMsToLocal(new Date().getTime(), text));
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
return Mustache.render(input, view);
|
||||
};
|
||||
|
||||
TemplateUtils.loadTemplates = async function(filePath) {
|
||||
let templates = [];
|
||||
let files = [];
|
||||
|
||||
if (await shim.fsDriver().exists(filePath)) {
|
||||
try {
|
||||
files = await shim.fsDriver().readDirStats(filePath);
|
||||
} catch (error) {
|
||||
let msg = error.message ? error.message : '';
|
||||
msg = `Could not read template names from ${filePath}\n${msg}`;
|
||||
error.message = msg;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Make sure templates are always in the same order
|
||||
// sensitivity ensures that the sort will ignore case
|
||||
files.sort((a, b) => { return a.path.localeCompare(b.path, undefined, {sensitivity: 'accent'}); });
|
||||
|
||||
files.forEach(async file => {
|
||||
if (file.path.endsWith('.md')) {
|
||||
try {
|
||||
let fileString = await shim.fsDriver().readFile(`${filePath}/${file.path}`, 'utf-8');
|
||||
templates.push({ label: file.path, value: fileString });
|
||||
} catch (error) {
|
||||
let msg = error.message ? error.message : '';
|
||||
msg = `Could not load template ${file.path}\n${msg}`;
|
||||
error.message = msg;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return templates;
|
||||
};
|
||||
|
||||
module.exports = TemplateUtils;
|
401
Server/dist/lib/WebDavApi.js
vendored
401
Server/dist/lib/WebDavApi.js
vendored
@ -1,401 +0,0 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const parseXmlString = require('xml2js').parseString;
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
const URL = require('url-parse');
|
||||
const { rtrimSlashes } = require('lib/path-utils.js');
|
||||
const base64 = require('base-64');
|
||||
|
||||
// Note that the d: namespace (the DAV namespace) is specific to Nextcloud. The RFC for example uses "D:" however
|
||||
// we make all the tags and attributes lowercase so we handle both the Nextcloud style and RFC. Hopefully other
|
||||
// implementations use the same namespaces. If not, extra processing can be done in `nameProcessor`, for
|
||||
// example to convert a custom namespace to "d:" so that it can be used by the rest of the code.
|
||||
// In general, we should only deal with things in "d:", which is the standard DAV namespace.
|
||||
|
||||
class WebDavApi {
|
||||
constructor(options) {
|
||||
this.logger_ = new Logger();
|
||||
this.options_ = options;
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
authToken() {
|
||||
if (!this.options_.username() || !this.options_.password()) return null;
|
||||
try {
|
||||
// Note: Non-ASCII passwords will throw an error about Latin1 characters - https://github.com/laurent22/joplin/issues/246
|
||||
// Tried various things like the below, but it didn't work on React Native:
|
||||
//return base64.encode(utf8.encode(this.options_.username() + ':' + this.options_.password()));
|
||||
return base64.encode(`${this.options_.username()}:${this.options_.password()}`);
|
||||
} catch (error) {
|
||||
error.message = `Cannot encode username/password: ${error.message}`;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
baseUrl() {
|
||||
return rtrimSlashes(this.options_.baseUrl());
|
||||
}
|
||||
|
||||
relativeBaseUrl() {
|
||||
const url = new URL(this.baseUrl());
|
||||
return url.pathname + url.query;
|
||||
}
|
||||
|
||||
async xmlToJson(xml) {
|
||||
let davNamespaces = []; // Yes, there can be more than one... xmlns:a="DAV:" xmlns:D="DAV:"
|
||||
|
||||
const nameProcessor = name => {
|
||||
if (name.indexOf('xmlns:') !== 0) {
|
||||
// Check if the current name is within the DAV namespace. If it is, normalise it
|
||||
// by moving it to the "d:" namespace, which is what all the functions are using.
|
||||
const p = name.split(':');
|
||||
if (p.length == 2) {
|
||||
const ns = p[0];
|
||||
if (davNamespaces.indexOf(ns) >= 0) {
|
||||
name = `d:${p[1]}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
return name.toLowerCase();
|
||||
};
|
||||
|
||||
const attrValueProcessor = (value, name) => {
|
||||
if (value.toLowerCase() === 'dav:') {
|
||||
const p = name.split(':');
|
||||
davNamespaces.push(p[p.length - 1]);
|
||||
}
|
||||
};
|
||||
|
||||
const options = {
|
||||
tagNameProcessors: [nameProcessor],
|
||||
attrNameProcessors: [nameProcessor],
|
||||
attrValueProcessors: [attrValueProcessor],
|
||||
};
|
||||
|
||||
return new Promise((resolve) => {
|
||||
parseXmlString(xml, options, (error, result) => {
|
||||
if (error) {
|
||||
resolve(null); // Error handled by caller which will display the XML text (or plain text) if null is returned from this function
|
||||
return;
|
||||
}
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
valueFromJson(json, keys, type) {
|
||||
let output = json;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
|
||||
// console.info(key, typeof key, typeof output, typeof output === 'object' && (key in output), Array.isArray(output));
|
||||
|
||||
if (typeof key === 'number' && !Array.isArray(output)) return null;
|
||||
if (typeof key === 'string' && (typeof output !== 'object' || !(key in output))) return null;
|
||||
output = output[key];
|
||||
}
|
||||
|
||||
if (type === 'string') {
|
||||
// If the XML has not attribute the value is directly a string
|
||||
// If the XML node has attributes, the value is under "_".
|
||||
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
|
||||
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
|
||||
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
|
||||
if (typeof output === 'object' && '_' in output) output = output['_'];
|
||||
if (typeof output !== 'string') return null;
|
||||
return output;
|
||||
}
|
||||
|
||||
if (type === 'object') {
|
||||
if (!Array.isArray(output) && typeof output === 'object') return output;
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type === 'array') {
|
||||
return Array.isArray(output) ? output : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
stringFromJson(json, keys) {
|
||||
return this.valueFromJson(json, keys, 'string');
|
||||
}
|
||||
|
||||
objectFromJson(json, keys) {
|
||||
return this.valueFromJson(json, keys, 'object');
|
||||
}
|
||||
|
||||
arrayFromJson(json, keys) {
|
||||
return this.valueFromJson(json, keys, 'array');
|
||||
}
|
||||
|
||||
resourcePropByName(resource, outputType, propName) {
|
||||
const propStats = resource['d:propstat'];
|
||||
let output = null;
|
||||
if (!Array.isArray(propStats)) throw new Error('Missing d:propstat property');
|
||||
for (let i = 0; i < propStats.length; i++) {
|
||||
const props = propStats[i]['d:prop'];
|
||||
if (!Array.isArray(props) || !props.length) continue;
|
||||
const prop = props[0];
|
||||
if (Array.isArray(prop[propName])) {
|
||||
output = prop[propName];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (outputType === 'string') {
|
||||
// If the XML has not attribute the value is directly a string
|
||||
// If the XML node has attributes, the value is under "_".
|
||||
// Eg for this XML, the string will be under {"_":"Thu, 01 Feb 2018 17:24:05 GMT"}:
|
||||
// <a:getlastmodified b:dt="dateTime.rfc1123">Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
// For this XML, the value will be "Thu, 01 Feb 2018 17:24:05 GMT"
|
||||
// <a:getlastmodified>Thu, 01 Feb 2018 17:24:05 GMT</a:getlastmodified>
|
||||
|
||||
output = output[0];
|
||||
|
||||
if (typeof output === 'object' && '_' in output) output = output['_'];
|
||||
if (typeof output !== 'string') return null;
|
||||
return output;
|
||||
}
|
||||
|
||||
if (outputType === 'array') {
|
||||
return output;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid output type: ${outputType}`);
|
||||
}
|
||||
|
||||
async execPropFind(path, depth, fields = null, options = null) {
|
||||
if (fields === null) fields = ['d:getlastmodified'];
|
||||
|
||||
let fieldsXml = '';
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
fieldsXml += `<${fields[i]}/>`;
|
||||
}
|
||||
|
||||
// To find all available properties:
|
||||
//
|
||||
// const body=`<?xml version="1.0" encoding="utf-8" ?>
|
||||
// <propfind xmlns="DAV:">
|
||||
// <propname/>
|
||||
// </propfind>`;
|
||||
|
||||
const body =
|
||||
`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:propfind xmlns:d="DAV:">
|
||||
<d:prop xmlns:oc="http://owncloud.org/ns">
|
||||
${fieldsXml}
|
||||
</d:prop>
|
||||
</d:propfind>`;
|
||||
|
||||
return this.exec('PROPFIND', path, body, { Depth: depth }, options);
|
||||
}
|
||||
|
||||
requestToCurl_(url, options) {
|
||||
let output = [];
|
||||
output.push('curl');
|
||||
output.push('-v');
|
||||
if (options.method) output.push(`-X ${options.method}`);
|
||||
if (options.headers) {
|
||||
for (let n in options.headers) {
|
||||
if (!options.headers.hasOwnProperty(n)) continue;
|
||||
output.push(`${'-H ' + '"'}${n}: ${options.headers[n]}"`);
|
||||
}
|
||||
}
|
||||
if (options.body) output.push(`${'--data ' + '\''}${options.body}'`);
|
||||
output.push(url);
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
handleNginxHack_(jsonResponse, newErrorHandler) {
|
||||
// Trying to fix 404 error issue with Nginx WebDAV server.
|
||||
// https://github.com/laurent22/joplin/issues/624
|
||||
// https://github.com/laurent22/joplin/issues/808
|
||||
// Not tested but someone confirmed it worked - https://github.com/laurent22/joplin/issues/808#issuecomment-443552858
|
||||
// and fix is narrowly scoped so shouldn't affect anything outside this particular edge case.
|
||||
//
|
||||
// The issue is that instead of an HTTP 404 status code, Nginx returns 200 but with this response:
|
||||
//
|
||||
// <?xml version="1.0" encoding="utf-8" ?>
|
||||
// <D:multistatus xmlns:D="DAV:">
|
||||
// <D:response>
|
||||
// <D:href>/notes/ecd4027a5271483984b00317433e2c66.md</D:href>
|
||||
// <D:propstat>
|
||||
// <D:prop/>
|
||||
// <D:status>HTTP/1.1 404 Not Found</D:status>
|
||||
// </D:propstat>
|
||||
// </D:response>
|
||||
// </D:multistatus>
|
||||
//
|
||||
// So we need to parse this and find that it is in fact a 404 error.
|
||||
//
|
||||
// HOWEVER, some implementations also return 404 for missing props, for example SeaFile:
|
||||
// (indicates that the props "getlastmodified" is not present, but this call is only
|
||||
// used when checking the conf, so we don't really need it)
|
||||
// https://github.com/laurent22/joplin/issues/1137
|
||||
//
|
||||
// <?xml version='1.0' encoding='UTF-8'?>
|
||||
// <ns0:multistatus xmlns:ns0="DAV:">
|
||||
// <ns0:response>
|
||||
// <ns0:href>/seafdav/joplin/</ns0:href>
|
||||
// <ns0:propstat>
|
||||
// <ns0:prop>
|
||||
// <ns0:getlastmodified/>
|
||||
// </ns0:prop>
|
||||
// <ns0:status>HTTP/1.1 404 Not Found</ns0:status>
|
||||
// </ns0:propstat>
|
||||
// <ns0:propstat>
|
||||
// <ns0:prop>
|
||||
// <ns0:resourcetype>
|
||||
// <ns0:collection/>
|
||||
// </ns0:resourcetype>
|
||||
// </ns0:prop>
|
||||
// <ns0:status>HTTP/1.1 200 OK</ns0:status>
|
||||
// </ns0:propstat>
|
||||
// </ns0:response>
|
||||
// </ns0:multistatus>
|
||||
//
|
||||
// As a simple fix for now it's enough to check if ALL the statuses are 404 - in that case
|
||||
// it really means that the file doesn't exist. Otherwise we can proceed as usual.
|
||||
const responseArray = this.arrayFromJson(jsonResponse, ['d:multistatus', 'd:response']);
|
||||
if (responseArray && responseArray.length === 1) {
|
||||
const propStats = this.arrayFromJson(jsonResponse, ['d:multistatus', 'd:response', 0, 'd:propstat']);
|
||||
if (!propStats.length) return;
|
||||
let count404 = 0;
|
||||
for (let i = 0; i < propStats.length; i++) {
|
||||
const status = this.arrayFromJson(jsonResponse, ['d:multistatus', 'd:response', 0, 'd:propstat', i, 'd:status']);
|
||||
if (status && status.length && status[0].indexOf('404') >= 0) count404++;
|
||||
}
|
||||
|
||||
if (count404 === propStats.length) throw newErrorHandler('Not found', 404);
|
||||
}
|
||||
}
|
||||
|
||||
// curl -u admin:123456 'http://nextcloud.local/remote.php/dav/files/admin/' -X PROPFIND --data '<?xml version="1.0" encoding="UTF-8"?>
|
||||
// <d:propfind xmlns:d="DAV:">
|
||||
// <d:prop xmlns:oc="http://owncloud.org/ns">
|
||||
// <d:getlastmodified/>
|
||||
// </d:prop>
|
||||
// </d:propfind>'
|
||||
|
||||
async exec(method, path = '', body = null, headers = null, options = null) {
|
||||
if (headers === null) headers = {};
|
||||
if (options === null) options = {};
|
||||
if (!options.responseFormat) options.responseFormat = 'json';
|
||||
if (!options.target) options.target = 'string';
|
||||
|
||||
const authToken = this.authToken();
|
||||
|
||||
if (authToken) headers['Authorization'] = `Basic ${authToken}`;
|
||||
|
||||
// On iOS, the network lib appends a If-None-Match header to PROPFIND calls, which is kind of correct because
|
||||
// the call is idempotent and thus could be cached. According to RFC-7232 though only GET and HEAD should have
|
||||
// this header for caching purposes. It makes no mention of PROPFIND.
|
||||
// So possibly because of this, Seafile (and maybe other WebDAV implementations) responds with a "412 Precondition Failed"
|
||||
// error when this header is present for PROPFIND call on existing resources. This is also kind of correct because there is a resource
|
||||
// with this eTag and since this is neither a GET nor HEAD call, it is supposed to respond with 412 if the resource is present.
|
||||
// The "solution", an ugly one, is to send a purposely invalid string as eTag, which will bypass the If-None-Match check - Seafile
|
||||
// finds out that no resource has this ID and simply sends the requested data.
|
||||
// Also add a random value to make sure the eTag is unique for each call.
|
||||
if (['GET', 'HEAD'].indexOf(method) < 0) headers['If-None-Match'] = `JoplinIgnore-${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
const fetchOptions = {};
|
||||
fetchOptions.headers = headers;
|
||||
fetchOptions.method = method;
|
||||
if (options.path) fetchOptions.path = options.path;
|
||||
if (body) fetchOptions.body = body;
|
||||
|
||||
const url = `${this.baseUrl()}/${path}`;
|
||||
|
||||
let response = null;
|
||||
|
||||
// console.info('WebDAV Call', method + ' ' + url, headers, options);
|
||||
// console.info(this.requestToCurl_(url, fetchOptions));
|
||||
|
||||
if (options.source == 'file' && (method == 'POST' || method == 'PUT')) {
|
||||
if (fetchOptions.path) {
|
||||
const fileStat = await shim.fsDriver().stat(fetchOptions.path);
|
||||
if (fileStat) fetchOptions.headers['Content-Length'] = `${fileStat.size}`;
|
||||
}
|
||||
response = await shim.uploadBlob(url, fetchOptions);
|
||||
} else if (options.target == 'string') {
|
||||
if (typeof body === 'string') fetchOptions.headers['Content-Length'] = `${shim.stringByteLength(body)}`;
|
||||
response = await shim.fetch(url, fetchOptions);
|
||||
} else {
|
||||
// file
|
||||
response = await shim.fetchBlob(url, fetchOptions);
|
||||
}
|
||||
|
||||
const responseText = await response.text();
|
||||
|
||||
// console.info('WebDAV Response', responseText);
|
||||
|
||||
// Creates an error object with as much data as possible as it will appear in the log, which will make debugging easier
|
||||
const newError = (message, code = 0) => {
|
||||
// Gives a shorter response for error messages. Useful for cases where a full HTML page is accidentally loaded instead of
|
||||
// JSON. That way the error message will still show there's a problem but without filling up the log or screen.
|
||||
const shortResponseText = (`${responseText}`).substr(0, 1024);
|
||||
return new JoplinError(`${method} ${path}: ${message} (${code}): ${shortResponseText}`, code);
|
||||
};
|
||||
|
||||
let responseJson_ = null;
|
||||
const loadResponseJson = async () => {
|
||||
if (!responseText) return null;
|
||||
if (responseJson_) return responseJson_;
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
responseJson_ = await this.xmlToJson(responseText);
|
||||
if (!responseJson_) throw newError('Cannot parse XML response', response.status);
|
||||
return responseJson_;
|
||||
};
|
||||
|
||||
if (!response.ok) {
|
||||
// When using fetchBlob we only get a string (not xml or json) back
|
||||
if (options.target === 'file') throw newError('fetchBlob error', response.status);
|
||||
|
||||
let json = null;
|
||||
try {
|
||||
json = await loadResponseJson();
|
||||
} catch (error) {
|
||||
// Just send back the plain text in newErro()
|
||||
}
|
||||
|
||||
if (json && json['d:error']) {
|
||||
const code = json['d:error']['s:exception'] ? json['d:error']['s:exception'].join(' ') : response.status;
|
||||
const message = json['d:error']['s:message'] ? json['d:error']['s:message'].join('\n') : 'Unknown error 1';
|
||||
throw newError(`${message} (Exception ${code})`, response.status);
|
||||
}
|
||||
|
||||
throw newError('Unknown error 2', response.status);
|
||||
}
|
||||
|
||||
if (options.responseFormat === 'text') return responseText;
|
||||
|
||||
// The following methods may have a response depending on the server but it's not
|
||||
// standard (some return a plain string, other XML, etc.) and we don't check the
|
||||
// response anyway since we rely on the HTTP status code so return null.
|
||||
if (['MKCOL', 'DELETE', 'PUT', 'MOVE'].indexOf(method) >= 0) return null;
|
||||
|
||||
const output = await loadResponseJson();
|
||||
this.handleNginxHack_(output, newError);
|
||||
|
||||
// Check that we didn't get for example an HTML page (as an error) instead of the JSON response
|
||||
// null responses are possible, for example for DELETE calls
|
||||
if (output !== null && typeof output === 'object' && !('d:multistatus' in output)) throw newError('Not a valid WebDAV response');
|
||||
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WebDavApi;
|
80
Server/dist/lib/WelcomeUtils.js
vendored
80
Server/dist/lib/WelcomeUtils.js
vendored
@ -1,80 +0,0 @@
|
||||
const welcomeAssets = require('./welcomeAssets');
|
||||
const Note = require('lib/models/Note');
|
||||
const Setting = require('lib/models/Setting');
|
||||
const Folder = require('lib/models/Folder');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const { shim } = require('lib/shim');
|
||||
const { uuid } = require('lib/uuid');
|
||||
const { fileExtension, basename } = require('lib/path-utils');
|
||||
const { pregQuote } = require('lib/string-utils');
|
||||
|
||||
class WelcomeUtils {
|
||||
static async createWelcomeItems() {
|
||||
const output = {
|
||||
defaultFolderId: null,
|
||||
};
|
||||
|
||||
const folderAssets = welcomeAssets.folders;
|
||||
const tempDir = Setting.value('resourceDir');
|
||||
|
||||
for (let i = 0; i < folderAssets.length; i++) {
|
||||
const folderAsset = folderAssets[i];
|
||||
const folder = await Folder.save({ title: `${folderAsset.title} (${Setting.appTypeToLabel(Setting.value('appType'))})` });
|
||||
if (!output.defaultFolderId) output.defaultFolderId = folder.id;
|
||||
}
|
||||
|
||||
const noteAssets = welcomeAssets.notes;
|
||||
|
||||
for (let i = noteAssets.length - 1; i >= 0; i--) {
|
||||
const noteAsset = noteAssets[i];
|
||||
|
||||
let noteBody = noteAsset.body;
|
||||
|
||||
for (let resourceUrl in noteAsset.resources) {
|
||||
if (!noteAsset.resources.hasOwnProperty(resourceUrl)) continue;
|
||||
const resourceAsset = noteAsset.resources[resourceUrl];
|
||||
const ext = fileExtension(resourceUrl);
|
||||
const tempFilePath = `${tempDir}/${uuid.create()}.tmp.${ext}`;
|
||||
await shim.fsDriver().writeFile(tempFilePath, resourceAsset.body, 'base64');
|
||||
const resource = await shim.createResourceFromPath(tempFilePath, {
|
||||
title: basename(resourceUrl),
|
||||
});
|
||||
await shim.fsDriver().remove(tempFilePath);
|
||||
|
||||
const regex = new RegExp(pregQuote(`(${resourceUrl})`), 'g');
|
||||
noteBody = noteBody.replace(regex, `(:/${resource.id})`);
|
||||
}
|
||||
|
||||
const note = await Note.save({
|
||||
parent_id: output.defaultFolderId,
|
||||
title: noteAsset.title,
|
||||
body: noteBody,
|
||||
});
|
||||
|
||||
if (noteAsset.tags) await Tag.setNoteTagsByTitles(note.id, noteAsset.tags);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
static async install(dispatch) {
|
||||
if (!Setting.value('welcome.enabled')) {
|
||||
Setting.setValue('welcome.wasBuilt', true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Setting.value('welcome.wasBuilt')) {
|
||||
const result = await WelcomeUtils.createWelcomeItems();
|
||||
Setting.setValue('welcome.wasBuilt', true);
|
||||
|
||||
dispatch({
|
||||
type: 'FOLDER_SELECT',
|
||||
id: result.defaultFolderId,
|
||||
});
|
||||
|
||||
Setting.setValue('activeFolderId', result.defaultFolderId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = WelcomeUtils;
|
93
Server/dist/lib/components/CameraView.js
vendored
93
Server/dist/lib/components/CameraView.js
vendored
@ -1,93 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { View, TouchableOpacity } = require('react-native');
|
||||
import { RNCamera } from 'react-native-camera';
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class CameraView extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
snapping: false,
|
||||
};
|
||||
|
||||
this.back_onPress = this.back_onPress.bind(this);
|
||||
this.photo_onPress = this.photo_onPress.bind(this);
|
||||
}
|
||||
|
||||
back_onPress() {
|
||||
if (this.props.onCancel) this.props.onCancel();
|
||||
}
|
||||
|
||||
async photo_onPress() {
|
||||
if (!this.camera || !this.props.onPhoto) return;
|
||||
|
||||
this.setState({ snapping: true });
|
||||
|
||||
const result = await this.camera.takePictureAsync({
|
||||
quality: 0.8,
|
||||
exif: true,
|
||||
fixOrientation: true,
|
||||
});
|
||||
|
||||
if (this.props.onPhoto) this.props.onPhoto(result);
|
||||
|
||||
this.setState({ snapping: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const photoIcon = this.state.snapping ? 'md-checkmark' : 'md-camera';
|
||||
|
||||
return (
|
||||
<View style={this.props.style}>
|
||||
<RNCamera
|
||||
style={{ flex: 1 }}
|
||||
ref={ref => {
|
||||
this.camera = ref;
|
||||
}}
|
||||
type={RNCamera.Constants.Type.back}
|
||||
captureAudio={false}
|
||||
androidCameraPermissionOptions={{
|
||||
title: _('Permission to use camera'),
|
||||
message: _('Your permission to use your camera is required.'),
|
||||
buttonPositive: _('OK'),
|
||||
buttonNegative: _('Cancel'),
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, justifyContent: 'space-between', flexDirection: 'column' }}>
|
||||
<View style={{ flex: 1, justifyContent: 'flex-start' }}>
|
||||
<TouchableOpacity onPress={this.back_onPress}>
|
||||
<View style={{ marginLeft: 5, marginTop: 5, borderRadius: 90, width: 50, height: 50, display: 'flex', backgroundColor: '#ffffff55', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={'md-arrow-back'}
|
||||
style={{
|
||||
fontSize: 40,
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'flex-end', flexDirection: 'row' }}>
|
||||
<TouchableOpacity onPress={this.photo_onPress}>
|
||||
<View style={{ marginBottom: 20, borderRadius: 90, width: 90, height: 90, backgroundColor: '#ffffffaa', display: 'flex', justifyContent: 'center', alignItems: 'center' }}>
|
||||
<Icon
|
||||
name={photoIcon}
|
||||
style={{
|
||||
fontSize: 60,
|
||||
color: 'black',
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</RNCamera>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = CameraView;
|
156
Server/dist/lib/components/Dropdown.js
vendored
156
Server/dist/lib/components/Dropdown.js
vendored
@ -1,156 +0,0 @@
|
||||
const React = require('react');
|
||||
const { TouchableOpacity, TouchableWithoutFeedback, Dimensions, Text, Modal, View } = require('react-native');
|
||||
const { ItemList } = require('lib/components/ItemList.js');
|
||||
|
||||
class Dropdown extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.headerRef_ = null;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
headerSize: { x: 0, y: 0, width: 0, height: 0 },
|
||||
listVisible: false,
|
||||
});
|
||||
}
|
||||
|
||||
updateHeaderCoordinates() {
|
||||
// https://stackoverflow.com/questions/30096038/react-native-getting-the-position-of-an-element
|
||||
this.headerRef_.measure((fx, fy, width, height, px, py) => {
|
||||
this.setState({
|
||||
headerSize: { x: px, y: py, width: width, height: height },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const items = this.props.items;
|
||||
const itemHeight = 60;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
|
||||
// Dimensions doesn't return quite the right dimensions so leave an extra gap to make
|
||||
// sure nothing is off screen.
|
||||
const listMaxHeight = windowHeight;
|
||||
const listHeight = Math.min(items.length * itemHeight, listMaxHeight);
|
||||
const maxListTop = windowHeight - listHeight;
|
||||
const listTop = Math.min(maxListTop, this.state.headerSize.y + this.state.headerSize.height);
|
||||
|
||||
const wrapperStyle = {
|
||||
width: this.state.headerSize.width,
|
||||
height: listHeight + 2, // +2 for the border (otherwise it makes the scrollbar appear)
|
||||
marginTop: listTop,
|
||||
marginLeft: this.state.headerSize.x,
|
||||
};
|
||||
|
||||
const itemListStyle = Object.assign({}, this.props.itemListStyle ? this.props.itemListStyle : {}, {
|
||||
borderWidth: 1,
|
||||
borderColor: '#ccc',
|
||||
});
|
||||
|
||||
const itemWrapperStyle = Object.assign({}, this.props.itemWrapperStyle ? this.props.itemWrapperStyle : {}, {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
height: itemHeight,
|
||||
paddingLeft: 20,
|
||||
paddingRight: 10,
|
||||
});
|
||||
|
||||
const headerWrapperStyle = Object.assign({}, this.props.headerWrapperStyle ? this.props.headerWrapperStyle : {}, {
|
||||
height: 35,
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const headerStyle = Object.assign({}, this.props.headerStyle ? this.props.headerStyle : {}, {
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
const headerArrowStyle = Object.assign({}, this.props.headerStyle ? this.props.headerStyle : {}, {
|
||||
flex: 0,
|
||||
marginRight: 10,
|
||||
});
|
||||
|
||||
const itemStyle = Object.assign({}, this.props.itemStyle ? this.props.itemStyle : {}, {});
|
||||
|
||||
let headerLabel = '...';
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.value === this.props.selectedValue) {
|
||||
headerLabel = item.label;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.props.labelTransform && this.props.labelTransform === 'trim') headerLabel = headerLabel.trim();
|
||||
|
||||
const closeList = () => {
|
||||
this.setState({ listVisible: false });
|
||||
};
|
||||
|
||||
const itemRenderer = item => {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
style={itemWrapperStyle}
|
||||
key={item.value}
|
||||
onPress={() => {
|
||||
closeList();
|
||||
if (this.props.onValueChange) this.props.onValueChange(item.value);
|
||||
}}
|
||||
>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={itemStyle} key={item.value}>
|
||||
{item.label}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<TouchableOpacity
|
||||
style={headerWrapperStyle}
|
||||
ref={ref => (this.headerRef_ = ref)}
|
||||
onPress={() => {
|
||||
this.updateHeaderCoordinates();
|
||||
this.setState({ listVisible: true });
|
||||
}}
|
||||
>
|
||||
<Text ellipsizeMode="tail" numberOfLines={1} style={headerStyle}>
|
||||
{headerLabel}
|
||||
</Text>
|
||||
<Text style={headerArrowStyle}>{'▼'}</Text>
|
||||
</TouchableOpacity>
|
||||
<Modal
|
||||
transparent={true}
|
||||
visible={this.state.listVisible}
|
||||
onRequestClose={() => {
|
||||
closeList();
|
||||
}}
|
||||
>
|
||||
<TouchableWithoutFeedback
|
||||
onPressOut={() => {
|
||||
closeList();
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={wrapperStyle}>
|
||||
<ItemList
|
||||
style={itemListStyle}
|
||||
items={this.props.items}
|
||||
itemHeight={itemHeight}
|
||||
itemRenderer={item => {
|
||||
return itemRenderer(item);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Dropdown };
|
110
Server/dist/lib/components/ItemList.js
vendored
110
Server/dist/lib/components/ItemList.js
vendored
@ -1,110 +0,0 @@
|
||||
const React = require('react');
|
||||
const { View, ScrollView } = require('react-native');
|
||||
|
||||
class ItemList extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.scrollTop_ = 0;
|
||||
}
|
||||
|
||||
itemCount(props = null) {
|
||||
if (props === null) props = this.props;
|
||||
return this.props.items ? this.props.items.length : this.props.itemComponents.length;
|
||||
}
|
||||
|
||||
updateStateItemIndexes(props = null, height = null) {
|
||||
if (props === null) props = this.props;
|
||||
|
||||
if (height === null) {
|
||||
if (!this.state) return;
|
||||
height = this.state.height;
|
||||
}
|
||||
|
||||
const topItemIndex = Math.max(0, Math.floor(this.scrollTop_ / props.itemHeight));
|
||||
const visibleItemCount = Math.ceil(height / props.itemHeight);
|
||||
|
||||
let bottomItemIndex = topItemIndex + visibleItemCount - 1;
|
||||
if (bottomItemIndex >= this.itemCount(props)) bottomItemIndex = this.itemCount(props) - 1;
|
||||
|
||||
this.setState({
|
||||
topItemIndex: topItemIndex,
|
||||
bottomItemIndex: bottomItemIndex,
|
||||
});
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
topItemIndex: 0,
|
||||
bottomItemIndex: 0,
|
||||
height: 0,
|
||||
itemHeight: this.props.itemHeight ? this.props.itemHeight : 0,
|
||||
});
|
||||
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.itemHeight) {
|
||||
this.setState({
|
||||
itemHeight: newProps.itemHeight,
|
||||
});
|
||||
}
|
||||
|
||||
this.updateStateItemIndexes(newProps);
|
||||
}
|
||||
|
||||
onScroll(event) {
|
||||
this.scrollTop_ = Math.floor(event.nativeEvent.contentOffset.y);
|
||||
this.updateStateItemIndexes();
|
||||
}
|
||||
|
||||
onLayout(event) {
|
||||
this.setState({ height: event.nativeEvent.layout.height });
|
||||
this.updateStateItemIndexes(null, event.nativeEvent.layout.height);
|
||||
}
|
||||
|
||||
render() {
|
||||
const style = this.props.style ? this.props.style : {};
|
||||
|
||||
//if (!this.props.itemHeight) throw new Error('itemHeight is required');
|
||||
|
||||
let itemComps = [];
|
||||
|
||||
if (this.props.items) {
|
||||
const items = this.props.items;
|
||||
|
||||
const blankItem = function(key, height) {
|
||||
return <View key={key} style={{ height: height }}></View>;
|
||||
};
|
||||
|
||||
itemComps = [blankItem('top', this.state.topItemIndex * this.props.itemHeight)];
|
||||
|
||||
for (let i = this.state.topItemIndex; i <= this.state.bottomItemIndex; i++) {
|
||||
const itemComp = this.props.itemRenderer(items[i]);
|
||||
itemComps.push(itemComp);
|
||||
}
|
||||
|
||||
itemComps.push(blankItem('bottom', (items.length - this.state.bottomItemIndex - 1) * this.props.itemHeight));
|
||||
} else {
|
||||
itemComps = this.props.itemComponents;
|
||||
}
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
scrollEventThrottle={500}
|
||||
onLayout={event => {
|
||||
this.onLayout(event);
|
||||
}}
|
||||
style={style}
|
||||
onScroll={event => {
|
||||
this.onScroll(event);
|
||||
}}
|
||||
>
|
||||
{itemComps}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { ItemList };
|
80
Server/dist/lib/components/ModalDialog.js
vendored
80
Server/dist/lib/components/ModalDialog.js
vendored
@ -1,80 +0,0 @@
|
||||
const React = require('react');
|
||||
const { Text, Modal, View, StyleSheet, Button } = require('react-native');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { _ } = require('lib/locale');
|
||||
|
||||
class ModalDialog extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
modalWrapper: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
},
|
||||
modalContentWrapper: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.backgroundColor,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.dividerColor,
|
||||
margin: 20,
|
||||
padding: 10,
|
||||
borderRadius: 5,
|
||||
},
|
||||
modalContentWrapper2: {
|
||||
flex: 1,
|
||||
},
|
||||
title: Object.assign({}, theme.normalText, {
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
paddingBottom: 10,
|
||||
fontWeight: 'bold',
|
||||
}),
|
||||
buttonRow: {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: theme.dividerColor,
|
||||
paddingTop: 10,
|
||||
},
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
render() {
|
||||
const ContentComponent = this.props.ContentComponent;
|
||||
const buttonBarEnabled = this.props.buttonBarEnabled !== false;
|
||||
|
||||
return (
|
||||
<View style={this.styles().modalWrapper}>
|
||||
<Modal transparent={true} visible={true} onRequestClose={() => {}}>
|
||||
<View elevation={10} style={this.styles().modalContentWrapper}>
|
||||
<Text style={this.styles().title}>{this.props.title}</Text>
|
||||
<View style={this.styles().modalContentWrapper2}>{ContentComponent}</View>
|
||||
<View style={this.styles().buttonRow}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('OK')} onPress={this.props.onOkPress}></Button>
|
||||
</View>
|
||||
<View style={{ flex: 1, marginLeft: 5 }}>
|
||||
<Button disabled={!buttonBarEnabled} title={_('Cancel')} onPress={this.props.onCancelPress}></Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ModalDialog;
|
BIN
Server/dist/lib/components/SaveIcon.png
vendored
BIN
Server/dist/lib/components/SaveIcon.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 344 B |
129
Server/dist/lib/components/action-button.js
vendored
129
Server/dist/lib/components/action-button.js
vendored
@ -1,129 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { StyleSheet } = require('react-native');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const ReactNativeActionButton = require('react-native-action-button').default;
|
||||
const { connect } = require('react-redux');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
actionButtonIcon: {
|
||||
fontSize: 20,
|
||||
height: 22,
|
||||
color: 'white',
|
||||
},
|
||||
itemText: {
|
||||
// fontSize: 14, // Cannot currently set fontsize since the bow surrounding the label has a fixed size
|
||||
},
|
||||
});
|
||||
|
||||
class ActionButtonComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
buttonIndex: 0,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if ('buttonIndex' in newProps) {
|
||||
this.setState({ buttonIndex: newProps.buttonIndex });
|
||||
}
|
||||
}
|
||||
|
||||
newTodo_press() {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: null,
|
||||
folderId: this.props.parentFolderId,
|
||||
itemType: 'todo',
|
||||
});
|
||||
}
|
||||
|
||||
newNote_press() {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: null,
|
||||
folderId: this.props.parentFolderId,
|
||||
itemType: 'note',
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let buttons = this.props.buttons ? this.props.buttons : [];
|
||||
|
||||
if (this.props.addFolderNoteButtons) {
|
||||
if (this.props.folders.length) {
|
||||
buttons.push({
|
||||
title: _('New to-do'),
|
||||
onPress: () => {
|
||||
this.newTodo_press();
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'md-checkbox-outline',
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
title: _('New note'),
|
||||
onPress: () => {
|
||||
this.newNote_press();
|
||||
},
|
||||
color: '#9b59b6',
|
||||
icon: 'md-document',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let buttonComps = [];
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
let button = buttons[i];
|
||||
let buttonTitle = button.title ? button.title : '';
|
||||
let key = `${buttonTitle.replace(/\s/g, '_')}_${button.icon}`;
|
||||
buttonComps.push(
|
||||
<ReactNativeActionButton.Item key={key} buttonColor={button.color} title={buttonTitle} onPress={button.onPress}>
|
||||
<Icon name={button.icon} style={styles.actionButtonIcon} />
|
||||
</ReactNativeActionButton.Item>
|
||||
);
|
||||
}
|
||||
|
||||
if (!buttonComps.length && !this.props.mainButton) {
|
||||
return <ReactNativeActionButton style={{ display: 'none' }} />;
|
||||
}
|
||||
|
||||
let mainButton = this.props.mainButton ? this.props.mainButton : {};
|
||||
let mainIcon = mainButton.icon ? <Icon name={mainButton.icon} style={styles.actionButtonIcon} /> : <Icon name="md-add" style={styles.actionButtonIcon} />;
|
||||
|
||||
if (this.props.multiStates) {
|
||||
if (!this.props.buttons || !this.props.buttons.length) throw new Error('Multi-state button requires at least one state');
|
||||
if (this.state.buttonIndex < 0 || this.state.buttonIndex >= this.props.buttons.length) throw new Error(`Button index out of bounds: ${this.state.buttonIndex}/${this.props.buttons.length}`);
|
||||
let button = this.props.buttons[this.state.buttonIndex];
|
||||
let mainIcon = <Icon name={button.icon} style={styles.actionButtonIcon} />;
|
||||
return (
|
||||
<ReactNativeActionButton
|
||||
icon={mainIcon}
|
||||
buttonColor="rgba(231,76,60,1)"
|
||||
onPress={() => {
|
||||
button.onPress();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ReactNativeActionButton textStyle={styles.itemText} icon={mainIcon} buttonColor="rgba(231,76,60,1)" onPress={function() {}}>
|
||||
{buttonComps}
|
||||
</ReactNativeActionButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ActionButton = connect(state => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
locale: state.settings.locale,
|
||||
};
|
||||
})(ActionButtonComponent);
|
||||
|
||||
module.exports = { ActionButton };
|
88
Server/dist/lib/components/app-nav.js
vendored
88
Server/dist/lib/components/app-nav.js
vendored
@ -1,88 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { NotesScreen } = require('lib/components/screens/notes.js');
|
||||
const { SearchScreen } = require('lib/components/screens/search.js');
|
||||
const { KeyboardAvoidingView, Keyboard, Platform, View } = require('react-native');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
class AppNavComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.previousRouteName_ = null;
|
||||
this.state = {
|
||||
autoCompletionBarExtraHeight: 0, // Extra padding for the auto completion bar at the top of the keyboard
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
if (Platform.OS === 'ios') {
|
||||
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
|
||||
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.keyboardDidShowListener) this.keyboardDidShowListener.remove();
|
||||
if (this.keyboardDidHideListener) this.keyboardDidHideListener.remove();
|
||||
this.keyboardDidShowListener = null;
|
||||
this.keyboardDidHideListener = null;
|
||||
}
|
||||
|
||||
keyboardDidShow() {
|
||||
this.setState({ autoCompletionBarExtraHeight: 30 });
|
||||
}
|
||||
|
||||
keyboardDidHide() {
|
||||
this.setState({ autoCompletionBarExtraHeight: 0 });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.route) throw new Error('Route must not be null');
|
||||
|
||||
// Note: certain screens are kept into memory, in particular Notes and Search
|
||||
// so that the scroll position is not lost when the user navigate away from them.
|
||||
|
||||
let route = this.props.route;
|
||||
let Screen = null;
|
||||
let notesScreenVisible = false;
|
||||
let searchScreenVisible = false;
|
||||
|
||||
if (route.routeName == 'Notes') {
|
||||
notesScreenVisible = true;
|
||||
} else if (route.routeName == 'Search') {
|
||||
searchScreenVisible = true;
|
||||
} else {
|
||||
Screen = this.props.screens[route.routeName].screen;
|
||||
}
|
||||
|
||||
// Keep the search screen loaded if the user is viewing a note from that search screen
|
||||
// so that if the back button is pressed, the screen is still loaded. However, unload
|
||||
// it if navigating away.
|
||||
let searchScreenLoaded = searchScreenVisible || (this.previousRouteName_ == 'Search' && route.routeName == 'Note');
|
||||
|
||||
this.previousRouteName_ = route.routeName;
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const style = { flex: 1, backgroundColor: theme.backgroundColor };
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : null} style={style}>
|
||||
<NotesScreen visible={notesScreenVisible} navigation={{ state: route }} />
|
||||
{searchScreenLoaded && <SearchScreen visible={searchScreenVisible} navigation={{ state: route }} />}
|
||||
{!notesScreenVisible && !searchScreenVisible && <Screen navigation={{ state: route }} />}
|
||||
<View style={{ height: this.state.autoCompletionBarExtraHeight }} />
|
||||
</KeyboardAvoidingView>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const AppNav = connect(state => {
|
||||
return {
|
||||
route: state.route,
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(AppNavComponent);
|
||||
|
||||
module.exports = { AppNav };
|
39
Server/dist/lib/components/base-screen.js
vendored
39
Server/dist/lib/components/base-screen.js
vendored
@ -1,39 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { StyleSheet } = require('react-native');
|
||||
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
const styleObject_ = {
|
||||
screen: {
|
||||
flex: 1,
|
||||
backgroundColor: globalStyle.backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
const styles_ = StyleSheet.create(styleObject_);
|
||||
|
||||
let rootStyles_ = {};
|
||||
|
||||
class BaseScreenComponent extends React.Component {
|
||||
styles() {
|
||||
return styles_;
|
||||
}
|
||||
|
||||
styleObject() {
|
||||
return styleObject_;
|
||||
}
|
||||
|
||||
rootStyle(themeId) {
|
||||
const theme = themeStyle(themeId);
|
||||
if (rootStyles_[themeId]) return rootStyles_[themeId];
|
||||
rootStyles_[themeId] = StyleSheet.create({
|
||||
root: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
});
|
||||
return rootStyles_[themeId];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { BaseScreenComponent };
|
70
Server/dist/lib/components/checkbox.js
vendored
70
Server/dist/lib/components/checkbox.js
vendored
@ -1,70 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { View, TouchableHighlight } = require('react-native');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
|
||||
const styles = {
|
||||
checkboxIcon: {
|
||||
fontSize: 20,
|
||||
height: 22,
|
||||
//marginRight: 10,
|
||||
},
|
||||
};
|
||||
|
||||
class Checkbox extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
checked: false,
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ checked: this.props.checked });
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if ('checked' in newProps) {
|
||||
this.setState({ checked: newProps.checked });
|
||||
}
|
||||
}
|
||||
|
||||
onPress() {
|
||||
let newChecked = !this.state.checked;
|
||||
this.setState({ checked: newChecked });
|
||||
if (this.props.onChange) this.props.onChange(newChecked);
|
||||
}
|
||||
|
||||
render() {
|
||||
const iconName = this.state.checked ? 'md-checkbox-outline' : 'md-square-outline';
|
||||
|
||||
let style = this.props.style ? Object.assign({}, this.props.style) : {};
|
||||
style.justifyContent = 'center';
|
||||
style.alignItems = 'center';
|
||||
|
||||
let checkboxIconStyle = Object.assign({}, styles.checkboxIcon);
|
||||
if (style.color) checkboxIconStyle.color = style.color;
|
||||
|
||||
if (style.paddingTop) checkboxIconStyle.marginTop = style.paddingTop;
|
||||
if (style.paddingBottom) checkboxIconStyle.marginBottom = style.paddingBottom;
|
||||
if (style.paddingLeft) checkboxIconStyle.marginLeft = style.paddingLeft;
|
||||
if (style.paddingRight) checkboxIconStyle.marginRight = style.paddingRight;
|
||||
|
||||
const thStyle = {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
};
|
||||
|
||||
if (style && style.display === 'none') return <View />;
|
||||
|
||||
//if (style.display) thStyle.display = style.display;
|
||||
|
||||
return (
|
||||
<TouchableHighlight onPress={() => this.onPress()} style={thStyle}>
|
||||
<Icon name={iconName} style={checkboxIconStyle} />
|
||||
</TouchableHighlight>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { Checkbox };
|
159
Server/dist/lib/components/global-style.js
vendored
159
Server/dist/lib/components/global-style.js
vendored
@ -1,159 +0,0 @@
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { Platform } = require('react-native');
|
||||
|
||||
const globalStyle = {
|
||||
fontSize: 16,
|
||||
margin: 15, // No text and no interactive component should be within this margin
|
||||
itemMarginTop: 10,
|
||||
itemMarginBottom: 10,
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#555555', // For regular text
|
||||
colorError: 'red',
|
||||
colorWarn: '#9A5B00',
|
||||
colorFaded: '#777777', // For less important text
|
||||
fontSizeSmaller: 14,
|
||||
dividerColor: '#dddddd',
|
||||
strongDividerColor: '#aaaaaa',
|
||||
selectedColor: '#e5e5e5',
|
||||
headerBackgroundColor: '#F0F0F0',
|
||||
disabledOpacity: 0.2,
|
||||
colorUrl: '#7B81FF',
|
||||
textSelectionColor: '#0096FF',
|
||||
|
||||
raisedBackgroundColor: '#0080EF',
|
||||
raisedColor: '#003363',
|
||||
raisedHighlightedColor: '#ffffff',
|
||||
|
||||
warningBackgroundColor: '#FFD08D',
|
||||
|
||||
// For WebView - must correspond to the properties above
|
||||
htmlFontSize: '16px',
|
||||
htmlColor: '#222222',
|
||||
htmlBackgroundColor: 'white',
|
||||
htmlDividerColor: 'rgb(230,230,230)',
|
||||
htmlLinkColor: 'rgb(80,130,190)',
|
||||
htmlLineHeight: '1.6em',
|
||||
|
||||
htmlCodeBackgroundColor: 'rgb(243, 243, 243)',
|
||||
htmlCodeBorderColor: 'rgb(220, 220, 220)',
|
||||
htmlCodeColor: 'rgb(0,0,0)',
|
||||
|
||||
codeThemeCss: 'hljs-atom-one-light.css',
|
||||
};
|
||||
|
||||
globalStyle.marginRight = globalStyle.margin;
|
||||
globalStyle.marginLeft = globalStyle.margin;
|
||||
globalStyle.marginTop = globalStyle.margin;
|
||||
globalStyle.marginBottom = globalStyle.margin;
|
||||
globalStyle.htmlMarginLeft = `${((globalStyle.marginLeft / 10) * 0.6).toFixed(2)}em`;
|
||||
|
||||
let themeCache_ = {};
|
||||
|
||||
function addExtraStyles(style) {
|
||||
style.icon = {
|
||||
color: style.color,
|
||||
fontSize: 30,
|
||||
};
|
||||
|
||||
style.lineInput = {
|
||||
color: style.color,
|
||||
backgroundColor: style.backgroundColor,
|
||||
borderBottomWidth: 1,
|
||||
borderColor: style.strongDividerColor,
|
||||
paddingBottom: 0,
|
||||
};
|
||||
|
||||
if (Platform.OS === 'ios') {
|
||||
delete style.lineInput.borderBottomWidth;
|
||||
delete style.lineInput.borderColor;
|
||||
}
|
||||
|
||||
style.buttonRow = {
|
||||
flexDirection: 'row',
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: style.dividerColor,
|
||||
paddingTop: 10,
|
||||
};
|
||||
|
||||
style.normalText = {
|
||||
color: style.color,
|
||||
fontSize: style.fontSize,
|
||||
};
|
||||
|
||||
style.urlText = {
|
||||
color: style.colorUrl,
|
||||
fontSize: style.fontSize,
|
||||
};
|
||||
|
||||
style.headerStyle = {
|
||||
color: style.color,
|
||||
fontSize: style.fontSize * 1.2,
|
||||
fontWeight: 'bold',
|
||||
};
|
||||
|
||||
style.headerWrapperStyle = {
|
||||
backgroundColor: style.headerBackgroundColor,
|
||||
};
|
||||
|
||||
return style;
|
||||
}
|
||||
|
||||
function editorFont(fontId) {
|
||||
// IMPORTANT: The font mapping must match the one in Setting.js
|
||||
const fonts = {
|
||||
[Setting.FONT_DEFAULT]: null,
|
||||
[Setting.FONT_MENLO]: 'Menlo',
|
||||
[Setting.FONT_COURIER_NEW]: 'Courier New',
|
||||
[Setting.FONT_AVENIR]: 'Avenir',
|
||||
[Setting.FONT_MONOSPACE]: 'monospace',
|
||||
};
|
||||
if (!fontId) {
|
||||
console.warn('Editor font not set! Falling back to default font."');
|
||||
fontId = Setting.FONT_DEFAULT;
|
||||
}
|
||||
return fonts[fontId];
|
||||
}
|
||||
|
||||
function themeStyle(theme) {
|
||||
if (!theme) {
|
||||
console.warn('Theme not set! Defaulting to Light theme.');
|
||||
theme = Setting.THEME_LIGHT;
|
||||
}
|
||||
|
||||
if (themeCache_[theme]) return themeCache_[theme];
|
||||
|
||||
let output = Object.assign({}, globalStyle);
|
||||
if (theme == Setting.THEME_LIGHT) return addExtraStyles(output);
|
||||
|
||||
output.backgroundColor = '#1D2024';
|
||||
output.color = '#dddddd';
|
||||
output.colorFaded = '#777777';
|
||||
output.dividerColor = '#555555';
|
||||
output.strongDividerColor = '#888888';
|
||||
output.selectedColor = '#333333';
|
||||
output.textSelectionColor = '#00AEFF';
|
||||
output.headerBackgroundColor = '#2D3136';
|
||||
|
||||
output.raisedBackgroundColor = '#0F2051';
|
||||
output.raisedColor = '#788BC3';
|
||||
output.raisedHighlightedColor = '#ffffff';
|
||||
|
||||
output.htmlColor = 'rgb(220,220,220)';
|
||||
output.htmlBackgroundColor = 'rgb(29,32,36)';
|
||||
output.htmlLinkColor = 'rgb(166,166,255)';
|
||||
|
||||
output.htmlDividerColor = '#3D444E';
|
||||
output.htmlLinkColor = 'rgb(166,166,255)';
|
||||
output.htmlCodeColor = '#ffffff';
|
||||
output.htmlCodeBackgroundColor = 'rgb(47, 48, 49)';
|
||||
output.htmlCodeBorderColor = 'rgb(70, 70, 70)';
|
||||
|
||||
output.codeThemeCss = 'hljs-atom-one-dark-reasonable.css';
|
||||
|
||||
output.colorUrl = '#7B81FF';
|
||||
|
||||
themeCache_[theme] = output;
|
||||
return addExtraStyles(themeCache_[theme]);
|
||||
}
|
||||
|
||||
module.exports = { globalStyle, themeStyle, editorFont };
|
223
Server/dist/lib/components/note-body-viewer.js
vendored
223
Server/dist/lib/components/note-body-viewer.js
vendored
@ -1,223 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { Platform, View } = require('react-native');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const MdToHtml = require('lib/renderers/MdToHtml.js');
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
|
||||
class NoteBodyViewer extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
resources: {},
|
||||
webViewLoaded: false,
|
||||
};
|
||||
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.mdToHtml_ = new MdToHtml();
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mdToHtml_ = null;
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
onLoadEnd() {
|
||||
setTimeout(() => {
|
||||
if (this.props.onLoadEnd) this.props.onLoadEnd();
|
||||
}, 100);
|
||||
|
||||
if (this.state.webViewLoaded) return;
|
||||
|
||||
// Need to display after a delay to avoid a white flash before
|
||||
// the content is displayed.
|
||||
setTimeout(() => {
|
||||
if (!this.isMounted_) return;
|
||||
this.setState({ webViewLoaded: true });
|
||||
}, 100);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const safeGetNoteProp = (props, propName) => {
|
||||
if (!props) return null;
|
||||
if (!props.note) return null;
|
||||
return props.note[propName];
|
||||
};
|
||||
|
||||
// To address https://github.com/laurent22/joplin/issues/433
|
||||
// If a checkbox in a note is ticked, the body changes, which normally would trigger a re-render
|
||||
// of this component, which has the unfortunate side effect of making the view scroll back to the top.
|
||||
// This re-rendering however is uncessary since the component is already visually updated via JS.
|
||||
// So here, if the note has not changed, we prevent the component from updating.
|
||||
// This fixes the above issue. A drawback of this is if the note is updated via sync, this change
|
||||
// will not be displayed immediately.
|
||||
const currentNoteId = safeGetNoteProp(this.props, 'id');
|
||||
const nextNoteId = safeGetNoteProp(nextProps, 'id');
|
||||
|
||||
if (currentNoteId !== nextNoteId || nextState.webViewLoaded !== this.state.webViewLoaded) return true;
|
||||
|
||||
// If the length of the body has changed, then it's something other than a checkbox that has changed,
|
||||
// for example a resource that has been attached to the note while in View mode. In that case, update.
|
||||
return (`${safeGetNoteProp(this.props, 'body')}`).length !== (`${safeGetNoteProp(nextProps, 'body')}`).length;
|
||||
}
|
||||
|
||||
rebuildMd() {
|
||||
// this.mdToHtml_.clearCache();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
render() {
|
||||
const note = this.props.note;
|
||||
const style = this.props.style;
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const bodyToRender = note ? note.body : '';
|
||||
|
||||
const mdOptions = {
|
||||
onResourceLoaded: () => {
|
||||
if (this.resourceLoadedTimeoutId_) {
|
||||
clearTimeout(this.resourceLoadedTimeoutId_);
|
||||
this.resourceLoadedTimeoutId_ = null;
|
||||
}
|
||||
|
||||
this.resourceLoadedTimeoutId_ = setTimeout(() => {
|
||||
this.resourceLoadedTimeoutId_ = null;
|
||||
this.forceUpdate();
|
||||
}, 100);
|
||||
},
|
||||
paddingBottom: '3.8em', // Extra bottom padding to make it possible to scroll past the action button (so that it doesn't overlap the text)
|
||||
highlightedKeywords: this.props.highlightedKeywords,
|
||||
resources: this.props.noteResources, //await shared.attachedResources(bodyToRender),
|
||||
codeTheme: theme.codeThemeCss,
|
||||
postMessageSyntax: 'window.ReactNativeWebView.postMessage',
|
||||
};
|
||||
|
||||
let result = this.mdToHtml_.render(bodyToRender, this.props.webViewStyle, mdOptions);
|
||||
let html = result.html;
|
||||
|
||||
const resourceDownloadMode = Setting.value('sync.resourceDownloadMode');
|
||||
|
||||
const injectedJs = [this.mdToHtml_.injectedJavaScript()];
|
||||
injectedJs.push(shim.injectedJs('webviewLib'));
|
||||
injectedJs.push('webviewLib.initialize({ postMessage: msg => { return window.ReactNativeWebView.postMessage(msg); } });');
|
||||
injectedJs.push(`
|
||||
const readyStateCheckInterval = setInterval(function() {
|
||||
if (document.readyState === "complete") {
|
||||
clearInterval(readyStateCheckInterval);
|
||||
if ("${resourceDownloadMode}" === "manual") webviewLib.setupResourceManualDownload();
|
||||
|
||||
const hash = "${this.props.noteHash}";
|
||||
// Gives it a bit of time before scrolling to the anchor
|
||||
// so that images are loaded.
|
||||
if (hash) {
|
||||
setTimeout(() => {
|
||||
const e = document.getElementById(hash);
|
||||
if (!e) {
|
||||
console.warn('Cannot find hash', hash);
|
||||
return;
|
||||
}
|
||||
e.scrollIntoView();
|
||||
}, 500);
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
`);
|
||||
|
||||
html =
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
</head>
|
||||
<body>
|
||||
${html}
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
|
||||
let webViewStyle = { backgroundColor: this.props.webViewStyle.backgroundColor };
|
||||
// On iOS, the onLoadEnd() event is never fired so always
|
||||
// display the webview (don't do the little trick
|
||||
// to avoid the white flash).
|
||||
if (Platform.OS !== 'ios') {
|
||||
webViewStyle.opacity = this.state.webViewLoaded ? 1 : 0.01;
|
||||
}
|
||||
|
||||
// On iOS scalesPageToFit work like this:
|
||||
//
|
||||
// Find the widest image, resize it *and everything else* by x% so that
|
||||
// the image fits within the viewport. The problem is that it means if there's
|
||||
// a large image, everything is going to be scaled to a very small size, making
|
||||
// the text unreadable.
|
||||
//
|
||||
// On Android:
|
||||
//
|
||||
// Find the widest elements and scale them (and them only) to fit within the viewport
|
||||
// It means it's going to scale large images, but the text will remain at the normal
|
||||
// size.
|
||||
//
|
||||
// That means we can use scalesPageToFix on Android but not on iOS.
|
||||
// The weird thing is that on iOS, scalesPageToFix=false along with a CSS
|
||||
// rule "img { max-width: 100% }", works like scalesPageToFix=true on Android.
|
||||
// So we use scalesPageToFix=false on iOS along with that CSS rule.
|
||||
|
||||
// `baseUrl` is where the images will be loaded from. So images must use a path relative to resourceDir.
|
||||
const source = {
|
||||
html: html,
|
||||
baseUrl: `file://${Setting.value('resourceDir')}/`,
|
||||
};
|
||||
|
||||
// Note: useWebKit={false} is needed to go around this bug:
|
||||
// https://github.com/react-native-community/react-native-webview/issues/376
|
||||
// However, if we add the <meta> tag as described there, it is no longer necessary and WebKit can be used!
|
||||
// https://github.com/react-native-community/react-native-webview/issues/312#issuecomment-501991406
|
||||
//
|
||||
// However, on iOS, due to the bug below, we cannot use WebKit:
|
||||
// https://github.com/react-native-community/react-native-webview/issues/312#issuecomment-503754654
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<WebView
|
||||
useWebKit={Platform.OS !== 'ios'}
|
||||
style={webViewStyle}
|
||||
source={source}
|
||||
injectedJavaScript={injectedJs.join('\n')}
|
||||
originWhitelist={['file://*', './*', 'http://*', 'https://*']}
|
||||
mixedContentMode="always"
|
||||
allowFileAccess={true}
|
||||
onLoadEnd={() => this.onLoadEnd()}
|
||||
onError={() => reg.logger().error('WebView error')}
|
||||
onMessage={event => {
|
||||
// Since RN 58 (or 59) messages are now escaped twice???
|
||||
let msg = unescape(unescape(event.nativeEvent.data));
|
||||
|
||||
console.info('Got IPC message: ', msg);
|
||||
|
||||
if (msg.indexOf('checkboxclick:') === 0) {
|
||||
const newBody = shared.toggleCheckbox(msg, this.props.note.body);
|
||||
if (this.props.onCheckboxChange) this.props.onCheckboxChange(newBody);
|
||||
} else if (msg.indexOf('markForDownload:') === 0) {
|
||||
msg = msg.split(':');
|
||||
const resourceId = msg[1];
|
||||
if (this.props.onMarkForDownload) this.props.onMarkForDownload({ resourceId: resourceId });
|
||||
} else {
|
||||
this.props.onJoplinLinkClick(msg);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { NoteBodyViewer };
|
154
Server/dist/lib/components/note-item.js
vendored
154
Server/dist/lib/components/note-item.js
vendored
@ -1,154 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { Text, TouchableOpacity, View, StyleSheet } = require('react-native');
|
||||
const { Checkbox } = require('lib/components/checkbox.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
class NoteItemComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
noteItem_press(noteId) {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: noteId,
|
||||
});
|
||||
}
|
||||
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
listItem: {
|
||||
flexDirection: 'row',
|
||||
//height: 40,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
alignItems: 'flex-start',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingTop: theme.itemMarginTop,
|
||||
paddingBottom: theme.itemMarginBottom,
|
||||
//backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
listItemText: {
|
||||
flex: 1,
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
selectionWrapper: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
};
|
||||
|
||||
styles.listItemWithCheckbox = Object.assign({}, styles.listItem);
|
||||
delete styles.listItemWithCheckbox.paddingTop;
|
||||
delete styles.listItemWithCheckbox.paddingBottom;
|
||||
delete styles.listItemWithCheckbox.paddingLeft;
|
||||
|
||||
styles.listItemTextWithCheckbox = Object.assign({}, styles.listItemText);
|
||||
styles.listItemTextWithCheckbox.marginTop = styles.listItem.paddingTop - 1;
|
||||
styles.listItemTextWithCheckbox.marginBottom = styles.listItem.paddingBottom;
|
||||
|
||||
styles.selectionWrapperSelected = Object.assign({}, styles.selectionWrapper);
|
||||
styles.selectionWrapperSelected.backgroundColor = theme.selectedColor;
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
async todoCheckbox_change(checked) {
|
||||
if (!this.props.note) return;
|
||||
|
||||
const newNote = {
|
||||
id: this.props.note.id,
|
||||
todo_completed: checked ? time.unixMs() : 0,
|
||||
};
|
||||
await Note.save(newNote);
|
||||
}
|
||||
|
||||
onPress() {
|
||||
if (!this.props.note) return;
|
||||
if (this.props.note.encryption_applied) return;
|
||||
|
||||
if (this.props.noteSelectionEnabled) {
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SELECTION_TOGGLE',
|
||||
id: this.props.note.id,
|
||||
});
|
||||
} else {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: this.props.note.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
onLongPress() {
|
||||
if (!this.props.note) return;
|
||||
|
||||
this.props.dispatch({
|
||||
type: this.props.noteSelectionEnabled ? 'NOTE_SELECTION_TOGGLE' : 'NOTE_SELECTION_START',
|
||||
id: this.props.note.id,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const note = this.props.note ? this.props.note : {};
|
||||
const isTodo = !!Number(note.is_todo);
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
// IOS: display: none crashes the app
|
||||
let checkboxStyle = !isTodo ? { display: 'none' } : { color: theme.color };
|
||||
|
||||
if (isTodo) {
|
||||
checkboxStyle.paddingRight = 10;
|
||||
checkboxStyle.paddingTop = theme.itemMarginTop;
|
||||
checkboxStyle.paddingBottom = theme.itemMarginBottom;
|
||||
checkboxStyle.paddingLeft = theme.marginLeft;
|
||||
}
|
||||
|
||||
const checkboxChecked = !!Number(note.todo_completed);
|
||||
|
||||
const listItemStyle = isTodo ? this.styles().listItemWithCheckbox : this.styles().listItem;
|
||||
const listItemTextStyle = isTodo ? this.styles().listItemTextWithCheckbox : this.styles().listItemText;
|
||||
const opacityStyle = isTodo && checkboxChecked ? { opacity: 0.4 } : {};
|
||||
const isSelected = this.props.noteSelectionEnabled && this.props.selectedNoteIds.indexOf(note.id) >= 0;
|
||||
|
||||
const selectionWrapperStyle = isSelected ? this.styles().selectionWrapperSelected : this.styles().selectionWrapper;
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={() => this.onPress()} onLongPress={() => this.onLongPress()} activeOpacity={0.5}>
|
||||
<View style={selectionWrapperStyle}>
|
||||
<View style={opacityStyle}>
|
||||
<View style={listItemStyle}>
|
||||
<Checkbox style={checkboxStyle} checked={checkboxChecked} onChange={checked => this.todoCheckbox_change(checked)} />
|
||||
<Text style={listItemTextStyle}>{Note.displayTitle(note)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const NoteItem = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
};
|
||||
})(NoteItemComponent);
|
||||
|
||||
module.exports = { NoteItem };
|
141
Server/dist/lib/components/note-list.js
vendored
141
Server/dist/lib/components/note-list.js
vendored
@ -1,141 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { connect } = require('react-redux');
|
||||
const { ListView, Text, StyleSheet, Button, View } = require('react-native');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { NoteItem } = require('lib/components/note-item.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
class NoteListComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
const ds = new ListView.DataSource({
|
||||
rowHasChanged: (r1, r2) => {
|
||||
return r1 !== r2;
|
||||
},
|
||||
});
|
||||
this.state = {
|
||||
dataSource: ds,
|
||||
items: [],
|
||||
selectedItemIds: [],
|
||||
};
|
||||
this.rootRef_ = null;
|
||||
this.styles_ = {};
|
||||
|
||||
this.createNotebookButton_click = this.createNotebookButton_click.bind(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
noItemMessage: {
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingTop: theme.marginTop,
|
||||
paddingBottom: theme.marginBottom,
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
textAlign: 'center',
|
||||
},
|
||||
noNotebookView: {
|
||||
|
||||
},
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
createNotebookButton_click() {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: null,
|
||||
});
|
||||
}
|
||||
|
||||
filterNotes(notes) {
|
||||
const todoFilter = 'all'; //Setting.value('todoFilter');
|
||||
if (todoFilter == 'all') return notes;
|
||||
|
||||
const now = time.unixMs();
|
||||
const maxInterval = 1000 * 60 * 60 * 24;
|
||||
const notRecentTime = now - maxInterval;
|
||||
|
||||
let output = [];
|
||||
for (let i = 0; i < notes.length; i++) {
|
||||
const note = notes[i];
|
||||
if (note.is_todo) {
|
||||
if (todoFilter == 'recent' && note.user_updated_time < notRecentTime && !!note.todo_completed) continue;
|
||||
if (todoFilter == 'nonCompleted' && !!note.todo_completed) continue;
|
||||
}
|
||||
output.push(note);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
const newDataSource = this.state.dataSource.cloneWithRows(this.filterNotes(this.props.items));
|
||||
this.setState({ dataSource: newDataSource });
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
// https://stackoverflow.com/questions/38186114/react-native-redux-and-listview
|
||||
this.setState({
|
||||
dataSource: this.state.dataSource.cloneWithRows(this.filterNotes(newProps.items)),
|
||||
});
|
||||
|
||||
// Make sure scroll position is reset when switching from one folder to another or to a tag list.
|
||||
if (this.rootRef_ && newProps.notesSource != this.props.notesSource) {
|
||||
this.rootRef_.scrollTo({ x: 0, y: 0, animated: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
|
||||
|
||||
if (this.state.dataSource.getRowCount()) {
|
||||
return (
|
||||
<ListView
|
||||
ref={ref => (this.rootRef_ = ref)}
|
||||
dataSource={this.state.dataSource}
|
||||
renderRow={note => {
|
||||
return <NoteItem note={note} />;
|
||||
}}
|
||||
enableEmptySections={true}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
if (!this.props.folders.length) {
|
||||
const noItemMessage = _('You currently have no notebooks.');
|
||||
return (
|
||||
<View style={this.styles().noNotebookView}>
|
||||
<Text style={this.styles().noItemMessage}>{noItemMessage}</Text>
|
||||
<Button title={_('Create a notebook')} onPress={this.createNotebookButton_click} />
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
const noItemMessage = _('There are currently no notes. Create one by clicking on the (+) button.');
|
||||
return <Text style={this.styles().noItemMessage}>{noItemMessage}</Text>;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const NoteList = connect(state => {
|
||||
return {
|
||||
items: state.notes,
|
||||
folders: state.folders,
|
||||
notesSource: state.notesSource,
|
||||
theme: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
};
|
||||
})(NoteListComponent);
|
||||
|
||||
module.exports = { NoteList };
|
452
Server/dist/lib/components/screen-header.js
vendored
452
Server/dist/lib/components/screen-header.js
vendored
@ -1,452 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { connect } = require('react-redux');
|
||||
const { Platform, View, Text, StyleSheet, TouchableOpacity, Image, ScrollView, Dimensions } = require('react-native');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
const NavService = require('lib/services/NavService.js');
|
||||
const { Menu, MenuOptions, MenuOption, MenuTrigger } = require('react-native-popup-menu');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { Dropdown } = require('lib/components/Dropdown.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
||||
// Rather than applying a padding to the whole bar, it is applied to each
|
||||
// individual component (button, picker, etc.) so that the touchable areas
|
||||
// are widder and to give more room to the picker component which has a larger
|
||||
// default height.
|
||||
const PADDING_V = 10;
|
||||
|
||||
class ScreenHeaderComponent extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = Setting.value('theme');
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
let styleObject = {
|
||||
container: {
|
||||
flexDirection: 'column',
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
alignItems: 'center',
|
||||
shadowColor: '#000000',
|
||||
elevation: 5,
|
||||
paddingTop: Platform.OS === 'ios' ? 15 : 0, // Extra padding for iOS because the top icons are there
|
||||
},
|
||||
divider: {
|
||||
borderBottomWidth: 1,
|
||||
borderColor: theme.dividerColor,
|
||||
backgroundColor: '#0000ff',
|
||||
},
|
||||
sideMenuButton: {
|
||||
flex: 1,
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: 5,
|
||||
marginRight: 2,
|
||||
paddingTop: PADDING_V,
|
||||
paddingBottom: PADDING_V,
|
||||
},
|
||||
iconButton: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
paddingLeft: 15,
|
||||
paddingRight: 15,
|
||||
paddingTop: PADDING_V,
|
||||
paddingBottom: PADDING_V,
|
||||
},
|
||||
saveButton: {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
borderWidth: 1,
|
||||
borderColor: theme.raisedHighlightedColor,
|
||||
borderRadius: 4,
|
||||
marginRight: 8,
|
||||
},
|
||||
saveButtonText: {
|
||||
textAlignVertical: 'center',
|
||||
color: theme.raisedHighlightedColor,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
savedButtonIcon: {
|
||||
fontSize: 20,
|
||||
color: theme.raisedHighlightedColor,
|
||||
width: 18,
|
||||
height: 18,
|
||||
},
|
||||
saveButtonIcon: {
|
||||
width: 18,
|
||||
height: 18,
|
||||
},
|
||||
contextMenuTrigger: {
|
||||
fontSize: 30,
|
||||
paddingLeft: 10,
|
||||
paddingRight: theme.marginRight,
|
||||
color: theme.raisedColor,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
contextMenu: {
|
||||
backgroundColor: theme.raisedBackgroundColor,
|
||||
},
|
||||
contextMenuItem: {
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
contextMenuItemText: {
|
||||
flex: 1,
|
||||
textAlignVertical: 'center',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingTop: theme.itemMarginTop,
|
||||
paddingBottom: theme.itemMarginBottom,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
titleText: {
|
||||
flex: 1,
|
||||
textAlignVertical: 'center',
|
||||
marginLeft: 10,
|
||||
color: theme.raisedHighlightedColor,
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.fontSize,
|
||||
paddingTop: 15,
|
||||
paddingBottom: 15,
|
||||
},
|
||||
warningBox: {
|
||||
backgroundColor: '#ff9900',
|
||||
flexDirection: 'row',
|
||||
padding: theme.marginLeft,
|
||||
},
|
||||
};
|
||||
|
||||
styleObject.topIcon = Object.assign({}, theme.icon);
|
||||
styleObject.topIcon.flex = 1;
|
||||
styleObject.topIcon.textAlignVertical = 'center';
|
||||
styleObject.topIcon.color = theme.raisedColor;
|
||||
|
||||
styleObject.backButton = Object.assign({}, styleObject.iconButton);
|
||||
styleObject.backButton.marginRight = 1;
|
||||
|
||||
styleObject.backButtonDisabled = Object.assign({}, styleObject.backButton, { opacity: theme.disabledOpacity });
|
||||
styleObject.saveButtonDisabled = Object.assign({}, styleObject.saveButton, { opacity: theme.disabledOpacity });
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styleObject);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
sideMenuButton_press() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_TOGGLE' });
|
||||
}
|
||||
|
||||
async backButton_press() {
|
||||
if (this.props.noteSelectionEnabled) {
|
||||
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
|
||||
} else {
|
||||
await BackButtonService.back();
|
||||
}
|
||||
}
|
||||
|
||||
searchButton_press() {
|
||||
NavService.go('Search');
|
||||
}
|
||||
|
||||
async deleteButton_press() {
|
||||
// Dialog needs to be displayed as a child of the parent component, otherwise
|
||||
// it won't be visible within the header component.
|
||||
const ok = await dialogs.confirm(this.props.parentComponent, _('Delete these notes?'));
|
||||
if (!ok) return;
|
||||
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
|
||||
await Note.batchDelete(noteIds);
|
||||
}
|
||||
|
||||
menu_select(value) {
|
||||
if (typeof value == 'function') {
|
||||
value();
|
||||
}
|
||||
}
|
||||
|
||||
log_press() {
|
||||
NavService.go('Log');
|
||||
}
|
||||
|
||||
status_press() {
|
||||
NavService.go('Status');
|
||||
}
|
||||
|
||||
warningBox_press() {
|
||||
NavService.go('EncryptionConfig');
|
||||
}
|
||||
|
||||
render() {
|
||||
function sideMenuButton(styles, onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={styles.sideMenuButton}>
|
||||
<Icon name="md-menu" style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function backButton(styles, onPress, disabled) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} disabled={disabled}>
|
||||
<View style={disabled ? styles.backButtonDisabled : styles.backButton}>
|
||||
<Icon name="md-arrow-back" style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function saveButton(styles, onPress, disabled, show) {
|
||||
if (!show) return null;
|
||||
|
||||
const icon = disabled ? <Icon name="md-checkmark" style={styles.savedButtonIcon} /> : <Image style={styles.saveButtonIcon} source={require('./SaveIcon.png')} />;
|
||||
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress} disabled={disabled} style={{ padding: 0 }}>
|
||||
<View style={disabled ? styles.saveButtonDisabled : styles.saveButton}>{icon}</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function searchButton(styles, onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={styles.iconButton}>
|
||||
<Icon name="md-search" style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function deleteButton(styles, onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={styles.iconButton}>
|
||||
<Icon name="md-trash" style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
function sortButton(styles, onPress) {
|
||||
return (
|
||||
<TouchableOpacity onPress={onPress}>
|
||||
<View style={styles.iconButton}>
|
||||
<Icon name="md-funnel" style={styles.topIcon} />
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
let key = 0;
|
||||
let menuOptionComponents = [];
|
||||
|
||||
if (!this.props.noteSelectionEnabled) {
|
||||
for (let i = 0; i < this.props.menuOptions.length; i++) {
|
||||
let o = this.props.menuOptions[i];
|
||||
|
||||
if (o.isDivider) {
|
||||
menuOptionComponents.push(<View key={`menuOption_${key++}`} style={this.styles().divider} />);
|
||||
} else {
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={o.onPress} key={`menuOption_${key++}`} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{o.title}</Text>
|
||||
</MenuOption>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (menuOptionComponents.length) {
|
||||
menuOptionComponents.push(<View key={`menuOption_${key++}`} style={this.styles().divider} />);
|
||||
}
|
||||
} else {
|
||||
menuOptionComponents.push(
|
||||
<MenuOption value={() => this.deleteButton_press()} key={'menuOption_delete'} style={this.styles().contextMenuItem}>
|
||||
<Text style={this.styles().contextMenuItemText}>{_('Delete')}</Text>
|
||||
</MenuOption>
|
||||
);
|
||||
}
|
||||
|
||||
const createTitleComponent = () => {
|
||||
const themeId = Setting.value('theme');
|
||||
const theme = themeStyle(themeId);
|
||||
const folderPickerOptions = this.props.folderPickerOptions;
|
||||
|
||||
if (folderPickerOptions && folderPickerOptions.enabled) {
|
||||
const addFolderChildren = (folders, pickerItems, indent) => {
|
||||
folders.sort((a, b) => {
|
||||
const aTitle = a && a.title ? a.title : '';
|
||||
const bTitle = b && b.title ? b.title : '';
|
||||
return aTitle.toLowerCase() < bTitle.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
const f = folders[i];
|
||||
pickerItems.push({ label: `${' '.repeat(indent)} ${Folder.displayTitle(f)}`, value: f.id });
|
||||
pickerItems = addFolderChildren(f.children, pickerItems, indent + 1);
|
||||
}
|
||||
|
||||
return pickerItems;
|
||||
};
|
||||
|
||||
const titlePickerItems = mustSelect => {
|
||||
const folders = this.props.folders.filter(f => f.id !== Folder.conflictFolderId());
|
||||
let output = [];
|
||||
if (mustSelect) output.push({ label: _('Move to notebook...'), value: null });
|
||||
const folderTree = Folder.buildTree(folders);
|
||||
output = addFolderChildren(folderTree, output, 0);
|
||||
return output;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={titlePickerItems(!!folderPickerOptions.mustSelect)}
|
||||
itemHeight={35}
|
||||
labelTransform="trim"
|
||||
selectedValue={'selectedFolderId' in folderPickerOptions ? folderPickerOptions.selectedFolderId : null}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: theme.raisedHighlightedColor,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={async (folderId, itemIndex) => {
|
||||
// If onValueChange is specified, use this as a callback, otherwise do the default
|
||||
// which is to take the selectedNoteIds from the state and move them to the
|
||||
// chosen folder.
|
||||
|
||||
if (folderPickerOptions.onValueChange) {
|
||||
folderPickerOptions.onValueChange(folderId, itemIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!folderId) return;
|
||||
const noteIds = this.props.selectedNoteIds;
|
||||
if (!noteIds.length) return;
|
||||
|
||||
const folder = await Folder.load(folderId);
|
||||
|
||||
const ok = noteIds.length > 1 ? await dialogs.confirm(this.props.parentComponent, _('Move %d notes to notebook "%s"?', noteIds.length, folder.title)) : true;
|
||||
if (!ok) return;
|
||||
|
||||
this.props.dispatch({ type: 'NOTE_SELECTION_END' });
|
||||
for (let i = 0; i < noteIds.length; i++) {
|
||||
await Note.moveToFolder(noteIds[i], folderId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
let title = 'title' in this.props && this.props.title !== null ? this.props.title : '';
|
||||
return <Text style={this.styles().titleText}>{title}</Text>;
|
||||
}
|
||||
};
|
||||
|
||||
const warningComp = this.props.showMissingMasterKeyMessage ? (
|
||||
<TouchableOpacity style={this.styles().warningBox} onPress={() => this.warningBox_press()} activeOpacity={0.8}>
|
||||
<Text style={{ flex: 1 }}>{_('Press to set the decryption password.')}</Text>
|
||||
</TouchableOpacity>
|
||||
) : null;
|
||||
|
||||
const showSideMenuButton = !!this.props.showSideMenuButton && !this.props.noteSelectionEnabled;
|
||||
const showSearchButton = !!this.props.showSearchButton && !this.props.noteSelectionEnabled;
|
||||
const showContextMenuButton = this.props.showContextMenuButton !== false;
|
||||
const showBackButton = !!this.props.noteSelectionEnabled || this.props.showBackButton !== false;
|
||||
|
||||
let backButtonDisabled = !this.props.historyCanGoBack;
|
||||
if (this.props.noteSelectionEnabled) backButtonDisabled = false;
|
||||
|
||||
const titleComp = createTitleComponent();
|
||||
const sideMenuComp = !showSideMenuButton ? null : sideMenuButton(this.styles(), () => this.sideMenuButton_press());
|
||||
const backButtonComp = !showBackButton ? null : backButton(this.styles(), () => this.backButton_press(), backButtonDisabled);
|
||||
const searchButtonComp = !showSearchButton ? null : searchButton(this.styles(), () => this.searchButton_press());
|
||||
const deleteButtonComp = this.props.noteSelectionEnabled ? deleteButton(this.styles(), () => this.deleteButton_press()) : null;
|
||||
const sortButtonComp = !this.props.noteSelectionEnabled && this.props.sortButton_press ? sortButton(this.styles(), () => this.props.sortButton_press()) : null;
|
||||
const windowHeight = Dimensions.get('window').height - 50;
|
||||
|
||||
const contextMenuStyle = { paddingTop: PADDING_V, paddingBottom: PADDING_V };
|
||||
|
||||
// HACK: if this button is removed during selection mode, the header layout is broken, so for now just make it 1 pixel large (normally it should be hidden)
|
||||
if (this.props.noteSelectionEnabled) contextMenuStyle.width = 1;
|
||||
|
||||
const menuComp =
|
||||
!menuOptionComponents.length || !showContextMenuButton ? null : (
|
||||
<Menu onSelect={value => this.menu_select(value)} style={this.styles().contextMenu}>
|
||||
<MenuTrigger style={contextMenuStyle}>
|
||||
<Icon name="md-more" style={this.styles().contextMenuTrigger} />
|
||||
</MenuTrigger>
|
||||
<MenuOptions>
|
||||
<ScrollView style={{ maxHeight: windowHeight }}>{menuOptionComponents}</ScrollView>
|
||||
</MenuOptions>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.styles().container}>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
{sideMenuComp}
|
||||
{backButtonComp}
|
||||
{saveButton(
|
||||
this.styles(),
|
||||
() => {
|
||||
if (this.props.onSaveButtonPress) this.props.onSaveButtonPress();
|
||||
},
|
||||
this.props.saveButtonDisabled === true,
|
||||
this.props.showSaveButton === true
|
||||
)}
|
||||
{titleComp}
|
||||
{searchButtonComp}
|
||||
{deleteButtonComp}
|
||||
{sortButtonComp}
|
||||
{menuComp}
|
||||
</View>
|
||||
{warningComp}
|
||||
<DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScreenHeaderComponent.defaultProps = {
|
||||
menuOptions: [],
|
||||
};
|
||||
|
||||
const ScreenHeader = connect(state => {
|
||||
return {
|
||||
historyCanGoBack: state.historyCanGoBack,
|
||||
locale: state.settings.locale,
|
||||
folders: state.folders,
|
||||
theme: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
showMissingMasterKeyMessage: state.notLoadedMasterKeys.length && state.masterKeys.length,
|
||||
};
|
||||
})(ScreenHeaderComponent);
|
||||
|
||||
module.exports = { ScreenHeader };
|
187
Server/dist/lib/components/screens/NoteTagsDialog.js
vendored
187
Server/dist/lib/components/screens/NoteTagsDialog.js
vendored
@ -1,187 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { StyleSheet, View, Text, FlatList, TouchableOpacity, TextInput } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const ModalDialog = require('lib/components/ModalDialog');
|
||||
const naturalCompare = require('string-natural-compare');
|
||||
|
||||
class NoteTagsDialogComponent extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
this.state = {
|
||||
noteTagIds: [],
|
||||
noteId: null,
|
||||
tagListData: [],
|
||||
newTags: '',
|
||||
savingTags: false,
|
||||
};
|
||||
|
||||
const noteHasTag = tagId => {
|
||||
for (let i = 0; i < this.state.tagListData.length; i++) {
|
||||
if (this.state.tagListData[i].id === tagId) return this.state.tagListData[i].selected;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const newTagTitles = () => {
|
||||
return this.state.newTags
|
||||
.split(',')
|
||||
.map(t => t.trim().toLowerCase())
|
||||
.filter(t => !!t);
|
||||
};
|
||||
|
||||
this.tag_press = tagId => {
|
||||
const newData = this.state.tagListData.slice();
|
||||
for (let i = 0; i < newData.length; i++) {
|
||||
const t = newData[i];
|
||||
if (t.id === tagId) {
|
||||
const newTag = Object.assign({}, t);
|
||||
newTag.selected = !newTag.selected;
|
||||
newData[i] = newTag;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({ tagListData: newData });
|
||||
};
|
||||
|
||||
this.renderTag = data => {
|
||||
const tag = data.item;
|
||||
const iconName = noteHasTag(tag.id) ? 'md-checkbox-outline' : 'md-square-outline';
|
||||
return (
|
||||
<TouchableOpacity key={tag.id} onPress={() => this.tag_press(tag.id)} style={this.styles().tag}>
|
||||
<View style={this.styles().tagIconText}>
|
||||
<Icon name={iconName} style={this.styles().tagCheckbox} />
|
||||
<Text style={this.styles().tagText}>{tag.title}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
};
|
||||
|
||||
this.tagKeyExtractor = (tag) => tag.id;
|
||||
|
||||
this.okButton_press = async () => {
|
||||
this.setState({ savingTags: true });
|
||||
|
||||
try {
|
||||
const tagIds = this.state.tagListData.filter(t => t.selected).map(t => t.id);
|
||||
await Tag.setNoteTagsByIds(this.state.noteId, tagIds);
|
||||
|
||||
const extraTitles = newTagTitles();
|
||||
for (let i = 0; i < extraTitles.length; i++) {
|
||||
await Tag.addNoteTagByTitle(this.state.noteId, extraTitles[i]);
|
||||
}
|
||||
} finally {
|
||||
this.setState({ savingTags: false });
|
||||
}
|
||||
|
||||
if (this.props.onCloseRequested) this.props.onCloseRequested();
|
||||
};
|
||||
|
||||
this.cancelButton_press = () => {
|
||||
if (this.props.onCloseRequested) this.props.onCloseRequested();
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
const noteId = this.props.noteId;
|
||||
this.setState({ noteId: noteId });
|
||||
this.loadNoteTags(noteId);
|
||||
}
|
||||
|
||||
async loadNoteTags(noteId) {
|
||||
const tags = await Tag.tagsByNoteId(noteId);
|
||||
const tagIds = tags.map(t => t.id);
|
||||
|
||||
const tagListData = this.props.tags.map(tag => {
|
||||
return {
|
||||
id: tag.id,
|
||||
title: tag.title,
|
||||
selected: tagIds.indexOf(tag.id) >= 0,
|
||||
};
|
||||
});
|
||||
|
||||
tagListData.sort((a, b) => {
|
||||
return naturalCompare.caseInsensitive(a.title, b.title);
|
||||
});
|
||||
|
||||
this.setState({ tagListData: tagListData });
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
tag: {
|
||||
padding: 10,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
},
|
||||
tagIconText: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
tagText: Object.assign({}, theme.normalText),
|
||||
tagCheckbox: {
|
||||
marginRight: 8,
|
||||
fontSize: 20,
|
||||
color: theme.color,
|
||||
},
|
||||
newTagBox: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
},
|
||||
newTagBoxLabel: Object.assign({}, theme.normalText, { marginRight: 8 }),
|
||||
newTagBoxInput: Object.assign({}, theme.lineInput, { flex: 1 }),
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const dialogContent = (
|
||||
<View style={{ flex: 1 }}>
|
||||
<View style={this.styles().newTagBox}>
|
||||
<Text style={this.styles().newTagBoxLabel}>{_('New tags:')}</Text>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
value={this.state.newTags}
|
||||
onChangeText={value => {
|
||||
this.setState({ newTags: value });
|
||||
}}
|
||||
style={this.styles().newTagBoxInput}
|
||||
/>
|
||||
</View>
|
||||
<FlatList data={this.state.tagListData} renderItem={this.renderTag} keyExtractor={this.tagKeyExtractor} />
|
||||
</View>
|
||||
);
|
||||
|
||||
return <ModalDialog theme={this.props.theme} ContentComponent={dialogContent} title={_('Type new tags or select from list')} onOkPress={this.okButton_press} onCancelPress={this.cancelButton_press} buttonBarEnabled={!this.state.savingTags} />;
|
||||
}
|
||||
}
|
||||
|
||||
const NoteTagsDialog = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
tags: state.tags,
|
||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
};
|
||||
})(NoteTagsDialogComponent);
|
||||
|
||||
module.exports = NoteTagsDialog;
|
489
Server/dist/lib/components/screens/config.js
vendored
489
Server/dist/lib/components/screens/config.js
vendored
@ -1,489 +0,0 @@
|
||||
const React = require('react');
|
||||
const { Platform, TouchableOpacity, Linking, View, Switch, StyleSheet, Text, Button, ScrollView, TextInput, Alert } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { Dropdown } = require('lib/components/Dropdown.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const shared = require('lib/components/shared/config-shared.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const NavService = require('lib/services/NavService.js');
|
||||
const VersionInfo = require('react-native-version-info').default;
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const { time } = require('lib/time-utils');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
const RNFS = require('react-native-fs');
|
||||
|
||||
import { PermissionsAndroid } from 'react-native';
|
||||
import Slider from '@react-native-community/slider';
|
||||
|
||||
class ConfigScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.styles_ = {};
|
||||
|
||||
this.state = {
|
||||
creatingReport: false,
|
||||
};
|
||||
|
||||
shared.init(this);
|
||||
|
||||
this.checkSyncConfig_ = async () => {
|
||||
await shared.checkSyncConfig(this, this.state.settings);
|
||||
};
|
||||
|
||||
this.e2eeConfig_ = () => {
|
||||
NavService.go('EncryptionConfig');
|
||||
};
|
||||
|
||||
this.saveButton_press = async () => {
|
||||
if (this.state.changedSettingKeys.includes('sync.target') && this.state.settings['sync.target'] === SyncTargetRegistry.nameToId('filesystem') && !(await this.checkFilesystemPermission())) {
|
||||
Alert.alert(_('Warning'), _('In order to use file system synchronisation your permission to write to external storage is required.'));
|
||||
// Save settings anyway, even if permission has not been granted
|
||||
}
|
||||
return shared.saveSettings(this);
|
||||
};
|
||||
|
||||
this.syncStatusButtonPress_ = () => {
|
||||
NavService.go('Status');
|
||||
};
|
||||
|
||||
this.exportDebugButtonPress_ = async () => {
|
||||
this.setState({ creatingReport: true });
|
||||
const service = new ReportService();
|
||||
|
||||
const logItems = await reg.logger().lastEntries(null);
|
||||
const logItemRows = [['Date', 'Level', 'Message']];
|
||||
for (let i = 0; i < logItems.length; i++) {
|
||||
const item = logItems[i];
|
||||
logItemRows.push([time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss'), item.level, item.message]);
|
||||
}
|
||||
const logItemCsv = service.csvCreate(logItemRows);
|
||||
|
||||
const itemListCsv = await service.basicItemList({ format: 'csv' });
|
||||
const filePath = `${RNFS.ExternalDirectoryPath}/syncReport-${new Date().getTime()}.txt`;
|
||||
|
||||
const finalText = [logItemCsv, itemListCsv].join('\n================================================================================\n');
|
||||
|
||||
await RNFS.writeFile(filePath, finalText);
|
||||
alert(`Debug report exported to ${filePath}`);
|
||||
this.setState({ creatingReport: false });
|
||||
};
|
||||
|
||||
this.fixSearchEngineIndexButtonPress_ = async () => {
|
||||
this.setState({ fixingSearchIndex: true });
|
||||
await SearchEngine.instance().rebuildIndex();
|
||||
this.setState({ fixingSearchIndex: false });
|
||||
};
|
||||
|
||||
this.logButtonPress_ = () => {
|
||||
NavService.go('Log');
|
||||
};
|
||||
}
|
||||
|
||||
async checkFilesystemPermission() {
|
||||
if (Platform.OS !== 'android') {
|
||||
// Not implemented yet
|
||||
return true;
|
||||
}
|
||||
const hasPermission = await PermissionsAndroid.check(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE);
|
||||
if (hasPermission) {
|
||||
return true;
|
||||
}
|
||||
const requestResult = await PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE, {
|
||||
title: _('Information'),
|
||||
message: _('In order to use file system synchronisation your permission to write to external storage is required.'),
|
||||
buttonPositive: _('OK'),
|
||||
});
|
||||
return requestResult === PermissionsAndroid.RESULTS.GRANTED;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({ settings: this.props.settings });
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
body: {
|
||||
flex: 1,
|
||||
justifyContent: 'flex-start',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
settingContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
paddingTop: theme.marginTop,
|
||||
paddingBottom: theme.marginBottom,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
},
|
||||
settingText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
paddingRight: 5,
|
||||
},
|
||||
descriptionText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
},
|
||||
sliderUnits: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
marginRight: 10,
|
||||
},
|
||||
settingDescriptionText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingBottom: theme.marginBottom,
|
||||
},
|
||||
permissionText: {
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
flex: 1,
|
||||
marginTop: 10,
|
||||
},
|
||||
settingControl: {
|
||||
color: theme.color,
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
styles.settingContainerNoBottomBorder = Object.assign({}, styles.settingContainer, {
|
||||
borderBottomWidth: 0,
|
||||
paddingBottom: theme.marginBottom / 2,
|
||||
});
|
||||
|
||||
styles.settingControl.borderBottomWidth = 1;
|
||||
styles.settingControl.borderBottomColor = theme.strongDividerColor;
|
||||
|
||||
styles.switchSettingText = Object.assign({}, styles.settingText);
|
||||
styles.switchSettingText.width = '80%';
|
||||
|
||||
styles.switchSettingContainer = Object.assign({}, styles.settingContainer);
|
||||
styles.switchSettingContainer.flexDirection = 'row';
|
||||
styles.switchSettingContainer.justifyContent = 'space-between';
|
||||
|
||||
styles.linkText = Object.assign({}, styles.settingText);
|
||||
styles.linkText.borderBottomWidth = 1;
|
||||
styles.linkText.borderBottomColor = theme.color;
|
||||
styles.linkText.flex = 0;
|
||||
styles.linkText.fontWeight = 'normal';
|
||||
|
||||
styles.headerWrapperStyle = Object.assign({}, styles.settingContainer, theme.headerWrapperStyle);
|
||||
|
||||
styles.switchSettingControl = Object.assign({}, styles.settingControl);
|
||||
delete styles.switchSettingControl.color;
|
||||
//styles.switchSettingControl.width = '20%';
|
||||
styles.switchSettingControl.flex = 0;
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
renderHeader(key, title) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
return (
|
||||
<View key={key} style={this.styles().headerWrapperStyle}>
|
||||
<Text style={theme.headerStyle}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderButton(key, title, clickHandler, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
let descriptionComp = null;
|
||||
if (options.description) {
|
||||
descriptionComp = (
|
||||
<View style={{ flex: 1, marginTop: 10 }}>
|
||||
<Text style={this.styles().descriptionText}>{options.description}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<View style={{ flex: 1, flexDirection: 'column' }}>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button title={title} onPress={clickHandler} disabled={!!options.disabled} />
|
||||
</View>
|
||||
{options.statusComp}
|
||||
{descriptionComp}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
sectionToComponent(key, section, settings) {
|
||||
const settingComps = [];
|
||||
|
||||
for (let i = 0; i < section.metadatas.length; i++) {
|
||||
const md = section.metadatas[i];
|
||||
|
||||
if (section.name === 'sync' && md.key === 'sync.resourceDownloadMode') {
|
||||
const syncTargetMd = SyncTargetRegistry.idToMetadata(settings['sync.target']);
|
||||
|
||||
if (syncTargetMd.supportsConfigCheck) {
|
||||
const messages = shared.checkSyncConfigMessages(this);
|
||||
const statusComp = !messages.length ? null : (
|
||||
<View style={{ flex: 1, marginTop: 10 }}>
|
||||
<Text style={this.styles().descriptionText}>{messages[0]}</Text>
|
||||
{messages.length >= 1 ? (
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Text style={this.styles().descriptionText}>{messages[1]}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
);
|
||||
|
||||
settingComps.push(this.renderButton('check_sync_config_button', _('Check synchronisation configuration'), this.checkSyncConfig_, { statusComp: statusComp }));
|
||||
}
|
||||
}
|
||||
|
||||
const settingComp = this.settingToComponent(md.key, settings[md.key]);
|
||||
settingComps.push(settingComp);
|
||||
}
|
||||
|
||||
if (section.name === 'sync') {
|
||||
settingComps.push(this.renderButton('e2ee_config_button', _('Encryption Config'), this.e2eeConfig_));
|
||||
}
|
||||
|
||||
if (!settingComps.length) return null;
|
||||
|
||||
return (
|
||||
<View key={key}>
|
||||
{this.renderHeader(section.name, Setting.sectionNameToLabel(section.name))}
|
||||
<View>{settingComps}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
settingToComponent(key, value) {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
let output = null;
|
||||
|
||||
const updateSettingValue = (key, value) => {
|
||||
return shared.updateSettingValue(this, key, value);
|
||||
};
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
const settingDescription = md.description ? md.description() : '';
|
||||
|
||||
if (md.isEnum) {
|
||||
value = value.toString();
|
||||
|
||||
let items = [];
|
||||
const settingOptions = md.options();
|
||||
for (let k in settingOptions) {
|
||||
if (!settingOptions.hasOwnProperty(k)) continue;
|
||||
items.push({ label: settingOptions[k], value: k.toString() });
|
||||
}
|
||||
|
||||
const descriptionComp = !settingDescription ? null : <Text style={this.styles().settingDescriptionText}>{settingDescription}</Text>;
|
||||
const containerStyle = !settingDescription ? this.styles().settingContainer : this.styles().settingContainerNoBottomBorder;
|
||||
|
||||
return (
|
||||
<View key={key} style={{ flexDirection: 'column', borderBottomWidth: 1, borderBottomColor: theme.dividerColor }}>
|
||||
<View style={containerStyle}>
|
||||
<Text key="label" style={this.styles().settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<Dropdown
|
||||
key="control"
|
||||
style={this.styles().settingControl}
|
||||
items={items}
|
||||
selectedValue={value}
|
||||
itemListStyle={{
|
||||
backgroundColor: theme.backgroundColor,
|
||||
}}
|
||||
headerStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
itemStyle={{
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
}}
|
||||
onValueChange={(itemValue) => {
|
||||
updateSettingValue(key, itemValue);
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
{descriptionComp}
|
||||
</View>
|
||||
);
|
||||
} else if (md.type == Setting.TYPE_BOOL) {
|
||||
return (
|
||||
<View key={key} style={this.styles().switchSettingContainer}>
|
||||
<Text key="label" style={this.styles().switchSettingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<Switch key="control" style={this.styles().switchSettingControl} value={value} onValueChange={value => updateSettingValue(key, value)} />
|
||||
</View>
|
||||
);
|
||||
} else if (md.type == Setting.TYPE_INT) {
|
||||
const unitLabel = md.unitLabel ? md.unitLabel(value) : value;
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<Text key="label" style={this.styles().settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', flex: 1 }}>
|
||||
<Text style={this.styles().sliderUnits}>{unitLabel}</Text>
|
||||
<Slider key="control" minimumTrackTintColor={theme.color} maximumTrackTintColor={theme.color} style={{ flex: 1 }} step={md.step} minimumValue={md.minimum} maximumValue={md.maximum} value={value} onValueChange={value => updateSettingValue(key, value)} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
} else if (md.type == Setting.TYPE_STRING) {
|
||||
return (
|
||||
<View key={key} style={this.styles().settingContainer}>
|
||||
<Text key="label" style={this.styles().settingText}>
|
||||
{md.label()}
|
||||
</Text>
|
||||
<TextInput autoCorrect={false} autoCompleteType="off" selectionColor={theme.textSelectionColor} autoCapitalize="none" key="control" style={this.styles().settingControl} value={value} onChangeText={value => updateSettingValue(key, value)} secureTextEntry={!!md.secure} />
|
||||
</View>
|
||||
);
|
||||
} else {
|
||||
//throw new Error('Unsupported setting type: ' + md.type);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
render() {
|
||||
const settings = this.state.settings;
|
||||
|
||||
const settingComps = shared.settingsToComponents2(this, 'mobile', settings);
|
||||
|
||||
settingComps.push(this.renderHeader('tools', _('Tools')));
|
||||
|
||||
settingComps.push(this.renderButton('status_button', _('Sync Status'), this.syncStatusButtonPress_));
|
||||
settingComps.push(this.renderButton('log_button', _('Log'), this.logButtonPress_));
|
||||
settingComps.push(this.renderButton('export_report_button', this.state.creatingReport ? _('Creating report...') : _('Export Debug Report'), this.exportDebugButtonPress_, { disabled: this.state.creatingReport }));
|
||||
settingComps.push(this.renderButton('fix_search_engine_index', this.state.fixingSearchIndex ? _('Fixing search index...') : _('Fix search index'), this.fixSearchEngineIndexButtonPress_, { disabled: this.state.fixingSearchIndex, description: _('Use this to rebuild the search index if there is a problem with search. It may take a long time depending on the number of notes.') }));
|
||||
|
||||
settingComps.push(this.renderHeader('moreInfo', _('More information')));
|
||||
|
||||
if (Platform.OS === 'android' && Platform.Version >= 23) {
|
||||
// Note: `PermissionsAndroid` doesn't work so we have to ask the user to manually
|
||||
// set these permissions. https://stackoverflow.com/questions/49771084/permission-always-returns-never-ask-again
|
||||
|
||||
settingComps.push(
|
||||
<View key="permission_info" style={this.styles().settingContainer}>
|
||||
<View key="permission_info_wrapper">
|
||||
<Text key="perm1a" style={this.styles().settingText}>
|
||||
{_('To work correctly, the app needs the following permissions. Please enable them in your phone settings, in Apps > Joplin > Permissions')}
|
||||
</Text>
|
||||
<Text key="perm2" style={this.styles().permissionText}>
|
||||
{_('- Storage: to allow attaching files to notes and to enable filesystem synchronisation.')}
|
||||
</Text>
|
||||
<Text key="perm3" style={this.styles().permissionText}>
|
||||
{_('- Camera: to allow taking a picture and attaching it to a note.')}
|
||||
</Text>
|
||||
<Text key="perm4" style={this.styles().permissionText}>
|
||||
{_('- Location: to allow attaching geo-location information to a note.')}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
settingComps.push(
|
||||
<View key="donate_link" style={this.styles().settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/donate/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={this.styles().linkText}>
|
||||
{_('Make a donation')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="website_link" style={this.styles().settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={this.styles().linkText}>
|
||||
{_('Joplin website')}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="privacy_link" style={this.styles().settingContainer}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/privacy/');
|
||||
}}
|
||||
>
|
||||
<Text key="label" style={this.styles().linkText}>
|
||||
Privacy Policy
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_app" style={this.styles().settingContainer}>
|
||||
<Text style={this.styles().settingText}>{`Joplin ${VersionInfo.appVersion}`}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_db" style={this.styles().settingContainer}>
|
||||
<Text style={this.styles().settingText}>{_('Database v%s', reg.db().version())}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
settingComps.push(
|
||||
<View key="version_info_fts" style={this.styles().settingContainer}>
|
||||
<Text style={this.styles().settingText}>{_('FTS enabled: %d', this.props.settings['db.ftsEnabled'])}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader title={_('Configuration')} showSaveButton={true} showSearchButton={false} showSideMenuButton={false} saveButtonDisabled={!this.state.changedSettingKeys.length} onSaveButtonPress={this.saveButton_press} />
|
||||
<ScrollView>{settingComps}</ScrollView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const ConfigScreen = connect(state => {
|
||||
return {
|
||||
settings: state.settings,
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(ConfigScreenComponent);
|
||||
|
||||
module.exports = { ConfigScreen };
|
@ -1,85 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { View, Button, Text, TextInput, TouchableOpacity, StyleSheet, ScrollView } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const Shared = require('lib/components/shared/dropbox-login-shared');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
class DropboxLoginScreenComponent extends BaseScreenComponent {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
this.shared_ = new Shared(this, msg => dialogs.info(this, msg), msg => dialogs.error(this, msg));
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.shared_.refreshUrl();
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
container: {
|
||||
padding: theme.margin,
|
||||
},
|
||||
stepText: Object.assign({}, theme.normalText, { marginBottom: theme.margin }),
|
||||
urlText: Object.assign({}, theme.urlText, { marginBottom: theme.margin }),
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
return (
|
||||
<View style={this.styles().screen}>
|
||||
<ScreenHeader title={_('Login with Dropbox')} />
|
||||
|
||||
<ScrollView style={this.styles().container}>
|
||||
<Text style={this.styles().stepText}>{_('To allow Joplin to synchronise with Dropbox, please follow the steps below:')}</Text>
|
||||
<Text style={this.styles().stepText}>{_('Step 1: Open this URL in your browser to authorise the application:')}</Text>
|
||||
<View>
|
||||
<TouchableOpacity onPress={this.shared_.loginUrl_click}>
|
||||
<Text style={this.styles().urlText}>{this.state.loginUrl}</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
<Text style={this.styles().stepText}>{_('Step 2: Enter the code provided by Dropbox:')}</Text>
|
||||
<TextInput placeholder={_('Enter code here')} placeholderTextColor={theme.colorFaded} selectionColor={theme.textSelectionColor} value={this.state.authCode} onChangeText={this.shared_.authCodeInput_change} style={theme.lineInput} />
|
||||
<View style={{ height: 10 }}></View>
|
||||
<Button disabled={this.state.checkingAuthToken} title={_('Submit')} onPress={this.shared_.submit_click}></Button>
|
||||
|
||||
{/* Add this extra padding to make sure the view is scrollable when the keyboard is visible on small screens (iPhone SE) */}
|
||||
<View style={{ height: 200 }}></View>
|
||||
</ScrollView>
|
||||
|
||||
<DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const DropboxLoginScreen = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(DropboxLoginScreenComponent);
|
||||
|
||||
module.exports = { DropboxLoginScreen };
|
@ -1,287 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { TextInput, TouchableOpacity, Linking, View, StyleSheet, Text, Button, ScrollView } = require('react-native');
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const shared = require('lib/components/shared/encryption-config-shared.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
||||
class EncryptionConfigScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
passwordPromptShow: false,
|
||||
passwordPromptAnswer: '',
|
||||
};
|
||||
|
||||
shared.constructor(this);
|
||||
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
initState(props) {
|
||||
return shared.initState(this, props);
|
||||
}
|
||||
|
||||
async refreshStats() {
|
||||
return shared.refreshStats(this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.initState(this.props);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||
this.initState(nextProps);
|
||||
}
|
||||
|
||||
async checkPasswords() {
|
||||
return shared.checkPasswords(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
if (this.styles_[themeId]) return this.styles_[themeId];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
titleText: {
|
||||
flex: 1,
|
||||
fontWeight: 'bold',
|
||||
flexDirection: 'column',
|
||||
fontSize: theme.fontSize,
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
marginTop: theme.marginTop,
|
||||
marginBottom: 5,
|
||||
color: theme.color,
|
||||
},
|
||||
normalText: {
|
||||
flex: 1,
|
||||
fontSize: theme.fontSize,
|
||||
color: theme.color,
|
||||
},
|
||||
container: {
|
||||
flex: 1,
|
||||
padding: theme.margin,
|
||||
},
|
||||
};
|
||||
|
||||
this.styles_[themeId] = StyleSheet.create(styles);
|
||||
return this.styles_[themeId];
|
||||
}
|
||||
|
||||
renderMasterKey(num, mk) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const onSaveClick = () => {
|
||||
return shared.onSavePasswordClick(this, mk);
|
||||
};
|
||||
|
||||
const onPasswordChange = text => {
|
||||
return shared.onPasswordChange(this, mk, text);
|
||||
};
|
||||
|
||||
const password = this.state.passwords[mk.id] ? this.state.passwords[mk.id] : '';
|
||||
const passwordOk = this.state.passwordChecks[mk.id] === true ? '✔' : '❌';
|
||||
|
||||
const inputStyle = { flex: 1, marginRight: 10, color: theme.color };
|
||||
inputStyle.borderBottomWidth = 1;
|
||||
inputStyle.borderBottomColor = theme.strongDividerColor;
|
||||
|
||||
return (
|
||||
<View key={mk.id}>
|
||||
<Text style={this.styles().titleText}>{_('Master Key %s', mk.id.substr(0, 6))}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Created: %s', time.formatMsToLocal(mk.created_time))}</Text>
|
||||
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
|
||||
<Text style={{ flex: 0, fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{_('Password:')}</Text>
|
||||
<TextInput selectionColor={theme.textSelectionColor} secureTextEntry={true} value={password} onChangeText={text => onPasswordChange(text)} style={inputStyle}></TextInput>
|
||||
<Text style={{ fontSize: theme.fontSize, marginRight: 10, color: theme.color }}>{passwordOk}</Text>
|
||||
<Button title={_('Save')} onPress={() => onSaveClick()}></Button>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
passwordPromptComponent() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const onEnableClick = async () => {
|
||||
try {
|
||||
const password = this.state.passwordPromptAnswer;
|
||||
if (!password) throw new Error(_('Password cannot be empty'));
|
||||
await EncryptionService.instance().generateMasterKeyAndEnableEncryption(password);
|
||||
this.setState({ passwordPromptShow: false });
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, borderColor: theme.dividerColor, borderWidth: 1, padding: 10, marginTop: 10, marginBottom: 10 }}>
|
||||
<Text style={{ fontSize: theme.fontSize, color: theme.color }}>{_('Enabling encryption means *all* your notes and attachments are going to be re-synchronised and sent encrypted to the sync target. Do not lose the password as, for security purposes, this will be the *only* way to decrypt the data! To enable encryption, please enter your password below.')}</Text>
|
||||
<TextInput
|
||||
selectionColor={theme.textSelectionColor}
|
||||
style={{ margin: 10, color: theme.color, borderWidth: 1, borderColor: theme.dividerColor }}
|
||||
secureTextEntry={true}
|
||||
value={this.state.passwordPromptAnswer}
|
||||
onChangeText={text => {
|
||||
this.setState({ passwordPromptAnswer: text });
|
||||
}}
|
||||
></TextInput>
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View style={{ flex: 1, marginRight: 10 }}>
|
||||
<Button
|
||||
title={_('Enable')}
|
||||
onPress={() => {
|
||||
onEnableClick();
|
||||
}}
|
||||
></Button>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button
|
||||
title={_('Cancel')}
|
||||
onPress={() => {
|
||||
this.setState({ passwordPromptShow: false });
|
||||
}}
|
||||
></Button>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const masterKeys = this.state.masterKeys;
|
||||
const decryptedItemsInfo = this.props.encryptionEnabled ? <Text style={this.styles().normalText}>{shared.decryptedStatText(this)}</Text> : null;
|
||||
|
||||
const mkComps = [];
|
||||
|
||||
let nonExistingMasterKeyIds = this.props.notLoadedMasterKeys.slice();
|
||||
|
||||
for (let i = 0; i < masterKeys.length; i++) {
|
||||
const mk = masterKeys[i];
|
||||
mkComps.push(this.renderMasterKey(i + 1, mk));
|
||||
|
||||
const idx = nonExistingMasterKeyIds.indexOf(mk.id);
|
||||
if (idx >= 0) nonExistingMasterKeyIds.splice(idx, 1);
|
||||
}
|
||||
|
||||
const onToggleButtonClick = async () => {
|
||||
if (this.props.encryptionEnabled) {
|
||||
const ok = await dialogs.confirm(this, _('Disabling encryption means *all* your notes and attachments are going to be re-synchronised and sent unencrypted to the sync target. Do you wish to continue?'));
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
await EncryptionService.instance().disableEncryption();
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
}
|
||||
} else {
|
||||
this.setState({
|
||||
passwordPromptShow: true,
|
||||
passwordPromptAnswer: '',
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let nonExistingMasterKeySection = null;
|
||||
|
||||
if (nonExistingMasterKeyIds.length) {
|
||||
const rows = [];
|
||||
for (let i = 0; i < nonExistingMasterKeyIds.length; i++) {
|
||||
const id = nonExistingMasterKeyIds[i];
|
||||
rows.push(
|
||||
<Text style={this.styles().normalText} key={id}>
|
||||
{id}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
nonExistingMasterKeySection = (
|
||||
<View>
|
||||
<Text style={this.styles().titleText}>{_('Missing Master Keys')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('The master keys with these IDs are used to encrypt some of your items, however the application does not currently have access to them. It is likely they will eventually be downloaded via synchronisation.')}</Text>
|
||||
<View style={{ marginTop: 10 }}>{rows}</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const passwordPromptComp = this.state.passwordPromptShow ? this.passwordPromptComponent() : null;
|
||||
const toggleButton = !this.state.passwordPromptShow ? (
|
||||
<View style={{ marginTop: 10 }}>
|
||||
<Button title={this.props.encryptionEnabled ? _('Disable encryption') : _('Enable encryption')} onPress={() => onToggleButtonClick()}></Button>
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader title={_('Encryption Config')} />
|
||||
<ScrollView style={this.styles().container}>
|
||||
{
|
||||
<View style={{ backgroundColor: theme.warningBackgroundColor, paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }}>
|
||||
<Text>{_('For more information about End-To-End Encryption (E2EE) and advice on how to enable it please check the documentation:')}</Text>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
Linking.openURL('https://joplinapp.org/e2ee/');
|
||||
}}
|
||||
>
|
||||
<Text>https://joplinapp.org/e2ee/</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
}
|
||||
|
||||
<Text style={this.styles().titleText}>{_('Status')}</Text>
|
||||
<Text style={this.styles().normalText}>{_('Encryption is: %s', this.props.encryptionEnabled ? _('Enabled') : _('Disabled'))}</Text>
|
||||
{decryptedItemsInfo}
|
||||
{toggleButton}
|
||||
{passwordPromptComp}
|
||||
{mkComps}
|
||||
{nonExistingMasterKeySection}
|
||||
<View style={{ flex: 1, height: 20 }}></View>
|
||||
</ScrollView>
|
||||
<DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const EncryptionConfigScreen = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
masterKeys: state.masterKeys,
|
||||
passwords: state.settings['encryption.passwordCache'],
|
||||
encryptionEnabled: state.settings['encryption.enabled'],
|
||||
activeMasterKeyId: state.settings['encryption.activeMasterKeyId'],
|
||||
notLoadedMasterKeys: state.notLoadedMasterKeys,
|
||||
};
|
||||
})(EncryptionConfigScreenComponent);
|
||||
|
||||
module.exports = { EncryptionConfigScreen };
|
128
Server/dist/lib/components/screens/folder.js
vendored
128
Server/dist/lib/components/screens/folder.js
vendored
@ -1,128 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { View, TextInput, StyleSheet } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class FolderScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
folder: Folder.new(),
|
||||
lastSavedFolder: null,
|
||||
};
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
textInput: {
|
||||
color: theme.color,
|
||||
paddingLeft: theme.marginLeft,
|
||||
marginTop: theme.marginTop,
|
||||
},
|
||||
};
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
if (!this.props.folderId) {
|
||||
const folder = Folder.new();
|
||||
this.setState({
|
||||
folder: folder,
|
||||
lastSavedFolder: Object.assign({}, folder),
|
||||
});
|
||||
} else {
|
||||
Folder.load(this.props.folderId).then(folder => {
|
||||
this.setState({
|
||||
folder: folder,
|
||||
lastSavedFolder: Object.assign({}, folder),
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
isModified() {
|
||||
if (!this.state.folder || !this.state.lastSavedFolder) return false;
|
||||
let diff = BaseModel.diffObjects(this.state.folder, this.state.lastSavedFolder);
|
||||
delete diff.type_;
|
||||
return !!Object.getOwnPropertyNames(diff).length;
|
||||
}
|
||||
|
||||
folderComponent_change(propName, propValue) {
|
||||
this.setState((prevState) => {
|
||||
let folder = Object.assign({}, prevState.folder);
|
||||
folder[propName] = propValue;
|
||||
return { folder: folder };
|
||||
});
|
||||
}
|
||||
|
||||
title_changeText(text) {
|
||||
this.folderComponent_change('title', text);
|
||||
}
|
||||
|
||||
async saveFolderButton_press() {
|
||||
let folder = Object.assign({}, this.state.folder);
|
||||
|
||||
try {
|
||||
folder = await Folder.save(folder, { userSideValidation: true });
|
||||
} catch (error) {
|
||||
dialogs.error(this, _('The notebook could not be saved: %s', error.message));
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
lastSavedFolder: Object.assign({}, folder),
|
||||
folder: folder,
|
||||
});
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Notes',
|
||||
folderId: folder.id,
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let saveButtonDisabled = !this.isModified();
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader title={_('Edit notebook')} showSaveButton={true} saveButtonDisabled={saveButtonDisabled} onSaveButtonPress={() => this.saveFolderButton_press()} showSideMenuButton={false} showSearchButton={false} />
|
||||
<TextInput placeholder={_('Enter notebook title')} underlineColorAndroid={theme.strongDividerColor} selectionColor={theme.textSelectionColor} style={this.styles().textInput} autoFocus={true} value={this.state.folder.title} onChangeText={text => this.title_changeText(text)} />
|
||||
<dialogs.DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const FolderScreen = connect(state => {
|
||||
return {
|
||||
folderId: state.selectedFolderId,
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(FolderScreenComponent);
|
||||
|
||||
module.exports = { FolderScreen };
|
139
Server/dist/lib/components/screens/log.js
vendored
139
Server/dist/lib/components/screens/log.js
vendored
@ -1,139 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { ListView, View, Text, Button, StyleSheet, Platform } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { time } = require('lib/time-utils');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
|
||||
class LogScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const ds = new ListView.DataSource({
|
||||
rowHasChanged: (r1, r2) => {
|
||||
return r1 !== r2;
|
||||
},
|
||||
});
|
||||
this.state = {
|
||||
dataSource: ds,
|
||||
showErrorsOnly: false,
|
||||
};
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
paddingLeft: 1,
|
||||
paddingRight: 1,
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
},
|
||||
rowText: {
|
||||
fontSize: 10,
|
||||
color: theme.color,
|
||||
},
|
||||
};
|
||||
|
||||
if (Platform.OS !== 'ios') {
|
||||
// Crashes on iOS with error "Unrecognized font family 'monospace'"
|
||||
styles.rowText.fontFamily = 'monospace';
|
||||
}
|
||||
|
||||
styles.rowTextError = Object.assign({}, styles.rowText);
|
||||
styles.rowTextError.color = theme.colorError;
|
||||
|
||||
styles.rowTextWarn = Object.assign({}, styles.rowText);
|
||||
styles.rowTextWarn.color = theme.colorWarn;
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.resfreshLogEntries();
|
||||
}
|
||||
|
||||
resfreshLogEntries(showErrorsOnly = null) {
|
||||
if (showErrorsOnly === null) showErrorsOnly = this.state.showErrorsOnly;
|
||||
|
||||
let levels = [Logger.LEVEL_DEBUG, Logger.LEVEL_INFO, Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
if (showErrorsOnly) levels = [Logger.LEVEL_WARN, Logger.LEVEL_ERROR];
|
||||
|
||||
reg
|
||||
.logger()
|
||||
.lastEntries(1000, { levels: levels })
|
||||
.then(entries => {
|
||||
const newDataSource = this.state.dataSource.cloneWithRows(entries);
|
||||
this.setState({ dataSource: newDataSource });
|
||||
});
|
||||
}
|
||||
|
||||
toggleErrorsOnly() {
|
||||
const showErrorsOnly = !this.state.showErrorsOnly;
|
||||
this.setState({ showErrorsOnly: showErrorsOnly });
|
||||
this.resfreshLogEntries(showErrorsOnly);
|
||||
}
|
||||
|
||||
render() {
|
||||
let renderRow = item => {
|
||||
let textStyle = this.styles().rowText;
|
||||
if (item.level == Logger.LEVEL_WARN) textStyle = this.styles().rowTextWarn;
|
||||
if (item.level == Logger.LEVEL_ERROR) textStyle = this.styles().rowTextError;
|
||||
|
||||
return (
|
||||
<View style={this.styles().row}>
|
||||
<Text style={textStyle}>{`${time.formatMsToLocal(item.timestamp, 'MM-DDTHH:mm:ss')}: ${item.message}`}</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// `enableEmptySections` is to fix this warning: https://github.com/FaridSafi/react-native-gifted-listview/issues/39
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader title={_('Log')} />
|
||||
<ListView dataSource={this.state.dataSource} renderRow={renderRow} enableEmptySections={true} />
|
||||
<View style={{ flexDirection: 'row' }}>
|
||||
<View style={{ flex: 1, marginRight: 5 }}>
|
||||
<Button
|
||||
title={_('Refresh')}
|
||||
onPress={() => {
|
||||
this.resfreshLogEntries();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
<View style={{ flex: 1 }}>
|
||||
<Button
|
||||
title={this.state.showErrorsOnly ? _('Show all') : _('Errors only')}
|
||||
onPress={() => {
|
||||
this.toggleErrorsOnly();
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const LogScreen = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(LogScreenComponent);
|
||||
|
||||
module.exports = { LogScreen };
|
927
Server/dist/lib/components/screens/note.js
vendored
927
Server/dist/lib/components/screens/note.js
vendored
@ -1,927 +0,0 @@
|
||||
const React = require('react');
|
||||
const { Platform, Clipboard, Keyboard, View, TextInput, StyleSheet, Linking, Image, Share } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { uuid } = require('lib/uuid.js');
|
||||
const RNFS = require('react-native-fs');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const md5 = require('md5');
|
||||
const { BackButtonService } = require('lib/services/back-button.js');
|
||||
const NavService = require('lib/services/NavService.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const { ActionButton } = require('lib/components/action-button.js');
|
||||
const { fileExtension, safeFileExtension } = require('lib/path-utils.js');
|
||||
const mimeUtils = require('lib/mime-utils.js').mime;
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const NoteTagsDialog = require('lib/components/screens/NoteTagsDialog');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { Checkbox } = require('lib/components/checkbox.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { shim } = require('lib/shim.js');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { themeStyle, editorFont } = require('lib/components/global-style.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { NoteBodyViewer } = require('lib/components/note-body-viewer.js');
|
||||
const { DocumentPicker, DocumentPickerUtil } = require('react-native-document-picker');
|
||||
const ImageResizer = require('react-native-image-resizer').default;
|
||||
const shared = require('lib/components/shared/note-screen-shared.js');
|
||||
const ImagePicker = require('react-native-image-picker');
|
||||
const { SelectDateTimeDialog } = require('lib/components/select-date-time-dialog.js');
|
||||
const ShareExtension = require('react-native-share-extension').default;
|
||||
const CameraView = require('lib/components/CameraView');
|
||||
const SearchEngine = require('lib/services/SearchEngine');
|
||||
const urlUtils = require('lib/urlUtils');
|
||||
|
||||
import FileViewer from 'react-native-file-viewer';
|
||||
|
||||
class NoteScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
note: Note.new(),
|
||||
mode: 'view',
|
||||
folder: null,
|
||||
lastSavedNote: null,
|
||||
isLoading: true,
|
||||
titleTextInputHeight: 20,
|
||||
alarmDialogShown: false,
|
||||
heightBumpView: 0,
|
||||
noteTagDialogShown: false,
|
||||
fromShare: false,
|
||||
showCamera: false,
|
||||
noteResources: {},
|
||||
|
||||
// HACK: For reasons I can't explain, when the WebView is present, the TextInput initially does not display (It's just a white rectangle with
|
||||
// no visible text). It will only appear when tapping it or doing certain action like selecting text on the webview. The bug started to
|
||||
// appear one day and did not go away - reverting to an old RN version did not help, undoing all
|
||||
// the commits till a working version did not help. The bug also does not happen in the simulator which makes it hard to fix.
|
||||
// Eventually, a way that "worked" is to add a 1px margin on top of the text input just after the webview has loaded, then removing this
|
||||
// margin. This forces RN to update the text input and to display it. Maybe that hack can be removed once RN is upgraded.
|
||||
// See https://github.com/laurent22/joplin/issues/1057
|
||||
HACK_webviewLoadingState: 0,
|
||||
};
|
||||
|
||||
this.doFocusUpdate_ = false;
|
||||
|
||||
// iOS doesn't support multiline text fields properly so disable it
|
||||
this.enableMultilineTitle_ = Platform.OS !== 'ios';
|
||||
|
||||
this.saveButtonHasBeenShown_ = false;
|
||||
|
||||
this.styles_ = {};
|
||||
|
||||
const saveDialog = async () => {
|
||||
if (this.isModified()) {
|
||||
let buttonId = await dialogs.pop(this, _('This note has been modified:'), [{ text: _('Save changes'), id: 'save' }, { text: _('Discard changes'), id: 'discard' }, { text: _('Cancel'), id: 'cancel' }]);
|
||||
|
||||
if (buttonId == 'cancel') return true;
|
||||
if (buttonId == 'save') await this.saveNoteButton_press();
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.navHandler = async () => {
|
||||
return await saveDialog();
|
||||
};
|
||||
|
||||
this.backHandler = async () => {
|
||||
const r = await saveDialog();
|
||||
if (r) return r;
|
||||
|
||||
if (!this.state.note.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.state.mode == 'edit') {
|
||||
Keyboard.dismiss();
|
||||
|
||||
this.setState({
|
||||
note: Object.assign({}, this.state.lastSavedNote),
|
||||
mode: 'view',
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
this.noteTagDialog_closeRequested = () => {
|
||||
this.setState({ noteTagDialogShown: false });
|
||||
};
|
||||
|
||||
this.onJoplinLinkClick_ = async msg => {
|
||||
try {
|
||||
if (msg.indexOf('joplin://') === 0) {
|
||||
const resourceUrlInfo = urlUtils.parseResourceUrl(msg);
|
||||
const itemId = resourceUrlInfo.itemId;
|
||||
const item = await BaseItem.loadItemById(itemId);
|
||||
if (!item) throw new Error(_('No item with ID %s', itemId));
|
||||
|
||||
if (item.type_ === BaseModel.TYPE_NOTE) {
|
||||
// Easier to just go back, then go to the note since
|
||||
// the Note screen doesn't handle reloading a different note
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_BACK',
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Note',
|
||||
noteId: item.id,
|
||||
noteHash: resourceUrlInfo.hash,
|
||||
});
|
||||
}, 5);
|
||||
} else if (item.type_ === BaseModel.TYPE_RESOURCE) {
|
||||
if (!(await Resource.isReady(item))) throw new Error(_('This attachment is not downloaded or not decrypted yet.'));
|
||||
const resourcePath = Resource.fullPath(item);
|
||||
await FileViewer.open(resourcePath);
|
||||
} else {
|
||||
throw new Error(_('The Joplin mobile app does not currently support this type of link: %s', BaseModel.modelTypeToName(item.type_)));
|
||||
}
|
||||
} else {
|
||||
if (msg.indexOf('file://') === 0) {
|
||||
throw new Error(_('Links with protocol "%s" are not supported', 'file://'));
|
||||
} else {
|
||||
Linking.openURL(msg);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
dialogs.error(this, error.message);
|
||||
}
|
||||
};
|
||||
|
||||
this.refreshResource = async resource => {
|
||||
if (!this.state.note || !this.state.note.body) return;
|
||||
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
|
||||
if (resourceIds.indexOf(resource.id) >= 0 && this.refs.noteBodyViewer) {
|
||||
shared.clearResourceCache();
|
||||
const attachedResources = await shared.attachedResources(this.state.note.body);
|
||||
this.setState({ noteResources: attachedResources }, () => {
|
||||
this.refs.noteBodyViewer.rebuildMd();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
this.takePhoto_onPress = this.takePhoto_onPress.bind(this);
|
||||
this.cameraView_onPhoto = this.cameraView_onPhoto.bind(this);
|
||||
this.cameraView_onCancel = this.cameraView_onCancel.bind(this);
|
||||
this.properties_onPress = this.properties_onPress.bind(this);
|
||||
this.onMarkForDownload = this.onMarkForDownload.bind(this);
|
||||
this.sideMenuOptions = this.sideMenuOptions.bind(this);
|
||||
this.folderPickerOptions_valueChanged = this.folderPickerOptions_valueChanged.bind(this);
|
||||
this.saveNoteButton_press = this.saveNoteButton_press.bind(this);
|
||||
this.onAlarmDialogAccept = this.onAlarmDialogAccept.bind(this);
|
||||
this.onAlarmDialogReject = this.onAlarmDialogReject.bind(this);
|
||||
this.todoCheckbox_change = this.todoCheckbox_change.bind(this);
|
||||
this.titleTextInput_contentSizeChange = this.titleTextInput_contentSizeChange.bind(this);
|
||||
this.title_changeText = this.title_changeText.bind(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
const themeId = this.props.theme;
|
||||
const theme = themeStyle(themeId);
|
||||
|
||||
const cacheKey = [themeId, this.state.titleTextInputHeight, this.state.HACK_webviewLoadingState].join('_');
|
||||
|
||||
if (this.styles_[cacheKey]) return this.styles_[cacheKey];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
bodyTextInput: {
|
||||
flex: 1,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
textAlignVertical: 'top',
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontSize: theme.fontSize,
|
||||
fontFamily: editorFont(this.props.editorFont),
|
||||
},
|
||||
noteBodyViewer: {
|
||||
flex: 1,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingTop: theme.marginTop,
|
||||
paddingBottom: theme.marginBottom,
|
||||
},
|
||||
checkbox: {
|
||||
color: theme.color,
|
||||
paddingRight: 10,
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingTop: 10, // Added for iOS (Not needed for Android??)
|
||||
paddingBottom: 10, // Added for iOS (Not needed for Android??)
|
||||
},
|
||||
};
|
||||
|
||||
styles.titleContainer = {
|
||||
flex: 0,
|
||||
flexDirection: 'row',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
borderBottomWidth: 1,
|
||||
};
|
||||
|
||||
styles.titleContainerTodo = Object.assign({}, styles.titleContainer);
|
||||
styles.titleContainerTodo.paddingLeft = 0;
|
||||
|
||||
styles.titleTextInput = {
|
||||
flex: 1,
|
||||
marginTop: 0,
|
||||
paddingLeft: 0,
|
||||
color: theme.color,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
fontWeight: 'bold',
|
||||
fontSize: theme.fontSize,
|
||||
paddingTop: 10, // Added for iOS (Not needed for Android??)
|
||||
paddingBottom: 10, // Added for iOS (Not needed for Android??)
|
||||
};
|
||||
|
||||
if (this.enableMultilineTitle_) styles.titleTextInput.height = this.state.titleTextInputHeight;
|
||||
if (this.state.HACK_webviewLoadingState === 1) styles.titleTextInput.marginTop = 1;
|
||||
|
||||
this.styles_[cacheKey] = StyleSheet.create(styles);
|
||||
return this.styles_[cacheKey];
|
||||
}
|
||||
|
||||
isModified() {
|
||||
return shared.isModified(this);
|
||||
}
|
||||
|
||||
async UNSAFE_componentWillMount() {
|
||||
BackButtonService.addHandler(this.backHandler);
|
||||
NavService.addHandler(this.navHandler);
|
||||
|
||||
shared.clearResourceCache();
|
||||
shared.installResourceHandling(this.refreshResource);
|
||||
|
||||
await shared.initState(this);
|
||||
|
||||
if (this.state.note && this.state.note.body && Setting.value('sync.resourceDownloadMode') === 'auto') {
|
||||
const resourceIds = await Note.linkedResourceIds(this.state.note.body);
|
||||
await ResourceFetcher.instance().markForDownload(resourceIds);
|
||||
}
|
||||
|
||||
this.focusUpdate();
|
||||
}
|
||||
|
||||
onMarkForDownload(event) {
|
||||
ResourceFetcher.instance().markForDownload(event.resourceId);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.doFocusUpdate_) {
|
||||
this.doFocusUpdate_ = false;
|
||||
this.focusUpdate();
|
||||
}
|
||||
|
||||
if (prevProps.showSideMenu !== this.props.showSideMenu && this.props.showSideMenu) {
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_SIDE_MENU_OPTIONS_SET',
|
||||
options: this.sideMenuOptions(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
BackButtonService.removeHandler(this.backHandler);
|
||||
NavService.removeHandler(this.navHandler);
|
||||
|
||||
shared.uninstallResourceHandling(this.refreshResource);
|
||||
|
||||
if (Platform.OS !== 'ios' && this.state.fromShare) {
|
||||
ShareExtension.close();
|
||||
}
|
||||
}
|
||||
|
||||
title_changeText(text) {
|
||||
shared.noteComponent_change(this, 'title', text);
|
||||
this.setState({ newAndNoTitleChangeNoteId: null });
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
body_changeText(text) {
|
||||
shared.noteComponent_change(this, 'body', text);
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
scheduleSave() {
|
||||
if (this.scheduleSaveIID_) {
|
||||
clearTimeout(this.scheduleSaveIID_);
|
||||
this.scheduleSaveIID_ = null;
|
||||
}
|
||||
|
||||
this.scheduleSaveIID_ = setTimeout(async () => {
|
||||
await shared.saveNoteButton_press(this);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async saveNoteButton_press(folderId = null) {
|
||||
await shared.saveNoteButton_press(this, folderId);
|
||||
|
||||
Keyboard.dismiss();
|
||||
}
|
||||
|
||||
async saveOneProperty(name, value) {
|
||||
await shared.saveOneProperty(this, name, value);
|
||||
}
|
||||
|
||||
async deleteNote_onPress() {
|
||||
let note = this.state.note;
|
||||
if (!note.id) return;
|
||||
|
||||
let ok = await dialogs.confirm(this, _('Delete note?'));
|
||||
if (!ok) return;
|
||||
|
||||
let folderId = note.parent_id;
|
||||
|
||||
await Note.delete(note.id);
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Notes',
|
||||
folderId: folderId,
|
||||
});
|
||||
}
|
||||
|
||||
async pickDocument() {
|
||||
return new Promise((resolve) => {
|
||||
DocumentPicker.show({ filetype: [DocumentPickerUtil.allFiles()] }, (error, res) => {
|
||||
if (error) {
|
||||
// Also returns an error if the user doesn't pick a file
|
||||
// so just resolve with null.
|
||||
console.info('pickDocument error:', error);
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(res);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async imageDimensions(uri) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Image.getSize(
|
||||
uri,
|
||||
(width, height) => {
|
||||
resolve({ width: width, height: height });
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
showImagePicker(options) {
|
||||
return new Promise((resolve) => {
|
||||
ImagePicker.launchImageLibrary(options, response => {
|
||||
resolve(response);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async resizeImage(localFilePath, targetPath, mimeType) {
|
||||
const maxSize = Resource.IMAGE_MAX_DIMENSION;
|
||||
|
||||
let dimensions = await this.imageDimensions(localFilePath);
|
||||
|
||||
reg.logger().info('Original dimensions ', dimensions);
|
||||
if (dimensions.width > maxSize || dimensions.height > maxSize) {
|
||||
dimensions.width = maxSize;
|
||||
dimensions.height = maxSize;
|
||||
}
|
||||
reg.logger().info('New dimensions ', dimensions);
|
||||
|
||||
const format = mimeType == 'image/png' ? 'PNG' : 'JPEG';
|
||||
reg.logger().info(`Resizing image ${localFilePath}`);
|
||||
const resizedImage = await ImageResizer.createResizedImage(localFilePath, dimensions.width, dimensions.height, format, 85); //, 0, targetPath);
|
||||
|
||||
const resizedImagePath = resizedImage.uri;
|
||||
reg.logger().info('Resized image ', resizedImagePath);
|
||||
reg.logger().info(`Moving ${resizedImagePath} => ${targetPath}`);
|
||||
|
||||
await RNFS.copyFile(resizedImagePath, targetPath);
|
||||
|
||||
try {
|
||||
await RNFS.unlink(resizedImagePath);
|
||||
} catch (error) {
|
||||
reg.logger().warn('Error when unlinking cached file: ', error);
|
||||
}
|
||||
}
|
||||
|
||||
async attachFile(pickerResponse, fileType) {
|
||||
if (!pickerResponse) {
|
||||
reg.logger().warn('Got no response from picker');
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickerResponse.error) {
|
||||
reg.logger().warn('Got error from picker', pickerResponse.error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickerResponse.didCancel) {
|
||||
reg.logger().info('User cancelled picker');
|
||||
return;
|
||||
}
|
||||
|
||||
const localFilePath = Platform.select({
|
||||
android: pickerResponse.uri,
|
||||
ios: decodeURI(pickerResponse.uri),
|
||||
});
|
||||
|
||||
let mimeType = pickerResponse.type;
|
||||
|
||||
if (!mimeType) {
|
||||
const ext = fileExtension(localFilePath);
|
||||
mimeType = mimeUtils.fromFileExtension(ext);
|
||||
}
|
||||
|
||||
if (!mimeType && fileType === 'image') {
|
||||
// Assume JPEG if we couldn't determine the file type. It seems to happen with the image picker
|
||||
// when the file path is something like content://media/external/images/media/123456
|
||||
// If the image is not a JPEG, something will throw an error below, but there's a good chance
|
||||
// it will work.
|
||||
reg.logger().info('Missing file type and could not detect it - assuming image/jpg');
|
||||
mimeType = 'image/jpg';
|
||||
}
|
||||
|
||||
reg.logger().info(`Got file: ${localFilePath}`);
|
||||
reg.logger().info(`Got type: ${mimeType}`);
|
||||
|
||||
let resource = Resource.new();
|
||||
resource.id = uuid.create();
|
||||
resource.mime = mimeType;
|
||||
resource.title = pickerResponse.fileName ? pickerResponse.fileName : '';
|
||||
resource.file_extension = safeFileExtension(fileExtension(pickerResponse.fileName ? pickerResponse.fileName : localFilePath));
|
||||
|
||||
if (!resource.mime) resource.mime = 'application/octet-stream';
|
||||
|
||||
let targetPath = Resource.fullPath(resource);
|
||||
|
||||
try {
|
||||
if (mimeType == 'image/jpeg' || mimeType == 'image/jpg' || mimeType == 'image/png') {
|
||||
await this.resizeImage(localFilePath, targetPath, pickerResponse.mime);
|
||||
} else {
|
||||
if (fileType === 'image') {
|
||||
dialogs.error(this, _('Unsupported image type: %s', mimeType));
|
||||
return;
|
||||
} else {
|
||||
await shim.fsDriver().copy(localFilePath, targetPath);
|
||||
|
||||
const stat = await shim.fsDriver().stat(targetPath);
|
||||
if (stat.size >= 10000000) {
|
||||
await shim.fsDriver().remove(targetPath);
|
||||
throw new Error('Resources larger than 10 MB are not currently supported as they may crash the mobile applications. The issue is being investigated and will be fixed at a later time.');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
reg.logger().warn('Could not attach file:', error);
|
||||
await dialogs.error(this, error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const itDoes = await shim.fsDriver().waitTillExists(targetPath);
|
||||
if (!itDoes) throw new Error(`Resource file was not created: ${targetPath}`);
|
||||
|
||||
const fileStat = await shim.fsDriver().stat(targetPath);
|
||||
resource.size = fileStat.size;
|
||||
|
||||
resource = await Resource.save(resource, { isNew: true });
|
||||
|
||||
const resourceTag = Resource.markdownTag(resource);
|
||||
|
||||
const newNote = Object.assign({}, this.state.note);
|
||||
newNote.body += `\n${resourceTag}`;
|
||||
this.setState({ note: newNote });
|
||||
|
||||
this.refreshResource(resource);
|
||||
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
async attachPhoto_onPress() {
|
||||
const response = await this.showImagePicker({ mediaType: 'photo' });
|
||||
await this.attachFile(response, 'image');
|
||||
}
|
||||
|
||||
takePhoto_onPress() {
|
||||
this.setState({ showCamera: true });
|
||||
}
|
||||
|
||||
cameraView_onPhoto(data) {
|
||||
this.attachFile(
|
||||
{
|
||||
uri: data.uri,
|
||||
didCancel: false,
|
||||
error: null,
|
||||
type: 'image/jpg',
|
||||
},
|
||||
'image'
|
||||
);
|
||||
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
|
||||
cameraView_onCancel() {
|
||||
this.setState({ showCamera: false });
|
||||
}
|
||||
|
||||
async attachFile_onPress() {
|
||||
const response = await this.pickDocument();
|
||||
await this.attachFile(response, 'all');
|
||||
}
|
||||
|
||||
toggleIsTodo_onPress() {
|
||||
shared.toggleIsTodo_onPress(this);
|
||||
|
||||
this.scheduleSave();
|
||||
}
|
||||
|
||||
tags_onPress() {
|
||||
if (!this.state.note || !this.state.note.id) return;
|
||||
|
||||
this.setState({ noteTagDialogShown: true });
|
||||
}
|
||||
|
||||
async share_onPress() {
|
||||
await Share.share({
|
||||
message: `${this.state.note.title}\n\n${this.state.note.body}`,
|
||||
title: this.state.note.title,
|
||||
});
|
||||
}
|
||||
|
||||
properties_onPress() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_OPEN' });
|
||||
}
|
||||
|
||||
setAlarm_onPress() {
|
||||
this.setState({ alarmDialogShown: true });
|
||||
}
|
||||
|
||||
async onAlarmDialogAccept(date) {
|
||||
let newNote = Object.assign({}, this.state.note);
|
||||
newNote.todo_due = date ? date.getTime() : 0;
|
||||
|
||||
await this.saveOneProperty('todo_due', date ? date.getTime() : 0);
|
||||
|
||||
this.setState({ alarmDialogShown: false });
|
||||
}
|
||||
|
||||
onAlarmDialogReject() {
|
||||
this.setState({ alarmDialogShown: false });
|
||||
}
|
||||
|
||||
async showOnMap_onPress() {
|
||||
if (!this.state.note.id) return;
|
||||
|
||||
let note = await Note.load(this.state.note.id);
|
||||
try {
|
||||
const url = Note.geolocationUrl(note);
|
||||
Linking.openURL(url);
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async showSource_onPress() {
|
||||
if (!this.state.note.id) return;
|
||||
|
||||
let note = await Note.load(this.state.note.id);
|
||||
try {
|
||||
Linking.openURL(note.source_url);
|
||||
} catch (error) {
|
||||
await dialogs.error(this, error.message);
|
||||
}
|
||||
}
|
||||
|
||||
copyMarkdownLink_onPress() {
|
||||
const note = this.state.note;
|
||||
Clipboard.setString(Note.markdownTag(note));
|
||||
}
|
||||
|
||||
sideMenuOptions() {
|
||||
const note = this.state.note;
|
||||
if (!note) return [];
|
||||
|
||||
const output = [];
|
||||
|
||||
const createdDateString = time.formatMsToLocal(note.user_created_time);
|
||||
const updatedDateString = time.formatMsToLocal(note.user_updated_time);
|
||||
|
||||
output.push({ title: _('Created: %s', createdDateString) });
|
||||
output.push({ title: _('Updated: %s', updatedDateString) });
|
||||
output.push({ isDivider: true });
|
||||
|
||||
output.push({
|
||||
title: _('View on map'),
|
||||
onPress: () => {
|
||||
this.showOnMap_onPress();
|
||||
},
|
||||
});
|
||||
if (note.source_url)
|
||||
output.push({
|
||||
title: _('Go to source URL'),
|
||||
onPress: () => {
|
||||
this.showSource_onPress();
|
||||
},
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
menuOptions() {
|
||||
const note = this.state.note;
|
||||
const isTodo = note && !!note.is_todo;
|
||||
const isSaved = note && note.id;
|
||||
|
||||
const cacheKey = md5([isTodo, isSaved].join('_'));
|
||||
if (!this.menuOptionsCache_) this.menuOptionsCache_ = {};
|
||||
|
||||
if (this.menuOptionsCache_[cacheKey]) return this.menuOptionsCache_[cacheKey];
|
||||
|
||||
let output = [];
|
||||
|
||||
// The file attachement modules only work in Android >= 5 (Version 21)
|
||||
// https://github.com/react-community/react-native-image-picker/issues/606
|
||||
let canAttachPicture = true;
|
||||
if (Platform.OS === 'android' && Platform.Version < 21) canAttachPicture = false;
|
||||
if (canAttachPicture) {
|
||||
output.push({
|
||||
title: _('Attach...'),
|
||||
onPress: async () => {
|
||||
const buttonId = await dialogs.pop(this, _('Choose an option'), [{ text: _('Take photo'), id: 'takePhoto' }, { text: _('Attach photo'), id: 'attachPhoto' }, { text: _('Attach any file'), id: 'attachFile' }]);
|
||||
|
||||
if (buttonId === 'takePhoto') this.takePhoto_onPress();
|
||||
if (buttonId === 'attachPhoto') this.attachPhoto_onPress();
|
||||
if (buttonId === 'attachFile') this.attachFile_onPress();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (isTodo) {
|
||||
output.push({
|
||||
title: _('Set alarm'),
|
||||
onPress: () => {
|
||||
this.setState({ alarmDialogShown: true });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
output.push({
|
||||
title: _('Share'),
|
||||
onPress: () => {
|
||||
this.share_onPress();
|
||||
},
|
||||
});
|
||||
if (isSaved)
|
||||
output.push({
|
||||
title: _('Tags'),
|
||||
onPress: () => {
|
||||
this.tags_onPress();
|
||||
},
|
||||
});
|
||||
output.push({
|
||||
title: isTodo ? _('Convert to note') : _('Convert to todo'),
|
||||
onPress: () => {
|
||||
this.toggleIsTodo_onPress();
|
||||
},
|
||||
});
|
||||
if (isSaved)
|
||||
output.push({
|
||||
title: _('Copy Markdown link'),
|
||||
onPress: () => {
|
||||
this.copyMarkdownLink_onPress();
|
||||
},
|
||||
});
|
||||
output.push({
|
||||
title: _('Properties'),
|
||||
onPress: () => {
|
||||
this.properties_onPress();
|
||||
},
|
||||
});
|
||||
output.push({
|
||||
title: _('Delete'),
|
||||
onPress: () => {
|
||||
this.deleteNote_onPress();
|
||||
},
|
||||
});
|
||||
|
||||
this.menuOptionsCache_ = {};
|
||||
this.menuOptionsCache_[cacheKey] = output;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async todoCheckbox_change(checked) {
|
||||
await this.saveOneProperty('todo_completed', checked ? time.unixMs() : 0);
|
||||
}
|
||||
|
||||
titleTextInput_contentSizeChange(event) {
|
||||
if (!this.enableMultilineTitle_) return;
|
||||
|
||||
let height = event.nativeEvent.contentSize.height;
|
||||
this.setState({ titleTextInputHeight: height });
|
||||
}
|
||||
|
||||
focusUpdate() {
|
||||
this.scheduleFocusUpdateIID_ = null;
|
||||
if (!this.state.note) return;
|
||||
let fieldToFocus = this.state.note.is_todo ? 'title' : 'body';
|
||||
if (this.state.mode === 'view') fieldToFocus = '';
|
||||
|
||||
if (fieldToFocus === 'title') this.refs.titleTextField.focus();
|
||||
if (fieldToFocus === 'body') this.refs.noteBodyTextField.focus();
|
||||
}
|
||||
|
||||
async folderPickerOptions_valueChanged(itemValue) {
|
||||
const note = this.state.note;
|
||||
|
||||
if (!note.id) {
|
||||
await this.saveNoteButton_press(itemValue);
|
||||
} else {
|
||||
await Note.moveToFolder(note.id, itemValue);
|
||||
}
|
||||
|
||||
note.parent_id = itemValue;
|
||||
|
||||
const folder = await Folder.load(note.parent_id);
|
||||
|
||||
this.setState({
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
note: note,
|
||||
folder: folder,
|
||||
});
|
||||
}
|
||||
|
||||
folderPickerOptions() {
|
||||
const options = {
|
||||
enabled: true,
|
||||
selectedFolderId: this.state.folder ? this.state.folder.id : null,
|
||||
onValueChange: this.folderPickerOptions_valueChanged,
|
||||
};
|
||||
|
||||
if (this.folderPickerOptions_ && options.selectedFolderId === this.folderPickerOptions_.selectedFolderId) return this.folderPickerOptions_;
|
||||
|
||||
this.folderPickerOptions_ = options;
|
||||
return this.folderPickerOptions_;
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.isLoading) {
|
||||
return (
|
||||
<View style={this.styles().screen}>
|
||||
<ScreenHeader />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
const note = this.state.note;
|
||||
const isTodo = !!Number(note.is_todo);
|
||||
|
||||
if (this.state.showCamera) {
|
||||
return <CameraView theme={this.props.theme} style={{ flex: 1 }} onPhoto={this.cameraView_onPhoto} onCancel={this.cameraView_onCancel} />;
|
||||
}
|
||||
|
||||
let bodyComponent = null;
|
||||
if (this.state.mode == 'view') {
|
||||
const onCheckboxChange = newBody => {
|
||||
this.saveOneProperty('body', newBody);
|
||||
};
|
||||
|
||||
// Currently keyword highlighting is supported only when FTS is available.
|
||||
let keywords = [];
|
||||
if (this.props.searchQuery && !!this.props.ftsEnabled) {
|
||||
const parsedQuery = SearchEngine.instance().parseQuery(this.props.searchQuery);
|
||||
keywords = SearchEngine.instance().allParsedQueryTerms(parsedQuery);
|
||||
}
|
||||
|
||||
// Note: as of 2018-12-29 it's important not to display the viewer if the note body is empty,
|
||||
// to avoid the HACK_webviewLoadingState related bug.
|
||||
bodyComponent =
|
||||
!note || !note.body.trim() ? null : (
|
||||
<NoteBodyViewer
|
||||
onJoplinLinkClick={this.onJoplinLinkClick_}
|
||||
ref="noteBodyViewer"
|
||||
style={this.styles().noteBodyViewer}
|
||||
webViewStyle={theme}
|
||||
note={note}
|
||||
noteResources={this.state.noteResources}
|
||||
highlightedKeywords={keywords}
|
||||
theme={this.props.theme}
|
||||
noteHash={this.props.noteHash}
|
||||
onCheckboxChange={newBody => {
|
||||
onCheckboxChange(newBody);
|
||||
}}
|
||||
onMarkForDownload={this.onMarkForDownload}
|
||||
onLoadEnd={() => {
|
||||
setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 1 });
|
||||
setTimeout(() => {
|
||||
this.setState({ HACK_webviewLoadingState: 0 });
|
||||
}, 50);
|
||||
}, 5);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
// autoFocus={fieldToFocus === 'body'}
|
||||
|
||||
// Note: blurOnSubmit is necessary to get multiline to work.
|
||||
// See https://github.com/facebook/react-native/issues/12717#issuecomment-327001997
|
||||
bodyComponent = <TextInput autoCapitalize="sentences" style={this.styles().bodyTextInput} ref="noteBodyTextField" multiline={true} value={note.body} onChangeText={text => this.body_changeText(text)} blurOnSubmit={false} selectionColor={theme.textSelectionColor} placeholder={_('Add body')} placeholderTextColor={theme.colorFaded} />;
|
||||
}
|
||||
|
||||
const renderActionButton = () => {
|
||||
let buttons = [];
|
||||
|
||||
buttons.push({
|
||||
title: _('Edit'),
|
||||
icon: 'md-create',
|
||||
onPress: () => {
|
||||
this.setState({ mode: 'edit' });
|
||||
|
||||
this.doFocusUpdate_ = true;
|
||||
},
|
||||
});
|
||||
|
||||
if (this.state.mode == 'edit') return null;
|
||||
|
||||
return <ActionButton multiStates={true} buttons={buttons} buttonIndex={0} />;
|
||||
};
|
||||
|
||||
const actionButtonComp = renderActionButton();
|
||||
|
||||
let showSaveButton = this.state.mode == 'edit' || this.isModified() || this.saveButtonHasBeenShown_;
|
||||
let saveButtonDisabled = !this.isModified();
|
||||
|
||||
if (showSaveButton) this.saveButtonHasBeenShown_ = true;
|
||||
|
||||
const titleContainerStyle = isTodo ? this.styles().titleContainerTodo : this.styles().titleContainer;
|
||||
|
||||
const dueDate = Note.dueDateObject(note);
|
||||
|
||||
const titleComp = (
|
||||
<View style={titleContainerStyle}>
|
||||
{isTodo && <Checkbox style={this.styles().checkbox} checked={!!Number(note.todo_completed)} onChange={this.todoCheckbox_change} />}
|
||||
<TextInput onContentSizeChange={this.titleTextInput_contentSizeChange} multiline={this.enableMultilineTitle_} ref="titleTextField" underlineColorAndroid="#ffffff00" autoCapitalize="sentences" style={this.styles().titleTextInput} value={note.title} onChangeText={this.title_changeText} selectionColor={theme.textSelectionColor} placeholder={_('Add title')} placeholderTextColor={theme.colorFaded} />
|
||||
</View>
|
||||
);
|
||||
|
||||
const noteTagDialog = !this.state.noteTagDialogShown ? null : <NoteTagsDialog onCloseRequested={this.noteTagDialog_closeRequested} />;
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader folderPickerOptions={this.folderPickerOptions()} menuOptions={this.menuOptions()} showSaveButton={showSaveButton} saveButtonDisabled={saveButtonDisabled} onSaveButtonPress={this.saveNoteButton_press} showSideMenuButton={false} showSearchButton={false} />
|
||||
{titleComp}
|
||||
{bodyComponent}
|
||||
{actionButtonComp}
|
||||
|
||||
<SelectDateTimeDialog shown={this.state.alarmDialogShown} date={dueDate} onAccept={this.onAlarmDialogAccept} onReject={this.onAlarmDialogReject} />
|
||||
|
||||
<DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
{noteTagDialog}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const NoteScreen = connect(state => {
|
||||
return {
|
||||
noteId: state.selectedNoteIds.length ? state.selectedNoteIds[0] : null,
|
||||
noteHash: state.selectedNoteHash,
|
||||
folderId: state.selectedFolderId,
|
||||
itemType: state.selectedItemType,
|
||||
folders: state.folders,
|
||||
searchQuery: state.searchQuery,
|
||||
theme: state.settings.theme,
|
||||
editorFont: [state.settings['style.editor.fontFamily']],
|
||||
ftsEnabled: state.settings['db.ftsEnabled'],
|
||||
sharedData: state.sharedData,
|
||||
showSideMenu: state.showSideMenu,
|
||||
};
|
||||
})(NoteScreenComponent);
|
||||
|
||||
module.exports = { NoteScreen };
|
257
Server/dist/lib/components/screens/notes.js
vendored
257
Server/dist/lib/components/screens/notes.js
vendored
@ -1,257 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { AppState, View, StyleSheet } = require('react-native');
|
||||
const { stateUtils } = require('lib/reducer.js');
|
||||
const { connect } = require('react-redux');
|
||||
const { NoteList } = require('lib/components/note-list.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { ActionButton } = require('lib/components/action-button.js');
|
||||
const { dialogs } = require('lib/dialogs.js');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
|
||||
class NotesScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.onAppStateChange_ = async () => {
|
||||
// Force an update to the notes list when app state changes
|
||||
let newProps = Object.assign({}, this.props);
|
||||
newProps.notesSource = '';
|
||||
await this.refreshNotes(newProps);
|
||||
};
|
||||
|
||||
this.sortButton_press = async () => {
|
||||
const buttons = [];
|
||||
const sortNoteOptions = Setting.enumOptions('notes.sortOrder.field');
|
||||
|
||||
const makeCheckboxText = function(selected, sign, label) {
|
||||
const s = sign === 'tick' ? '✓' : '⬤';
|
||||
return (selected ? `${s} ` : '') + label;
|
||||
};
|
||||
|
||||
for (let field in sortNoteOptions) {
|
||||
if (!sortNoteOptions.hasOwnProperty(field)) continue;
|
||||
buttons.push({
|
||||
text: makeCheckboxText(Setting.value('notes.sortOrder.field') === field, 'bullet', sortNoteOptions[field]),
|
||||
id: { name: 'notes.sortOrder.field', value: field },
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
text: makeCheckboxText(Setting.value('notes.sortOrder.reverse'), 'tick', `[ ${Setting.settingMetadata('notes.sortOrder.reverse').label()} ]`),
|
||||
id: { name: 'notes.sortOrder.reverse', value: !Setting.value('notes.sortOrder.reverse') },
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
text: makeCheckboxText(Setting.value('uncompletedTodosOnTop'), 'tick', `[ ${Setting.settingMetadata('uncompletedTodosOnTop').label()} ]`),
|
||||
id: { name: 'uncompletedTodosOnTop', value: !Setting.value('uncompletedTodosOnTop') },
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
text: makeCheckboxText(Setting.value('showCompletedTodos'), 'tick', `[ ${Setting.settingMetadata('showCompletedTodos').label()} ]`),
|
||||
id: { name: 'showCompletedTodos', value: !Setting.value('showCompletedTodos') },
|
||||
});
|
||||
|
||||
const r = await dialogs.pop(this, Setting.settingMetadata('notes.sortOrder.field').label(), buttons);
|
||||
if (!r) return;
|
||||
|
||||
Setting.setValue(r.name, r.value);
|
||||
};
|
||||
}
|
||||
|
||||
styles() {
|
||||
if (!this.styles_) this.styles_ = {};
|
||||
const themeId = this.props.theme;
|
||||
const cacheKey = themeId;
|
||||
|
||||
if (this.styles_[cacheKey]) return this.styles_[cacheKey];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
noteList: {
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
this.styles_[cacheKey] = StyleSheet.create(styles);
|
||||
return this.styles_[cacheKey];
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
await this.refreshNotes();
|
||||
AppState.addEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
async componentWillUnmount() {
|
||||
AppState.removeEventListener('change', this.onAppStateChange_);
|
||||
}
|
||||
|
||||
async componentDidUpdate(prevProps) {
|
||||
if (prevProps.notesOrder !== this.props.notesOrder || prevProps.selectedFolderId != this.props.selectedFolderId || prevProps.selectedTagId != this.props.selectedTagId || prevProps.selectedSmartFilterId != this.props.selectedSmartFilterId || prevProps.notesParentType != this.props.notesParentType) {
|
||||
await this.refreshNotes(this.props);
|
||||
}
|
||||
}
|
||||
|
||||
async refreshNotes(props = null) {
|
||||
if (props === null) props = this.props;
|
||||
|
||||
let options = {
|
||||
order: props.notesOrder,
|
||||
uncompletedTodosOnTop: props.uncompletedTodosOnTop,
|
||||
showCompletedTodos: props.showCompletedTodos,
|
||||
caseInsensitive: true,
|
||||
};
|
||||
|
||||
const parent = this.parentItem(props);
|
||||
if (!parent) return;
|
||||
|
||||
const source = JSON.stringify({
|
||||
options: options,
|
||||
parentId: parent.id,
|
||||
});
|
||||
|
||||
if (source == props.notesSource) return;
|
||||
|
||||
let notes = [];
|
||||
if (props.notesParentType === 'Folder') {
|
||||
notes = await Note.previews(props.selectedFolderId, options);
|
||||
} else if (props.notesParentType === 'Tag') {
|
||||
notes = await Tag.notes(props.selectedTagId, options);
|
||||
} else if (props.notesParentType === 'SmartFilter') {
|
||||
notes = await Note.previews(null, options);
|
||||
}
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NOTE_UPDATE_ALL',
|
||||
notes: notes,
|
||||
notesSource: source,
|
||||
});
|
||||
}
|
||||
|
||||
deleteFolder_onPress(folderId) {
|
||||
dialogs.confirm(this, _('Delete notebook? All notes and sub-notebooks within this notebook will also be deleted.')).then(ok => {
|
||||
if (!ok) return;
|
||||
|
||||
Folder.delete(folderId)
|
||||
.then(() => {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Notes',
|
||||
smartFilterId: 'c3176726992c11e9ac940492261af972',
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
alert(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editFolder_onPress(folderId) {
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: folderId,
|
||||
});
|
||||
}
|
||||
|
||||
parentItem(props = null) {
|
||||
if (!props) props = this.props;
|
||||
|
||||
let output = null;
|
||||
if (props.notesParentType == 'Folder') {
|
||||
output = Folder.byId(props.folders, props.selectedFolderId);
|
||||
} else if (props.notesParentType == 'Tag') {
|
||||
output = Tag.byId(props.tags, props.selectedTagId);
|
||||
} else if (props.notesParentType == 'SmartFilter') {
|
||||
output = { id: this.props.selectedSmartFilterId, title: _('All notes') };
|
||||
} else {
|
||||
return null;
|
||||
// throw new Error('Invalid parent type: ' + props.notesParentType);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
folderPickerOptions() {
|
||||
const options = {
|
||||
enabled: this.props.noteSelectionEnabled,
|
||||
mustSelect: true,
|
||||
};
|
||||
|
||||
if (this.folderPickerOptions_ && options.enabled === this.folderPickerOptions_.enabled) return this.folderPickerOptions_;
|
||||
|
||||
this.folderPickerOptions_ = options;
|
||||
return this.folderPickerOptions_;
|
||||
}
|
||||
|
||||
render() {
|
||||
const parent = this.parentItem();
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
let rootStyle = {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
if (!this.props.visible) {
|
||||
rootStyle.flex = 0.001; // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it
|
||||
}
|
||||
|
||||
if (!parent) {
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={title} showSideMenuButton={true} showBackButton={false} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
let title = parent ? parent.title : null;
|
||||
const addFolderNoteButtons = this.props.selectedFolderId && this.props.selectedFolderId != Folder.conflictFolderId();
|
||||
const thisComp = this;
|
||||
const actionButtonComp = this.props.noteSelectionEnabled || !this.props.visible ? null : <ActionButton addFolderNoteButtons={addFolderNoteButtons} parentFolderId={this.props.selectedFolderId}></ActionButton>;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={title} showBackButton={false} parentComponent={thisComp} sortButton_press={this.sortButton_press} folderPickerOptions={this.folderPickerOptions()} showSearchButton={true} showSideMenuButton={true} />
|
||||
<NoteList style={this.styles().noteList} />
|
||||
{actionButtonComp}
|
||||
<DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const NotesScreen = connect(state => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
tags: state.tags,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedNoteIds: state.selectedNoteIds,
|
||||
selectedTagId: state.selectedTagId,
|
||||
selectedSmartFilterId: state.selectedSmartFilterId,
|
||||
notesParentType: state.notesParentType,
|
||||
notes: state.notes,
|
||||
notesSource: state.notesSource,
|
||||
uncompletedTodosOnTop: state.settings.uncompletedTodosOnTop,
|
||||
showCompletedTodos: state.settings.showCompletedTodos,
|
||||
theme: state.settings.theme,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
notesOrder: stateUtils.notesOrder(state.settings),
|
||||
};
|
||||
})(NotesScreenComponent);
|
||||
|
||||
module.exports = { NotesScreen };
|
123
Server/dist/lib/components/screens/onedrive-login.js
vendored
123
Server/dist/lib/components/screens/onedrive-login.js
vendored
@ -1,123 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { View } = require('react-native');
|
||||
const { Button } = require('react-native');
|
||||
const { WebView } = require('react-native-webview');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const parseUri = require('lib/parseUri');
|
||||
|
||||
class OneDriveLoginScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = { webviewUrl: '' };
|
||||
this.authCode_ = null;
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.setState({
|
||||
webviewUrl: this.startUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
startUrl() {
|
||||
return reg
|
||||
.syncTarget()
|
||||
.api()
|
||||
.authCodeUrl(this.redirectUrl());
|
||||
}
|
||||
|
||||
redirectUrl() {
|
||||
return reg
|
||||
.syncTarget()
|
||||
.api()
|
||||
.nativeClientRedirectUrl();
|
||||
}
|
||||
|
||||
async webview_load(noIdeaWhatThisIs) {
|
||||
// This is deprecated according to the doc but since the non-deprecated property (source)
|
||||
// doesn't exist, use this for now. The whole component is completely undocumented
|
||||
// at the moment so it's likely to change.
|
||||
const url = noIdeaWhatThisIs.url;
|
||||
const parsedUrl = parseUri(url);
|
||||
|
||||
if (!this.authCode_ && parsedUrl && parsedUrl.queryKey && parsedUrl.queryKey.code) {
|
||||
this.authCode_ = parsedUrl.queryKey.code;
|
||||
|
||||
try {
|
||||
await reg
|
||||
.syncTarget()
|
||||
.api()
|
||||
.execTokenRequest(this.authCode_, this.redirectUrl(), true);
|
||||
this.props.dispatch({ type: 'NAV_BACK' });
|
||||
reg.scheduleSync(0);
|
||||
} catch (error) {
|
||||
alert(`Could not login to OneDrive. Please try again\n\n${error.message}\n\n${url}`);
|
||||
}
|
||||
|
||||
this.authCode_ = null;
|
||||
}
|
||||
}
|
||||
|
||||
async webview_error() {
|
||||
alert('Could not load page. Please check your connection and try again.');
|
||||
}
|
||||
|
||||
retryButton_click() {
|
||||
// It seems the only way it would reload the page is by loading an unrelated
|
||||
// URL, waiting a bit, and then loading the actual URL. There's probably
|
||||
// a better way to do this.
|
||||
|
||||
this.setState({
|
||||
webviewUrl: 'https://microsoft.com',
|
||||
});
|
||||
this.forceUpdate();
|
||||
|
||||
setTimeout(() => {
|
||||
this.setState({
|
||||
webviewUrl: this.startUrl(),
|
||||
});
|
||||
this.forceUpdate();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
render() {
|
||||
const source = {
|
||||
uri: this.state.webviewUrl,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={this.styles().screen}>
|
||||
<ScreenHeader title={_('Login with OneDrive')} />
|
||||
<WebView
|
||||
source={source}
|
||||
onNavigationStateChange={o => {
|
||||
this.webview_load(o);
|
||||
}}
|
||||
onError={() => {
|
||||
this.webview_error();
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title={_('Refresh')}
|
||||
onPress={() => {
|
||||
this.retryButton_click();
|
||||
}}
|
||||
></Button>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const OneDriveLoginScreen = connect(() => {
|
||||
return {};
|
||||
})(OneDriveLoginScreenComponent);
|
||||
|
||||
module.exports = { OneDriveLoginScreen };
|
209
Server/dist/lib/components/screens/search.js
vendored
209
Server/dist/lib/components/screens/search.js
vendored
@ -1,209 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { StyleSheet, View, TextInput, FlatList, TouchableHighlight } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const { NoteItem } = require('lib/components/note-item.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const SearchEngineUtils = require('lib/services/SearchEngineUtils');
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
|
||||
class SearchScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
query: '',
|
||||
notes: [],
|
||||
};
|
||||
this.isMounted_ = false;
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
body: {
|
||||
flex: 1,
|
||||
},
|
||||
searchContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
borderWidth: 1,
|
||||
borderColor: theme.dividerColor,
|
||||
},
|
||||
};
|
||||
|
||||
styles.searchTextInput = Object.assign({}, theme.lineInput);
|
||||
styles.searchTextInput.paddingLeft = theme.marginLeft;
|
||||
styles.searchTextInput.flex = 1;
|
||||
styles.searchTextInput.backgroundColor = theme.backgroundColor;
|
||||
styles.searchTextInput.color = theme.color;
|
||||
|
||||
styles.clearIcon = Object.assign({}, theme.icon);
|
||||
styles.clearIcon.color = theme.colorFaded;
|
||||
styles.clearIcon.paddingRight = theme.marginRight;
|
||||
styles.clearIcon.backgroundColor = theme.backgroundColor;
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ query: this.props.query });
|
||||
this.refreshSearch(this.props.query);
|
||||
this.isMounted_ = true;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.isMounted_ = false;
|
||||
}
|
||||
|
||||
// UNSAFE_componentWillReceiveProps(newProps) {
|
||||
// console.info('UNSAFE_componentWillReceiveProps', newProps);
|
||||
|
||||
// let newState = {};
|
||||
// if ('query' in newProps && !this.state.query) newState.query = newProps.query;
|
||||
|
||||
// if (Object.getOwnPropertyNames(newState).length) {
|
||||
// this.setState(newState);
|
||||
// this.refreshSearch(newState.query);
|
||||
// }
|
||||
// }
|
||||
|
||||
searchTextInput_submit() {
|
||||
const query = this.state.query.trim();
|
||||
if (!query) return;
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'SEARCH_QUERY',
|
||||
query: query,
|
||||
});
|
||||
|
||||
this.setState({ query: query });
|
||||
this.refreshSearch(query);
|
||||
}
|
||||
|
||||
clearButton_press() {
|
||||
this.props.dispatch({
|
||||
type: 'SEARCH_QUERY',
|
||||
query: '',
|
||||
});
|
||||
|
||||
this.setState({ query: '' });
|
||||
this.refreshSearch('');
|
||||
}
|
||||
|
||||
async refreshSearch(query = null) {
|
||||
if (!this.props.visible) return;
|
||||
|
||||
query = query === null ? this.state.query.trim : query.trim();
|
||||
|
||||
let notes = [];
|
||||
|
||||
if (query) {
|
||||
if (this.props.settings['db.ftsEnabled']) {
|
||||
notes = await SearchEngineUtils.notesForQuery(query);
|
||||
} else {
|
||||
let p = query.split(' ');
|
||||
let temp = [];
|
||||
for (let i = 0; i < p.length; i++) {
|
||||
let t = p[i].trim();
|
||||
if (!t) continue;
|
||||
temp.push(t);
|
||||
}
|
||||
|
||||
notes = await Note.previews(null, {
|
||||
anywherePattern: `*${temp.join('*')}*`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.isMounted_) return;
|
||||
|
||||
this.setState({ notes: notes });
|
||||
}
|
||||
|
||||
searchTextInput_changeText(text) {
|
||||
this.setState({ query: text });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.isMounted_) return null;
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
let rootStyle = {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
if (!this.props.visible) {
|
||||
rootStyle.flex = 0.001; // This is a bit of a hack but it seems to work fine - it makes the component invisible but without unmounting it
|
||||
}
|
||||
|
||||
const thisComponent = this;
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader
|
||||
title={_('Search')}
|
||||
parentComponent={thisComponent}
|
||||
folderPickerOptions={{
|
||||
enabled: this.props.noteSelectionEnabled,
|
||||
mustSelect: true,
|
||||
}}
|
||||
showSideMenuButton={false}
|
||||
showSearchButton={false}
|
||||
/>
|
||||
<View style={this.styles().body}>
|
||||
<View style={this.styles().searchContainer}>
|
||||
<TextInput
|
||||
style={this.styles().searchTextInput}
|
||||
autoFocus={this.props.visible}
|
||||
underlineColorAndroid="#ffffff00"
|
||||
onSubmitEditing={() => {
|
||||
this.searchTextInput_submit();
|
||||
}}
|
||||
onChangeText={text => this.searchTextInput_changeText(text)}
|
||||
value={this.state.query}
|
||||
selectionColor={theme.textSelectionColor}
|
||||
/>
|
||||
<TouchableHighlight onPress={() => this.clearButton_press()}>
|
||||
<Icon name="md-close-circle" style={this.styles().clearIcon} />
|
||||
</TouchableHighlight>
|
||||
</View>
|
||||
|
||||
<FlatList data={this.state.notes} keyExtractor={(item) => item.id} renderItem={event => <NoteItem note={event.item} />} />
|
||||
</View>
|
||||
<DialogBox
|
||||
ref={dialogbox => {
|
||||
this.dialogbox = dialogbox;
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SearchScreen = connect(state => {
|
||||
return {
|
||||
query: state.searchQuery,
|
||||
theme: state.settings.theme,
|
||||
settings: state.settings,
|
||||
noteSelectionEnabled: state.noteSelectionEnabled,
|
||||
};
|
||||
})(SearchScreenComponent);
|
||||
|
||||
module.exports = { SearchScreen };
|
143
Server/dist/lib/components/screens/status.js
vendored
143
Server/dist/lib/components/screens/status.js
vendored
@ -1,143 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { StyleSheet, View, Text, Button, FlatList } = require('react-native');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const { connect } = require('react-redux');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { ReportService } = require('lib/services/report.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
body: {
|
||||
flex: 1,
|
||||
margin: globalStyle.margin,
|
||||
},
|
||||
});
|
||||
|
||||
class StatusScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
report: [],
|
||||
};
|
||||
}
|
||||
|
||||
UNSAFE_componentWillMount() {
|
||||
this.resfreshScreen();
|
||||
}
|
||||
|
||||
async resfreshScreen() {
|
||||
let service = new ReportService();
|
||||
let report = await service.status(Setting.value('sync.target'));
|
||||
this.setState({ report: report });
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const renderBody = report => {
|
||||
let baseStyle = {
|
||||
paddingLeft: 6,
|
||||
paddingRight: 6,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 2,
|
||||
flex: 0,
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
};
|
||||
|
||||
let lines = [];
|
||||
|
||||
for (let i = 0; i < report.length; i++) {
|
||||
let section = report[i];
|
||||
|
||||
let style = Object.assign({}, baseStyle);
|
||||
style.fontWeight = 'bold';
|
||||
if (i > 0) style.paddingTop = 20;
|
||||
lines.push({ key: `section_${i}`, isSection: true, text: section.title });
|
||||
|
||||
for (let n in section.body) {
|
||||
if (!section.body.hasOwnProperty(n)) continue;
|
||||
style = Object.assign({}, baseStyle);
|
||||
const item = section.body[n];
|
||||
|
||||
let text = '';
|
||||
|
||||
let retryHandler = null;
|
||||
if (typeof item === 'object') {
|
||||
if (item.canRetry) {
|
||||
retryHandler = async () => {
|
||||
await item.retryHandler();
|
||||
this.resfreshScreen();
|
||||
};
|
||||
}
|
||||
text = item.text;
|
||||
} else {
|
||||
text = item;
|
||||
}
|
||||
|
||||
lines.push({ key: `item_${i}_${n}`, text: text, retryHandler: retryHandler });
|
||||
}
|
||||
|
||||
lines.push({ key: `divider2_${i}`, isDivider: true });
|
||||
}
|
||||
|
||||
return (
|
||||
<FlatList
|
||||
data={lines}
|
||||
renderItem={({ item }) => {
|
||||
let style = Object.assign({}, baseStyle);
|
||||
|
||||
if (item.isSection === true) {
|
||||
style.fontWeight = 'bold';
|
||||
style.marginBottom = 5;
|
||||
}
|
||||
|
||||
style.flex = 1;
|
||||
|
||||
const retryButton = item.retryHandler ? (
|
||||
<View style={{ flex: 0 }}>
|
||||
<Button title={_('Retry')} onPress={item.retryHandler} />
|
||||
</View>
|
||||
) : null;
|
||||
|
||||
if (item.isDivider) {
|
||||
return <View style={{ borderBottomWidth: 1, borderBottomColor: theme.dividerColor, marginTop: 20, marginBottom: 20 }} />;
|
||||
} else {
|
||||
return (
|
||||
<View style={{ flex: 1, flexDirection: 'row' }}>
|
||||
<Text style={style}>{item.text}</Text>
|
||||
{retryButton}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
let body = renderBody(this.state.report);
|
||||
|
||||
return (
|
||||
<View style={this.rootStyle(this.props.theme).root}>
|
||||
<ScreenHeader title={_('Status')} />
|
||||
<View style={styles.body}>{body}</View>
|
||||
<Button title={_('Refresh')} onPress={() => this.resfreshScreen()} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const StatusScreen = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(StatusScreenComponent);
|
||||
|
||||
module.exports = { StatusScreen };
|
114
Server/dist/lib/components/screens/tags.js
vendored
114
Server/dist/lib/components/screens/tags.js
vendored
@ -1,114 +0,0 @@
|
||||
const React = require('react');
|
||||
|
||||
const { View, Text, FlatList, StyleSheet, TouchableOpacity } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Tag = require('lib/models/Tag.js');
|
||||
const { themeStyle } = require('lib/components/global-style.js');
|
||||
const { ScreenHeader } = require('lib/components/screen-header.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { BaseScreenComponent } = require('lib/components/base-screen.js');
|
||||
|
||||
class TagsScreenComponent extends BaseScreenComponent {
|
||||
static navigationOptions() {
|
||||
return { header: null };
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.state = {
|
||||
tags: [],
|
||||
};
|
||||
|
||||
this.tagList_renderItem = this.tagList_renderItem.bind(this);
|
||||
this.tagList_keyExtractor = this.tagList_keyExtractor.bind(this);
|
||||
this.tagItem_press = this.tagItem_press.bind(this);
|
||||
}
|
||||
|
||||
styles() {
|
||||
if (this.styles_) return this.styles_;
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
this.styles_ = StyleSheet.create({
|
||||
listItem: {
|
||||
flexDirection: 'row',
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.dividerColor,
|
||||
alignItems: 'flex-start',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
paddingTop: theme.itemMarginTop,
|
||||
paddingBottom: theme.itemMarginBottom,
|
||||
},
|
||||
listItemText: {
|
||||
flex: 1,
|
||||
color: theme.color,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
});
|
||||
|
||||
return this.styles_;
|
||||
}
|
||||
|
||||
tagItem_press(event) {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Notes',
|
||||
tagId: event.id,
|
||||
});
|
||||
}
|
||||
|
||||
tagList_renderItem(event) {
|
||||
const tag = event.item;
|
||||
return (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
this.tagItem_press({ id: tag.id });
|
||||
}}
|
||||
>
|
||||
<View style={this.styles().listItem}>
|
||||
<Text style={this.styles().listItemText}>{tag.title}</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
tagList_keyExtractor(item) {
|
||||
return item.id;
|
||||
}
|
||||
|
||||
async componentDidMount() {
|
||||
const tags = await Tag.allWithNotes();
|
||||
tags.sort((a, b) => {
|
||||
return a.title.toLowerCase() < b.title.toLowerCase() ? -1 : +1;
|
||||
});
|
||||
this.setState({ tags: tags });
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
let rootStyle = {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={rootStyle}>
|
||||
<ScreenHeader title={_('Tags')} parentComponent={this} showSearchButton={false} />
|
||||
<FlatList style={{ flex: 1 }} data={this.state.tags} renderItem={this.tagList_renderItem} keyExtractor={this.tagList_keyExtractor} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const TagsScreen = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(TagsScreenComponent);
|
||||
|
||||
module.exports = { TagsScreen };
|
@ -1,107 +0,0 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import PopupDialog, { DialogTitle, DialogButton } from 'react-native-popup-dialog';
|
||||
import DatePicker from 'react-native-datepicker';
|
||||
import moment from 'moment';
|
||||
import { _ } from 'lib/locale.js';
|
||||
|
||||
class SelectDateTimeDialog extends React.PureComponent {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.dialog_ = null;
|
||||
this.shown_ = false;
|
||||
this.state = { date: null };
|
||||
|
||||
this.onReject = this.onReject.bind(this);
|
||||
}
|
||||
|
||||
UNSAFE_componentWillReceiveProps(newProps) {
|
||||
if (newProps.date != this.state.date) {
|
||||
this.setState({ date: newProps.date });
|
||||
}
|
||||
|
||||
if ('shown' in newProps) {
|
||||
this.show(newProps.shown);
|
||||
}
|
||||
}
|
||||
|
||||
show(doShow = true) {
|
||||
if (doShow) {
|
||||
this.dialog_.show();
|
||||
} else {
|
||||
this.dialog_.dismiss();
|
||||
}
|
||||
|
||||
this.shown_ = doShow;
|
||||
}
|
||||
|
||||
dismiss() {
|
||||
this.show(false);
|
||||
}
|
||||
|
||||
dateTimeFormat() {
|
||||
return 'MM/DD/YYYY HH:mm';
|
||||
}
|
||||
|
||||
stringToDate(s) {
|
||||
return moment(s, this.dateTimeFormat()).toDate();
|
||||
}
|
||||
|
||||
onAccept() {
|
||||
if (this.props.onAccept) this.props.onAccept(this.state.date);
|
||||
}
|
||||
|
||||
onReject() {
|
||||
if (this.props.onReject) this.props.onReject();
|
||||
}
|
||||
|
||||
onClear() {
|
||||
if (this.props.onAccept) this.props.onAccept(null);
|
||||
}
|
||||
|
||||
render() {
|
||||
const clearAlarmText = _('Clear alarm'); // For unknown reasons, this particular string doesn't get translated if it's directly in the text property below
|
||||
|
||||
const popupActions = [
|
||||
<DialogButton text={_('Save alarm')} align="center" onPress={() => this.onAccept()} key="saveButton" />,
|
||||
<DialogButton text={clearAlarmText} align="center" onPress={() => this.onClear()} key="clearButton" />,
|
||||
<DialogButton text={_('Cancel')} align="center" onPress={() => this.onReject()} key="cancelButton" />,
|
||||
];
|
||||
|
||||
return (
|
||||
<PopupDialog
|
||||
ref={(dialog) => { this.dialog_ = dialog; }}
|
||||
dialogTitle={<DialogTitle title={_('Set alarm')} />}
|
||||
actions={popupActions}
|
||||
dismissOnTouchOutside={false}
|
||||
width={0.9}
|
||||
height={350}
|
||||
>
|
||||
<View style={{flex:1, margin: 20, alignItems:'center'}}>
|
||||
<DatePicker
|
||||
date={this.state.date}
|
||||
mode="datetime"
|
||||
placeholder={_('Select date')}
|
||||
format={this.dateTimeFormat()}
|
||||
confirmBtnText={_('Confirm')}
|
||||
cancelBtnText={_('Cancel')}
|
||||
onDateChange={(date) => { this.setState({ date: this.stringToDate(date) }); }}
|
||||
style={{width:300}}
|
||||
customStyles={{
|
||||
btnConfirm: {
|
||||
paddingVertical: 0,
|
||||
},
|
||||
btnCancel: {
|
||||
paddingVertical: 0,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
</PopupDialog>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { SelectDateTimeDialog };
|
135
Server/dist/lib/components/shared/config-shared.js
vendored
135
Server/dist/lib/components/shared/config-shared.js
vendored
@ -1,135 +0,0 @@
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const ObjectUtils = require('lib/ObjectUtils');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { createSelector } = require('reselect');
|
||||
|
||||
const shared = {};
|
||||
|
||||
shared.init = function(comp) {
|
||||
if (!comp.state) comp.state = {};
|
||||
comp.state.checkSyncConfigResult = null;
|
||||
comp.state.settings = {};
|
||||
comp.state.changedSettingKeys = [];
|
||||
};
|
||||
|
||||
shared.checkSyncConfig = async function(comp, settings) {
|
||||
const syncTargetId = settings['sync.target'];
|
||||
const SyncTargetClass = SyncTargetRegistry.classById(syncTargetId);
|
||||
const options = Setting.subValues(`sync.${syncTargetId}`, settings);
|
||||
comp.setState({ checkSyncConfigResult: 'checking' });
|
||||
const result = await SyncTargetClass.checkConfig(ObjectUtils.convertValuesToFunctions(options));
|
||||
comp.setState({ checkSyncConfigResult: result });
|
||||
};
|
||||
|
||||
shared.checkSyncConfigMessages = function(comp) {
|
||||
const result = comp.state.checkSyncConfigResult;
|
||||
const output = [];
|
||||
|
||||
if (result === 'checking') {
|
||||
output.push(_('Checking... Please wait.'));
|
||||
} else if (result && result.ok) {
|
||||
output.push(_('Success! Synchronisation configuration appears to be correct.'));
|
||||
} else if (result && !result.ok) {
|
||||
output.push(_('Error. Please check that URL, username, password, etc. are correct and that the sync target is accessible. The reported error was:'));
|
||||
output.push(result.errorMessage);
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
shared.updateSettingValue = function(comp, key, value) {
|
||||
const settings = Object.assign({}, comp.state.settings);
|
||||
const changedSettingKeys = comp.state.changedSettingKeys.slice();
|
||||
settings[key] = Setting.formatValue(key, value);
|
||||
if (changedSettingKeys.indexOf(key) < 0) changedSettingKeys.push(key);
|
||||
|
||||
comp.setState({
|
||||
settings: settings,
|
||||
changedSettingKeys: changedSettingKeys,
|
||||
});
|
||||
};
|
||||
|
||||
shared.saveSettings = function(comp) {
|
||||
for (let key in comp.state.settings) {
|
||||
if (!comp.state.settings.hasOwnProperty(key)) continue;
|
||||
if (comp.state.changedSettingKeys.indexOf(key) < 0) continue;
|
||||
console.info('Saving', key, comp.state.settings[key]);
|
||||
Setting.setValue(key, comp.state.settings[key]);
|
||||
}
|
||||
|
||||
comp.setState({ changedSettingKeys: [] });
|
||||
};
|
||||
|
||||
shared.settingsToComponents = function(comp, device, settings) {
|
||||
const keys = Setting.keys(true, device);
|
||||
const settingComps = [];
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (!Setting.isPublic(key)) continue;
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.show && !md.show(settings)) continue;
|
||||
|
||||
const settingComp = comp.settingToComponent(key, settings[key]);
|
||||
if (!settingComp) continue;
|
||||
settingComps.push(settingComp);
|
||||
}
|
||||
|
||||
return settingComps;
|
||||
};
|
||||
|
||||
const deviceSelector = (state) => state.device;
|
||||
const settingsSelector = (state) => state.settings;
|
||||
|
||||
shared.settingsSections = createSelector(
|
||||
deviceSelector,
|
||||
settingsSelector,
|
||||
(device, settings) => {
|
||||
const keys = Setting.keys(true, device);
|
||||
const metadatas = [];
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (!Setting.isPublic(key)) continue;
|
||||
|
||||
const md = Setting.settingMetadata(key);
|
||||
if (md.show && !md.show(settings)) continue;
|
||||
|
||||
metadatas.push(md);
|
||||
}
|
||||
|
||||
const output = Setting.groupMetadatasBySections(metadatas);
|
||||
|
||||
output.push({
|
||||
name: 'encryption',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
output.push({
|
||||
name: 'server',
|
||||
metadatas: [],
|
||||
isScreen: true,
|
||||
});
|
||||
|
||||
return output;
|
||||
}
|
||||
);
|
||||
|
||||
shared.settingsToComponents2 = function(comp, device, settings, selectedSectionName = '') {
|
||||
const sectionComps = [];
|
||||
const sections = shared.settingsSections({ device, settings });
|
||||
|
||||
for (let i = 0; i < sections.length; i++) {
|
||||
const section = sections[i];
|
||||
const sectionComp = comp.sectionToComponent(section.name, section, settings, selectedSectionName === section.name);
|
||||
if (!sectionComp) continue;
|
||||
sectionComps.push(sectionComp);
|
||||
}
|
||||
|
||||
return sectionComps;
|
||||
};
|
||||
|
||||
module.exports = shared;
|
@ -1,71 +0,0 @@
|
||||
const { shim } = require('lib/shim');
|
||||
const SyncTargetRegistry = require('lib/SyncTargetRegistry');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const Setting = require('lib/models/Setting');
|
||||
|
||||
class Shared {
|
||||
constructor(comp, showInfoMessageBox, showErrorMessageBox) {
|
||||
this.comp_ = comp;
|
||||
|
||||
this.dropboxApi_ = null;
|
||||
|
||||
this.comp_.state = {
|
||||
loginUrl: '',
|
||||
authCode: '',
|
||||
checkingAuthToken: false,
|
||||
};
|
||||
|
||||
this.loginUrl_click = () => {
|
||||
if (!this.comp_.state.loginUrl) return;
|
||||
shim.openUrl(this.comp_.state.loginUrl);
|
||||
};
|
||||
|
||||
this.authCodeInput_change = event => {
|
||||
this.comp_.setState({
|
||||
authCode: typeof event === 'object' ? event.target.value : event,
|
||||
});
|
||||
};
|
||||
|
||||
this.submit_click = async () => {
|
||||
this.comp_.setState({ checkingAuthToken: true });
|
||||
|
||||
const api = await this.dropboxApi();
|
||||
try {
|
||||
const response = await api.execAuthToken(this.comp_.state.authCode);
|
||||
|
||||
Setting.setValue(`sync.${this.syncTargetId()}.auth`, response.access_token);
|
||||
api.setAuthToken(response.access_token);
|
||||
await showInfoMessageBox(_('The application has been authorised!'));
|
||||
this.comp_.props.dispatch({ type: 'NAV_BACK' });
|
||||
reg.scheduleSync();
|
||||
} catch (error) {
|
||||
await showErrorMessageBox(_('Could not authorise application:\n\n%s\n\nPlease try again.', error.message));
|
||||
} finally {
|
||||
this.comp_.setState({ checkingAuthToken: false });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
syncTargetId() {
|
||||
return SyncTargetRegistry.nameToId('dropbox');
|
||||
}
|
||||
|
||||
async dropboxApi() {
|
||||
if (this.dropboxApi_) return this.dropboxApi_;
|
||||
|
||||
const syncTarget = reg.syncTarget(this.syncTargetId());
|
||||
this.dropboxApi_ = await syncTarget.api();
|
||||
return this.dropboxApi_;
|
||||
}
|
||||
|
||||
async refreshUrl() {
|
||||
const api = await this.dropboxApi();
|
||||
|
||||
this.comp_.setState({
|
||||
loginUrl: api.loginUrl(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Shared;
|
@ -1,91 +0,0 @@
|
||||
const EncryptionService = require('lib/services/EncryptionService');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const BaseItem = require('lib/models/BaseItem.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
|
||||
const shared = {};
|
||||
|
||||
shared.constructor = function(comp) {
|
||||
comp.state = {
|
||||
masterKeys: [],
|
||||
passwords: {},
|
||||
passwordChecks: {},
|
||||
stats: {
|
||||
encrypted: null,
|
||||
total: null,
|
||||
},
|
||||
};
|
||||
comp.isMounted_ = false;
|
||||
|
||||
comp.refreshStatsIID_ = null;
|
||||
};
|
||||
|
||||
shared.initState = function(comp, props) {
|
||||
comp.setState(
|
||||
{
|
||||
masterKeys: props.masterKeys,
|
||||
passwords: props.passwords ? props.passwords : {},
|
||||
},
|
||||
() => {
|
||||
comp.checkPasswords();
|
||||
}
|
||||
);
|
||||
|
||||
comp.refreshStats();
|
||||
|
||||
if (comp.refreshStatsIID_) {
|
||||
clearInterval(comp.refreshStatsIID_);
|
||||
comp.refreshStatsIID_ = null;
|
||||
}
|
||||
|
||||
comp.refreshStatsIID_ = setInterval(() => {
|
||||
if (!comp.isMounted_) {
|
||||
clearInterval(comp.refreshStatsIID_);
|
||||
comp.refreshStatsIID_ = null;
|
||||
return;
|
||||
}
|
||||
comp.refreshStats();
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
shared.refreshStats = async function(comp) {
|
||||
const stats = await BaseItem.encryptedItemsStats();
|
||||
comp.setState({ stats: stats });
|
||||
};
|
||||
|
||||
shared.checkPasswords = async function(comp) {
|
||||
const passwordChecks = Object.assign({}, comp.state.passwordChecks);
|
||||
for (let i = 0; i < comp.state.masterKeys.length; i++) {
|
||||
const mk = comp.state.masterKeys[i];
|
||||
const password = comp.state.passwords[mk.id];
|
||||
const ok = password ? await EncryptionService.instance().checkMasterKeyPassword(mk, password) : false;
|
||||
passwordChecks[mk.id] = ok;
|
||||
}
|
||||
comp.setState({ passwordChecks: passwordChecks });
|
||||
};
|
||||
|
||||
shared.decryptedStatText = function(comp) {
|
||||
const stats = comp.state.stats;
|
||||
const doneCount = stats.encrypted !== null ? stats.total - stats.encrypted : '-';
|
||||
const totalCount = stats.total !== null ? stats.total : '-';
|
||||
return _('Decrypted items: %s / %s', doneCount, totalCount);
|
||||
};
|
||||
|
||||
shared.onSavePasswordClick = function(comp, mk) {
|
||||
const password = comp.state.passwords[mk.id];
|
||||
if (!password) {
|
||||
Setting.deleteObjectKey('encryption.passwordCache', mk.id);
|
||||
} else {
|
||||
Setting.setObjectKey('encryption.passwordCache', mk.id, password);
|
||||
}
|
||||
|
||||
comp.checkPasswords();
|
||||
};
|
||||
|
||||
shared.onPasswordChange = function(comp, mk, password) {
|
||||
const passwords = comp.state.passwords;
|
||||
passwords[mk.id] = password;
|
||||
comp.setState({ passwords: passwords });
|
||||
};
|
||||
|
||||
module.exports = shared;
|
@ -1,293 +0,0 @@
|
||||
const { reg } = require('lib/registry.js');
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const BaseModel = require('lib/BaseModel.js');
|
||||
const Note = require('lib/models/Note.js');
|
||||
const Resource = require('lib/models/Resource.js');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher.js');
|
||||
const DecryptionWorker = require('lib/services/DecryptionWorker.js');
|
||||
const Setting = require('lib/models/Setting.js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
const shared = {};
|
||||
|
||||
// If saveNoteButton_press is called multiple times in short intervals, it might result in
|
||||
// the same new note being created twice, so we need to a mutex to access this function.
|
||||
const saveNoteMutex_ = new Mutex();
|
||||
|
||||
shared.noteExists = async function(noteId) {
|
||||
const existingNote = await Note.load(noteId);
|
||||
return !!existingNote;
|
||||
};
|
||||
|
||||
shared.saveNoteButton_press = async function(comp, folderId = null, options = null) {
|
||||
options = Object.assign(
|
||||
{},
|
||||
{
|
||||
autoTitle: true,
|
||||
},
|
||||
options
|
||||
);
|
||||
|
||||
const releaseMutex = await saveNoteMutex_.acquire();
|
||||
|
||||
let note = Object.assign({}, comp.state.note);
|
||||
|
||||
// Note has been deleted while user was modifying it. In that case, we
|
||||
// just save a new note by clearing the note ID.
|
||||
if (note.id && !(await shared.noteExists(note.id))) delete note.id;
|
||||
|
||||
if (folderId) {
|
||||
note.parent_id = folderId;
|
||||
} else if (!note.parent_id) {
|
||||
const activeFolderId = Setting.value('activeFolderId');
|
||||
let folder = await Folder.load(activeFolderId);
|
||||
if (!folder) folder = await Folder.defaultFolder();
|
||||
if (!folder) return releaseMutex();
|
||||
note.parent_id = folder.id;
|
||||
}
|
||||
|
||||
let isNew = !note.id;
|
||||
|
||||
let saveOptions = { userSideValidation: true };
|
||||
if (!isNew) {
|
||||
saveOptions.fields = BaseModel.diffObjectsFields(comp.state.lastSavedNote, note);
|
||||
}
|
||||
|
||||
const hasAutoTitle = comp.state.newAndNoTitleChangeNoteId || (isNew && !note.title);
|
||||
if (hasAutoTitle && options.autoTitle) {
|
||||
note.title = Note.defaultTitle(note);
|
||||
if (saveOptions.fields && saveOptions.fields.indexOf('title') < 0) saveOptions.fields.push('title');
|
||||
}
|
||||
|
||||
const savedNote = 'fields' in saveOptions && !saveOptions.fields.length ? Object.assign({}, note) : await Note.save(note, saveOptions);
|
||||
|
||||
const stateNote = comp.state.note;
|
||||
|
||||
// Note was reloaded while being saved.
|
||||
if (!isNew && (!stateNote || stateNote.id !== savedNote.id)) return releaseMutex();
|
||||
|
||||
// Re-assign any property that might have changed during saving (updated_time, etc.)
|
||||
note = Object.assign(note, savedNote);
|
||||
|
||||
if (stateNote.id === note.id) {
|
||||
// But we preserve the current title and body because
|
||||
// the user might have changed them between the time
|
||||
// saveNoteButton_press was called and the note was
|
||||
// saved (it's done asynchronously).
|
||||
//
|
||||
// If the title was auto-assigned above, we don't restore
|
||||
// it from the state because it will be empty there.
|
||||
if (!hasAutoTitle) note.title = stateNote.title;
|
||||
note.body = stateNote.body;
|
||||
}
|
||||
|
||||
let newState = {
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
note: note,
|
||||
};
|
||||
|
||||
if (isNew && hasAutoTitle) newState.newAndNoTitleChangeNoteId = note.id;
|
||||
|
||||
if (!options.autoTitle) newState.newAndNoTitleChangeNoteId = null;
|
||||
|
||||
comp.setState(newState);
|
||||
|
||||
// await shared.refreshAttachedResources(comp, newState.note.body);
|
||||
|
||||
if (isNew) {
|
||||
Note.updateGeolocation(note.id).then(geoNote => {
|
||||
const stateNote = comp.state.note;
|
||||
if (!stateNote || !geoNote) return;
|
||||
if (stateNote.id !== geoNote.id) return; // Another note has been loaded while geoloc was being retrieved
|
||||
|
||||
// Geo-location for this note has been saved to the database however the properties
|
||||
// are is not in the state so set them now.
|
||||
|
||||
const geoInfo = {
|
||||
longitude: geoNote.longitude,
|
||||
latitude: geoNote.latitude,
|
||||
altitude: geoNote.altitude,
|
||||
};
|
||||
|
||||
const modNote = Object.assign({}, stateNote, geoInfo);
|
||||
const modLastSavedNote = Object.assign({}, comp.state.lastSavedNote, geoInfo);
|
||||
|
||||
comp.setState({ note: modNote, lastSavedNote: modLastSavedNote });
|
||||
});
|
||||
}
|
||||
|
||||
if (isNew) {
|
||||
// Clear the newNote item now that the note has been saved, and
|
||||
// make sure that the note we're editing is selected.
|
||||
comp.props.dispatch({
|
||||
type: 'NOTE_SELECT',
|
||||
id: savedNote.id,
|
||||
});
|
||||
}
|
||||
|
||||
releaseMutex();
|
||||
};
|
||||
|
||||
shared.saveOneProperty = async function(comp, name, value) {
|
||||
let note = Object.assign({}, comp.state.note);
|
||||
|
||||
// Note has been deleted while user was modifying it. In that, we
|
||||
// just save a new note by clearing the note ID.
|
||||
if (note.id && !(await shared.noteExists(note.id))) delete note.id;
|
||||
|
||||
// reg.logger().info('Saving note property: ', note.id, name, value);
|
||||
|
||||
if (note.id) {
|
||||
let toSave = { id: note.id };
|
||||
toSave[name] = value;
|
||||
toSave = await Note.save(toSave);
|
||||
note[name] = toSave[name];
|
||||
|
||||
comp.setState({
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
note: note,
|
||||
});
|
||||
} else {
|
||||
note[name] = value;
|
||||
comp.setState({ note: note });
|
||||
}
|
||||
};
|
||||
|
||||
shared.noteComponent_change = function(comp, propName, propValue) {
|
||||
let newState = {};
|
||||
|
||||
let note = Object.assign({}, comp.state.note);
|
||||
note[propName] = propValue;
|
||||
newState.note = note;
|
||||
|
||||
comp.setState(newState);
|
||||
};
|
||||
|
||||
let resourceCache_ = {};
|
||||
|
||||
shared.clearResourceCache = function() {
|
||||
resourceCache_ = {};
|
||||
};
|
||||
|
||||
shared.attachedResources = async function(noteBody) {
|
||||
if (!noteBody) return {};
|
||||
const resourceIds = await Note.linkedItemIdsByType(BaseModel.TYPE_RESOURCE, noteBody);
|
||||
|
||||
const output = {};
|
||||
for (let i = 0; i < resourceIds.length; i++) {
|
||||
const id = resourceIds[i];
|
||||
|
||||
if (resourceCache_[id]) {
|
||||
output[id] = resourceCache_[id];
|
||||
} else {
|
||||
const resource = await Resource.load(id);
|
||||
const localState = await Resource.localState(resource);
|
||||
|
||||
const o = {
|
||||
item: resource,
|
||||
localState: localState,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
resourceCache_[id] = o;
|
||||
output[id] = o;
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
shared.isModified = function(comp) {
|
||||
if (!comp.state.note || !comp.state.lastSavedNote) return false;
|
||||
let diff = BaseModel.diffObjects(comp.state.lastSavedNote, comp.state.note);
|
||||
delete diff.type_;
|
||||
return !!Object.getOwnPropertyNames(diff).length;
|
||||
};
|
||||
|
||||
shared.initState = async function(comp) {
|
||||
let note = null;
|
||||
let mode = 'view';
|
||||
if (!comp.props.noteId) {
|
||||
note = comp.props.itemType == 'todo' ? Note.newTodo(comp.props.folderId) : Note.new(comp.props.folderId);
|
||||
mode = 'edit';
|
||||
} else {
|
||||
note = await Note.load(comp.props.noteId);
|
||||
}
|
||||
|
||||
const folder = Folder.byId(comp.props.folders, note.parent_id);
|
||||
|
||||
comp.setState({
|
||||
lastSavedNote: Object.assign({}, note),
|
||||
note: note,
|
||||
mode: mode,
|
||||
folder: folder,
|
||||
isLoading: false,
|
||||
fromShare: comp.props.sharedData ? true : false,
|
||||
noteResources: await shared.attachedResources(note ? note.body : ''),
|
||||
});
|
||||
|
||||
if (comp.props.sharedData) {
|
||||
this.noteComponent_change(comp, 'body', comp.props.sharedData.value);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line require-atomic-updates
|
||||
comp.lastLoadedNoteId_ = note ? note.id : null;
|
||||
};
|
||||
|
||||
shared.toggleIsTodo_onPress = function(comp) {
|
||||
let newNote = Note.toggleIsTodo(comp.state.note);
|
||||
let newState = { note: newNote };
|
||||
comp.setState(newState);
|
||||
};
|
||||
|
||||
shared.toggleCheckbox = function(ipcMessage, noteBody) {
|
||||
let newBody = noteBody.split('\n');
|
||||
const p = ipcMessage.split(':');
|
||||
const lineIndex = Number(p[p.length - 1]);
|
||||
if (lineIndex >= newBody.length) {
|
||||
reg.logger().warn('Checkbox line out of bounds: ', ipcMessage);
|
||||
return newBody.join('\n');
|
||||
}
|
||||
|
||||
let line = newBody[lineIndex];
|
||||
|
||||
const noCrossIndex = line.trim().indexOf('- [ ] ');
|
||||
let crossIndex = line.trim().indexOf('- [x] ');
|
||||
if (crossIndex < 0) crossIndex = line.trim().indexOf('- [X] ');
|
||||
|
||||
if (noCrossIndex < 0 && crossIndex < 0) {
|
||||
reg.logger().warn('Could not find matching checkbox for message: ', ipcMessage);
|
||||
return newBody.join('\n');
|
||||
}
|
||||
|
||||
let isCrossLine = false;
|
||||
|
||||
if (noCrossIndex >= 0 && crossIndex >= 0) {
|
||||
isCrossLine = crossIndex < noCrossIndex;
|
||||
} else {
|
||||
isCrossLine = crossIndex >= 0;
|
||||
}
|
||||
|
||||
if (!isCrossLine) {
|
||||
line = line.replace(/- \[ \] /, '- [x] ');
|
||||
} else {
|
||||
line = line.replace(/- \[x\] /i, '- [ ] ');
|
||||
}
|
||||
|
||||
newBody[lineIndex] = line;
|
||||
return newBody.join('\n');
|
||||
};
|
||||
|
||||
shared.installResourceHandling = function(refreshResourceHandler) {
|
||||
ResourceFetcher.instance().on('downloadComplete', refreshResourceHandler);
|
||||
ResourceFetcher.instance().on('downloadStarted', refreshResourceHandler);
|
||||
DecryptionWorker.instance().on('resourceDecrypted', refreshResourceHandler);
|
||||
};
|
||||
|
||||
shared.uninstallResourceHandling = function(refreshResourceHandler) {
|
||||
ResourceFetcher.instance().off('downloadComplete', refreshResourceHandler);
|
||||
ResourceFetcher.instance().off('downloadStarted', refreshResourceHandler);
|
||||
DecryptionWorker.instance().off('resourceDecrypted', refreshResourceHandler);
|
||||
};
|
||||
|
||||
module.exports = shared;
|
@ -1,35 +0,0 @@
|
||||
const Setting = require('lib/models/Setting');
|
||||
const Tag = require('lib/models/Tag');
|
||||
const { reg } = require('lib/registry.js');
|
||||
const ResourceFetcher = require('lib/services/ResourceFetcher');
|
||||
|
||||
const reduxSharedMiddleware = async function(store, next, action) {
|
||||
const newState = store.getState();
|
||||
|
||||
let refreshTags = false;
|
||||
|
||||
if (action.type == 'FOLDER_SET_COLLAPSED' || action.type == 'FOLDER_TOGGLE') {
|
||||
Setting.setValue('collapsedFolderIds', newState.collapsedFolderIds);
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && !!action.key.match(/^sync\.\d+\.path$/)) {
|
||||
reg.resetSyncTarget();
|
||||
}
|
||||
|
||||
if (action.type === 'SETTING_UPDATE_ONE' && action.key === 'sync.resourceDownloadMode') {
|
||||
ResourceFetcher.instance().autoAddResources();
|
||||
}
|
||||
|
||||
if (action.type == 'NOTE_DELETE') {
|
||||
refreshTags = true;
|
||||
}
|
||||
|
||||
if (refreshTags) {
|
||||
store.dispatch({
|
||||
type: 'TAG_UPDATE_ALL',
|
||||
items: await Tag.allWithNotes(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = reduxSharedMiddleware;
|
@ -1,120 +0,0 @@
|
||||
const Folder = require('lib/models/Folder');
|
||||
const BaseModel = require('lib/BaseModel');
|
||||
|
||||
let shared = {};
|
||||
|
||||
function folderHasChildren_(folders, folderId) {
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
let folder = folders[i];
|
||||
if (folder.parent_id === folderId) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function folderIsVisible(folders, folderId, collapsedFolderIds) {
|
||||
if (!collapsedFolderIds || !collapsedFolderIds.length) return true;
|
||||
|
||||
while (true) {
|
||||
let folder = BaseModel.byId(folders, folderId);
|
||||
if (!folder) throw new Error(`No folder with id ${folder.id}`);
|
||||
if (!folder.parent_id) return true;
|
||||
if (collapsedFolderIds.indexOf(folder.parent_id) >= 0) return false;
|
||||
folderId = folder.parent_id;
|
||||
}
|
||||
}
|
||||
|
||||
function renderFoldersRecursive_(props, renderItem, items, parentId, depth, order) {
|
||||
const folders = props.folders;
|
||||
for (let i = 0; i < folders.length; i++) {
|
||||
let folder = folders[i];
|
||||
if (!Folder.idsEqual(folder.parent_id, parentId)) continue;
|
||||
if (!folderIsVisible(props.folders, folder.id, props.collapsedFolderIds)) continue;
|
||||
const hasChildren = folderHasChildren_(folders, folder.id);
|
||||
order.push(folder.id);
|
||||
items.push(renderItem(folder, props.selectedFolderId == folder.id && props.notesParentType == 'Folder', hasChildren, depth));
|
||||
if (hasChildren) {
|
||||
const result = renderFoldersRecursive_(props, renderItem, items, folder.id, depth + 1, order);
|
||||
items = result.items;
|
||||
order = result.order;
|
||||
}
|
||||
}
|
||||
return {
|
||||
items: items,
|
||||
order: order,
|
||||
};
|
||||
}
|
||||
|
||||
shared.renderFolders = function(props, renderItem) {
|
||||
return renderFoldersRecursive_(props, renderItem, [], '', 0, []);
|
||||
};
|
||||
|
||||
shared.renderTags = function(props, renderItem) {
|
||||
let tags = props.tags.slice();
|
||||
tags.sort((a, b) => {
|
||||
return a.title < b.title ? -1 : +1;
|
||||
});
|
||||
let tagItems = [];
|
||||
const order = [];
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tag = tags[i];
|
||||
order.push(tag.id);
|
||||
tagItems.push(renderItem(tag, props.selectedTagId == tag.id && props.notesParentType == 'Tag'));
|
||||
}
|
||||
return {
|
||||
items: tagItems,
|
||||
order: order,
|
||||
};
|
||||
};
|
||||
|
||||
// shared.renderSearches = function(props, renderItem) {
|
||||
// let searches = props.searches.slice();
|
||||
// let searchItems = [];
|
||||
// const order = [];
|
||||
// for (let i = 0; i < searches.length; i++) {
|
||||
// const search = searches[i];
|
||||
// order.push(search.id);
|
||||
// searchItems.push(renderItem(search, props.selectedSearchId == search.id && props.notesParentType == 'Search'));
|
||||
// }
|
||||
// return {
|
||||
// items: searchItems,
|
||||
// order: order,
|
||||
// };
|
||||
// }
|
||||
|
||||
shared.synchronize_press = async function(comp) {
|
||||
const { reg } = require('lib/registry.js');
|
||||
|
||||
const action = comp.props.syncStarted ? 'cancel' : 'start';
|
||||
|
||||
if (!(await reg.syncTarget().isAuthenticated())) {
|
||||
if (reg.syncTarget().authRouteName()) {
|
||||
comp.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: reg.syncTarget().authRouteName(),
|
||||
});
|
||||
return 'auth';
|
||||
}
|
||||
|
||||
reg.logger().info('Not authentified with sync target - please check your credential.');
|
||||
return 'error';
|
||||
}
|
||||
|
||||
let sync = null;
|
||||
try {
|
||||
sync = await reg.syncTarget().synchronizer();
|
||||
} catch (error) {
|
||||
reg.logger().info('Could not acquire synchroniser:');
|
||||
reg.logger().info(error);
|
||||
return 'error';
|
||||
}
|
||||
|
||||
if (action == 'cancel') {
|
||||
sync.cancel();
|
||||
return 'cancel';
|
||||
} else {
|
||||
reg.scheduleSync(0);
|
||||
return 'sync';
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = shared;
|
111
Server/dist/lib/components/side-menu-content-note.js
vendored
111
Server/dist/lib/components/side-menu-content-note.js
vendored
@ -1,111 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { TouchableOpacity, Text, StyleSheet, ScrollView, View } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
|
||||
|
||||
class SideMenuContentNoteComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.styles_ = {};
|
||||
}
|
||||
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
menu: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
},
|
||||
sidebarIcon: {
|
||||
fontSize: 22,
|
||||
color: theme.color,
|
||||
},
|
||||
};
|
||||
|
||||
styles.sideButton = Object.assign({}, styles.button, { flex: 0 });
|
||||
styles.sideButtonDisabled = Object.assign({}, styles.sideButton, { opacity: 0.6 });
|
||||
styles.sideButtonText = Object.assign({}, styles.buttonText);
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
renderDivider(key) {
|
||||
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: globalStyle.dividerColor }} key={key}></View>;
|
||||
}
|
||||
|
||||
renderSideBarButton(key, title, iconName, onPressHandler) {
|
||||
const content = (
|
||||
<View key={key} style={onPressHandler ? this.styles().sideButton : this.styles().sideButtonDisabled}>
|
||||
{!iconName ? null : <Icon name={iconName} style={this.styles().sidebarIcon} />}
|
||||
<Text style={this.styles().sideButtonText}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!onPressHandler) return content;
|
||||
|
||||
return (
|
||||
<TouchableOpacity key={key} onPress={onPressHandler}>
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
let items = [];
|
||||
|
||||
const options = this.props.options ? this.props.options : [];
|
||||
let dividerIndex = 0;
|
||||
|
||||
for (const option of options) {
|
||||
if (option.isDivider) {
|
||||
items.push(this.renderDivider(`divider_${dividerIndex++}`));
|
||||
} else {
|
||||
items.push(this.renderSideBarButton(option.title, option.title, null, option.onPress));
|
||||
}
|
||||
}
|
||||
|
||||
let style = {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: globalStyle.dividerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
paddingTop: 10,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={{ flex: 1, opacity: this.props.opacity }}>
|
||||
<ScrollView scrollsToTop={false} style={this.styles().menu}>
|
||||
{items}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SideMenuContentNote = connect(state => {
|
||||
return {
|
||||
theme: state.settings.theme,
|
||||
};
|
||||
})(SideMenuContentNoteComponent);
|
||||
|
||||
module.exports = { SideMenuContentNote };
|
406
Server/dist/lib/components/side-menu-content.js
vendored
406
Server/dist/lib/components/side-menu-content.js
vendored
@ -1,406 +0,0 @@
|
||||
const React = require('react');
|
||||
const Component = React.Component;
|
||||
const { Easing, Animated, TouchableOpacity, Text, StyleSheet, ScrollView, View, Alert } = require('react-native');
|
||||
const { connect } = require('react-redux');
|
||||
const Icon = require('react-native-vector-icons/Ionicons').default;
|
||||
const Folder = require('lib/models/Folder.js');
|
||||
const { Synchronizer } = require('lib/synchronizer.js');
|
||||
const NavService = require('lib/services/NavService.js');
|
||||
const { _ } = require('lib/locale.js');
|
||||
const { globalStyle, themeStyle } = require('lib/components/global-style.js');
|
||||
const shared = require('lib/components/shared/side-menu-shared.js');
|
||||
|
||||
class SideMenuContentComponent extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
syncReportText: '',
|
||||
};
|
||||
this.styles_ = {};
|
||||
|
||||
this.tagButton_press = this.tagButton_press.bind(this);
|
||||
this.newFolderButton_press = this.newFolderButton_press.bind(this);
|
||||
this.synchronize_press = this.synchronize_press.bind(this);
|
||||
this.configButton_press = this.configButton_press.bind(this);
|
||||
this.allNotesButton_press = this.allNotesButton_press.bind(this);
|
||||
this.renderFolderItem = this.renderFolderItem.bind(this);
|
||||
|
||||
this.syncIconRotationValue = new Animated.Value(0);
|
||||
this.syncIconRotation = this.syncIconRotationValue.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: ['360deg', '0deg'],
|
||||
});
|
||||
}
|
||||
|
||||
styles() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
if (this.styles_[this.props.theme]) return this.styles_[this.props.theme];
|
||||
this.styles_ = {};
|
||||
|
||||
let styles = {
|
||||
menu: {
|
||||
flex: 1,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
},
|
||||
button: {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
},
|
||||
buttonText: {
|
||||
flex: 1,
|
||||
color: theme.color,
|
||||
paddingLeft: 10,
|
||||
fontSize: theme.fontSize,
|
||||
},
|
||||
syncStatus: {
|
||||
paddingLeft: theme.marginLeft,
|
||||
paddingRight: theme.marginRight,
|
||||
color: theme.colorFaded,
|
||||
fontSize: theme.fontSizeSmaller,
|
||||
flex: 0,
|
||||
},
|
||||
sidebarIcon: {
|
||||
fontSize: 22,
|
||||
color: theme.color,
|
||||
},
|
||||
};
|
||||
|
||||
styles.folderButton = Object.assign({}, styles.button);
|
||||
styles.folderButton.paddingLeft = 0;
|
||||
styles.folderButtonText = Object.assign({}, styles.buttonText);
|
||||
styles.folderButtonSelected = Object.assign({}, styles.folderButton);
|
||||
styles.folderButtonSelected.backgroundColor = theme.selectedColor;
|
||||
styles.folderIcon = Object.assign({}, theme.icon);
|
||||
styles.folderIcon.color = theme.colorFaded; //'#0072d5';
|
||||
styles.folderIcon.paddingTop = 3;
|
||||
|
||||
styles.sideButton = Object.assign({}, styles.button, { flex: 0 });
|
||||
styles.sideButtonSelected = Object.assign({}, styles.sideButton, { backgroundColor: theme.selectedColor });
|
||||
styles.sideButtonText = Object.assign({}, styles.buttonText);
|
||||
|
||||
this.styles_[this.props.theme] = StyleSheet.create(styles);
|
||||
return this.styles_[this.props.theme];
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.syncStarted !== prevProps.syncStarted) {
|
||||
if (this.props.syncStarted) {
|
||||
this.syncIconAnimation = Animated.loop(
|
||||
Animated.timing(this.syncIconRotationValue, {
|
||||
toValue: 1,
|
||||
duration: 3000,
|
||||
easing: Easing.linear,
|
||||
})
|
||||
);
|
||||
|
||||
this.syncIconAnimation.start();
|
||||
} else {
|
||||
if (this.syncIconAnimation) this.syncIconAnimation.stop();
|
||||
this.syncIconAnimation = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
folder_press(folder) {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Notes',
|
||||
folderId: folder.id,
|
||||
});
|
||||
}
|
||||
|
||||
async folder_longPress(folder) {
|
||||
if (folder === 'all') return;
|
||||
|
||||
Alert.alert(
|
||||
'',
|
||||
_('Notebook: %s', folder.title),
|
||||
[
|
||||
{
|
||||
text: _('Rename'),
|
||||
onPress: () => {
|
||||
if (folder.encryption_applied) {
|
||||
alert(_('Encrypted notebooks cannot be renamed'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: folder.id,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _('Delete'),
|
||||
onPress: () => {
|
||||
Alert.alert('', _('Delete notebook "%s"?\n\nAll notes and sub-notebooks within this notebook will also be deleted.', folder.title), [
|
||||
{
|
||||
text: _('OK'),
|
||||
onPress: () => {
|
||||
Folder.delete(folder.id);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: _('Cancel'),
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
},
|
||||
]);
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: _('Cancel'),
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
{
|
||||
cancelable: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
folder_togglePress(folder) {
|
||||
this.props.dispatch({
|
||||
type: 'FOLDER_TOGGLE',
|
||||
id: folder.id,
|
||||
});
|
||||
}
|
||||
|
||||
tagButton_press() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Tags',
|
||||
});
|
||||
}
|
||||
|
||||
configButton_press() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
NavService.go('Config');
|
||||
}
|
||||
|
||||
allNotesButton_press() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Notes',
|
||||
smartFilterId: 'c3176726992c11e9ac940492261af972',
|
||||
});
|
||||
}
|
||||
|
||||
newFolderButton_press() {
|
||||
this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
|
||||
this.props.dispatch({
|
||||
type: 'NAV_GO',
|
||||
routeName: 'Folder',
|
||||
folderId: null,
|
||||
});
|
||||
}
|
||||
|
||||
async synchronize_press() {
|
||||
const actionDone = await shared.synchronize_press(this);
|
||||
if (actionDone === 'auth') this.props.dispatch({ type: 'SIDE_MENU_CLOSE' });
|
||||
}
|
||||
|
||||
renderFolderItem(folder, selected, hasChildren, depth) {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const folderButtonStyle = {
|
||||
flex: 1,
|
||||
flexDirection: 'row',
|
||||
height: 36,
|
||||
alignItems: 'center',
|
||||
paddingRight: theme.marginRight,
|
||||
};
|
||||
if (selected) folderButtonStyle.backgroundColor = theme.selectedColor;
|
||||
folderButtonStyle.paddingLeft = depth * 10 + theme.marginLeft;
|
||||
|
||||
const iconWrapperStyle = { paddingLeft: 10, paddingRight: 10 };
|
||||
if (selected) iconWrapperStyle.backgroundColor = theme.selectedColor;
|
||||
|
||||
let iconWrapper = null;
|
||||
|
||||
const iconName = this.props.collapsedFolderIds.indexOf(folder.id) >= 0 ? 'md-arrow-dropdown' : 'md-arrow-dropup';
|
||||
const iconComp = <Icon name={iconName} style={this.styles().folderIcon} />;
|
||||
|
||||
iconWrapper = !hasChildren ? null : (
|
||||
<TouchableOpacity
|
||||
style={iconWrapperStyle}
|
||||
folderid={folder.id}
|
||||
onPress={() => {
|
||||
if (hasChildren) this.folder_togglePress(folder);
|
||||
}}
|
||||
>
|
||||
{iconComp}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
return (
|
||||
<View key={folder.id} style={{ flex: 1, flexDirection: 'row' }}>
|
||||
<TouchableOpacity
|
||||
style={{ flex: 1 }}
|
||||
onPress={() => {
|
||||
this.folder_press(folder);
|
||||
}}
|
||||
onLongPress={() => {
|
||||
this.folder_longPress(folder);
|
||||
}}
|
||||
>
|
||||
<View style={folderButtonStyle}>
|
||||
<Text numberOfLines={1} style={this.styles().folderButtonText}>
|
||||
{Folder.displayTitle(folder)}
|
||||
</Text>
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
{iconWrapper}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderSideBarButton(key, title, iconName, onPressHandler = null, selected = false) {
|
||||
let icon = <Icon name={iconName} style={this.styles().sidebarIcon} />;
|
||||
|
||||
if (key === 'synchronize_button') {
|
||||
icon = <Animated.View style={{ transform: [{ rotate: this.syncIconRotation }] }}>{icon}</Animated.View>;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<View key={key} style={selected ? this.styles().sideButtonSelected : this.styles().sideButton}>
|
||||
{icon}
|
||||
<Text style={this.styles().sideButtonText}>{title}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
if (!onPressHandler) return content;
|
||||
|
||||
return (
|
||||
<TouchableOpacity key={key} onPress={onPressHandler}>
|
||||
{content}
|
||||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
makeDivider(key) {
|
||||
return <View style={{ marginTop: 15, marginBottom: 15, flex: -1, borderBottomWidth: 1, borderBottomColor: globalStyle.dividerColor }} key={key}></View>;
|
||||
}
|
||||
|
||||
renderBottomPanel() {
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
const items = [];
|
||||
|
||||
items.push(this.makeDivider('divider_1'));
|
||||
|
||||
items.push(this.renderSideBarButton('newFolder_button', _('New Notebook'), 'md-folder-open', this.newFolderButton_press));
|
||||
|
||||
items.push(this.renderSideBarButton('tag_button', _('Tags'), 'md-pricetag', this.tagButton_press));
|
||||
|
||||
items.push(this.renderSideBarButton('config_button', _('Configuration'), 'md-settings', this.configButton_press));
|
||||
|
||||
items.push(this.makeDivider('divider_2'));
|
||||
|
||||
let lines = Synchronizer.reportToLines(this.props.syncReport);
|
||||
const syncReportText = lines.join('\n');
|
||||
|
||||
let decryptionReportText = '';
|
||||
if (this.props.decryptionWorker && this.props.decryptionWorker.state !== 'idle' && this.props.decryptionWorker.itemCount) {
|
||||
decryptionReportText = _('Decrypting items: %d/%d', this.props.decryptionWorker.itemIndex + 1, this.props.decryptionWorker.itemCount);
|
||||
}
|
||||
|
||||
let resourceFetcherText = '';
|
||||
if (this.props.resourceFetcher && this.props.resourceFetcher.toFetchCount) {
|
||||
resourceFetcherText = _('Fetching resources: %d/%d', this.props.resourceFetcher.fetchingCount, this.props.resourceFetcher.toFetchCount);
|
||||
}
|
||||
|
||||
let fullReport = [];
|
||||
if (syncReportText) fullReport.push(syncReportText);
|
||||
if (resourceFetcherText) fullReport.push(resourceFetcherText);
|
||||
if (decryptionReportText) fullReport.push(decryptionReportText);
|
||||
|
||||
items.push(this.renderSideBarButton('synchronize_button', !this.props.syncStarted ? _('Synchronise') : _('Cancel'), 'md-sync', this.synchronize_press));
|
||||
|
||||
if (fullReport.length)
|
||||
items.push(
|
||||
<Text key="sync_report" style={this.styles().syncStatus}>
|
||||
{fullReport.join('\n')}
|
||||
</Text>
|
||||
);
|
||||
|
||||
return <View style={{ flex: 0, flexDirection: 'column', paddingBottom: theme.marginBottom }}>{items}</View>;
|
||||
}
|
||||
|
||||
render() {
|
||||
let items = [];
|
||||
|
||||
const theme = themeStyle(this.props.theme);
|
||||
|
||||
// HACK: inner height of ScrollView doesn't appear to be calculated correctly when
|
||||
// using padding. So instead creating blank elements for padding bottom and top.
|
||||
items.push(<View style={{ height: globalStyle.marginTop }} key="bottom_top_hack" />);
|
||||
|
||||
items.push(this.renderSideBarButton('all_notes', _('All notes'), 'md-document', this.allNotesButton_press, this.props.notesParentType === 'SmartFilter'));
|
||||
|
||||
items.push(this.makeDivider('divider_all'));
|
||||
|
||||
items.push(this.renderSideBarButton('folder_header', _('Notebooks'), 'md-folder'));
|
||||
|
||||
if (this.props.folders.length) {
|
||||
const result = shared.renderFolders(this.props, this.renderFolderItem);
|
||||
const folderItems = result.items;
|
||||
items = items.concat(folderItems);
|
||||
}
|
||||
|
||||
let style = {
|
||||
flex: 1,
|
||||
borderRightWidth: 1,
|
||||
borderRightColor: globalStyle.dividerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={style}>
|
||||
<View style={{ flex: 1, opacity: this.props.opacity }}>
|
||||
<ScrollView scrollsToTop={false} style={this.styles().menu}>
|
||||
{items}
|
||||
</ScrollView>
|
||||
{this.renderBottomPanel()}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const SideMenuContent = connect(state => {
|
||||
return {
|
||||
folders: state.folders,
|
||||
syncStarted: state.syncStarted,
|
||||
syncReport: state.syncReport,
|
||||
selectedFolderId: state.selectedFolderId,
|
||||
selectedTagId: state.selectedTagId,
|
||||
notesParentType: state.notesParentType,
|
||||
locale: state.settings.locale,
|
||||
theme: state.settings.theme,
|
||||
// Don't do the opacity animation as it means re-rendering the list multiple times
|
||||
// opacity: state.sideMenuOpenPercent,
|
||||
collapsedFolderIds: state.collapsedFolderIds,
|
||||
decryptionWorker: state.decryptionWorker,
|
||||
resourceFetcher: state.resourceFetcher,
|
||||
};
|
||||
})(SideMenuContentComponent);
|
||||
|
||||
module.exports = { SideMenuContent };
|
12
Server/dist/lib/components/side-menu.js
vendored
12
Server/dist/lib/components/side-menu.js
vendored
@ -1,12 +0,0 @@
|
||||
const { connect } = require('react-redux');
|
||||
const SideMenu_ = require('react-native-side-menu').default;
|
||||
|
||||
class SideMenuComponent extends SideMenu_ {}
|
||||
|
||||
const MySideMenu = connect(state => {
|
||||
return {
|
||||
isOpen: state.showSideMenu,
|
||||
};
|
||||
})(SideMenuComponent);
|
||||
|
||||
module.exports = { SideMenu: MySideMenu };
|
@ -1,78 +0,0 @@
|
||||
module.exports = `/*
|
||||
|
||||
Atom One Dark With support for ReasonML by Gidi Morris, based off work by Daniel Gamage
|
||||
|
||||
Original One Dark Syntax theme from https://github.com/atom/one-dark-syntax
|
||||
|
||||
*/
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
line-height: 1.3em;
|
||||
color: #abb2bf;
|
||||
background: #282c34;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.hljs-keyword, .hljs-operator {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match {
|
||||
color: #F92672;
|
||||
}
|
||||
.hljs-pattern-match .hljs-constructor {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-function .hljs-params {
|
||||
color: #A6E22E;
|
||||
}
|
||||
.hljs-function .hljs-params .hljs-typing {
|
||||
color: #FD971F;
|
||||
}
|
||||
.hljs-module-access .hljs-module {
|
||||
color: #7e57c2;
|
||||
}
|
||||
.hljs-constructor {
|
||||
color: #e2b93d;
|
||||
}
|
||||
.hljs-constructor .hljs-string {
|
||||
color: #9CCC65;
|
||||
}
|
||||
.hljs-comment, .hljs-quote {
|
||||
color: #b18eb1;
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-doctag, .hljs-formula {
|
||||
color: #c678dd;
|
||||
}
|
||||
.hljs-section, .hljs-name, .hljs-selector-tag, .hljs-deletion, .hljs-subst {
|
||||
color: #e06c75;
|
||||
}
|
||||
.hljs-literal {
|
||||
color: #56b6c2;
|
||||
}
|
||||
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string {
|
||||
color: #98c379;
|
||||
}
|
||||
.hljs-built_in, .hljs-class .hljs-title {
|
||||
color: #e6c07b;
|
||||
}
|
||||
.hljs-attr, .hljs-variable, .hljs-template-variable, .hljs-type, .hljs-selector-class, .hljs-selector-attr, .hljs-selector-pseudo, .hljs-number {
|
||||
color: #d19a66;
|
||||
}
|
||||
.hljs-symbol, .hljs-bullet, .hljs-link, .hljs-meta, .hljs-selector-id, .hljs-title {
|
||||
color: #61aeee;
|
||||
}
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
@ -1,97 +0,0 @@
|
||||
module.exports = `/*
|
||||
|
||||
Atom One Light by Daniel Gamage
|
||||
Original One Light Syntax theme from https://github.com/atom/one-light-syntax
|
||||
|
||||
base: #fafafa
|
||||
mono-1: #383a42
|
||||
mono-2: #686b77
|
||||
mono-3: #a0a1a7
|
||||
hue-1: #0184bb
|
||||
hue-2: #4078f2
|
||||
hue-3: #a626a4
|
||||
hue-4: #50a14f
|
||||
hue-5: #e45649
|
||||
hue-5-2: #c91243
|
||||
hue-6: #986801
|
||||
hue-6-2: #c18401
|
||||
|
||||
*/
|
||||
|
||||
.hljs {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 0.5em;
|
||||
color: #383a42;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.hljs-comment,
|
||||
.hljs-quote {
|
||||
color: #a0a1a7;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-doctag,
|
||||
.hljs-keyword,
|
||||
.hljs-formula {
|
||||
color: #a626a4;
|
||||
}
|
||||
|
||||
.hljs-section,
|
||||
.hljs-name,
|
||||
.hljs-selector-tag,
|
||||
.hljs-deletion,
|
||||
.hljs-subst {
|
||||
color: #e45649;
|
||||
}
|
||||
|
||||
.hljs-literal {
|
||||
color: #0184bb;
|
||||
}
|
||||
|
||||
.hljs-string,
|
||||
.hljs-regexp,
|
||||
.hljs-addition,
|
||||
.hljs-attribute,
|
||||
.hljs-meta-string {
|
||||
color: #50a14f;
|
||||
}
|
||||
|
||||
.hljs-built_in,
|
||||
.hljs-class .hljs-title {
|
||||
color: #c18401;
|
||||
}
|
||||
|
||||
.hljs-attr,
|
||||
.hljs-variable,
|
||||
.hljs-template-variable,
|
||||
.hljs-type,
|
||||
.hljs-selector-class,
|
||||
.hljs-selector-attr,
|
||||
.hljs-selector-pseudo,
|
||||
.hljs-number {
|
||||
color: #986801;
|
||||
}
|
||||
|
||||
.hljs-symbol,
|
||||
.hljs-bullet,
|
||||
.hljs-link,
|
||||
.hljs-meta,
|
||||
.hljs-selector-id,
|
||||
.hljs-title {
|
||||
color: #4078f2;
|
||||
}
|
||||
|
||||
.hljs-emphasis {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.hljs-strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hljs-link {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
2
Server/dist/lib/csstojs/katex.css.js
vendored
2
Server/dist/lib/csstojs/katex.css.js
vendored
File diff suppressed because one or more lines are too long
70
Server/dist/lib/database-driver-node.js
vendored
70
Server/dist/lib/database-driver-node.js
vendored
@ -1,70 +0,0 @@
|
||||
const sqlite3 = require('sqlite3').verbose();
|
||||
const Promise = require('promise');
|
||||
|
||||
class DatabaseDriverNode {
|
||||
open(options) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_ = new sqlite3.Database(options.name, sqlite3.OPEN_READWRITE | sqlite3.OPEN_CREATE, error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
sqliteErrorToJsError(error, sql = null, params = null) {
|
||||
let msg = [error.toString()];
|
||||
if (sql) msg.push(sql);
|
||||
if (params) msg.push(params);
|
||||
let output = new Error(msg.join(': '));
|
||||
if (error.code) output.code = error.code;
|
||||
return output;
|
||||
}
|
||||
|
||||
selectOne(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.get(sql, params, (error, row) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.all(sql, params, (error, row) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(row);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
if (!params) params = {};
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.run(sql, params, error => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
lastInsertId() {
|
||||
throw new Error('NOT IMPLEMENTED');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DatabaseDriverNode };
|
74
Server/dist/lib/database-driver-react-native.js
vendored
74
Server/dist/lib/database-driver-react-native.js
vendored
@ -1,74 +0,0 @@
|
||||
const SQLite = require('react-native-sqlite-storage');
|
||||
|
||||
class DatabaseDriverReactNative {
|
||||
constructor() {
|
||||
this.lastInsertId_ = null;
|
||||
}
|
||||
|
||||
open(options) {
|
||||
//SQLite.DEBUG(true);
|
||||
return new Promise((resolve, reject) => {
|
||||
SQLite.openDatabase(
|
||||
{ name: options.name },
|
||||
db => {
|
||||
this.db_ = db;
|
||||
resolve();
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
sqliteErrorToJsError(error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
selectOne(sql, params = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(
|
||||
sql,
|
||||
params,
|
||||
r => {
|
||||
resolve(r.rows.length ? r.rows.item(0) : null);
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
selectAll(sql, params = null) {
|
||||
return this.exec(sql, params).then(r => {
|
||||
let output = [];
|
||||
for (let i = 0; i < r.rows.length; i++) {
|
||||
output.push(r.rows.item(i));
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
exec(sql, params = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.db_.executeSql(
|
||||
sql,
|
||||
params,
|
||||
r => {
|
||||
if ('insertId' in r) this.lastInsertId_ = r.insertId;
|
||||
resolve(r);
|
||||
},
|
||||
error => {
|
||||
reject(error);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
lastInsertId() {
|
||||
return this.lastInsertId_;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { DatabaseDriverReactNative };
|
319
Server/dist/lib/database.js
vendored
319
Server/dist/lib/database.js
vendored
@ -1,319 +0,0 @@
|
||||
const { Logger } = require('lib/logger.js');
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const Mutex = require('async-mutex').Mutex;
|
||||
|
||||
class Database {
|
||||
constructor(driver) {
|
||||
this.debugMode_ = false;
|
||||
this.driver_ = driver;
|
||||
this.logger_ = new Logger();
|
||||
this.logExcludedQueryTypes_ = [];
|
||||
this.batchTransactionMutex_ = new Mutex();
|
||||
}
|
||||
|
||||
setLogExcludedQueryTypes(v) {
|
||||
this.logExcludedQueryTypes_ = v;
|
||||
}
|
||||
|
||||
// Converts the SQLite error to a regular JS error
|
||||
// so that it prints a stacktrace when passed to
|
||||
// console.error()
|
||||
sqliteErrorToJsError(error, sql = null, params = null) {
|
||||
return this.driver().sqliteErrorToJsError(error, sql, params);
|
||||
}
|
||||
|
||||
setLogger(l) {
|
||||
this.logger_ = l;
|
||||
}
|
||||
|
||||
logger() {
|
||||
return this.logger_;
|
||||
}
|
||||
|
||||
driver() {
|
||||
return this.driver_;
|
||||
}
|
||||
|
||||
async open(options) {
|
||||
await this.driver().open(options);
|
||||
this.logger().info('Database was open successfully');
|
||||
}
|
||||
|
||||
escapeField(field) {
|
||||
if (field == '*') return '*';
|
||||
let p = field.split('.');
|
||||
if (p.length == 1) return `\`${field}\``;
|
||||
if (p.length == 2) return `${p[0]}.\`${p[1]}\``;
|
||||
|
||||
throw new Error(`Invalid field format: ${field}`);
|
||||
}
|
||||
|
||||
escapeFields(fields) {
|
||||
if (fields == '*') return '*';
|
||||
|
||||
let output = [];
|
||||
for (let i = 0; i < fields.length; i++) {
|
||||
output.push(this.escapeField(fields[i]));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async tryCall(callName, sql, params) {
|
||||
if (typeof sql === 'object') {
|
||||
params = sql.params;
|
||||
sql = sql.sql;
|
||||
}
|
||||
|
||||
let waitTime = 50;
|
||||
let totalWaitTime = 0;
|
||||
while (true) {
|
||||
try {
|
||||
this.logQuery(sql, params);
|
||||
let result = await this.driver()[callName](sql, params);
|
||||
return result; // No exception was thrown
|
||||
} catch (error) {
|
||||
if (error && (error.code == 'SQLITE_IOERR' || error.code == 'SQLITE_BUSY')) {
|
||||
if (totalWaitTime >= 20000) throw this.sqliteErrorToJsError(error, sql, params);
|
||||
// NOTE: don't put logger statements here because it might log to the database, which
|
||||
// could result in an error being thrown again.
|
||||
// this.logger().warn(sprintf('Error %s: will retry in %s milliseconds', error.code, waitTime));
|
||||
// this.logger().warn('Error was: ' + error.toString());
|
||||
await time.msleep(waitTime);
|
||||
totalWaitTime += waitTime;
|
||||
waitTime *= 1.5;
|
||||
} else {
|
||||
throw this.sqliteErrorToJsError(error, sql, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async selectOne(sql, params = null) {
|
||||
return this.tryCall('selectOne', sql, params);
|
||||
}
|
||||
|
||||
async selectAll(sql, params = null) {
|
||||
return this.tryCall('selectAll', sql, params);
|
||||
}
|
||||
|
||||
async selectAllFields(sql, params, field) {
|
||||
const rows = await this.tryCall('selectAll', sql, params);
|
||||
const output = [];
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const v = rows[i][field];
|
||||
if (!v) throw new Error(`No such field: ${field}. Query was: ${sql}`);
|
||||
output.push(rows[i][field]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async exec(sql, params = null) {
|
||||
return this.tryCall('exec', sql, params);
|
||||
}
|
||||
|
||||
async transactionExecBatch(queries) {
|
||||
if (queries.length <= 0) return;
|
||||
|
||||
if (queries.length == 1) {
|
||||
let q = this.wrapQuery(queries[0]);
|
||||
await this.exec(q.sql, q.params);
|
||||
return;
|
||||
}
|
||||
|
||||
// There can be only one transaction running at a time so use a mutex
|
||||
const release = await this.batchTransactionMutex_.acquire();
|
||||
|
||||
try {
|
||||
await this.exec('BEGIN TRANSACTION');
|
||||
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
let query = this.wrapQuery(queries[i]);
|
||||
await this.exec(query.sql, query.params);
|
||||
}
|
||||
|
||||
await this.exec('COMMIT');
|
||||
} catch (error) {
|
||||
await this.exec('ROLLBACK');
|
||||
throw error;
|
||||
} finally {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
static enumId(type, s) {
|
||||
if (type == 'settings') {
|
||||
if (s == 'int') return 1;
|
||||
if (s == 'string') return 2;
|
||||
}
|
||||
if (type == 'fieldType') {
|
||||
if (s) s = s.toUpperCase();
|
||||
if (s == 'INTEGER') s = 'INT';
|
||||
if (!(`TYPE_${s}` in this)) throw new Error(`Unkonwn fieldType: ${s}`);
|
||||
return this[`TYPE_${s}`];
|
||||
}
|
||||
if (type == 'syncTarget') {
|
||||
if (s == 'memory') return 1;
|
||||
if (s == 'filesystem') return 2;
|
||||
if (s == 'onedrive') return 3;
|
||||
}
|
||||
throw new Error(`Unknown enum type or value: ${type}, ${s}`);
|
||||
}
|
||||
|
||||
static enumName(type, id) {
|
||||
if (type === 'fieldType') {
|
||||
if (id === Database.TYPE_UNKNOWN) return 'unknown';
|
||||
if (id === Database.TYPE_INT) return 'int';
|
||||
if (id === Database.TYPE_TEXT) return 'text';
|
||||
if (id === Database.TYPE_NUMERIC) return 'numeric';
|
||||
throw new Error(`Invalid type id: ${id}`);
|
||||
}
|
||||
}
|
||||
|
||||
static formatValue(type, value) {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (type == this.TYPE_INT) return Number(value);
|
||||
if (type == this.TYPE_TEXT) return value;
|
||||
if (type == this.TYPE_NUMERIC) return Number(value);
|
||||
throw new Error(`Unknown type: ${type}`);
|
||||
}
|
||||
|
||||
sqlStringToLines(sql) {
|
||||
let output = [];
|
||||
let lines = sql.split('\n');
|
||||
let statement = '';
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
if (line == '') continue;
|
||||
if (line.substr(0, 2) == '--') continue;
|
||||
statement += line.trim();
|
||||
if (line[line.length - 1] == ',') statement += ' ';
|
||||
if (line[line.length - 1] == ';') {
|
||||
output.push(statement);
|
||||
statement = '';
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
logQuery(sql, params = null) {
|
||||
if (this.logExcludedQueryTypes_.length) {
|
||||
const temp = sql.toLowerCase();
|
||||
for (let i = 0; i < this.logExcludedQueryTypes_.length; i++) {
|
||||
if (temp.indexOf(this.logExcludedQueryTypes_[i].toLowerCase()) === 0) return;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger().debug(sql);
|
||||
if (params !== null && params.length) this.logger().debug(JSON.stringify(params));
|
||||
}
|
||||
|
||||
static insertQuery(tableName, data) {
|
||||
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
|
||||
|
||||
let keySql = '';
|
||||
let valueSql = '';
|
||||
let params = [];
|
||||
for (let key in data) {
|
||||
if (!data.hasOwnProperty(key)) continue;
|
||||
if (key[key.length - 1] == '_') continue;
|
||||
if (keySql != '') keySql += ', ';
|
||||
if (valueSql != '') valueSql += ', ';
|
||||
keySql += `\`${key}\``;
|
||||
valueSql += '?';
|
||||
params.push(data[key]);
|
||||
}
|
||||
return {
|
||||
sql: `INSERT INTO \`${tableName}\` (${keySql}) VALUES (${valueSql})`,
|
||||
params: params,
|
||||
};
|
||||
}
|
||||
|
||||
static updateQuery(tableName, data, where) {
|
||||
if (!data || !Object.keys(data).length) throw new Error('Data is empty');
|
||||
|
||||
let sql = '';
|
||||
let params = [];
|
||||
for (let key in data) {
|
||||
if (!data.hasOwnProperty(key)) continue;
|
||||
if (key[key.length - 1] == '_') continue;
|
||||
if (sql != '') sql += ', ';
|
||||
sql += `\`${key}\`=?`;
|
||||
params.push(data[key]);
|
||||
}
|
||||
|
||||
if (typeof where != 'string') {
|
||||
let s = [];
|
||||
for (let n in where) {
|
||||
if (!where.hasOwnProperty(n)) continue;
|
||||
params.push(where[n]);
|
||||
s.push(`\`${n}\`=?`);
|
||||
}
|
||||
where = s.join(' AND ');
|
||||
}
|
||||
|
||||
return {
|
||||
sql: `UPDATE \`${tableName}\` SET ${sql} WHERE ${where}`,
|
||||
params: params,
|
||||
};
|
||||
}
|
||||
|
||||
alterColumnQueries(tableName, fields) {
|
||||
let fieldsNoType = [];
|
||||
for (let n in fields) {
|
||||
if (!fields.hasOwnProperty(n)) continue;
|
||||
fieldsNoType.push(n);
|
||||
}
|
||||
|
||||
let fieldsWithType = [];
|
||||
for (let n in fields) {
|
||||
if (!fields.hasOwnProperty(n)) continue;
|
||||
fieldsWithType.push(`${this.escapeField(n)} ${fields[n]}`);
|
||||
}
|
||||
|
||||
let sql = `
|
||||
CREATE TEMPORARY TABLE _BACKUP_TABLE_NAME_(_FIELDS_TYPE_);
|
||||
INSERT INTO _BACKUP_TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _TABLE_NAME_;
|
||||
DROP TABLE _TABLE_NAME_;
|
||||
CREATE TABLE _TABLE_NAME_(_FIELDS_TYPE_);
|
||||
INSERT INTO _TABLE_NAME_ SELECT _FIELDS_NO_TYPE_ FROM _BACKUP_TABLE_NAME_;
|
||||
DROP TABLE _BACKUP_TABLE_NAME_;
|
||||
`;
|
||||
|
||||
sql = sql.replace(/_BACKUP_TABLE_NAME_/g, this.escapeField(`${tableName}_backup`));
|
||||
sql = sql.replace(/_TABLE_NAME_/g, this.escapeField(tableName));
|
||||
sql = sql.replace(/_FIELDS_NO_TYPE_/g, this.escapeFields(fieldsNoType).join(','));
|
||||
sql = sql.replace(/_FIELDS_TYPE_/g, fieldsWithType.join(','));
|
||||
|
||||
return sql.trim().split('\n');
|
||||
}
|
||||
|
||||
wrapQueries(queries) {
|
||||
let output = [];
|
||||
for (let i = 0; i < queries.length; i++) {
|
||||
output.push(this.wrapQuery(queries[i]));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
wrapQuery(sql, params = null) {
|
||||
if (!sql) throw new Error(`Cannot wrap empty string: ${sql}`);
|
||||
|
||||
if (sql.constructor === Array) {
|
||||
let output = {};
|
||||
output.sql = sql[0];
|
||||
output.params = sql.length >= 2 ? sql[1] : null;
|
||||
return output;
|
||||
} else if (typeof sql === 'string') {
|
||||
return { sql: sql, params: params };
|
||||
} else {
|
||||
return sql; // Already wrapped
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Database.TYPE_UNKNOWN = 0;
|
||||
Database.TYPE_INT = 1;
|
||||
Database.TYPE_TEXT = 2;
|
||||
Database.TYPE_NUMERIC = 3;
|
||||
|
||||
module.exports = { Database };
|
76
Server/dist/lib/dialogs.js
vendored
76
Server/dist/lib/dialogs.js
vendored
@ -1,76 +0,0 @@
|
||||
const DialogBox = require('react-native-dialogbox').default;
|
||||
const { Keyboard } = require('react-native');
|
||||
|
||||
// Add this at the bottom of the component:
|
||||
//
|
||||
// <DialogBox ref={dialogbox => { this.dialogbox = dialogbox }}/>
|
||||
|
||||
let dialogs = {};
|
||||
|
||||
dialogs.confirm = (parentComponent, message) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
parentComponent.dialogbox.confirm({
|
||||
content: message,
|
||||
|
||||
ok: {
|
||||
callback: () => {
|
||||
resolve(true);
|
||||
},
|
||||
},
|
||||
|
||||
cancel: {
|
||||
callback: () => {
|
||||
resolve(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
dialogs.pop = (parentComponent, message, buttons, options = null) => {
|
||||
if (!parentComponent) throw new Error('parentComponent is required');
|
||||
if (!('dialogbox' in parentComponent)) throw new Error('A "dialogbox" component must be defined on the parent component!');
|
||||
|
||||
if (!options) options = {};
|
||||
if (!('buttonFlow' in options)) options.buttonFlow = 'auto';
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Keyboard.dismiss();
|
||||
|
||||
let btns = [];
|
||||
for (let i = 0; i < buttons.length; i++) {
|
||||
btns.push({
|
||||
text: buttons[i].text,
|
||||
callback: () => {
|
||||
parentComponent.dialogbox.close();
|
||||
resolve(buttons[i].id);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
parentComponent.dialogbox.pop({
|
||||
content: message,
|
||||
btns: btns,
|
||||
buttonFlow: options.buttonFlow,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
dialogs.error = (parentComponent, message) => {
|
||||
Keyboard.dismiss();
|
||||
return parentComponent.dialogbox.alert(message);
|
||||
};
|
||||
|
||||
dialogs.info = (parentComponent, message) => {
|
||||
Keyboard.dismiss();
|
||||
return parentComponent.dialogbox.alert(message);
|
||||
};
|
||||
|
||||
dialogs.DialogBox = DialogBox;
|
||||
|
||||
module.exports = { dialogs };
|
234
Server/dist/lib/file-api-driver-dropbox.js
vendored
234
Server/dist/lib/file-api-driver-dropbox.js
vendored
@ -1,234 +0,0 @@
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const { shim } = require('lib/shim');
|
||||
const JoplinError = require('lib/JoplinError');
|
||||
|
||||
class FileApiDriverDropbox {
|
||||
constructor(api) {
|
||||
this.api_ = api;
|
||||
}
|
||||
|
||||
api() {
|
||||
return this.api_;
|
||||
}
|
||||
|
||||
requestRepeatCount() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
makePath_(path) {
|
||||
if (!path) return '';
|
||||
return `/${path}`;
|
||||
}
|
||||
|
||||
hasErrorCode_(error, errorCode) {
|
||||
if (!error || typeof error.code !== 'string') return false;
|
||||
return error.code.indexOf(errorCode) >= 0;
|
||||
}
|
||||
|
||||
async stat(path) {
|
||||
try {
|
||||
const metadata = await this.api().exec('POST', 'files/get_metadata', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
|
||||
return this.metadataToStat_(metadata, path);
|
||||
} catch (error) {
|
||||
if (this.hasErrorCode_(error, 'not_found')) {
|
||||
// ignore
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
metadataToStat_(md, path) {
|
||||
const output = {
|
||||
path: path,
|
||||
updated_time: md.server_modified ? new Date(md.server_modified) : new Date(),
|
||||
isDir: md['.tag'] === 'folder',
|
||||
};
|
||||
|
||||
if (md['.tag'] === 'deleted') output.isDeleted = true;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
metadataToStats_(mds) {
|
||||
const output = [];
|
||||
for (let i = 0; i < mds.length; i++) {
|
||||
output.push(this.metadataToStat_(mds[i], mds[i].name));
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async setTimestamp() {
|
||||
throw new Error('Not implemented'); // Not needed anymore
|
||||
}
|
||||
|
||||
async delta(path, options) {
|
||||
const context = options ? options.context : null;
|
||||
let cursor = context ? context.cursor : null;
|
||||
|
||||
while (true) {
|
||||
const urlPath = cursor ? 'files/list_folder/continue' : 'files/list_folder';
|
||||
const body = cursor ? { cursor: cursor } : { path: this.makePath_(path), include_deleted: true };
|
||||
|
||||
try {
|
||||
const response = await this.api().exec('POST', urlPath, body);
|
||||
|
||||
const output = {
|
||||
items: this.metadataToStats_(response.entries),
|
||||
hasMore: response.has_more,
|
||||
context: { cursor: response.cursor },
|
||||
};
|
||||
|
||||
return output;
|
||||
} catch (error) {
|
||||
// If there's an error related to an invalid cursor, clear the cursor and retry.
|
||||
if (cursor) {
|
||||
if ((error && error.httpStatus === 400) || this.hasErrorCode_(error, 'reset')) {
|
||||
// console.info('Clearing cursor and retrying', error);
|
||||
cursor = null;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async list(path) {
|
||||
let response = await this.api().exec('POST', 'files/list_folder', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
|
||||
let output = this.metadataToStats_(response.entries);
|
||||
|
||||
while (response.has_more) {
|
||||
response = await this.api().exec('POST', 'files/list_folder/continue', {
|
||||
cursor: response.cursor,
|
||||
});
|
||||
|
||||
output = output.concat(this.metadataToStats_(response.entries));
|
||||
}
|
||||
|
||||
return {
|
||||
items: output,
|
||||
hasMore: false,
|
||||
context: { cursor: response.cursor },
|
||||
};
|
||||
}
|
||||
|
||||
async get(path, options) {
|
||||
if (!options) options = {};
|
||||
if (!options.responseFormat) options.responseFormat = 'text';
|
||||
|
||||
try {
|
||||
const response = await this.api().exec(
|
||||
'POST',
|
||||
'files/download',
|
||||
null,
|
||||
{
|
||||
'Dropbox-API-Arg': JSON.stringify({ path: this.makePath_(path) }),
|
||||
},
|
||||
options
|
||||
);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (this.hasErrorCode_(error, 'not_found')) {
|
||||
return null;
|
||||
} else if (this.hasErrorCode_(error, 'restricted_content')) {
|
||||
throw new JoplinError('Cannot download because content is restricted by Dropbox', 'rejectedByTarget');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
try {
|
||||
await this.api().exec('POST', 'files/create_folder_v2', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.hasErrorCode_(error, 'path/conflict')) {
|
||||
// Ignore
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
// See https://github.com/facebook/react-native/issues/14445#issuecomment-352965210
|
||||
if (typeof content === 'string') content = shim.Buffer.from(content, 'utf8');
|
||||
|
||||
try {
|
||||
await this.api().exec(
|
||||
'POST',
|
||||
'files/upload',
|
||||
content,
|
||||
{
|
||||
'Dropbox-API-Arg': JSON.stringify({
|
||||
path: this.makePath_(path),
|
||||
mode: 'overwrite',
|
||||
mute: true, // Don't send a notification to user since there can be many of these updates
|
||||
}),
|
||||
},
|
||||
options
|
||||
);
|
||||
} catch (error) {
|
||||
if (this.hasErrorCode_(error, 'restricted_content')) {
|
||||
throw new JoplinError('Cannot upload because content is restricted by Dropbox', 'rejectedByTarget');
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async delete(path) {
|
||||
try {
|
||||
await this.api().exec('POST', 'files/delete_v2', {
|
||||
path: this.makePath_(path),
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.hasErrorCode_(error, 'not_found')) {
|
||||
// ignore
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async move() {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
format() {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
async clearRoot() {
|
||||
const entries = await this.list('');
|
||||
const batchDelete = [];
|
||||
for (let i = 0; i < entries.items.length; i++) {
|
||||
batchDelete.push({ path: this.makePath_(entries.items[i].path) });
|
||||
}
|
||||
|
||||
const response = await this.api().exec('POST', 'files/delete_batch', { entries: batchDelete });
|
||||
const jobId = response.async_job_id;
|
||||
|
||||
while (true) {
|
||||
const check = await this.api().exec('POST', 'files/delete_batch/check', { async_job_id: jobId });
|
||||
if (check['.tag'] === 'complete') break;
|
||||
|
||||
// It returns "failed" if it didn't work but anyway throw an error if it's anything other than complete or in_progress
|
||||
if (check['.tag'] !== 'in_progress') {
|
||||
throw new Error(`Batch delete failed? ${JSON.stringify(check)}`);
|
||||
}
|
||||
await time.sleep(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { FileApiDriverDropbox };
|
231
Server/dist/lib/file-api-driver-local.js
vendored
231
Server/dist/lib/file-api-driver-local.js
vendored
@ -1,231 +0,0 @@
|
||||
const { basicDelta } = require('lib/file-api');
|
||||
|
||||
// NOTE: when synchronising with the file system the time resolution is the second (unlike milliseconds for OneDrive for instance).
|
||||
// What it means is that if, for example, client 1 changes a note at time t, and client 2 changes the same note within the same second,
|
||||
// both clients will not know about each others updates during the next sync. They will simply both sync their note and whoever
|
||||
// comes last will overwrite (on the remote storage) the note of the other client. Both client will then have a different note at
|
||||
// that point and that will only be resolved if one of them changes the note and sync (if they don't change it, it will never get resolved).
|
||||
//
|
||||
// This is compound with the fact that we can't have a reliable delta API on the file system so we need to check all the timestamps
|
||||
// every time and rely on this exclusively to know about changes.
|
||||
//
|
||||
// This explains occasional failures of the fuzzing program (it finds that the clients end up with two different notes after sync). To
|
||||
// check that it is indeed the problem, check log-database.txt of both clients, search for the note ID, and most likely both notes
|
||||
// will have been modified at the same exact second at some point. If not, it's another bug that needs to be investigated.
|
||||
|
||||
class FileApiDriverLocal {
|
||||
fsErrorToJsError_(error, path = null) {
|
||||
let msg = error.toString();
|
||||
if (path !== null) msg += `. Path: ${path}`;
|
||||
let output = new Error(msg);
|
||||
if (error.code) output.code = error.code;
|
||||
return output;
|
||||
}
|
||||
|
||||
fsDriver() {
|
||||
if (!FileApiDriverLocal.fsDriver_) throw new Error('FileApiDriverLocal.fsDriver_ not set!');
|
||||
return FileApiDriverLocal.fsDriver_;
|
||||
}
|
||||
|
||||
async stat(path) {
|
||||
try {
|
||||
const s = await this.fsDriver().stat(path);
|
||||
if (!s) return null;
|
||||
return this.metadataFromStat_(s);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error);
|
||||
}
|
||||
}
|
||||
|
||||
metadataFromStat_(stat) {
|
||||
return {
|
||||
path: stat.path,
|
||||
// created_time: stat.birthtime.getTime(),
|
||||
updated_time: stat.mtime.getTime(),
|
||||
isDir: stat.isDirectory(),
|
||||
};
|
||||
}
|
||||
|
||||
metadataFromStats_(stats) {
|
||||
let output = [];
|
||||
for (let i = 0; i < stats.length; i++) {
|
||||
const mdStat = this.metadataFromStat_(stats[i]);
|
||||
output.push(mdStat);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
async setTimestamp(path, timestampMs) {
|
||||
try {
|
||||
await this.fsDriver().setTimestamp(path, new Date(timestampMs));
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error);
|
||||
}
|
||||
}
|
||||
|
||||
async delta(path, options) {
|
||||
const getStatFn = async path => {
|
||||
const stats = await this.fsDriver().readDirStats(path);
|
||||
return this.metadataFromStats_(stats);
|
||||
};
|
||||
|
||||
try {
|
||||
const output = await basicDelta(path, getStatFn, options);
|
||||
return output;
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
}
|
||||
|
||||
async list(path) {
|
||||
try {
|
||||
const stats = await this.fsDriver().readDirStats(path);
|
||||
const output = this.metadataFromStats_(stats);
|
||||
|
||||
return {
|
||||
items: output,
|
||||
hasMore: false,
|
||||
context: null,
|
||||
};
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
}
|
||||
|
||||
async get(path, options) {
|
||||
let output = null;
|
||||
|
||||
try {
|
||||
if (options.target === 'file') {
|
||||
//output = await fs.copy(path, options.path, { overwrite: true });
|
||||
output = await this.fsDriver().copy(path, options.path);
|
||||
} else {
|
||||
//output = await fs.readFile(path, options.encoding);
|
||||
output = await this.fsDriver().readFile(path, options.encoding);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code == 'ENOENT') return null;
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
if (await this.fsDriver().exists(path)) return;
|
||||
|
||||
try {
|
||||
await this.fsDriver().mkdir(path);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// fs.exists(path, (exists) => {
|
||||
// if (exists) {
|
||||
// resolve();
|
||||
// return;
|
||||
// }
|
||||
|
||||
// fs.mkdirp(path, (error) => {
|
||||
// if (error) {
|
||||
// reject(this.fsErrorToJsError_(error));
|
||||
// } else {
|
||||
// resolve();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
try {
|
||||
if (options.source === 'file') {
|
||||
await this.fsDriver().copy(options.path, path);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.fsDriver().writeFile(path, content, 'utf8');
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
// if (!options) options = {};
|
||||
|
||||
// if (options.source === 'file') content = await fs.readFile(options.path);
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// fs.writeFile(path, content, function(error) {
|
||||
// if (error) {
|
||||
// reject(this.fsErrorToJsError_(error));
|
||||
// } else {
|
||||
// resolve();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
async delete(path) {
|
||||
try {
|
||||
await this.fsDriver().unlink(path);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, path);
|
||||
}
|
||||
|
||||
// return new Promise((resolve, reject) => {
|
||||
// fs.unlink(path, function(error) {
|
||||
// if (error) {
|
||||
// if (error && error.code == 'ENOENT') {
|
||||
// // File doesn't exist - it's fine
|
||||
// resolve();
|
||||
// } else {
|
||||
// reject(this.fsErrorToJsError_(error));
|
||||
// }
|
||||
// } else {
|
||||
// resolve();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
}
|
||||
|
||||
async move(oldPath, newPath) {
|
||||
try {
|
||||
await this.fsDriver().move(oldPath, newPath);
|
||||
} catch (error) {
|
||||
throw this.fsErrorToJsError_(error, oldPath);
|
||||
}
|
||||
|
||||
// let lastError = null;
|
||||
|
||||
// for (let i = 0; i < 5; i++) {
|
||||
// try {
|
||||
// let output = await fs.move(oldPath, newPath, { overwrite: true });
|
||||
// return output;
|
||||
// } catch (error) {
|
||||
// lastError = error;
|
||||
// // Normally cannot happen with the `overwrite` flag but sometime it still does.
|
||||
// // In this case, retry.
|
||||
// if (error.code == 'EEXIST') {
|
||||
// await time.sleep(1);
|
||||
// continue;
|
||||
// }
|
||||
// throw this.fsErrorToJsError_(error);
|
||||
// }
|
||||
// }
|
||||
|
||||
// throw lastError;
|
||||
}
|
||||
|
||||
format() {
|
||||
throw new Error('Not supported');
|
||||
}
|
||||
|
||||
async clearRoot(baseDir) {
|
||||
await this.fsDriver().remove(baseDir);
|
||||
await this.fsDriver().mkdir(baseDir);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { FileApiDriverLocal };
|
160
Server/dist/lib/file-api-driver-memory.js
vendored
160
Server/dist/lib/file-api-driver-memory.js
vendored
@ -1,160 +0,0 @@
|
||||
const { time } = require('lib/time-utils.js');
|
||||
const fs = require('fs-extra');
|
||||
const { basicDelta } = require('lib/file-api');
|
||||
|
||||
class FileApiDriverMemory {
|
||||
constructor() {
|
||||
this.items_ = [];
|
||||
this.deletedItems_ = [];
|
||||
}
|
||||
|
||||
encodeContent_(content) {
|
||||
if (content instanceof Buffer) {
|
||||
return content.toString('base64');
|
||||
} else {
|
||||
return Buffer.from(content).toString('base64');
|
||||
}
|
||||
}
|
||||
|
||||
decodeContent_(content) {
|
||||
return Buffer.from(content, 'base64').toString('utf-8');
|
||||
}
|
||||
|
||||
itemIndexByPath(path) {
|
||||
for (let i = 0; i < this.items_.length; i++) {
|
||||
if (this.items_[i].path == path) return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
itemByPath(path) {
|
||||
let index = this.itemIndexByPath(path);
|
||||
return index < 0 ? null : this.items_[index];
|
||||
}
|
||||
|
||||
newItem(path, isDir = false) {
|
||||
let now = time.unixMs();
|
||||
return {
|
||||
path: path,
|
||||
isDir: isDir,
|
||||
updated_time: now, // In milliseconds!!
|
||||
// created_time: now, // In milliseconds!!
|
||||
content: '',
|
||||
};
|
||||
}
|
||||
|
||||
stat(path) {
|
||||
let item = this.itemByPath(path);
|
||||
return Promise.resolve(item ? Object.assign({}, item) : null);
|
||||
}
|
||||
|
||||
async setTimestamp(path, timestampMs) {
|
||||
let item = this.itemByPath(path);
|
||||
if (!item) return Promise.reject(new Error(`File not found: ${path}`));
|
||||
item.updated_time = timestampMs;
|
||||
}
|
||||
|
||||
async list(path) {
|
||||
let output = [];
|
||||
|
||||
for (let i = 0; i < this.items_.length; i++) {
|
||||
let item = this.items_[i];
|
||||
if (item.path == path) continue;
|
||||
if (item.path.indexOf(`${path}/`) === 0) {
|
||||
let s = item.path.substr(path.length + 1);
|
||||
if (s.split('/').length === 1) {
|
||||
let it = Object.assign({}, item);
|
||||
it.path = it.path.substr(path.length + 1);
|
||||
output.push(it);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
items: output,
|
||||
hasMore: false,
|
||||
context: null,
|
||||
});
|
||||
}
|
||||
|
||||
async get(path, options) {
|
||||
let item = this.itemByPath(path);
|
||||
if (!item) return Promise.resolve(null);
|
||||
if (item.isDir) return Promise.reject(new Error(`${path} is a directory, not a file`));
|
||||
|
||||
let output = null;
|
||||
if (options.target === 'file') {
|
||||
await fs.writeFile(options.path, Buffer.from(item.content, 'base64'));
|
||||
} else {
|
||||
const content = this.decodeContent_(item.content);
|
||||
output = Promise.resolve(content);
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
async mkdir(path) {
|
||||
let index = this.itemIndexByPath(path);
|
||||
if (index >= 0) return;
|
||||
this.items_.push(this.newItem(path, true));
|
||||
}
|
||||
|
||||
async put(path, content, options = null) {
|
||||
if (!options) options = {};
|
||||
|
||||
if (options.source === 'file') content = await fs.readFile(options.path);
|
||||
|
||||
let index = this.itemIndexByPath(path);
|
||||
if (index < 0) {
|
||||
let item = this.newItem(path, false);
|
||||
item.content = this.encodeContent_(content);
|
||||
this.items_.push(item);
|
||||
} else {
|
||||
this.items_[index].content = this.encodeContent_(content);
|
||||
this.items_[index].updated_time = time.unix();
|
||||
}
|
||||
}
|
||||
|
||||
async delete(path) {
|
||||
let index = this.itemIndexByPath(path);
|
||||
if (index >= 0) {
|
||||
let item = Object.assign({}, this.items_[index]);
|
||||
item.isDeleted = true;
|
||||
item.updated_time = time.unixMs();
|
||||
this.deletedItems_.push(item);
|
||||
this.items_.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
async move(oldPath, newPath) {
|
||||
let sourceItem = this.itemByPath(oldPath);
|
||||
if (!sourceItem) return Promise.reject(new Error(`Path not found: ${oldPath}`));
|
||||
this.delete(newPath); // Overwrite if newPath already exists
|
||||
sourceItem.path = newPath;
|
||||
}
|
||||
|
||||
async format() {
|
||||
this.items_ = [];
|
||||
}
|
||||
|
||||
async delta(path, options = null) {
|
||||
const getStatFn = async path => {
|
||||
let output = this.items_.slice();
|
||||
for (let i = 0; i < output.length; i++) {
|
||||
const item = Object.assign({}, output[i]);
|
||||
item.path = item.path.substr(path.length + 1);
|
||||
output[i] = item;
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const output = await basicDelta(path, getStatFn, options);
|
||||
return output;
|
||||
}
|
||||
|
||||
async clearRoot() {
|
||||
this.items_ = [];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { FileApiDriverMemory };
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user