Compare commits

...

20 Commits

Author SHA1 Message Date
azivner
378e8f35e5 release 0.15.0 2018-06-07 23:09:21 -04:00
azivner
bdb5e2f13f no results also for "add link" 2018-06-07 23:08:41 -04:00
azivner
8211bed449 renamed icons according to their size, fixes #113 2018-06-07 23:02:21 -04:00
azivner
b243632483 usability improvements to autocomplete ("no results" etc.), needs refactoring 2018-06-07 20:18:46 -04:00
azivner
e4d2513451 close search button 2018-06-07 19:50:16 -04:00
azivner
385144451b renamed hideInAutocomplete label to archived 2018-06-07 19:26:28 -04:00
azivner
c8c533844e icons for render & toggle edit since text titles caused unwanted horizontal scrolling in smaller window sizes 2018-06-06 23:37:57 -04:00
azivner
0e69f0c079 fix recent notes issues 2018-06-06 22:38:36 -04:00
azivner
aee60c444f added "show results in full text" 2018-06-05 23:28:10 -04:00
azivner
e7a504c66b fixes and optimizations for search 2018-06-05 22:47:47 -04:00
azivner
45d9c7164c search refactorings 2018-06-05 19:12:52 -04:00
azivner
bd913a63a8 search note fixes 2018-06-04 23:21:45 -04:00
azivner
5a1938c078 better sizing of search pane 2018-06-04 20:22:41 -04:00
azivner
015cd68756 renaming/refactoring of search services 2018-06-04 19:48:02 -04:00
azivner
76c0e5b2b8 new UI for search, closes #108 (still needs cleanup) 2018-06-03 20:42:25 -04:00
azivner
0f8f707acd persisting zoom setting in electron, fixes #112 2018-06-02 13:02:20 -04:00
azivner
083cccea28 better protected/unprotected note indicator, fixes #110 2018-06-02 11:47:16 -04:00
azivner
31b76b23ce release 0.14.1 2018-06-02 09:39:37 -04:00
azivner
af529f82e5 fixed false sync error reporting 2018-06-02 09:39:04 -04:00
azivner
fc6669d254 initialization and schema fixes, closes #111 2018-06-01 22:26:37 -04:00
61 changed files with 516 additions and 237 deletions

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

View File

@@ -0,0 +1 @@
UPDATE labels SET name = 'archived' WHERE name = 'hideInAutocomplete'

View File

@@ -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" ( CREATE TABLE IF NOT EXISTS "sync" (
`id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, `id` INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
`entityName` TEXT NOT NULL, `entityName` TEXT NOT NULL,
@@ -29,7 +24,7 @@ CREATE TABLE IF NOT EXISTS "note_revisions" (
`isProtected` INT NOT NULL DEFAULT 0, `isProtected` INT NOT NULL DEFAULT 0,
`dateModifiedFrom` TEXT NOT NULL, `dateModifiedFrom` TEXT NOT NULL,
`dateModifiedTo` 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` ( CREATE INDEX `IDX_note_revisions_noteId` ON `note_revisions` (
`noteId` `noteId`
); );
@@ -49,7 +44,7 @@ CREATE TABLE IF NOT EXISTS "images"
isDeleted INT NOT NULL DEFAULT 0, isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL, dateModified TEXT NOT NULL,
dateCreated TEXT NOT NULL dateCreated TEXT NOT NULL
); , hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE note_images CREATE TABLE note_images
( (
noteImageId TEXT PRIMARY KEY NOT NULL, noteImageId TEXT PRIMARY KEY NOT NULL,
@@ -58,7 +53,7 @@ CREATE TABLE note_images
isDeleted INT NOT NULL DEFAULT 0, isDeleted INT NOT NULL DEFAULT 0,
dateModified TEXT NOT NULL, dateModified TEXT NOT NULL,
dateCreated 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_noteId ON note_images (noteId);
CREATE INDEX IDX_note_images_imageId ON note_images (imageId); CREATE INDEX IDX_note_images_imageId ON note_images (imageId);
CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, 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, token TEXT NOT NULL,
dateCreated TEXT NOT NULL, dateCreated TEXT NOT NULL,
isDeleted INT NOT NULL DEFAULT 0 isDeleted INT NOT NULL DEFAULT 0
); , hash TEXT DEFAULT "" NOT NULL);
CREATE TABLE IF NOT EXISTS "branches" ( CREATE TABLE IF NOT EXISTS "branches" (
`branchId` TEXT NOT NULL, `branchId` TEXT NOT NULL,
`noteId` TEXT NOT NULL, `noteId` TEXT NOT NULL,
@@ -77,7 +72,7 @@ CREATE TABLE IF NOT EXISTS "branches" (
`prefix` TEXT, `prefix` TEXT,
`isExpanded` BOOLEAN, `isExpanded` BOOLEAN,
`isDeleted` INTEGER NOT NULL DEFAULT 0, `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`) PRIMARY KEY(`branchId`)
); );
CREATE INDEX `IDX_branches_noteId` ON `branches` ( CREATE INDEX `IDX_branches_noteId` ON `branches` (
@@ -87,12 +82,6 @@ CREATE INDEX `IDX_branches_noteId_parentNoteId` ON `branches` (
`noteId`, `noteId`,
`parentNoteId` `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 CREATE TABLE labels
( (
labelId TEXT not null primary key, labelId TEXT not null primary key,
@@ -103,18 +92,11 @@ CREATE TABLE labels
dateCreated TEXT not null, dateCreated TEXT not null,
dateModified TEXT not null, dateModified TEXT not null,
isDeleted INT not null isDeleted INT not null
); , hash TEXT DEFAULT "" NOT NULL);
CREATE INDEX IDX_labels_name_value CREATE INDEX IDX_labels_name_value
on labels (name, value); on labels (name, value);
CREATE INDEX IDX_labels_noteId CREATE INDEX IDX_labels_noteId
on 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" ( CREATE TABLE IF NOT EXISTS "notes" (
`noteId` TEXT NOT NULL, `noteId` TEXT NOT NULL,
`title` TEXT NOT NULL DEFAULT "unnamed", `title` TEXT NOT NULL DEFAULT "unnamed",
@@ -124,9 +106,31 @@ CREATE TABLE IF NOT EXISTS "notes" (
`dateCreated` TEXT NOT NULL, `dateCreated` TEXT NOT NULL,
`dateModified` TEXT NOT NULL, `dateModified` TEXT NOT NULL,
type TEXT NOT NULL DEFAULT 'text', 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`) PRIMARY KEY(`noteId`)
); );
CREATE INDEX `IDX_notes_isDeleted` ON `notes` ( CREATE INDEX IDX_branches_parentNoteId ON branches (parentNoteId);
`isDeleted` 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
); );

