Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
378e8f35e5 | ||
|
|
bdb5e2f13f | ||
|
|
8211bed449 | ||
|
|
b243632483 | ||
|
|
e4d2513451 | ||
|
|
385144451b | ||
|
|
c8c533844e | ||
|
|
0e69f0c079 | ||
|
|
aee60c444f | ||
|
|
e7a504c66b | ||
|
|
45d9c7164c | ||
|
|
bd913a63a8 | ||
|
|
5a1938c078 | ||
|
|
015cd68756 | ||
|
|
76c0e5b2b8 | ||
|
|
0f8f707acd | ||
|
|
083cccea28 | ||
|
|
31b76b23ce | ||
|
|
af529f82e5 | ||
|
|
fc6669d254 |
2
db/migrations/0097__add_zoomFactor.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO options (optionId, name, value, dateCreated, dateModified, isSynced)
|
||||
VALUES ('zoomFactor_key', 'zoomFactor', '1.0', '2018-06-01T03:35:55.041Z', '2018-06-01T03:35:55.041Z', 0);
|
||||
1
db/migrations/0098__rename_hideInAutocomplete.sql
Normal file
@@ -0,0 +1 @@
|
||||
UPDATE labels SET name = 'archived' WHERE name = 'hideInAutocomplete'
|
||||
@@ -1,8 +1,3 @@
|
||||
CREATE TABLE IF NOT EXISTS "options" (
|
||||
`name` TEXT NOT NULL PRIMARY KEY,
|
||||
`value` TEXT,
|
||||
`dateModified` INT,
|
||||
isSynced INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS "sync" (
|
||||
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
`entityName` TEXT NOT NULL,
|
||||
@@ -29,7 +24,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
|
||||
`isProtected` INT NOT NULL DEFAULT 0,
|
||||
`dateModifiedFrom` TEXT NOT NULL,
|
||||
`dateModifiedTo` TEXT NOT NULL
|
||||
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL);
|
||||
, type TEXT DEFAULT '' NOT NULL, mime TEXT DEFAULT '' NOT NULL, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
|
||||
`noteId`
|
||||
);
|
||||
@@ -49,7 +44,7 @@ CREATE TABLE IF NOT EXISTS "images"
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
dateModified TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE note_images
|
||||
(
|
||||
noteImageId TEXT PRIMARY KEY NOT NULL,
|
||||
@@ -58,7 +53,7 @@ CREATE TABLE note_images
|
||||
isDeleted INT NOT NULL DEFAULT 0,
|
||||
dateModified TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_note_images_noteId ON note_images (noteId);
|
||||
CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
|
||||
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId);
|
||||
@@ -68,7 +63,7 @@ CREATE TABLE IF NOT EXISTS "api_tokens"
|
||||
token TEXT NOT NULL,
|
||||
dateCreated TEXT NOT NULL,
|
||||
isDeleted INT NOT NULL DEFAULT 0
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`branchId` TEXT NOT NULL,
|
||||
`noteId` TEXT NOT NULL,
|
||||
@@ -77,7 +72,7 @@ CREATE TABLE IF NOT EXISTS "branches" (
|
||||
`prefix` TEXT,
|
||||
`isExpanded` BOOLEAN,
|
||||
`isDeleted` INTEGER NOT NULL DEFAULT 0,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL, hash TEXT DEFAULT "" NOT NULL, dateCreated TEXT NOT NULL DEFAULT '1970-01-01T00:00:00.000Z',
|
||||
PRIMARY KEY(`branchId`)
|
||||
);
|
||||
CREATE INDEX `IDX_branches_noteId` ON `branches` (
|
||||
@@ -87,12 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
|
||||
`noteId`,
|
||||
`parentNoteId`
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
|
||||
`branchId` TEXT NOT NULL PRIMARY KEY,
|
||||
`notePath` TEXT NOT NULL,
|
||||
`dateAccessed` TEXT NOT NULL,
|
||||
isDeleted INT
|
||||
);
|
||||
CREATE TABLE labels
|
||||
(
|
||||
labelId TEXT not null primary key,
|
||||
@@ -103,18 +92,11 @@ CREATE TABLE labels
|
||||
dateCreated TEXT not null,
|
||||
dateModified TEXT not null,
|
||||
isDeleted INT not null
|
||||
);
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE INDEX IDX_labels_name_value
|
||||
on labels (name, value);
|
||||
CREATE INDEX IDX_labels_noteId
|
||||
on labels (noteId);
|
||||
CREATE TABLE IF NOT EXISTS "event_log"
|
||||
(
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
noteId TEXT,
|
||||
comment TEXT,
|
||||
dateAdded TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`noteId` TEXT NOT NULL,
|
||||
`title` TEXT NOT NULL DEFAULT "unnamed",
|
||||
@@ -124,9 +106,31 @@ CREATE TABLE IF NOT EXISTS "notes" (
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
`dateModified` TEXT NOT NULL,
|
||||
type TEXT NOT NULL DEFAULT 'text',
|
||||
mime TEXT NOT NULL DEFAULT 'text/html',
|
||||
mime TEXT NOT NULL DEFAULT 'text/html', hash TEXT DEFAULT "" NOT NULL,
|
||||
PRIMARY KEY(`noteId`)
|
||||
);
|
||||
CREATE INDEX `IDX_notes_isDeleted` ON `notes` (
|
||||
`isDeleted`
|
||||
CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
|
||||
CREATE INDEX IDX_notes_type
|
||||
on notes (type);
|
||||
CREATE TABLE IF NOT EXISTS "recent_notes" (
|
||||
`branchId` TEXT NOT NULL PRIMARY KEY,
|
||||
`notePath` TEXT NOT NULL,
|
||||
`dateCreated` TEXT NOT NULL,
|
||||
isDeleted INT
|
||||
, hash TEXT DEFAULT "" NOT NULL);
|
||||
CREATE TABLE IF NOT EXISTS "event_log" (
|
||||
`eventId` TEXT NOT NULL PRIMARY KEY,
|
||||
`noteId` TEXT,
|
||||
`comment` TEXT,
|
||||
`dateCreated` TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS "options"
|
||||
(
|
||||
optionId TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT not null,
|
||||
value TEXT,
|
||||
dateModified INT,
|
||||
isSynced INTEGER default 0 not null,
|
||||
hash TEXT default "" not null,
|
||||
dateCreated TEXT default '1970-01-01T00:00:00.000Z' not null
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"description": "Trilium Notes",
|
||||
"version": "0.14.0",
|
||||
"version": "0.15.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "electron.js",
|
||||
"repository": {
|
||||
|
||||
@@ -5,8 +5,8 @@ const dateUtils = require('../services/date_utils');
|
||||
|
||||
class Option extends Entity {
|
||||
static get tableName() { return "options"; }
|
||||
static get primaryKeyName() { return "name"; }
|
||||
static get hashedProperties() { return ["name", "value"]; }
|
||||
static get primaryKeyName() { return "optionId"; }
|
||||
static get hashedProperties() { return ["optionId", "name", "value"]; }
|
||||
|
||||
beforeSaving() {
|
||||
super.beforeSaving();
|
||||
|
||||
|
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 511 B |
|
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
|
Before Width: | Height: | Size: 339 B After Width: | Height: | Size: 339 B |
|
Before Width: | Height: | Size: 463 B After Width: | Height: | Size: 463 B |
BIN
src/public/images/icons/edit-20.png
Normal file
|
After Width: | Height: | Size: 312 B |
|
Before Width: | Height: | Size: 288 B After Width: | Height: | Size: 288 B |
|
Before Width: | Height: | Size: 284 B After Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 292 B After Width: | Height: | Size: 292 B |
|
Before Width: | Height: | Size: 511 B After Width: | Height: | Size: 511 B |
|
Before Width: | Height: | Size: 155 B After Width: | Height: | Size: 155 B |
|
Before Width: | Height: | Size: 323 B |
|
Before Width: | Height: | Size: 358 B After Width: | Height: | Size: 358 B |
|
Before Width: | Height: | Size: 252 B After Width: | Height: | Size: 252 B |
BIN
src/public/images/icons/play-20.png
Normal file
|
After Width: | Height: | Size: 288 B |
BIN
src/public/images/icons/save-20.png
Normal file
|
After Width: | Height: | Size: 388 B |
BIN
src/public/images/icons/search-20.png
Normal file
|
After Width: | Height: | Size: 431 B |
|
Before Width: | Height: | Size: 419 B After Width: | Height: | Size: 419 B |
|
Before Width: | Height: | Size: 354 B After Width: | Height: | Size: 354 B |
BIN
src/public/images/icons/shield-20.png
Normal file
|
After Width: | Height: | Size: 388 B |
BIN
src/public/images/icons/shield-off-20.png
Normal file
|
After Width: | Height: | Size: 462 B |
|
Before Width: | Height: | Size: 240 B After Width: | Height: | Size: 240 B |
|
Before Width: | Height: | Size: 337 B |
BIN
src/public/images/icons/x-20.png
Normal file
|
After Width: | Height: | Size: 259 B |
@@ -57,7 +57,15 @@ async function showDialog() {
|
||||
source: async function(request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
response(result);
|
||||
if (result.length > 0) {
|
||||
response(result);
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
minLength: 2,
|
||||
change: async () => {
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import linkService from '../services/link.js';
|
||||
import server from '../services/server.js';
|
||||
import searchNotesService from '../services/search_notes.js';
|
||||
|
||||
const $dialog = $("#jump-to-note-dialog");
|
||||
const $autoComplete = $("#jump-to-note-autocomplete");
|
||||
const $form = $("#jump-to-note-form");
|
||||
const $jumpToNoteButton = $("#jump-to-note-button");
|
||||
const $showInFullTextButton = $("#show-in-full-text-button");
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
@@ -20,7 +23,18 @@ async function showDialog() {
|
||||
source: async function(request, response) {
|
||||
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term));
|
||||
|
||||
response(result);
|
||||
if (result.length > 0) {
|
||||
response(result);
|
||||
}
|
||||
else {
|
||||
response([{
|
||||
label: "No results",
|
||||
value: "No results"
|
||||
}]);
|
||||
}
|
||||
},
|
||||
focus: function(event, ui) {
|
||||
return $(ui.item).val() !== 'No results';
|
||||
},
|
||||
minLength: 2
|
||||
});
|
||||
@@ -41,12 +55,32 @@ function goToNote() {
|
||||
}
|
||||
}
|
||||
|
||||
function showInFullText(e) {
|
||||
// stop from propagating upwards (dangerous especially with ctrl+enter executable javascript notes)
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const searchText = $autoComplete.val();
|
||||
|
||||
searchNotesService.resetSearch();
|
||||
searchNotesService.showSearch();
|
||||
searchNotesService.doSearch(searchText);
|
||||
|
||||
$dialog.dialog('close');
|
||||
}
|
||||
|
||||
$form.submit(() => {
|
||||
goToNote();
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
$jumpToNoteButton.click(goToNote);
|
||||
|
||||
$showInFullTextButton.click(showInFullText);
|
||||
|
||||
$dialog.bind('keydown', 'ctrl+return', showInFullText);
|
||||
|
||||
export default {
|
||||
showDialog
|
||||
};
|
||||
@@ -1,47 +1,18 @@
|
||||
import treeService from '../services/tree.js';
|
||||
import messagingService from '../services/messaging.js';
|
||||
import server from '../services/server.js';
|
||||
import utils from "../services/utils.js";
|
||||
import treeUtils from "../services/tree_utils.js";
|
||||
|
||||
const $dialog = $("#recent-notes-dialog");
|
||||
const $searchInput = $('#recent-notes-search-input');
|
||||
|
||||
// list of recent note paths
|
||||
let list = [];
|
||||
|
||||
async function reload() {
|
||||
const result = await server.get('recent-notes');
|
||||
|
||||
list = result.map(r => r.notePath);
|
||||
}
|
||||
|
||||
function addRecentNote(branchId, notePath) {
|
||||
setTimeout(async () => {
|
||||
// we include the note into recent list only if the user stayed on the note at least 5 seconds
|
||||
if (notePath && notePath === treeService.getCurrentNotePath()) {
|
||||
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
|
||||
|
||||
list = result.map(r => r.notePath);
|
||||
}
|
||||
}, 1500);
|
||||
}
|
||||
|
||||
async function getNoteTitle(notePath) {
|
||||
let noteTitle;
|
||||
|
||||
try {
|
||||
noteTitle = await treeUtils.getNotePathTitle(notePath);
|
||||
}
|
||||
catch (e) {
|
||||
noteTitle = "[error - can't find note title]";
|
||||
|
||||
messagingService.logError("Could not find title for notePath=" + notePath + ", stack=" + e.stack);
|
||||
}
|
||||
|
||||
return noteTitle;
|
||||
}
|
||||
|
||||
async function showDialog() {
|
||||
glob.activeDialog = $dialog;
|
||||
|
||||
@@ -54,16 +25,17 @@ async function showDialog() {
|
||||
|
||||
$searchInput.val('');
|
||||
|
||||
// remove the current note
|
||||
const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
|
||||
const items = [];
|
||||
const result = await server.get('recent-notes');
|
||||
|
||||
for (const notePath of recNotes) {
|
||||
items.push({
|
||||
label: await getNoteTitle(notePath),
|
||||
value: notePath
|
||||
});
|
||||
}
|
||||
// remove the current note
|
||||
const recNotes = result.filter(note => note.notePath !== treeService.getCurrentNotePath());
|
||||
|
||||
const items = recNotes.map(rn => {
|
||||
return {
|
||||
label: rn.title,
|
||||
value: rn.notePath
|
||||
};
|
||||
});
|
||||
|
||||
$searchInput.autocomplete({
|
||||
source: items,
|
||||
@@ -96,18 +68,7 @@ async function showDialog() {
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(reload, 100);
|
||||
|
||||
messagingService.subscribeToMessages(syncData => {
|
||||
if (syncData.some(sync => sync.entityName === 'recent_notes')) {
|
||||
console.log(utils.now(), "Reloading recent notes because of background changes");
|
||||
|
||||
reload();
|
||||
}
|
||||
});
|
||||
|
||||
export default {
|
||||
showDialog,
|
||||
addRecentNote,
|
||||
reload
|
||||
addRecentNote
|
||||
};
|
||||
@@ -6,7 +6,7 @@ class NoteShort {
|
||||
this.isProtected = row.isProtected;
|
||||
this.type = row.type;
|
||||
this.mime = row.mime;
|
||||
this.hideInAutocomplete = row.hideInAutocomplete;
|
||||
this.archived = row.archived;
|
||||
}
|
||||
|
||||
isJson() {
|
||||
@@ -59,7 +59,7 @@ class NoteShort {
|
||||
get dto() {
|
||||
const dto = Object.assign({}, this);
|
||||
delete dto.treeCache;
|
||||
delete dto.hideInAutocomplete;
|
||||
delete dto.archived;
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
2
src/public/javascripts/services/bootstrap.js
vendored
@@ -17,7 +17,7 @@ import messagingService from './messaging.js';
|
||||
import noteDetailService from './note_detail.js';
|
||||
import noteType from './note_type.js';
|
||||
import protected_session from './protected_session.js';
|
||||
import searchTreeService from './search_tree.js';
|
||||
import searchNotesService from './search_notes.js';
|
||||
import ScriptApi from './script_api.js';
|
||||
import ScriptContext from './script_context.js';
|
||||
import sync from './sync.js';
|
||||
|
||||
@@ -114,13 +114,14 @@ const contextMenuOptions = {
|
||||
// Modify menu entries depending on node status
|
||||
$tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "insertChildNote", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "delete", isNotRoot);
|
||||
$tree.contextmenu("enableEntry", "delete", isNotRoot && parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "copy", isNotRoot);
|
||||
$tree.contextmenu("enableEntry", "cut", isNotRoot);
|
||||
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
|
||||
$tree.contextmenu("enableEntry", "editBranchPrefix", parentNote.type !== 'search');
|
||||
|
||||
// Activate node on right-click
|
||||
node.setActive();
|
||||
|
||||
@@ -2,6 +2,7 @@ import utils from "./utils.js";
|
||||
import treeService from "./tree.js";
|
||||
import linkService from "./link.js";
|
||||
import fileService from "./file.js";
|
||||
import zoomService from "./zoom.js";
|
||||
import noteRevisionsDialog from "../dialogs/note_revisions.js";
|
||||
import optionsDialog from "../dialogs/options.js";
|
||||
import addLinkDialog from "../dialogs/add_link.js";
|
||||
@@ -10,7 +11,7 @@ import jumpToNoteDialog from "../dialogs/jump_to_note.js";
|
||||
import noteSourceDialog from "../dialogs/note_source.js";
|
||||
import recentChangesDialog from "../dialogs/recent_changes.js";
|
||||
import sqlConsoleDialog from "../dialogs/sql_console.js";
|
||||
import searchTreeService from "./search_tree.js";
|
||||
import searchNotesService from "./search_notes.js";
|
||||
import labelsDialog from "../dialogs/labels.js";
|
||||
import protectedSessionService from "./protected_session.js";
|
||||
|
||||
@@ -22,7 +23,7 @@ function registerEntrypoints() {
|
||||
|
||||
utils.bindShortcut('ctrl+l', addLinkDialog.showDialog);
|
||||
|
||||
$("#jump-to-note-button").click(jumpToNoteDialog.showDialog);
|
||||
$("#jump-to-note-dialog-button").click(jumpToNoteDialog.showDialog);
|
||||
utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
|
||||
|
||||
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
|
||||
@@ -38,8 +39,8 @@ function registerEntrypoints() {
|
||||
$("#recent-notes-button").click(recentNotesDialog.showDialog);
|
||||
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
|
||||
|
||||
$("#toggle-search-button").click(searchTreeService.toggleSearch);
|
||||
utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch);
|
||||
$("#toggle-search-button").click(searchNotesService.toggleSearch);
|
||||
utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
|
||||
|
||||
$(".show-labels-button").click(labelsDialog.showDialog);
|
||||
utils.bindShortcut('alt+l', labelsDialog.showDialog);
|
||||
@@ -109,27 +110,10 @@ function registerEntrypoints() {
|
||||
$("#note-detail-text").focus();
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'ctrl+-', () => {
|
||||
if (utils.isElectron()) {
|
||||
const webFrame = require('electron').webFrame;
|
||||
|
||||
if (webFrame.getZoomFactor() > 0.2) {
|
||||
webFrame.setZoomFactor(webFrame.getZoomFactor() - 0.1);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
$(document).bind('keydown', 'ctrl+=', () => {
|
||||
if (utils.isElectron()) {
|
||||
const webFrame = require('electron').webFrame;
|
||||
|
||||
webFrame.setZoomFactor(webFrame.getZoomFactor() + 0.1);
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
if (utils.isElectron()) {
|
||||
$(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
|
||||
$(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
|
||||
}
|
||||
|
||||
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const $noteDetailComponents = $(".note-detail-component");
|
||||
const $protectButton = $("#protect-button");
|
||||
const $unprotectButton = $("#unprotect-button");
|
||||
const $noteDetailWrapper = $("#note-detail-wrapper");
|
||||
const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
|
||||
const $noteIdDisplay = $("#note-id-display");
|
||||
const $labelList = $("#label-list");
|
||||
const $labelListInner = $("#label-list-inner");
|
||||
@@ -116,9 +117,9 @@ async function saveNoteIfChanged() {
|
||||
function setNoteBackgroundIfProtected(note) {
|
||||
const isProtected = !!note.isProtected;
|
||||
|
||||
$noteDetailWrapper.toggleClass("protected", isProtected);
|
||||
$protectButton.toggle(!isProtected);
|
||||
$unprotectButton.toggle(isProtected);
|
||||
$noteDetailComponentWrapper.toggleClass("protected", isProtected);
|
||||
$protectButton.toggleClass("active", isProtected);
|
||||
$unprotectButton.toggleClass("active", !isProtected);
|
||||
}
|
||||
|
||||
let isNewNoteCreated = false;
|
||||
@@ -150,6 +151,8 @@ async function loadNoteDetail(noteId) {
|
||||
|
||||
$noteIdDisplay.html(noteId);
|
||||
|
||||
setNoteBackgroundIfProtected(currentNote);
|
||||
|
||||
await handleProtectedSession();
|
||||
|
||||
$noteDetailWrapper.show();
|
||||
@@ -170,7 +173,6 @@ async function loadNoteDetail(noteId) {
|
||||
noteChangeDisabled = false;
|
||||
}
|
||||
|
||||
setNoteBackgroundIfProtected(currentNote);
|
||||
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
|
||||
|
||||
// after loading new note make sure editor is scrolled to the top
|
||||
|
||||
9
src/public/javascripts/services/options_init.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import server from "./server.js";
|
||||
|
||||
const optionsReady = new Promise((resolve, reject) => {
|
||||
$(document).ready(() => server.get('options').then(resolve));
|
||||
});
|
||||
|
||||
export default {
|
||||
optionsReady
|
||||
}
|
||||
@@ -80,11 +80,10 @@ async function setupProtectedSession() {
|
||||
$noteDetailWrapper.show();
|
||||
|
||||
protectedSessionDeferred.resolve();
|
||||
protectedSessionDeferred = null;
|
||||
|
||||
$protectedSessionOnButton.addClass('active');
|
||||
$protectedSessionOffButton.removeClass('active');
|
||||
|
||||
protectedSessionDeferred = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +104,10 @@ async function enterProtectedSessionOnServer(password) {
|
||||
}
|
||||
|
||||
async function protectNoteAndSendToServer() {
|
||||
if (noteDetailService.getCurrentNote().isProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureProtectedSession(true, true);
|
||||
|
||||
const note = noteDetailService.getCurrentNote();
|
||||
@@ -118,6 +121,10 @@ async function protectNoteAndSendToServer() {
|
||||
}
|
||||
|
||||
async function unprotectNoteAndSendToServer() {
|
||||
if (!noteDetailService.getCurrentNote().isProtected) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ensureProtectedSession(true, true);
|
||||
|
||||
const note = noteDetailService.getCurrentNote();
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import utils from "./utils.js";
|
||||
import server from "./server.js";
|
||||
import optionsInitService from './options_init.js';
|
||||
|
||||
let lastProtectedSessionOperationDate = null;
|
||||
let protectedSessionTimeout = null;
|
||||
let protectedSessionId = null;
|
||||
|
||||
$(document).ready(() => {
|
||||
server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout);
|
||||
});
|
||||
optionsInitService.optionsReady.then(options => protectedSessionTimeout = options.protectedSessionTimeout);
|
||||
|
||||
setInterval(() => {
|
||||
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import treeService from './tree.js';
|
||||
import server from './server.js';
|
||||
import treeUtils from "./tree_utils.js";
|
||||
|
||||
const $tree = $("#tree");
|
||||
const $searchInput = $("input[name='search-text']");
|
||||
@@ -7,40 +8,62 @@ const $resetSearchButton = $("#reset-search-button");
|
||||
const $doSearchButton = $("#do-search-button");
|
||||
const $saveSearchButton = $("#save-search-button");
|
||||
const $searchBox = $("#search-box");
|
||||
const $searchResults = $("#search-results");
|
||||
const $searchResultsInner = $("#search-results-inner");
|
||||
const $closeSearchButton = $("#close-search-button");
|
||||
|
||||
function showSearch() {
|
||||
$searchBox.show();
|
||||
$searchInput.focus();
|
||||
}
|
||||
|
||||
function hideSearch() {
|
||||
resetSearch();
|
||||
|
||||
$searchResults.hide();
|
||||
$searchBox.hide();
|
||||
}
|
||||
|
||||
function toggleSearch() {
|
||||
if ($searchBox.is(":hidden")) {
|
||||
$searchBox.show();
|
||||
$searchInput.focus();
|
||||
showSearch();
|
||||
}
|
||||
else {
|
||||
resetSearch();
|
||||
|
||||
$searchBox.hide();
|
||||
hideSearch();
|
||||
}
|
||||
}
|
||||
|
||||
function resetSearch() {
|
||||
$searchInput.val("");
|
||||
|
||||
getTree().clearFilter();
|
||||
}
|
||||
|
||||
function getTree() {
|
||||
return $tree.fancytree('getTree');
|
||||
}
|
||||
|
||||
async function doSearch() {
|
||||
const searchText = $searchInput.val();
|
||||
|
||||
const noteIds = await server.get('search/' + encodeURIComponent(searchText));
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
await treeService.expandToNote(noteId, {noAnimation: true, noEvents: true});
|
||||
async function doSearch(searchText) {
|
||||
if (searchText) {
|
||||
$searchInput.val(searchText);
|
||||
}
|
||||
else {
|
||||
searchText = $searchInput.val();
|
||||
}
|
||||
|
||||
// Pass a string to perform case insensitive matching
|
||||
getTree().filterBranches(node => noteIds.includes(node.data.noteId));
|
||||
const results = await server.get('search/' + encodeURIComponent(searchText));
|
||||
|
||||
$searchResultsInner.empty();
|
||||
$searchResults.show();
|
||||
|
||||
for (const result of results) {
|
||||
const link = $('<a>', {
|
||||
href: 'javascript:',
|
||||
text: result.title
|
||||
}).attr('action', 'note').attr('note-path', result.path);
|
||||
|
||||
const $result = $('<li>').append(link);
|
||||
|
||||
$searchResultsInner.append($result);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSearch() {
|
||||
@@ -71,6 +94,11 @@ $resetSearchButton.click(resetSearch);
|
||||
|
||||
$saveSearchButton.click(saveSearch);
|
||||
|
||||
$closeSearchButton.click(hideSearch);
|
||||
|
||||
export default {
|
||||
toggleSearch
|
||||
toggleSearch,
|
||||
resetSearch,
|
||||
showSearch,
|
||||
doSearch
|
||||
};
|
||||
@@ -74,14 +74,21 @@ async function prepareRealBranch(parentNote) {
|
||||
|
||||
async function prepareSearchBranch(note) {
|
||||
const fullNote = await noteDetailService.loadNote(note.noteId);
|
||||
const noteIds = await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString));
|
||||
const results = await server.get('search/' + encodeURIComponent(fullNote.jsonContent.searchString));
|
||||
|
||||
const noteIds = results.map(res => res.noteId);
|
||||
|
||||
// force to load all the notes at once instead of one by one
|
||||
await treeCache.getNotes(noteIds);
|
||||
|
||||
for (const result of results) {
|
||||
const origBranch = await treeCache.getBranch(result.branchId);
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
const branch = new Branch(treeCache, {
|
||||
branchId: "virt" + utils.randomString(10),
|
||||
noteId: noteId,
|
||||
noteId: result.noteId,
|
||||
parentNoteId: note.noteId,
|
||||
prefix: '',
|
||||
prefix: origBranch.prefix,
|
||||
virtual: true
|
||||
});
|
||||
|
||||
|
||||
@@ -52,6 +52,15 @@ async function getNotePathTitle(notePath) {
|
||||
|
||||
const titlePath = [];
|
||||
|
||||
if (notePath.startsWith('root/')) {
|
||||
notePath = notePath.substr(5);
|
||||
}
|
||||
|
||||
// special case when we want just root's title
|
||||
if (notePath === 'root') {
|
||||
return await getNoteTitle(notePath);
|
||||
}
|
||||
|
||||
let parentNoteId = 'root';
|
||||
|
||||
for (const noteId of notePath.split('/')) {
|
||||
|
||||
42
src/public/javascripts/services/zoom.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import server from "./server.js";
|
||||
import utils from "./utils.js";
|
||||
import optionsInitService from "./options_init.js";
|
||||
|
||||
function decreaseZoomFactor() {
|
||||
const webFrame = require('electron').webFrame;
|
||||
|
||||
if (webFrame.getZoomFactor() > 0.2) {
|
||||
const webFrame = require('electron').webFrame;
|
||||
const newZoomFactor = webFrame.getZoomFactor() - 0.1;
|
||||
|
||||
webFrame.setZoomFactor(newZoomFactor);
|
||||
|
||||
server.put('options/zoomFactor/' + newZoomFactor);
|
||||
}
|
||||
}
|
||||
|
||||
function increaseZoomFactor() {
|
||||
const webFrame = require('electron').webFrame;
|
||||
const newZoomFactor = webFrame.getZoomFactor() + 0.1;
|
||||
|
||||
webFrame.setZoomFactor(newZoomFactor);
|
||||
|
||||
server.put('options/zoomFactor/' + newZoomFactor);
|
||||
}
|
||||
|
||||
function setZoomFactor(zoomFactor) {
|
||||
zoomFactor = parseFloat(zoomFactor);
|
||||
|
||||
const webFrame = require('electron').webFrame;
|
||||
webFrame.setZoomFactor(zoomFactor);
|
||||
}
|
||||
|
||||
if (utils.isElectron()) {
|
||||
optionsInitService.optionsReady.then(options => setZoomFactor(options.zoomFactor))
|
||||
}
|
||||
|
||||
export default {
|
||||
decreaseZoomFactor,
|
||||
increaseZoomFactor,
|
||||
setZoomFactor
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#note-detail-wrapper.protected, #note-detail-wrapper.protected .CodeMirror {
|
||||
#note-detail-component-wrapper.protected, #note-detail-component-wrapper.protected .CodeMirror {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
@@ -66,31 +66,31 @@ ul.fancytree-container {
|
||||
|
||||
/* icons from https://feathericons.com */
|
||||
span.fancytree-node > span.fancytree-icon {
|
||||
background: url("../images/icons/file.png") 0 0;
|
||||
background: url("../images/icons/file-16.png") 0 0;
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-folder > span.fancytree-icon {
|
||||
background: url("../images/icons/folder.png") 0 0;
|
||||
background: url("../images/icons/folder-16.png") 0 0;
|
||||
}
|
||||
|
||||
span.fancytree-node.code > span.fancytree-icon {
|
||||
background: url("../images/icons/code.png") 0 0;
|
||||
background: url("../images/icons/code-16.png") 0 0;
|
||||
}
|
||||
|
||||
span.fancytree-node.fancytree-folder.code > span.fancytree-icon {
|
||||
background: url("../images/icons/code-folder.png") 0 0;
|
||||
background: url("../images/icons/code-folder-16.png") 0 0;
|
||||
}
|
||||
|
||||
span.fancytree-node.file > span.fancytree-icon {
|
||||
background: url("../images/icons/paperclip.png") 0 0;
|
||||
background: url("../images/icons/paperclip-16.png") 0 0;
|
||||
}
|
||||
|
||||
span.fancytree-node.render > span.fancytree-icon {
|
||||
background: url("../images/icons/play.png") 0 0;
|
||||
background: url("../images/icons/play-16.png") 0 0;
|
||||
}
|
||||
|
||||
span.fancytree-node.search > span.fancytree-icon {
|
||||
background: url("../images/icons/search-small.png") 0 0;
|
||||
background: url("../images/icons/search-small-16.png") 0 0;
|
||||
}
|
||||
|
||||
span.fancytree-node.protected > span.fancytree-icon {
|
||||
@@ -106,7 +106,7 @@ span.fancytree-node.fancytree-active-clone:not(.fancytree-active) .fancytree-tit
|
||||
}
|
||||
|
||||
span.fancytree-node.tree-root > span.fancytree-icon {
|
||||
background: url("../images/icons/tree-root.png") 0 0;
|
||||
background: url("../images/icons/tree-root-16.png") 0 0;
|
||||
}
|
||||
|
||||
/* first nesting level has lower left padding to avoid extra left padding. Other levels are not affected */
|
||||
@@ -138,10 +138,6 @@ span.fancytree-active:not(.fancytree-focused) .fancytree-title {
|
||||
width: 24px;
|
||||
}
|
||||
|
||||
#protect-button, #unprotect-button {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ui-widget-content a:not(.ui-tabs-anchor) {
|
||||
color: #337ab7 !important;
|
||||
}
|
||||
@@ -170,11 +166,27 @@ div.ui-tooltip {
|
||||
|
||||
#tree {
|
||||
overflow: auto;
|
||||
flex-grow: 100;
|
||||
flex-shrink: 100;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
flex-basis: 60%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#search-results {
|
||||
padding: 0 5px 5px 15px;
|
||||
flex-basis: 40%;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
margin-top: 10px;
|
||||
display: none;
|
||||
overflow: auto;
|
||||
border-bottom: 2px solid #ddd;
|
||||
}
|
||||
|
||||
#search-results ul {
|
||||
padding: 5px 5px 5px 15px;
|
||||
}
|
||||
|
||||
/*
|
||||
* .electron-in-page-search-window is a class specified to default
|
||||
* <webview> element for search window.
|
||||
@@ -359,12 +371,13 @@ div.ui-tooltip {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.btn {
|
||||
.btn:not(.btn-primary) {
|
||||
border-color: #ddd;
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.btn.active {
|
||||
background-color: #ddd;
|
||||
.btn.active:not(.btn-primary) {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
#note-path-list .current a {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use strict";
|
||||
|
||||
const autocompleteService = require('../../services/autocomplete');
|
||||
const noteCacheService = require('../../services/note_cache');
|
||||
|
||||
async function getAutocomplete(req) {
|
||||
const query = req.query.query;
|
||||
|
||||
const results = autocompleteService.getResults(query);
|
||||
const results = noteCacheService.findNotes(query);
|
||||
|
||||
return results.map(res => {
|
||||
return {
|
||||
value: res.title + ' (' + res.path + ')',
|
||||
title: res.title
|
||||
label: res.title
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const optionService = require('../../services/options');
|
||||
const log = require('../../services/log');
|
||||
|
||||
// options allowed to be updated directly in options dialog
|
||||
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval'];
|
||||
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval', 'zoomFactor'];
|
||||
|
||||
async function getOptions() {
|
||||
const options = await sql.getMap("SELECT name, value FROM options WHERE name IN ("
|
||||
@@ -20,6 +21,8 @@ async function updateOption(req) {
|
||||
return [400, "not allowed option to set"];
|
||||
}
|
||||
|
||||
log.info(`Updating option ${name} to ${value}`);
|
||||
|
||||
await optionService.setOption(name, value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use strict";
|
||||
|
||||
const repository = require('../../services/repository');
|
||||
const dateUtils = require('../../services/date_utils');
|
||||
const optionService = require('../../services/options');
|
||||
const RecentNote = require('../../entities/recent_note');
|
||||
const noteCacheService = require('../../services/note_cache');
|
||||
|
||||
async function getRecentNotes() {
|
||||
return await repository.getEntities(`
|
||||
const recentNotes = await repository.getEntities(`
|
||||
SELECT
|
||||
recent_notes.*
|
||||
FROM
|
||||
@@ -18,6 +18,12 @@ async function getRecentNotes() {
|
||||
ORDER BY
|
||||
dateCreated DESC
|
||||
LIMIT 200`);
|
||||
|
||||
for (const rn of recentNotes) {
|
||||
rn.title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/'));
|
||||
}
|
||||
|
||||
return recentNotes;
|
||||
}
|
||||
|
||||
async function addRecentNote(req) {
|
||||
|
||||
@@ -2,15 +2,69 @@
|
||||
|
||||
const sql = require('../../services/sql');
|
||||
const noteService = require('../../services/notes');
|
||||
const noteCacheService = require('../../services/note_cache');
|
||||
const parseFilters = require('../../services/parse_filters');
|
||||
const buildSearchQuery = require('../../services/build_search_query');
|
||||
|
||||
async function searchNotes(req) {
|
||||
const {labelFilters, searchText} = parseFilters(req.params.searchString);
|
||||
|
||||
const {query, params} = buildSearchQuery(labelFilters, searchText);
|
||||
let labelFiltersNoteIds = null;
|
||||
|
||||
const noteIds = await sql.getColumn(query, params);
|
||||
if (labelFilters.length > 0) {
|
||||
const {query, params} = buildSearchQuery(labelFilters, searchText);
|
||||
|
||||
labelFiltersNoteIds = await sql.getColumn(query, params);
|
||||
}
|
||||
|
||||
let searchTextResults = null;
|
||||
|
||||
if (searchText.trim().length > 0) {
|
||||
searchTextResults = noteCacheService.findNotes(searchText);
|
||||
|
||||
let fullTextNoteIds = await getFullTextResults(searchText);
|
||||
|
||||
for (const noteId of fullTextNoteIds) {
|
||||
if (!searchTextResults.some(item => item.noteId === noteId)) {
|
||||
const result = noteCacheService.getNotePath(noteId);
|
||||
|
||||
if (result) {
|
||||
searchTextResults.push(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let results;
|
||||
|
||||
if (labelFiltersNoteIds && searchTextResults) {
|
||||
results = labelFiltersNoteIds.filter(item => searchTextResults.includes(item.noteId));
|
||||
}
|
||||
else if (labelFiltersNoteIds) {
|
||||
results = labelFiltersNoteIds.map(noteCacheService.getNotePath).filter(res => !!res);
|
||||
}
|
||||
else {
|
||||
results = searchTextResults;
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async function getFullTextResults(searchText) {
|
||||
const tokens = searchText.toLowerCase().split(" ");
|
||||
const tokenSql = ["1=1"];
|
||||
|
||||
for (const token of tokens) {
|
||||
// FIXME: escape token!
|
||||
tokenSql.push(`(title LIKE '%${token}%' OR content LIKE '%${token}%')`);
|
||||
}
|
||||
|
||||
const noteIds = await sql.getColumn(`
|
||||
SELECT DISTINCT noteId
|
||||
FROM notes
|
||||
WHERE isDeleted = 0
|
||||
AND isProtected = 0
|
||||
AND ${tokenSql.join(' AND ')}`);
|
||||
|
||||
return noteIds;
|
||||
}
|
||||
@@ -20,7 +74,7 @@ async function saveSearchToNote(req) {
|
||||
searchString: req.params.searchString
|
||||
};
|
||||
|
||||
const {note} = await noteService.createNote('root', 'Search note', noteContent, {
|
||||
const {note} = await noteService.createNote('root', req.params.searchString, noteContent, {
|
||||
json: true,
|
||||
type: 'search',
|
||||
mime: "application/json"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
const build = require('./build');
|
||||
const packageJson = require('../../package');
|
||||
|
||||
const APP_DB_VERSION = 96;
|
||||
const APP_DB_VERSION = 98;
|
||||
|
||||
module.exports = {
|
||||
appVersion: packageJson.version,
|
||||
|
||||
@@ -1 +1 @@
|
||||
module.exports = { buildDate:"2018-05-31T23:23:44-04:00", buildRevision: "80d2457b23b916b869fe2a91ae4e65e33ab42549" };
|
||||
module.exports = { buildDate:"2018-06-07T23:09:21-04:00", buildRevision: "bdb5e2f13f379f6b2a6fe06624239d2cf43a086f" };
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = function(labelFilters, searchText) {
|
||||
module.exports = function(labelFilters) {
|
||||
const joins = [];
|
||||
const joinParams = [];
|
||||
let where = '1';
|
||||
@@ -44,16 +44,6 @@ module.exports = function(labelFilters, searchText) {
|
||||
let searchCondition = '';
|
||||
const searchParams = [];
|
||||
|
||||
if (searchText.trim() !== '') {
|
||||
// searching in protected notes is pointless because of encryption
|
||||
searchCondition = ' AND (notes.isProtected = 0 AND (notes.title LIKE ? OR notes.content LIKE ?))';
|
||||
|
||||
searchText = '%' + searchText.trim() + '%';
|
||||
|
||||
searchParams.push(searchText);
|
||||
searchParams.push(searchText); // two occurences in searchCondition
|
||||
}
|
||||
|
||||
const query = `SELECT DISTINCT notes.noteId FROM notes
|
||||
${joins.join('\r\n')}
|
||||
WHERE
|
||||
|
||||
@@ -16,8 +16,11 @@ const RecentNote = require('../entities/recent_note');
|
||||
const Option = require('../entities/option');
|
||||
|
||||
async function getHash(entityConstructor, whereBranch) {
|
||||
let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} `
|
||||
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`);
|
||||
// subselect is necessary to have correct ordering in GROUP_CONCAT
|
||||
const query = `SELECT GROUP_CONCAT(hash) FROM (SELECT hash FROM ${entityConstructor.tableName} `
|
||||
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName})`;
|
||||
|
||||
let contentToHash = await sql.getValue(query);
|
||||
|
||||
if (!contentToHash) { // might be null in case of no rows
|
||||
contentToHash = "";
|
||||
|
||||
@@ -6,7 +6,7 @@ const Label = require('../entities/label');
|
||||
const BUILTIN_LABELS = [
|
||||
'disableVersioning',
|
||||
'calendarRoot',
|
||||
'hideInAutocomplete',
|
||||
'archived',
|
||||
'excludeFromExport',
|
||||
'run',
|
||||
'manualTransactionHandling',
|
||||
|
||||
@@ -8,8 +8,9 @@ const utils = require('./utils');
|
||||
let noteTitles;
|
||||
let protectedNoteTitles;
|
||||
let noteIds;
|
||||
let childParentToBranchId = {};
|
||||
const childToParent = {};
|
||||
const hideInAutocomplete = {};
|
||||
const archived = {};
|
||||
|
||||
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
|
||||
let prefixes = {};
|
||||
@@ -20,21 +21,22 @@ async function load() {
|
||||
|
||||
prefixes = await sql.getMap(`SELECT noteId || '-' || parentNoteId, prefix FROM branches WHERE prefix IS NOT NULL AND prefix != ''`);
|
||||
|
||||
const relations = await sql.getRows(`SELECT noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
|
||||
const relations = await sql.getRows(`SELECT branchId, noteId, parentNoteId FROM branches WHERE isDeleted = 0`);
|
||||
|
||||
for (const rel of relations) {
|
||||
childToParent[rel.noteId] = childToParent[rel.noteId] || [];
|
||||
childToParent[rel.noteId].push(rel.parentNoteId);
|
||||
childParentToBranchId[`${rel.noteId}-${rel.parentNoteId}`] = rel.branchId;
|
||||
}
|
||||
|
||||
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'hideInAutocomplete'`);
|
||||
const hiddenLabels = await sql.getColumn(`SELECT noteId FROM labels WHERE isDeleted = 0 AND name = 'archived'`);
|
||||
|
||||
for (const noteId of hiddenLabels) {
|
||||
hideInAutocomplete[noteId] = true;
|
||||
archived[noteId] = true;
|
||||
}
|
||||
}
|
||||
|
||||
function getResults(query) {
|
||||
function findNotes(query) {
|
||||
if (!noteTitles || query.length <= 2) {
|
||||
return [];
|
||||
}
|
||||
@@ -49,7 +51,7 @@ function getResults(query) {
|
||||
}
|
||||
|
||||
for (const noteId of noteIds) {
|
||||
if (hideInAutocomplete[noteId]) {
|
||||
if (archived[noteId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -59,7 +61,7 @@ function getResults(query) {
|
||||
}
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
if (hideInAutocomplete[parentNoteId]) {
|
||||
if (archived[parentNoteId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -91,8 +93,12 @@ function search(noteId, tokens, path, results) {
|
||||
|
||||
if (retPath) {
|
||||
const noteTitle = getNoteTitleForPath(retPath);
|
||||
const thisNoteId = retPath[retPath.length - 1];
|
||||
const thisParentNoteId = retPath[retPath.length - 2];
|
||||
|
||||
results.push({
|
||||
noteId: thisNoteId,
|
||||
branchId: childParentToBranchId[`${thisNoteId}-${thisParentNoteId}`],
|
||||
title: noteTitle,
|
||||
path: retPath.join('/')
|
||||
});
|
||||
@@ -111,7 +117,7 @@ function search(noteId, tokens, path, results) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) {
|
||||
if (parentNoteId === 'root' || archived[parentNoteId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -155,6 +161,15 @@ function getNoteTitle(noteId, parentNoteId) {
|
||||
function getNoteTitleForPath(path) {
|
||||
const titles = [];
|
||||
|
||||
if (path[0] === 'root') {
|
||||
if (path.length === 1) {
|
||||
return getNoteTitle('root');
|
||||
}
|
||||
else {
|
||||
path = path.slice(1);
|
||||
}
|
||||
}
|
||||
|
||||
let parentNoteId = 'root';
|
||||
|
||||
for (const noteId of path) {
|
||||
@@ -180,6 +195,10 @@ function getSomePath(noteId, path) {
|
||||
}
|
||||
|
||||
for (const parentNoteId of parents) {
|
||||
if (archived[parentNoteId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const retPath = getSomePath(parentNoteId, path.concat([noteId]));
|
||||
|
||||
if (retPath) {
|
||||
@@ -190,6 +209,22 @@ function getSomePath(noteId, path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
function getNotePath(noteId) {
|
||||
const retPath = getSomePath(noteId, []);
|
||||
|
||||
if (retPath) {
|
||||
const noteTitle = getNoteTitleForPath(retPath);
|
||||
const parentNoteId = childToParent[noteId][0];
|
||||
|
||||
return {
|
||||
noteId: noteId,
|
||||
branchId: childParentToBranchId[`${noteId}-${parentNoteId}`],
|
||||
title: noteTitle,
|
||||
path: retPath.join('/')
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
|
||||
if (entityName === 'notes') {
|
||||
const note = await repository.getNote(entityId);
|
||||
@@ -211,6 +246,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
|
||||
|
||||
if (branch.isDeleted) {
|
||||
delete prefixes[branch.noteId + '-' + branch.parentNoteId];
|
||||
delete childParentToBranchId[branch.noteId + '-' + branch.parentNoteId];
|
||||
}
|
||||
else {
|
||||
if (branch.prefix) {
|
||||
@@ -219,21 +255,22 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
|
||||
|
||||
childToParent[branch.noteId] = childToParent[branch.noteId] || [];
|
||||
childToParent[branch.noteId].push(branch.parentNoteId);
|
||||
childParentToBranchId[branch.noteId + '-' + branch.parentNoteId] = branch.branchId;
|
||||
}
|
||||
}
|
||||
else if (entityName === 'labels') {
|
||||
const label = await repository.getLabel(entityId);
|
||||
|
||||
if (label.name === 'hideInAutocomplete') {
|
||||
// we're not using label object directly, since there might be other non-deleted hideInAutocomplete label
|
||||
if (label.name === 'archived') {
|
||||
// we're not using label object directly, since there might be other non-deleted archived label
|
||||
const hideLabel = await repository.getEntity(`SELECT * FROM labels WHERE isDeleted = 0
|
||||
AND name = 'hideInAutocomplete' AND noteId = ?`, [label.noteId]);
|
||||
AND name = 'archived' AND noteId = ?`, [label.noteId]);
|
||||
|
||||
if (hideLabel) {
|
||||
hideInAutocomplete[label.noteId] = true;
|
||||
archived[label.noteId] = true;
|
||||
}
|
||||
else {
|
||||
delete hideInAutocomplete[label.noteId];
|
||||
delete archived[label.noteId];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -250,5 +287,7 @@ eventService.subscribe(eventService.ENTER_PROTECTED_SESSION, async () => {
|
||||
sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
|
||||
|
||||
module.exports = {
|
||||
getResults
|
||||
findNotes,
|
||||
getNotePath,
|
||||
getNoteTitleForPath
|
||||
};
|
||||
@@ -53,6 +53,8 @@ async function initOptions(startNotePath) {
|
||||
|
||||
await createOption('lastSyncedPull', appInfo.dbVersion, false);
|
||||
await createOption('lastSyncedPush', 0, false);
|
||||
|
||||
await createOption('zoomFactor', 1.0, false);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -94,6 +94,12 @@ async function isDbUpToDate() {
|
||||
}
|
||||
|
||||
async function isUserInitialized() {
|
||||
const optionsTable = await sql.getRows("SELECT name FROM sqlite_master WHERE type='table' AND name='options'");
|
||||
|
||||
if (optionsTable.length !== 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const username = await sql.getValue("SELECT value FROM options WHERE name = 'username'");
|
||||
|
||||
return !!username;
|
||||
|
||||
@@ -79,6 +79,32 @@ function stripTags(text) {
|
||||
return text.replace(/<(?:.|\n)*?>/gm, '');
|
||||
}
|
||||
|
||||
function intersection(a, b) {
|
||||
return a.filter(value => b.indexOf(value) !== -1);
|
||||
}
|
||||
|
||||
function union(a, b) {
|
||||
const obj = {};
|
||||
|
||||
for (let i = a.length-1; i >= 0; i--) {
|
||||
obj[a[i]] = a[i];
|
||||
}
|
||||
|
||||
for (let i = b.length-1; i >= 0; i--) {
|
||||
obj[b[i]] = b[i];
|
||||
}
|
||||
|
||||
const res = [];
|
||||
|
||||
for (const k in obj) {
|
||||
if (obj.hasOwnProperty(k)) { // <-- optional
|
||||
res.push(obj[k]);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
randomSecureToken,
|
||||
randomString,
|
||||
@@ -93,5 +119,7 @@ module.exports = {
|
||||
stopWatch,
|
||||
unescapeHtml,
|
||||
toObject,
|
||||
stripTags
|
||||
stripTags,
|
||||
intersection,
|
||||
union
|
||||
};
|
||||
@@ -15,16 +15,16 @@
|
||||
|
||||
<div id="history-navigation" style="display: none;">
|
||||
<a id="history-back-button" title="Go to previous note." class="icon-action"
|
||||
style="background: url('/images/icons/back.png')"></a>
|
||||
style="background: url('/images/icons/back-24.png')"></a>
|
||||
|
||||
|
||||
|
||||
<a id="history-forward-button" title="Go to next note." class="icon-action"
|
||||
style="background: url('/images/icons/forward.png')"></a>
|
||||
style="background: url('/images/icons/forward-24.png')"></a>
|
||||
</div>
|
||||
|
||||
<div style="flex-grow: 100; display: flex;">
|
||||
<button class="btn btn-xs" id="jump-to-note-button" title="CTRL+J">Jump to note</button>
|
||||
<button class="btn btn-xs" id="jump-to-note-dialog-button" title="CTRL+J">Jump to note</button>
|
||||
<button class="btn btn-xs" id="recent-notes-button" title="CTRL+E">Recent notes</button>
|
||||
<button class="btn btn-xs" id="recent-changes-button">Recent changes</button>
|
||||
<div>
|
||||
@@ -57,18 +57,18 @@
|
||||
</div>
|
||||
|
||||
<div style="grid-area: left-pane; display: flex; flex-direction: column;" class="hide-toggle">
|
||||
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 20px 0 20px; border: 1px solid #ccc;">
|
||||
<div style="display: flex; justify-content: space-around; padding: 10px 0 10px 0; margin: 0 10px 0 16px; border: 1px solid #ccc;">
|
||||
<a id="create-top-level-note-button" title="Create new top level note" class="icon-action"
|
||||
style="background: url('/images/icons/file-plus.png')"></a>
|
||||
style="background: url('/images/icons/file-plus-24.png')"></a>
|
||||
|
||||
<a id="collapse-tree-button" title="Collapse note tree" class="icon-action"
|
||||
style="background: url('/images/icons/list.png')"></a>
|
||||
style="background: url('/images/icons/list-24.png')"></a>
|
||||
|
||||
<a id="scroll-to-current-note-button" title="Scroll to current note. Shortcut CTRL+." class="icon-action"
|
||||
style="background: url('/images/icons/crosshair.png')"></a>
|
||||
style="background: url('/images/icons/crosshair-24.png')"></a>
|
||||
|
||||
<a id="toggle-search-button" title="Search in notes" class="icon-action"
|
||||
style="background: url('/images/icons/search.png')"></a>
|
||||
style="background: url('/images/icons/search-24.png')"></a>
|
||||
</div>
|
||||
|
||||
<input type="file" id="import-upload" style="display: none" />
|
||||
@@ -76,16 +76,34 @@
|
||||
<div id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
|
||||
<div style="display: flex; align-items: center;">
|
||||
<input name="search-text" placeholder="Search text, labels" style="flex-grow: 100; margin-left: 5px; margin-right: 5px;" autocomplete="off">
|
||||
<button id="do-search-button" class="btn btn-primary btn-sm" title="Search">Search</button>
|
||||
</div>
|
||||
<button id="do-search-button" class="btn btn-sm" title="Search (enter)" style="padding: 4px;">
|
||||
<img src="/images/icons/search-20.png" alt="Search"/>
|
||||
</button>
|
||||
|
||||
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;">
|
||||
<button id="reset-search-button" class="btn btn-sm" title="Reset search">Reset search</button>
|
||||
|
||||
|
||||
<button id="save-search-button" class="btn btn-sm" title="Save search">Save search</button>
|
||||
<button id="save-search-button" class="btn btn-sm" title="Save search" style="padding: 4px;">
|
||||
<img src="/images/icons/save-20.png" alt="Save search"/>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
<button id="close-search-button" class="btn btn-sm" title="Close search" style="padding: 4px;">
|
||||
<img src="/images/icons/x-20.png" alt="Close search"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="search-results">
|
||||
<strong>Search results:</strong>
|
||||
|
||||
<ul id="search-results-inner">
|
||||
<li>aaa</li>
|
||||
<li>bbb</li>
|
||||
<li>ccc</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="tree"></div>
|
||||
</div>
|
||||
|
||||
@@ -105,30 +123,45 @@
|
||||
|
||||
<span id="note-id-display" title="Note ID"></span>
|
||||
|
||||
<a title="Protect the note so that password will be required to view the note"
|
||||
class="icon-action"
|
||||
id="protect-button"
|
||||
style="display: none; background: url('images/icons/lock.png')"></a>
|
||||
|
||||
<a title="Unprotect note so that password will not be required to access this note in the future"
|
||||
class="icon-action"
|
||||
id="unprotect-button"
|
||||
style="display: none; background: url('images/icons/unlock.png')"></a>
|
||||
|
||||
|
||||
<button class="btn btn-sm"
|
||||
style="display: none; margin-right: 10px; padding: 4px;"
|
||||
title="Toggle edit"
|
||||
id="toggle-edit-button">
|
||||
<img src="/images/icons/edit-20.png" alt="Toggle edit"/>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm"
|
||||
style="display: none; margin-right: 10px"
|
||||
id="toggle-edit-button">Toggle edit</button>
|
||||
|
||||
<button class="btn btn-sm"
|
||||
style="display: none; margin-right: 10px"
|
||||
id="render-button">Render <kbd>Ctrl+Enter</kbd></button>
|
||||
style="display: none; margin-right: 10px; padding: 4px;"
|
||||
title="Render (Ctrl+Enter)"
|
||||
id="render-button">
|
||||
<img src="/images/icons/play-20.png" alt="Render"/>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-sm"
|
||||
style="display: none; margin-right: 10px"
|
||||
id="execute-script-button">Execute <kbd>Ctrl+Enter</kbd></button>
|
||||
|
||||
<div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button"
|
||||
class="btn"
|
||||
id="protect-button"
|
||||
title="Protected note can be viewed and edited only after entering password"
|
||||
style="padding: 4px;">
|
||||
<img src="/images/icons/shield-20.png"/>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn"
|
||||
id="unprotect-button"
|
||||
title="Not protected note can be viewed without entering password"
|
||||
style="padding: 4px;">
|
||||
<img src="/images/icons/shield-off-20.png"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="dropdown" id="note-type" data-bind="visible: type() != 'search'">
|
||||
<button data-bind="disable: isDisabled()" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm">
|
||||
Type: <span data-bind="text: typeString()"></span>
|
||||
@@ -248,7 +281,7 @@
|
||||
<input id="recent-notes-search-input" class="form-control"/>
|
||||
</div>
|
||||
|
||||
<div id="add-link-dialog" title="Add link" style="display: none;">
|
||||
<div id="add-link-dialog" title="Add note link" style="display: none;">
|
||||
<form id="add-link-form">
|
||||
<div id="add-link-type-div" class="radio">
|
||||
<label title="Add HTML link to the selected note at cursor in current note">
|
||||
@@ -266,7 +299,7 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="note-autocomplete">Note</label>
|
||||
<input id="note-autocomplete" style="width: 100%;">
|
||||
<input id="note-autocomplete" placeholder="search for note by its name" style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="add-link-title-form-group">
|
||||
@@ -279,7 +312,7 @@
|
||||
<input id="clone-prefix" style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<button class="btn btn-sm">Add link</button>
|
||||
<button class="btn btn-sm">Add note link</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -287,10 +320,14 @@
|
||||
<form id="jump-to-note-form">
|
||||
<div class="form-group">
|
||||
<label for="jump-to-note-autocomplete">Note</label>
|
||||
<input id="jump-to-note-autocomplete" style="width: 100%;">
|
||||
<input id="jump-to-note-autocomplete" placeholder="search for note by its name" style="width: 100%;">
|
||||
</div>
|
||||
|
||||
<button name="action" value="jump" class="btn btn-sm">Jump <kbd>enter</kbd></button>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<button id="jump-to-note-button" class="btn btn-sm btn-primary">Jump <kbd>enter</kbd></button>
|
||||
|
||||
<button id="show-in-full-text-button" class="btn btn-sm">Search in full text <kbd>ctrl+enter</kbd></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
3
src/www
@@ -18,6 +18,7 @@ const log = require('./services/log');
|
||||
const appInfo = require('./services/app_info');
|
||||
const messagingService = require('./services/messaging');
|
||||
const utils = require('./services/utils');
|
||||
const sqlInit = require('./services/sql_init.js');
|
||||
|
||||
const port = normalizePort(config['Network']['port'] || '3000');
|
||||
app.set('port', port);
|
||||
@@ -54,7 +55,7 @@ httpServer.listen(port);
|
||||
httpServer.on('error', onError);
|
||||
httpServer.on('listening', onListening);
|
||||
|
||||
messagingService.init(httpServer, sessionParser);
|
||||
sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
|
||||
|
||||
if (utils.isElectron()) {
|
||||
const electronRouting = require('./routes/electron');
|
||||
|
||||