Compare commits
	
		
			17 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | 378e8f35e5 | ||
|  | bdb5e2f13f | ||
|  | 8211bed449 | ||
|  | b243632483 | ||
|  | e4d2513451 | ||
|  | 385144451b | ||
|  | c8c533844e | ||
|  | 0e69f0c079 | ||
|  | aee60c444f | ||
|  | e7a504c66b | ||
|  | 45d9c7164c | ||
|  | bd913a63a8 | ||
|  | 5a1938c078 | ||
|  | 015cd68756 | ||
|  | 76c0e5b2b8 | ||
|  | 0f8f707acd | ||
|  | 083cccea28 | 
							
								
								
									
										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,7 +1,7 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.14.1", | ||||
|   "version": "0.15.0", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "repository": { | ||||
|   | ||||
| 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-06-02T09:39:37-04:00", buildRevision: "af529f82e5080f01b26ac7db104a8041f137dc48" }; | ||||
| 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  | ||||
|   | ||||
| @@ -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 = { | ||||
|   | ||||
| @@ -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> | ||||
|  | ||||
|   | ||||