View File

@@ -1,7 +1,7 @@
{ {
"name": "trilium", "name": "trilium",
"description": "Trilium Notes", "description": "Trilium Notes",
"version": "0.14.0", "version": "0.15.0",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"main": "electron.js", "main": "electron.js",
"repository": { "repository": {

View File

@@ -5,8 +5,8 @@ const dateUtils = require('../services/date_utils');
class Option extends Entity { class Option extends Entity {
static get tableName() { return "options"; } static get tableName() { return "options"; }
static get primaryKeyName() { return "name"; } static get primaryKeyName() { return "optionId"; }
static get hashedProperties() { return ["name", "value"]; } static get hashedProperties() { return ["optionId", "name", "value"]; }
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();

View File

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 511 B

View File

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

View File

Before

Width:  |  Height:  |  Size: 339 B

After

Width:  |  Height:  |  Size: 339 B

View File

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 463 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

View File

Before

Width:  |  Height:  |  Size: 288 B

After

Width:  |  Height:  |  Size: 288 B

View File

Before

Width:  |  Height:  |  Size: 284 B

After

Width:  |  Height:  |  Size: 284 B

View File

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 292 B

View File

Before

Width:  |  Height:  |  Size: 511 B

After

Width:  |  Height:  |  Size: 511 B

View File

Before

Width:  |  Height:  |  Size: 155 B

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 B

View File

Before

Width:  |  Height:  |  Size: 358 B

After

Width:  |  Height:  |  Size: 358 B

View File

Before

Width:  |  Height:  |  Size: 252 B

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 431 B

View File

Before

Width:  |  Height:  |  Size: 419 B

After

Width:  |  Height:  |  Size: 419 B

View File

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

View File

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 B

View File

@@ -57,7 +57,15 @@ async function showDialog() {
source: async function(request, response) { source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term)); 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, minLength: 2,
change: async () => { change: async () => {

View File

@@ -1,10 +1,13 @@
import treeService from '../services/tree.js'; import treeService from '../services/tree.js';
import linkService from '../services/link.js'; import linkService from '../services/link.js';
import server from '../services/server.js'; import server from '../services/server.js';
import searchNotesService from '../services/search_notes.js';
const $dialog = $("#jump-to-note-dialog"); const $dialog = $("#jump-to-note-dialog");
const $autoComplete = $("#jump-to-note-autocomplete"); const $autoComplete = $("#jump-to-note-autocomplete");
const $form = $("#jump-to-note-form"); const $form = $("#jump-to-note-form");
const $jumpToNoteButton = $("#jump-to-note-button");
const $showInFullTextButton = $("#show-in-full-text-button");
async function showDialog() { async function showDialog() {
glob.activeDialog = $dialog; glob.activeDialog = $dialog;
@@ -20,7 +23,18 @@ async function showDialog() {
source: async function(request, response) { source: async function(request, response) {
const result = await server.get('autocomplete?query=' + encodeURIComponent(request.term)); 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 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(() => { $form.submit(() => {
goToNote(); goToNote();
return false; return false;
}); });
$jumpToNoteButton.click(goToNote);
$showInFullTextButton.click(showInFullText);
$dialog.bind('keydown', 'ctrl+return', showInFullText);
export default { export default {
showDialog showDialog
}; };

View File

@@ -1,47 +1,18 @@
import treeService from '../services/tree.js'; import treeService from '../services/tree.js';
import messagingService from '../services/messaging.js';
import server from '../services/server.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 $dialog = $("#recent-notes-dialog");
const $searchInput = $('#recent-notes-search-input'); 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) { function addRecentNote(branchId, notePath) {
setTimeout(async () => { setTimeout(async () => {
// we include the note into recent list only if the user stayed on the note at least 5 seconds // we include the note into recent list only if the user stayed on the note at least 5 seconds
if (notePath && notePath === treeService.getCurrentNotePath()) { if (notePath && notePath === treeService.getCurrentNotePath()) {
const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath)); const result = await server.put('recent-notes/' + branchId + '/' + encodeURIComponent(notePath));
list = result.map(r => r.notePath);
} }
}, 1500); }, 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() { async function showDialog() {
glob.activeDialog = $dialog; glob.activeDialog = $dialog;
@@ -54,16 +25,17 @@ async function showDialog() {
$searchInput.val(''); $searchInput.val('');
// remove the current note const result = await server.get('recent-notes');
const recNotes = list.filter(note => note !== treeService.getCurrentNotePath());
const items = [];
for (const notePath of recNotes) { // remove the current note
items.push({ const recNotes = result.filter(note => note.notePath !== treeService.getCurrentNotePath());
label: await getNoteTitle(notePath),
value: notePath const items = recNotes.map(rn => {
}); return {
} label: rn.title,
value: rn.notePath
};
});
$searchInput.autocomplete({ $searchInput.autocomplete({
source: items, 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 { export default {
showDialog, showDialog,
addRecentNote, addRecentNote
reload
}; };

View File

@@ -6,7 +6,7 @@ class NoteShort {
this.isProtected = row.isProtected; this.isProtected = row.isProtected;
this.type = row.type; this.type = row.type;
this.mime = row.mime; this.mime = row.mime;
this.hideInAutocomplete = row.hideInAutocomplete; this.archived = row.archived;
} }
isJson() { isJson() {
@@ -59,7 +59,7 @@ class NoteShort {
get dto() { get dto() {
const dto = Object.assign({}, this); const dto = Object.assign({}, this);
delete dto.treeCache; delete dto.treeCache;
delete dto.hideInAutocomplete; delete dto.archived;
return dto; return dto;
} }

View File

@@ -17,7 +17,7 @@ import messagingService from './messaging.js';
import noteDetailService from './note_detail.js'; import noteDetailService from './note_detail.js';
import noteType from './note_type.js'; import noteType from './note_type.js';
import protected_session from './protected_session.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 ScriptApi from './script_api.js';
import ScriptContext from './script_context.js'; import ScriptContext from './script_context.js';
import sync from './sync.js'; import sync from './sync.js';

View File

@@ -114,13 +114,14 @@ const contextMenuOptions = {
// Modify menu entries depending on node status // Modify menu entries depending on node status
$tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search'); $tree.contextmenu("enableEntry", "insertNoteHere", isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "insertChildNote", note.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", "copy", isNotRoot);
$tree.contextmenu("enableEntry", "cut", isNotRoot); $tree.contextmenu("enableEntry", "cut", isNotRoot);
$tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search'); $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0 && isNotRoot && parentNote.type !== 'search');
$tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search'); $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0 && note.type !== 'search');
$tree.contextmenu("enableEntry", "importBranch", note.type !== 'search'); $tree.contextmenu("enableEntry", "importBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search'); $tree.contextmenu("enableEntry", "exportBranch", note.type !== 'search');
$tree.contextmenu("enableEntry", "editBranchPrefix", parentNote.type !== 'search');
// Activate node on right-click // Activate node on right-click
node.setActive(); node.setActive();

View File

@@ -2,6 +2,7 @@ import utils from "./utils.js";
import treeService from "./tree.js"; import treeService from "./tree.js";
import linkService from "./link.js"; import linkService from "./link.js";
import fileService from "./file.js"; import fileService from "./file.js";
import zoomService from "./zoom.js";
import noteRevisionsDialog from "../dialogs/note_revisions.js"; import noteRevisionsDialog from "../dialogs/note_revisions.js";
import optionsDialog from "../dialogs/options.js"; import optionsDialog from "../dialogs/options.js";
import addLinkDialog from "../dialogs/add_link.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 noteSourceDialog from "../dialogs/note_source.js";
import recentChangesDialog from "../dialogs/recent_changes.js"; import recentChangesDialog from "../dialogs/recent_changes.js";
import sqlConsoleDialog from "../dialogs/sql_console.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 labelsDialog from "../dialogs/labels.js";
import protectedSessionService from "./protected_session.js"; import protectedSessionService from "./protected_session.js";
@@ -22,7 +23,7 @@ function registerEntrypoints() {
utils.bindShortcut('ctrl+l', addLinkDialog.showDialog); 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); utils.bindShortcut('ctrl+j', jumpToNoteDialog.showDialog);
$("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions); $("#show-note-revisions-button").click(noteRevisionsDialog.showCurrentNoteRevisions);
@@ -38,8 +39,8 @@ function registerEntrypoints() {
$("#recent-notes-button").click(recentNotesDialog.showDialog); $("#recent-notes-button").click(recentNotesDialog.showDialog);
utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog); utils.bindShortcut('ctrl+e', recentNotesDialog.showDialog);
$("#toggle-search-button").click(searchTreeService.toggleSearch); $("#toggle-search-button").click(searchNotesService.toggleSearch);
utils.bindShortcut('ctrl+s', searchTreeService.toggleSearch); utils.bindShortcut('ctrl+s', searchNotesService.toggleSearch);
$(".show-labels-button").click(labelsDialog.showDialog); $(".show-labels-button").click(labelsDialog.showDialog);
utils.bindShortcut('alt+l', labelsDialog.showDialog); utils.bindShortcut('alt+l', labelsDialog.showDialog);
@@ -109,27 +110,10 @@ function registerEntrypoints() {
$("#note-detail-text").focus(); $("#note-detail-text").focus();
}); });
$(document).bind('keydown', 'ctrl+-', () => { if (utils.isElectron()) {
if (utils.isElectron()) { $(document).bind('keydown', 'ctrl+-', zoomService.decreaseZoomFactor);
const webFrame = require('electron').webFrame; $(document).bind('keydown', 'ctrl+=', zoomService.increaseZoomFactor);
}
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;
}
});
$("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus()); $("#note-title").bind('keydown', 'return', () => $("#note-detail-text").focus());

View File

@@ -22,6 +22,7 @@ const $noteDetailComponents = $(".note-detail-component");
const $protectButton = $("#protect-button"); const $protectButton = $("#protect-button");
const $unprotectButton = $("#unprotect-button"); const $unprotectButton = $("#unprotect-button");
const $noteDetailWrapper = $("#note-detail-wrapper"); const $noteDetailWrapper = $("#note-detail-wrapper");
const $noteDetailComponentWrapper = $("#note-detail-component-wrapper");
const $noteIdDisplay = $("#note-id-display"); const $noteIdDisplay = $("#note-id-display");
const $labelList = $("#label-list"); const $labelList = $("#label-list");
const $labelListInner = $("#label-list-inner"); const $labelListInner = $("#label-list-inner");
@@ -116,9 +117,9 @@ async function saveNoteIfChanged() {
function setNoteBackgroundIfProtected(note) { function setNoteBackgroundIfProtected(note) {
const isProtected = !!note.isProtected; const isProtected = !!note.isProtected;
$noteDetailWrapper.toggleClass("protected", isProtected); $noteDetailComponentWrapper.toggleClass("protected", isProtected);
$protectButton.toggle(!isProtected); $protectButton.toggleClass("active", isProtected);
$unprotectButton.toggle(isProtected); $unprotectButton.toggleClass("active", !isProtected);
} }
let isNewNoteCreated = false; let isNewNoteCreated = false;
@@ -150,6 +151,8 @@ async function loadNoteDetail(noteId) {
$noteIdDisplay.html(noteId); $noteIdDisplay.html(noteId);
setNoteBackgroundIfProtected(currentNote);
await handleProtectedSession(); await handleProtectedSession();
$noteDetailWrapper.show(); $noteDetailWrapper.show();
@@ -170,7 +173,6 @@ async function loadNoteDetail(noteId) {
noteChangeDisabled = false; noteChangeDisabled = false;
} }
setNoteBackgroundIfProtected(currentNote);
treeService.setBranchBackgroundBasedOnProtectedStatus(noteId); treeService.setBranchBackgroundBasedOnProtectedStatus(noteId);
// after loading new note make sure editor is scrolled to the top // after loading new note make sure editor is scrolled to the top

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

View File

@@ -80,11 +80,10 @@ async function setupProtectedSession() {
$noteDetailWrapper.show(); $noteDetailWrapper.show();
protectedSessionDeferred.resolve(); protectedSessionDeferred.resolve();
protectedSessionDeferred = null;
$protectedSessionOnButton.addClass('active'); $protectedSessionOnButton.addClass('active');
$protectedSessionOffButton.removeClass('active'); $protectedSessionOffButton.removeClass('active');
protectedSessionDeferred = null;
} }
} }
@@ -105,6 +104,10 @@ async function enterProtectedSessionOnServer(password) {
} }
async function protectNoteAndSendToServer() { async function protectNoteAndSendToServer() {
if (noteDetailService.getCurrentNote().isProtected) {
return;
}
await ensureProtectedSession(true, true); await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote(); const note = noteDetailService.getCurrentNote();
@@ -118,6 +121,10 @@ async function protectNoteAndSendToServer() {
} }
async function unprotectNoteAndSendToServer() { async function unprotectNoteAndSendToServer() {
if (!noteDetailService.getCurrentNote().isProtected) {
return;
}
await ensureProtectedSession(true, true); await ensureProtectedSession(true, true);
const note = noteDetailService.getCurrentNote(); const note = noteDetailService.getCurrentNote();

View File

@@ -1,13 +1,11 @@
import utils from "./utils.js"; import utils from "./utils.js";
import server from "./server.js"; import optionsInitService from './options_init.js';
let lastProtectedSessionOperationDate = null; let lastProtectedSessionOperationDate = null;
let protectedSessionTimeout = null; let protectedSessionTimeout = null;
let protectedSessionId = null; let protectedSessionId = null;
$(document).ready(() => { optionsInitService.optionsReady.then(options => protectedSessionTimeout = options.protectedSessionTimeout);
server.get('options').then(options => protectedSessionTimeout = options.protectedSessionTimeout);
});
setInterval(() => { setInterval(() => {
if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) { if (lastProtectedSessionOperationDate !== null && new Date().getTime() - lastProtectedSessionOperationDate.getTime() > protectedSessionTimeout * 1000) {

View File

@@ -1,5 +1,6 @@
import treeService from './tree.js'; import treeService from './tree.js';
import server from './server.js'; import server from './server.js';
import treeUtils from "./tree_utils.js";
const $tree = $("#tree"); const $tree = $("#tree");
const $searchInput = $("input[name='search-text']"); const $searchInput = $("input[name='search-text']");
@@ -7,40 +8,62 @@ const $resetSearchButton = $("#reset-search-button");
const $doSearchButton = $("#do-search-button"); const $doSearchButton = $("#do-search-button");
const $saveSearchButton = $("#save-search-button"); const $saveSearchButton = $("#save-search-button");
const $searchBox = $("#search-box"); 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() { function toggleSearch() {
if ($searchBox.is(":hidden")) { if ($searchBox.is(":hidden")) {
$searchBox.show(); showSearch();
$searchInput.focus();
} }
else { else {
resetSearch(); hideSearch();
$searchBox.hide();
} }
} }
function resetSearch() { function resetSearch() {
$searchInput.val(""); $searchInput.val("");
getTree().clearFilter();
} }
function getTree() { function getTree() {
return $tree.fancytree('getTree'); return $tree.fancytree('getTree');
} }
async function doSearch() { async function doSearch(searchText) {
const searchText = $searchInput.val(); if (searchText) {
$searchInput.val(searchText);
const noteIds = await server.get('search/' + encodeURIComponent(searchText)); }
else {
for (const noteId of noteIds) { searchText = $searchInput.val();
await treeService.expandToNote(noteId, {noAnimation: true, noEvents: true});
} }
// Pass a string to perform case insensitive matching const results = await server.get('search/' + encodeURIComponent(searchText));
getTree().filterBranches(node => noteIds.includes(node.data.noteId));
$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() { async function saveSearch() {
@@ -71,6 +94,11 @@ $resetSearchButton.click(resetSearch);
$saveSearchButton.click(saveSearch); $saveSearchButton.click(saveSearch);
$closeSearchButton.click(hideSearch);
export default { export default {
toggleSearch toggleSearch,
resetSearch,
showSearch,
doSearch
}; };

View File

@@ -74,14 +74,21 @@ async function prepareRealBranch(parentNote) {
async function prepareSearchBranch(note) { async function prepareSearchBranch(note) {
const fullNote = await noteDetailService.loadNote(note.noteId); 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, { const branch = new Branch(treeCache, {
branchId: "virt" + utils.randomString(10), branchId: "virt" + utils.randomString(10),
noteId: noteId, noteId: result.noteId,
parentNoteId: note.noteId, parentNoteId: note.noteId,
prefix: '', prefix: origBranch.prefix,
virtual: true virtual: true
}); });

View File

@@ -52,6 +52,15 @@ async function getNotePathTitle(notePath) {
const titlePath = []; 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'; let parentNoteId = 'root';
for (const noteId of notePath.split('/')) { for (const noteId of notePath.split('/')) {

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

File diff suppressed because one or more lines are too long

View File

@@ -51,7 +51,7 @@
overflow: auto; 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; background-color: #eee;
} }
@@ -66,31 +66,31 @@ ul.fancytree-container {
/* icons from https://feathericons.com */ /* icons from https://feathericons.com */
span.fancytree-node > span.fancytree-icon { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 { 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 */ /* 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; width: 24px;
} }
#protect-button, #unprotect-button {
display: none;
}
.ui-widget-content a:not(.ui-tabs-anchor) { .ui-widget-content a:not(.ui-tabs-anchor) {
color: #337ab7 !important; color: #337ab7 !important;
} }
@@ -170,11 +166,27 @@ div.ui-tooltip {
#tree { #tree {
overflow: auto; overflow: auto;
flex-grow: 100; flex-grow: 1;
flex-shrink: 100; flex-shrink: 1;
flex-basis: 60%;
margin-top: 10px; 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 * .electron-in-page-search-window is a class specified to default
* <webview> element for search window. * <webview> element for search window.
@@ -359,12 +371,13 @@ div.ui-tooltip {
display: flex; display: flex;
} }
.btn { .btn:not(.btn-primary) {
border-color: #ddd; border-color: #ddd;
background-color: #eee;
} }
.btn.active { .btn.active:not(.btn-primary) {
background-color: #ddd; background-color: #ccc;
} }
#note-path-list .current a { #note-path-list .current a {

View File

@@ -1,16 +1,16 @@
"use strict"; "use strict";
const autocompleteService = require('../../services/autocomplete'); const noteCacheService = require('../../services/note_cache');
async function getAutocomplete(req) { async function getAutocomplete(req) {
const query = req.query.query; const query = req.query.query;
const results = autocompleteService.getResults(query); const results = noteCacheService.findNotes(query);
return results.map(res => { return results.map(res => {
return { return {
value: res.title + ' (' + res.path + ')', value: res.title + ' (' + res.path + ')',
title: res.title label: res.title
} }
}); });
} }

View File

@@ -2,9 +2,10 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const log = require('../../services/log');
// options allowed to be updated directly in options dialog // options allowed to be updated directly in options dialog
const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval']; const ALLOWED_OPTIONS = ['protectedSessionTimeout', 'noteRevisionSnapshotTimeInterval', 'zoomFactor'];
async function getOptions() { async function getOptions() {
const options = await sql.getMap("SELECT name, value FROM options WHERE name IN (" 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"]; return [400, "not allowed option to set"];
} }
log.info(`Updating option ${name} to ${value}`);
await optionService.setOption(name, value); await optionService.setOption(name, value);
} }

View File

@@ -1,12 +1,12 @@
"use strict"; "use strict";
const repository = require('../../services/repository'); const repository = require('../../services/repository');
const dateUtils = require('../../services/date_utils');
const optionService = require('../../services/options'); const optionService = require('../../services/options');
const RecentNote = require('../../entities/recent_note'); const RecentNote = require('../../entities/recent_note');
const noteCacheService = require('../../services/note_cache');
async function getRecentNotes() { async function getRecentNotes() {
return await repository.getEntities(` const recentNotes = await repository.getEntities(`
SELECT SELECT
recent_notes.* recent_notes.*
FROM FROM
@@ -18,6 +18,12 @@ async function getRecentNotes() {
ORDER BY ORDER BY
dateCreated DESC dateCreated DESC
LIMIT 200`); LIMIT 200`);
for (const rn of recentNotes) {
rn.title = noteCacheService.getNoteTitleForPath(rn.notePath.split('/'));
}
return recentNotes;
} }
async function addRecentNote(req) { async function addRecentNote(req) {

View File

@@ -2,15 +2,69 @@
const sql = require('../../services/sql'); const sql = require('../../services/sql');
const noteService = require('../../services/notes'); const noteService = require('../../services/notes');
const noteCacheService = require('../../services/note_cache');
const parseFilters = require('../../services/parse_filters'); const parseFilters = require('../../services/parse_filters');
const buildSearchQuery = require('../../services/build_search_query'); const buildSearchQuery = require('../../services/build_search_query');
async function searchNotes(req) { async function searchNotes(req) {
const {labelFilters, searchText} = parseFilters(req.params.searchString); 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; return noteIds;
} }
@@ -20,7 +74,7 @@ async function saveSearchToNote(req) {
searchString: req.params.searchString 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, json: true,
type: 'search', type: 'search',
mime: "application/json" mime: "application/json"

Binary file not shown.

View File

@@ -3,7 +3,7 @@
const build = require('./build'); const build = require('./build');
const packageJson = require('../../package'); const packageJson = require('../../package');
const APP_DB_VERSION = 96; const APP_DB_VERSION = 98;
module.exports = { module.exports = {
appVersion: packageJson.version, appVersion: packageJson.version,

View File

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

View File

@@ -1,4 +1,4 @@
module.exports = function(labelFilters, searchText) { module.exports = function(labelFilters) {
const joins = []; const joins = [];
const joinParams = []; const joinParams = [];
let where = '1'; let where = '1';
@@ -44,16 +44,6 @@ module.exports = function(labelFilters, searchText) {
let searchCondition = ''; let searchCondition = '';
const searchParams = []; 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 const query = `SELECT DISTINCT notes.noteId FROM notes
${joins.join('\r\n')} ${joins.join('\r\n')}
WHERE WHERE

View File

@@ -16,8 +16,11 @@ const RecentNote = require('../entities/recent_note');
const Option = require('../entities/option'); const Option = require('../entities/option');
async function getHash(entityConstructor, whereBranch) { async function getHash(entityConstructor, whereBranch) {
let contentToHash = await sql.getValue(`SELECT GROUP_CONCAT(hash) FROM ${entityConstructor.tableName} ` // subselect is necessary to have correct ordering in GROUP_CONCAT
+ (whereBranch ? `WHERE ${whereBranch} ` : '') + `ORDER BY ${entityConstructor.primaryKeyName}`); 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 if (!contentToHash) { // might be null in case of no rows
contentToHash = ""; contentToHash = "";

View File

@@ -6,7 +6,7 @@ const Label = require('../entities/label');
const BUILTIN_LABELS = [ const BUILTIN_LABELS = [
'disableVersioning', 'disableVersioning',
'calendarRoot', 'calendarRoot',
'hideInAutocomplete', 'archived',
'excludeFromExport', 'excludeFromExport',
'run', 'run',
'manualTransactionHandling', 'manualTransactionHandling',

View File

@@ -8,8 +8,9 @@ const utils = require('./utils');
let noteTitles; let noteTitles;
let protectedNoteTitles; let protectedNoteTitles;
let noteIds; let noteIds;
let childParentToBranchId = {};
const childToParent = {}; const childToParent = {};
const hideInAutocomplete = {}; const archived = {};
// key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here // key is 'childNoteId-parentNoteId' as a replacement for branchId which we don't use here
let prefixes = {}; 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 != ''`); 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) { for (const rel of relations) {
childToParent[rel.noteId] = childToParent[rel.noteId] || []; childToParent[rel.noteId] = childToParent[rel.noteId] || [];
childToParent[rel.noteId].push(rel.parentNoteId); 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) { for (const noteId of hiddenLabels) {
hideInAutocomplete[noteId] = true; archived[noteId] = true;
} }
} }
function getResults(query) { function findNotes(query) {
if (!noteTitles || query.length <= 2) { if (!noteTitles || query.length <= 2) {
return []; return [];
} }
@@ -49,7 +51,7 @@ function getResults(query) {
} }
for (const noteId of noteIds) { for (const noteId of noteIds) {
if (hideInAutocomplete[noteId]) { if (archived[noteId]) {
continue; continue;
} }
@@ -59,7 +61,7 @@ function getResults(query) {
} }
for (const parentNoteId of parents) { for (const parentNoteId of parents) {
if (hideInAutocomplete[parentNoteId]) { if (archived[parentNoteId]) {
continue; continue;
} }
@@ -91,8 +93,12 @@ function search(noteId, tokens, path, results) {
if (retPath) { if (retPath) {
const noteTitle = getNoteTitleForPath(retPath); const noteTitle = getNoteTitleForPath(retPath);
const thisNoteId = retPath[retPath.length - 1];
const thisParentNoteId = retPath[retPath.length - 2];
results.push({ results.push({
noteId: thisNoteId,
branchId: childParentToBranchId[`${thisNoteId}-${thisParentNoteId}`],
title: noteTitle, title: noteTitle,
path: retPath.join('/') path: retPath.join('/')
}); });
@@ -111,7 +117,7 @@ function search(noteId, tokens, path, results) {
return; return;
} }
if (parentNoteId === 'root' || hideInAutocomplete[parentNoteId]) { if (parentNoteId === 'root' || archived[parentNoteId]) {
continue; continue;
} }
@@ -155,6 +161,15 @@ function getNoteTitle(noteId, parentNoteId) {
function getNoteTitleForPath(path) { function getNoteTitleForPath(path) {
const titles = []; const titles = [];
if (path[0] === 'root') {
if (path.length === 1) {
return getNoteTitle('root');
}
else {
path = path.slice(1);
}
}
let parentNoteId = 'root'; let parentNoteId = 'root';
for (const noteId of path) { for (const noteId of path) {
@@ -180,6 +195,10 @@ function getSomePath(noteId, path) {
} }
for (const parentNoteId of parents) { for (const parentNoteId of parents) {
if (archived[parentNoteId]) {
continue;
}
const retPath = getSomePath(parentNoteId, path.concat([noteId])); const retPath = getSomePath(parentNoteId, path.concat([noteId]));
if (retPath) { if (retPath) {
@@ -190,6 +209,22 @@ function getSomePath(noteId, path) {
return false; 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}) => { eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId}) => {
if (entityName === 'notes') { if (entityName === 'notes') {
const note = await repository.getNote(entityId); const note = await repository.getNote(entityId);
@@ -211,6 +246,7 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
if (branch.isDeleted) { if (branch.isDeleted) {
delete prefixes[branch.noteId + '-' + branch.parentNoteId]; delete prefixes[branch.noteId + '-' + branch.parentNoteId];
delete childParentToBranchId[branch.noteId + '-' + branch.parentNoteId];
} }
else { else {
if (branch.prefix) { if (branch.prefix) {
@@ -219,21 +255,22 @@ eventService.subscribe(eventService.ENTITY_CHANGED, async ({entityName, entityId
childToParent[branch.noteId] = childToParent[branch.noteId] || []; childToParent[branch.noteId] = childToParent[branch.noteId] || [];
childToParent[branch.noteId].push(branch.parentNoteId); childToParent[branch.noteId].push(branch.parentNoteId);
childParentToBranchId[branch.noteId + '-' + branch.parentNoteId] = branch.branchId;
} }
} }
else if (entityName === 'labels') { else if (entityName === 'labels') {
const label = await repository.getLabel(entityId); const label = await repository.getLabel(entityId);
if (label.name === 'hideInAutocomplete') { if (label.name === 'archived') {
// we're not using label object directly, since there might be other non-deleted hideInAutocomplete label // 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 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) { if (hideLabel) {
hideInAutocomplete[label.noteId] = true; archived[label.noteId] = true;
} }
else { 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)); sqlInit.dbReady.then(() => utils.stopWatch("Autocomplete load", load));
module.exports = { module.exports = {
getResults findNotes,
getNotePath,
getNoteTitleForPath
}; };

View File

@@ -53,6 +53,8 @@ async function initOptions(startNotePath) {
await createOption('lastSyncedPull', appInfo.dbVersion, false); await createOption('lastSyncedPull', appInfo.dbVersion, false);
await createOption('lastSyncedPush', 0, false); await createOption('lastSyncedPush', 0, false);
await createOption('zoomFactor', 1.0, false);
} }
module.exports = { module.exports = {

View File

@@ -94,6 +94,12 @@ async function isDbUpToDate() {
} }
async function isUserInitialized() { 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'"); const username = await sql.getValue("SELECT value FROM options WHERE name = 'username'");
return !!username; return !!username;

View File

@@ -79,6 +79,32 @@ function stripTags(text) {
return text.replace(/<(?:.|\n)*?>/gm, ''); 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 = { module.exports = {
randomSecureToken, randomSecureToken,
randomString, randomString,
@@ -93,5 +119,7 @@ module.exports = {
stopWatch, stopWatch,
unescapeHtml, unescapeHtml,
toObject, toObject,
stripTags stripTags,
intersection,
union
}; };

View File

@@ -15,16 +15,16 @@
<div id="history-navigation" style="display: none;"> <div id="history-navigation" style="display: none;">
<a id="history-back-button" title="Go to previous note." class="icon-action" <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>
&nbsp; &nbsp; &nbsp; &nbsp;
<a id="history-forward-button" title="Go to next note." class="icon-action" <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>
<div style="flex-grow: 100; display: flex;"> <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-notes-button" title="CTRL+E">Recent notes</button>
<button class="btn btn-xs" id="recent-changes-button">Recent changes</button> <button class="btn btn-xs" id="recent-changes-button">Recent changes</button>
<div> <div>
@@ -57,18 +57,18 @@
</div> </div>
<div style="grid-area: left-pane; display: flex; flex-direction: column;" class="hide-toggle"> <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" <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" <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" <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" <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> </div>
<input type="file" id="import-upload" style="display: none" /> <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 id="search-box" style="display: none; padding: 10px; margin-top: 10px;">
<div style="display: flex; align-items: center;"> <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"> <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> <button id="do-search-button" class="btn btn-sm" title="Search (enter)" style="padding: 4px;">
</div> <img src="/images/icons/search-20.png" alt="Search"/>
</button>
<div style="display: flex; align-items: center; justify-content: space-evenly; margin-top: 10px;"> &nbsp;
<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>
&nbsp;
<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> </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 id="tree"></div>
</div> </div>
@@ -105,30 +123,45 @@
<span id="note-id-display" title="Note ID"></span> <span id="note-id-display" title="Note ID"></span>
<a title="Protect the note so that password will be required to view the note" <button class="btn btn-sm"
class="icon-action" style="display: none; margin-right: 10px; padding: 4px;"
id="protect-button" title="Toggle edit"
style="display: none; background: url('images/icons/lock.png')"></a> id="toggle-edit-button">
<img src="/images/icons/edit-20.png" alt="Toggle edit"/>
<a title="Unprotect note so that password will not be required to access this note in the future" </button>
class="icon-action"
id="unprotect-button"
style="display: none; background: url('images/icons/unlock.png')"></a>
&nbsp; &nbsp;
<button class="btn btn-sm" <button class="btn btn-sm"
style="display: none; margin-right: 10px" style="display: none; margin-right: 10px; padding: 4px;"
id="toggle-edit-button">Toggle edit</button> title="Render (Ctrl+Enter)"
id="render-button">
<button class="btn btn-sm" <img src="/images/icons/play-20.png" alt="Render"/>
style="display: none; margin-right: 10px" </button>
id="render-button">Render <kbd>Ctrl+Enter</kbd></button>
<button class="btn btn-sm" <button class="btn btn-sm"
style="display: none; margin-right: 10px" style="display: none; margin-right: 10px"
id="execute-script-button">Execute <kbd>Ctrl+Enter</kbd></button> 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>
&nbsp; &nbsp;
<div class="dropdown" id="note-type" data-bind="visible: type() != 'search'"> <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"> <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> Type: <span data-bind="text: typeString()"></span>
@@ -248,7 +281,7 @@
<input id="recent-notes-search-input" class="form-control"/> <input id="recent-notes-search-input" class="form-control"/>
</div> </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"> <form id="add-link-form">
<div id="add-link-type-div" class="radio"> <div id="add-link-type-div" class="radio">
<label title="Add HTML link to the selected note at cursor in current note"> <label title="Add HTML link to the selected note at cursor in current note">
@@ -266,7 +299,7 @@
<div class="form-group"> <div class="form-group">
<label for="note-autocomplete">Note</label> <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>
<div class="form-group" id="add-link-title-form-group"> <div class="form-group" id="add-link-title-form-group">
@@ -279,7 +312,7 @@
<input id="clone-prefix" style="width: 100%;"> <input id="clone-prefix" style="width: 100%;">
</div> </div>
<button class="btn btn-sm">Add link</button> <button class="btn btn-sm">Add note link</button>
</form> </form>
</div> </div>
@@ -287,10 +320,14 @@
<form id="jump-to-note-form"> <form id="jump-to-note-form">
<div class="form-group"> <div class="form-group">
<label for="jump-to-note-autocomplete">Note</label> <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> </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> </form>
</div> </div>

View File

@@ -18,6 +18,7 @@ const log = require('./services/log');
const appInfo = require('./services/app_info'); const appInfo = require('./services/app_info');
const messagingService = require('./services/messaging'); const messagingService = require('./services/messaging');
const utils = require('./services/utils'); const utils = require('./services/utils');
const sqlInit = require('./services/sql_init.js');
const port = normalizePort(config['Network']['port'] || '3000'); const port = normalizePort(config['Network']['port'] || '3000');
app.set('port', port); app.set('port', port);
@@ -54,7 +55,7 @@ httpServer.listen(port);
httpServer.on('error', onError); httpServer.on('error', onError);
httpServer.on('listening', onListening); httpServer.on('listening', onListening);
messagingService.init(httpServer, sessionParser); sqlInit.dbReady.then(() => messagingService.init(httpServer, sessionParser));
if (utils.isElectron()) { if (utils.isElectron()) {
const electronRouting = require('./routes/electron'); const electronRouting = require('./routes/electron');