mirror of
				https://github.com/zadam/trilium.git
				synced 2025-10-31 10:26:08 +01:00 
			
		
		
		
	Compare commits
	
		
			30 Commits
		
	
	
		
			v0.6.0-bet
			...
			v0.7.0-bet
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|  | e8c52e25f0 | ||
|  | a149c6a105 | ||
|  | 131af9ab12 | ||
|  | aa2bbc6575 | ||
|  | 78e8c15786 | ||
|  | fda4146150 | ||
|  | ddc885066e | ||
|  | 08bc2afb49 | ||
|  | 1d0220b03d | ||
|  | 3033f7cc08 | ||
|  | 6b9ff47c88 | ||
|  | fd02c6102d | ||
|  | 30c712a6be | ||
|  | 3928c96640 | ||
|  | d86f655658 | ||
|  | abdad1c3ae | ||
|  | 9e5f1a0a87 | ||
|  | cdde6a4d8e | ||
|  | 8028b09351 | ||
|  | ebe66eaed9 | ||
|  | 5bce9a5f94 | ||
|  | dfd9927310 | ||
|  | 9bf1735bde | ||
|  | 2e8eeda5ab | ||
|  | 1cef0ce5f9 | ||
|  | 1efac99828 | ||
|  | 0e9473119e | ||
|  | 7bbfef7af3 | ||
|  | 5cb93509c1 | ||
|  | 89e89e04d8 | 
							
								
								
									
										1
									
								
								db/migrations/0076__add_attribute_name_index.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								db/migrations/0076__add_attribute_name_index.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| CREATE INDEX IDX_attributes_name_value ON attributes (name, value); | ||||
							
								
								
									
										23
									
								
								db/migrations/0077__non_null_attribute_value.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								db/migrations/0077__non_null_attribute_value.sql
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| UPDATE attributes SET value = '' WHERE value IS NULL; | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "attributes_mig" | ||||
| ( | ||||
|   attributeId TEXT PRIMARY KEY NOT NULL, | ||||
|   noteId TEXT NOT NULL, | ||||
|   name TEXT NOT NULL, | ||||
|   value TEXT NOT NULL DEFAULT '', | ||||
|   position INT NOT NULL DEFAULT 0, | ||||
|   dateCreated TEXT NOT NULL, | ||||
|   dateModified TEXT NOT NULL, | ||||
|   isDeleted INT NOT NULL | ||||
| ); | ||||
|  | ||||
| INSERT INTO attributes_mig (attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted) | ||||
|     SELECT attributeId, noteId, name, value, position, dateCreated, dateModified, isDeleted FROM attributes; | ||||
|  | ||||
| DROP TABLE attributes; | ||||
|  | ||||
| ALTER TABLE attributes_mig RENAME TO attributes; | ||||
|  | ||||
| CREATE INDEX IDX_attributes_noteId ON attributes (noteId); | ||||
| CREATE INDEX IDX_attributes_name_value ON attributes (name, value); | ||||
| @@ -120,6 +120,7 @@ CREATE INDEX IDX_note_images_noteId ON note_images (noteId); | ||||
| CREATE INDEX IDX_note_images_imageId ON note_images (imageId); | ||||
| CREATE INDEX IDX_note_images_noteId_imageId ON note_images (noteId, imageId); | ||||
| CREATE INDEX IDX_attributes_noteId ON attributes (noteId); | ||||
| CREATE INDEX IDX_attributes_name_value ON attributes (name, value); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "api_tokens" | ||||
| ( | ||||
|   | ||||
							
								
								
									
										24
									
								
								electron.js
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								electron.js
									
									
									
									
									
								
							| @@ -3,9 +3,11 @@ | ||||
| const electron = require('electron'); | ||||
| const path = require('path'); | ||||
| const config = require('./src/services/config'); | ||||
| const log = require('./src/services/log'); | ||||
| const url = require("url"); | ||||
|  | ||||
| const app = electron.app; | ||||
| const globalShortcut = electron.globalShortcut; | ||||
|  | ||||
| // Adds debug features like hotkeys for triggering dev tools and reload | ||||
| require('electron-debug')(); | ||||
| @@ -13,6 +15,8 @@ require('electron-debug')(); | ||||
| // Prevent window being garbage collected | ||||
| let mainWindow; | ||||
|  | ||||
| require('electron-dl')({ saveAs: true }); | ||||
|  | ||||
| function onClosed() { | ||||
|     // Dereference the window | ||||
|     // For multiple windows store them in an array | ||||
| @@ -67,6 +71,26 @@ app.on('activate', () => { | ||||
|  | ||||
| app.on('ready', () => { | ||||
|     mainWindow = createMainWindow(); | ||||
|  | ||||
|     const result = globalShortcut.register('CommandOrControl+Alt+P', async () => { | ||||
|         const date_notes = require('./src/services/date_notes'); | ||||
|         const utils = require('./src/services/utils'); | ||||
|  | ||||
|         const parentNoteId = await date_notes.getDateNoteId(utils.nowDate()); | ||||
|  | ||||
|         // window may be hidden / not in focus | ||||
|         mainWindow.focus(); | ||||
|  | ||||
|         mainWindow.webContents.send('create-day-sub-note', parentNoteId); | ||||
|     }); | ||||
|  | ||||
|     if (!result) { | ||||
|         log.error("Could not register global shortcut CTRL+ALT+P"); | ||||
|     } | ||||
| }); | ||||
|  | ||||
| app.on('will-quit', () => { | ||||
|     globalShortcut.unregisterAll(); | ||||
| }); | ||||
|  | ||||
| require('./src/www'); | ||||
|   | ||||
							
								
								
									
										148
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										148
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -1,6 +1,6 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "version": "0.4.1", | ||||
|   "version": "0.6.2", | ||||
|   "lockfileVersion": 1, | ||||
|   "requires": true, | ||||
|   "dependencies": { | ||||
| @@ -3061,19 +3061,19 @@ | ||||
|       "integrity": "sha1-zIcsFoiArjxxiXYv1f/ACJbJUYo=" | ||||
|     }, | ||||
|     "electron": { | ||||
|       "version": "1.8.2-beta.4", | ||||
|       "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2-beta.4.tgz", | ||||
|       "integrity": "sha1-GDayBO6s6dx3Bi7Ugg/bxsvZoZU=", | ||||
|       "version": "1.8.2", | ||||
|       "resolved": "https://registry.npmjs.org/electron/-/electron-1.8.2.tgz", | ||||
|       "integrity": "sha512-0TV5Hy92g8ACnPn+PVol6a/2uk+khzmRtWxhah/FcKs6StCytm5hD14QqOdZxEdJN8HljXIVCayN/wJX+0wDiQ==", | ||||
|       "requires": { | ||||
|         "@types/node": "8.5.9", | ||||
|         "@types/node": "8.9.4", | ||||
|         "electron-download": "3.3.0", | ||||
|         "extract-zip": "1.6.5" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "@types/node": { | ||||
|           "version": "8.5.9", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.5.9.tgz", | ||||
|           "integrity": "sha512-s+c3AjymyAccTI4hcgNFK4mToH8l+hyPDhu4LIkn71lRy56FLijGu00fyLgldjM/846Pmk9N4KFUs2P8GDs0pA==" | ||||
|           "version": "8.9.4", | ||||
|           "resolved": "https://registry.npmjs.org/@types/node/-/node-8.9.4.tgz", | ||||
|           "integrity": "sha512-dSvD36qnQs78G1BPsrZFdPpvLgMW/dnvr5+nTW2csMs5TiP9MOXrjUbnMZOEwnIuBklXtn7b6TPA2Cuq07bDHA==" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
| @@ -3206,6 +3206,16 @@ | ||||
|         "electron-localshortcut": "3.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "electron-dl": { | ||||
|       "version": "1.11.0", | ||||
|       "resolved": "https://registry.npmjs.org/electron-dl/-/electron-dl-1.11.0.tgz", | ||||
|       "integrity": "sha512-iL9qHzzWOuL9bus+UT+P72SwrDQcFTV6QHqcbhwgqjCC9/K5jhdRzG0dIMB3TzYlk6rmApanPqh9DvWykwIH1Q==", | ||||
|       "requires": { | ||||
|         "ext-name": "5.0.0", | ||||
|         "pupa": "1.0.0", | ||||
|         "unused-filename": "1.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "electron-download": { | ||||
|       "version": "3.3.0", | ||||
|       "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-3.3.0.tgz", | ||||
| @@ -3325,9 +3335,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "electron-packager": { | ||||
|       "version": "10.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-10.1.1.tgz", | ||||
|       "integrity": "sha1-MWp/ossf/CYz9YBcn8IJE8vAnZQ=", | ||||
|       "version": "11.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/electron-packager/-/electron-packager-11.0.1.tgz", | ||||
|       "integrity": "sha1-wtH/nsqBEL6evIGCbiqSHATRIA4=", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "asar": "0.14.0", | ||||
| @@ -3343,13 +3353,19 @@ | ||||
|         "pify": "3.0.0", | ||||
|         "plist": "2.1.0", | ||||
|         "pruner": "0.0.7", | ||||
|         "rcedit": "0.9.0", | ||||
|         "rcedit": "1.0.0", | ||||
|         "resolve": "1.4.0", | ||||
|         "sanitize-filename": "1.6.1", | ||||
|         "semver": "5.4.1", | ||||
|         "yargs-parser": "8.1.0" | ||||
|         "yargs-parser": "9.0.2" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "camelcase": { | ||||
|           "version": "4.1.0", | ||||
|           "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", | ||||
|           "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "electron-download": { | ||||
|           "version": "4.1.0", | ||||
|           "resolved": "https://registry.npmjs.org/electron-download/-/electron-download-4.1.0.tgz", | ||||
| @@ -3437,6 +3453,12 @@ | ||||
|           "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "rcedit": { | ||||
|           "version": "1.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-1.0.0.tgz", | ||||
|           "integrity": "sha512-W7DNa34x/3OgWyDHsI172AG/Lr/lZ+PkavFkHj0QhhkBRcV9QTmRJE1tDKrWkx8XHPSBsmZkNv9OKue6pncLFQ==", | ||||
|           "dev": true | ||||
|         }, | ||||
|         "sumchecker": { | ||||
|           "version": "2.0.2", | ||||
|           "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-2.0.2.tgz", | ||||
| @@ -3456,20 +3478,29 @@ | ||||
|               } | ||||
|             } | ||||
|           } | ||||
|         }, | ||||
|         "yargs-parser": { | ||||
|           "version": "9.0.2", | ||||
|           "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-9.0.2.tgz", | ||||
|           "integrity": "sha1-nM9qQ0YP5O1Aqbto9I1DuKaMwHc=", | ||||
|           "dev": true, | ||||
|           "requires": { | ||||
|             "camelcase": "4.1.0" | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "electron-prebuilt-compile": { | ||||
|       "version": "1.8.2-beta.4", | ||||
|       "resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2-beta.4.tgz", | ||||
|       "integrity": "sha512-whVdRgFEDovWSFrAsbMXIiush6RQ8IV3XhYdL59zShck4U1eXGmdkaBCy+2tlkGmUGr0fRu+S4FpUx2ebBkRhQ==", | ||||
|       "version": "1.8.2", | ||||
|       "resolved": "https://registry.npmjs.org/electron-prebuilt-compile/-/electron-prebuilt-compile-1.8.2.tgz", | ||||
|       "integrity": "sha512-wiDVjy8S0PA/K/TUM0lw5gzZ+SmyVVGQ0qt9iFYXHJc6t8TzDXFY3DsoK37H3A7nWnkvXvoPdpJ5/h9KbTMoAw==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "babel-plugin-array-includes": "2.0.3", | ||||
|         "babel-plugin-transform-async-to-generator": "6.24.1", | ||||
|         "babel-preset-es2016-node5": "1.1.2", | ||||
|         "babel-preset-react": "6.24.1", | ||||
|         "electron": "1.8.2-beta.4", | ||||
|         "electron": "1.8.2", | ||||
|         "electron-compile": "6.4.2", | ||||
|         "electron-compilers": "5.9.0", | ||||
|         "yargs": "6.6.0" | ||||
| @@ -4353,6 +4384,23 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "ext-list": { | ||||
|       "version": "2.2.2", | ||||
|       "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", | ||||
|       "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", | ||||
|       "requires": { | ||||
|         "mime-db": "1.30.0" | ||||
|       } | ||||
|     }, | ||||
|     "ext-name": { | ||||
|       "version": "5.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", | ||||
|       "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", | ||||
|       "requires": { | ||||
|         "ext-list": "2.2.2", | ||||
|         "sort-keys-length": "1.0.1" | ||||
|       } | ||||
|     }, | ||||
|     "extend": { | ||||
|       "version": "3.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", | ||||
| @@ -5901,8 +5949,7 @@ | ||||
|     "is-plain-obj": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", | ||||
|       "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", | ||||
|       "dev": true | ||||
|       "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=" | ||||
|     }, | ||||
|     "is-png": { | ||||
|       "version": "1.1.0", | ||||
| @@ -7131,6 +7178,11 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "modify-filename": { | ||||
|       "version": "1.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/modify-filename/-/modify-filename-1.1.0.tgz", | ||||
|       "integrity": "sha1-mi3sg4Bvuy2XXyK+7IWcoms5OqE=" | ||||
|     }, | ||||
|     "moment": { | ||||
|       "version": "2.20.1", | ||||
|       "resolved": "https://registry.npmjs.org/moment/-/moment-2.20.1.tgz", | ||||
| @@ -7543,6 +7595,11 @@ | ||||
|         "mimic-fn": "1.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "open": { | ||||
|       "version": "0.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/open/-/open-0.0.5.tgz", | ||||
|       "integrity": "sha1-QsPhjslUZra/DcQvOilFw/DK2Pw=" | ||||
|     }, | ||||
|     "optimist": { | ||||
|       "version": "0.6.1", | ||||
|       "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", | ||||
| @@ -8370,6 +8427,11 @@ | ||||
|       "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", | ||||
|       "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=" | ||||
|     }, | ||||
|     "pupa": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/pupa/-/pupa-1.0.0.tgz", | ||||
|       "integrity": "sha1-mpVopa9+ZXuEYqbp1TKHQ1YM7/Y=" | ||||
|     }, | ||||
|     "q": { | ||||
|       "version": "1.5.1", | ||||
|       "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", | ||||
| @@ -8472,12 +8534,6 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "rcedit": { | ||||
|       "version": "0.9.0", | ||||
|       "resolved": "https://registry.npmjs.org/rcedit/-/rcedit-0.9.0.tgz", | ||||
|       "integrity": "sha1-ORDfVzRTmeKwMl9KUZAH+J5V7xw=", | ||||
|       "dev": true | ||||
|     }, | ||||
|     "read-all-stream": { | ||||
|       "version": "3.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/read-all-stream/-/read-all-stream-3.1.0.tgz", | ||||
| @@ -9171,11 +9227,18 @@ | ||||
|       "version": "1.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", | ||||
|       "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "is-plain-obj": "1.1.0" | ||||
|       } | ||||
|     }, | ||||
|     "sort-keys-length": { | ||||
|       "version": "1.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", | ||||
|       "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", | ||||
|       "requires": { | ||||
|         "sort-keys": "1.1.2" | ||||
|       } | ||||
|     }, | ||||
|     "source-map": { | ||||
|       "version": "0.5.7", | ||||
|       "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", | ||||
| @@ -10948,6 +11011,22 @@ | ||||
|       "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", | ||||
|       "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" | ||||
|     }, | ||||
|     "unused-filename": { | ||||
|       "version": "1.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/unused-filename/-/unused-filename-1.0.0.tgz", | ||||
|       "integrity": "sha1-00CID3GuIRXrqhMlvvBcxmhEacY=", | ||||
|       "requires": { | ||||
|         "modify-filename": "1.1.0", | ||||
|         "path-exists": "3.0.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "path-exists": { | ||||
|           "version": "3.0.0", | ||||
|           "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", | ||||
|           "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "unzip-response": { | ||||
|       "version": "2.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz", | ||||
| @@ -11694,23 +11773,6 @@ | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "yargs-parser": { | ||||
|       "version": "8.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-8.1.0.tgz", | ||||
|       "integrity": "sha512-yP+6QqN8BmrgW2ggLtTbdrOyBNSI7zBa4IykmiV5R1wl1JWNxQvWhMfMdmzIYtKU7oP3OOInY/tl2ov3BDjnJQ==", | ||||
|       "dev": true, | ||||
|       "requires": { | ||||
|         "camelcase": "4.1.0" | ||||
|       }, | ||||
|       "dependencies": { | ||||
|         "camelcase": { | ||||
|           "version": "4.1.0", | ||||
|           "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz", | ||||
|           "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0=", | ||||
|           "dev": true | ||||
|         } | ||||
|       } | ||||
|     }, | ||||
|     "yauzl": { | ||||
|       "version": "2.4.1", | ||||
|       "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.4.1.tgz", | ||||
|   | ||||
							
								
								
									
										10
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										10
									
								
								package.json
									
									
									
									
									
								
							| @@ -1,7 +1,7 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "description": "Trilium Notes", | ||||
|   "version": "0.6.0-beta", | ||||
|   "version": "0.7.0-beta", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "electron.js", | ||||
|   "repository": { | ||||
| @@ -27,8 +27,9 @@ | ||||
|     "debug": "~3.1.0", | ||||
|     "devtron": "^1.4.0", | ||||
|     "ejs": "~2.5.7", | ||||
|     "electron": "^1.8.2-beta.4", | ||||
|     "electron": "^1.8.2", | ||||
|     "electron-debug": "^1.5.0", | ||||
|     "electron-dl": "^1.11.0", | ||||
|     "electron-in-page-search": "^1.2.4", | ||||
|     "express": "~4.16.2", | ||||
|     "express-promise-wrap": "^0.2.2", | ||||
| @@ -45,6 +46,7 @@ | ||||
|     "jimp": "^0.2.28", | ||||
|     "moment": "^2.20.1", | ||||
|     "multer": "^1.3.0", | ||||
|     "open": "0.0.5", | ||||
|     "rand-token": "^0.4.0", | ||||
|     "request": "^2.83.0", | ||||
|     "request-promise": "^4.2.2", | ||||
| @@ -60,8 +62,8 @@ | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "electron-compile": "^6.4.2", | ||||
|     "electron-packager": "^10.1.1", | ||||
|     "electron-prebuilt-compile": "1.8.2-beta.4", | ||||
|     "electron-packager": "^11.0.1", | ||||
|     "electron-prebuilt-compile": "1.8.2", | ||||
|     "electron-rebuild": "^1.7.3", | ||||
|     "tape": "^4.8.0", | ||||
|     "xo": "^0.18.0" | ||||
|   | ||||
| @@ -23,6 +23,10 @@ class Note extends Entity { | ||||
|         return this.type === "code" && this.mime === "application/json"; | ||||
|     } | ||||
|  | ||||
|     isJavaScript() { | ||||
|         return this.type === "code" && this.mime === "application/javascript"; | ||||
|     } | ||||
|  | ||||
|     async getAttributes() { | ||||
|         return this.repository.getEntities("SELECT * FROM attributes WHERE noteId = ? AND isDeleted = 0", [this.noteId]); | ||||
|     } | ||||
|   | ||||
| @@ -38,6 +38,7 @@ async function getDateNoteIdForReddit(dateTimeStr, rootNoteId) { | ||||
|         redditDateNoteId = await createNote(dateNoteId, "Reddit"); | ||||
|  | ||||
|         await attributes.createAttribute(redditDateNoteId, REDDIT_DATE_ATTRIBUTE, dateStr); | ||||
|         await attributes.createAttribute(redditDateNoteId, "hide_in_autocomplete"); | ||||
|     } | ||||
|  | ||||
|     return redditDateNoteId; | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| const api = (function() { | ||||
|     const pluginButtonsEl = $("#plugin-buttons"); | ||||
|     const $pluginButtons = $("#plugin-buttons"); | ||||
|  | ||||
|     async function activateNote(notePath) { | ||||
|         await noteTree.activateNode(notePath); | ||||
| @@ -10,7 +10,7 @@ const api = (function() { | ||||
|  | ||||
|         button.attr('id', buttonId); | ||||
|  | ||||
|         pluginButtonsEl.append(button); | ||||
|         $pluginButtons.append(button); | ||||
|     } | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const contextMenu = (function() { | ||||
|     const treeEl = $("#tree"); | ||||
|     const $tree = $("#tree"); | ||||
|  | ||||
|     let clipboardIds = []; | ||||
|     let clipboardMode = null; | ||||
| @@ -93,8 +93,8 @@ const contextMenu = (function() { | ||||
|         beforeOpen: (event, ui) => { | ||||
|             const node = $.ui.fancytree.getNode(ui.target); | ||||
|             // Modify menu entries depending on node status | ||||
|             treeEl.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); | ||||
|             treeEl.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); | ||||
|             $tree.contextmenu("enableEntry", "pasteAfter", clipboardIds.length > 0); | ||||
|             $tree.contextmenu("enableEntry", "pasteInto", clipboardIds.length > 0); | ||||
|  | ||||
|             // Activate node on right-click | ||||
|             node.setActive(); | ||||
|   | ||||
| @@ -30,7 +30,8 @@ const recentNotes = (function() { | ||||
|         $dialog.dialog({ | ||||
|             modal: true, | ||||
|             width: 800, | ||||
|             height: 400 | ||||
|             height: 100, | ||||
|             position: { my: "center top+100", at: "top", of: window } | ||||
|         }); | ||||
|  | ||||
|         $searchInput.val(''); | ||||
|   | ||||
| @@ -17,26 +17,34 @@ const sqlConsole = (function() { | ||||
|             width: $(window).width(), | ||||
|             height: $(window).height(), | ||||
|             open: function() { | ||||
|                 CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||
|                 CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||
|  | ||||
|                 CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||
|  | ||||
|                 codeEditor = CodeMirror($query[0], { | ||||
|                     value: "", | ||||
|                     viewportMargin: Infinity, | ||||
|                     indentUnit: 4, | ||||
|                     highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false } | ||||
|                 }); | ||||
|  | ||||
|                 codeEditor.setOption("mode", "text/x-sqlite"); | ||||
|                 CodeMirror.autoLoadMode(codeEditor, "sql"); | ||||
|  | ||||
|                 codeEditor.focus(); | ||||
|                 initEditor(); | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async function initEditor() { | ||||
|         if (!codeEditor) { | ||||
|             await requireLibrary(CODE_MIRROR); | ||||
|  | ||||
|             CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||
|             CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||
|  | ||||
|             CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||
|  | ||||
|             codeEditor = CodeMirror($query[0], { | ||||
|                 value: "", | ||||
|                 viewportMargin: Infinity, | ||||
|                 indentUnit: 4, | ||||
|                 highlightSelectionMatches: {showToken: /\w/, annotateScrollbar: false} | ||||
|             }); | ||||
|  | ||||
|             codeEditor.setOption("mode", "text/x-sqlite"); | ||||
|             CodeMirror.autoLoadMode(codeEditor, "sql"); | ||||
|         } | ||||
|  | ||||
|         codeEditor.focus(); | ||||
|     } | ||||
|  | ||||
|     async function execute() { | ||||
|         const sqlQuery = codeEditor.getValue(); | ||||
|  | ||||
|   | ||||
| @@ -126,6 +126,10 @@ $.ui.autocomplete.filter = (array, terms) => { | ||||
|  | ||||
|         if (found) { | ||||
|             results.push(item); | ||||
|  | ||||
|             if (results.length > 100) { | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -192,4 +196,43 @@ $(document).ready(() => { | ||||
|             executeScript(script); | ||||
|         } | ||||
|     }); | ||||
| }); | ||||
|  | ||||
| if (isElectron()) { | ||||
|     require('electron').ipcRenderer.on('create-day-sub-note', async function(event, parentNoteId) { | ||||
|         // this might occur when day note had to be created | ||||
|         if (!noteTree.noteExists(parentNoteId)) { | ||||
|             await noteTree.reload(); | ||||
|         } | ||||
|  | ||||
|         await noteTree.activateNode(parentNoteId); | ||||
|  | ||||
|         setTimeout(() => { | ||||
|             const node = noteTree.getCurrentNode(); | ||||
|  | ||||
|             noteTree.createNote(node, node.data.noteId, 'into', node.data.isProtected); | ||||
|         }, 500); | ||||
|     }); | ||||
| } | ||||
|  | ||||
| function uploadAttachment() { | ||||
|     $("#file-upload").trigger('click'); | ||||
| } | ||||
|  | ||||
| $("#file-upload").change(async function() { | ||||
|     const formData = new FormData(); | ||||
|     formData.append('upload', this.files[0]); | ||||
|  | ||||
|     const resp = await $.ajax({ | ||||
|         url: baseApiUrl + 'attachments/upload/' + noteEditor.getCurrentNoteId(), | ||||
|         headers: server.getHeaders(), | ||||
|         data: formData, | ||||
|         type: 'POST', | ||||
|         contentType: false, // NEEDED, DON'T OMIT THIS | ||||
|         processData: false, // NEEDED, DON'T OMIT THIS | ||||
|     }); | ||||
|  | ||||
|     await noteTree.reload(); | ||||
|  | ||||
|     await noteTree.activateNode(resp.noteId); | ||||
| }); | ||||
| @@ -41,11 +41,11 @@ const link = (function() { | ||||
|     function goToLink(e) { | ||||
|         e.preventDefault(); | ||||
|  | ||||
|         const linkEl = $(e.target); | ||||
|         let notePath = linkEl.attr("note-path"); | ||||
|         const $link = $(e.target); | ||||
|         let notePath = $link.attr("note-path"); | ||||
|  | ||||
|         if (!notePath) { | ||||
|             const address = linkEl.attr("note-path") ? linkEl.attr("note-path") : linkEl.attr('href'); | ||||
|             const address = $link.attr("note-path") ? $link.attr("note-path") : $link.attr('href'); | ||||
|  | ||||
|             if (!address) { | ||||
|                 return; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const messaging = (function() { | ||||
|     const changesToPushCountEl = $("#changes-to-push-count"); | ||||
|     const $changesToPushCount = $("#changes-to-push-count"); | ||||
|  | ||||
|     function logError(message) { | ||||
|         console.log(now(), message); // needs to be separate from .trace() | ||||
| @@ -52,7 +52,7 @@ const messaging = (function() { | ||||
|             // we don't detect image changes here since images themselves are immutable and references should be | ||||
|             // updated in note detail as well | ||||
|  | ||||
|             changesToPushCountEl.html(message.changesToPushCount); | ||||
|             $changesToPushCount.html(message.changesToPushCount); | ||||
|         } | ||||
|         else if (message.type === 'sync-hash-check-failed') { | ||||
|             showError("Sync check failed!", 60000); | ||||
|   | ||||
| @@ -1,16 +1,24 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const noteEditor = (function() { | ||||
|     const noteTitleEl = $("#note-title"); | ||||
|     const noteDetailEl = $('#note-detail'); | ||||
|     const noteDetailCodeEl = $('#note-detail-code'); | ||||
|     const noteDetailRenderEl = $('#note-detail-render'); | ||||
|     const protectButton = $("#protect-button"); | ||||
|     const unprotectButton = $("#unprotect-button"); | ||||
|     const noteDetailWrapperEl = $("#note-detail-wrapper"); | ||||
|     const noteIdDisplayEl = $("#note-id-display"); | ||||
|     const attributeListEl = $("#attribute-list"); | ||||
|     const attributeListInnerEl = $("#attribute-list-inner"); | ||||
|     const $noteTitle = $("#note-title"); | ||||
|  | ||||
|     const $noteDetail = $('#note-detail'); | ||||
|     const $noteDetailCode = $('#note-detail-code'); | ||||
|     const $noteDetailRender = $('#note-detail-render'); | ||||
|     const $noteDetailAttachment = $('#note-detail-attachment'); | ||||
|  | ||||
|     const $protectButton = $("#protect-button"); | ||||
|     const $unprotectButton = $("#unprotect-button"); | ||||
|     const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||
|     const $noteIdDisplay = $("#note-id-display"); | ||||
|     const $attributeList = $("#attribute-list"); | ||||
|     const $attributeListInner = $("#attribute-list-inner"); | ||||
|     const $attachmentFileName = $("#attachment-filename"); | ||||
|     const $attachmentFileType = $("#attachment-filetype"); | ||||
|     const $attachmentFileSize = $("#attachment-filesize"); | ||||
|     const $attachmentDownload = $("#attachment-download"); | ||||
|     const $attachmentOpen = $("#attachment-open"); | ||||
|  | ||||
|     let editor = null; | ||||
|     let codeEditor = null; | ||||
| @@ -80,14 +88,14 @@ const noteEditor = (function() { | ||||
|         else if (note.detail.type === 'code') { | ||||
|             note.detail.content = codeEditor.getValue(); | ||||
|         } | ||||
|         else if (note.detail.type === 'render') { | ||||
|         else if (note.detail.type === 'render' || note.detail.type === 'file') { | ||||
|             // nothing | ||||
|         } | ||||
|         else { | ||||
|             throwError("Unrecognized type: " + note.detail.type); | ||||
|         } | ||||
|  | ||||
|         const title = noteTitleEl.val(); | ||||
|         const title = $noteTitle.val(); | ||||
|  | ||||
|         note.detail.title = title; | ||||
|  | ||||
| @@ -105,9 +113,9 @@ const noteEditor = (function() { | ||||
|     function setNoteBackgroundIfProtected(note) { | ||||
|         const isProtected = !!note.detail.isProtected; | ||||
|  | ||||
|         noteDetailWrapperEl.toggleClass("protected", isProtected); | ||||
|         protectButton.toggle(!isProtected); | ||||
|         unprotectButton.toggle(isProtected); | ||||
|         $noteDetailWrapper.toggleClass("protected", isProtected); | ||||
|         $protectButton.toggle(!isProtected); | ||||
|         $unprotectButton.toggle(isProtected); | ||||
|     } | ||||
|  | ||||
|     let isNewNoteCreated = false; | ||||
| @@ -116,16 +124,66 @@ const noteEditor = (function() { | ||||
|         isNewNoteCreated = true; | ||||
|     } | ||||
|  | ||||
|     async function setContent(content) { | ||||
|         if (currentNote.detail.type === 'text') { | ||||
|             if (!editor) { | ||||
|                 await requireLibrary(CKEDITOR); | ||||
|  | ||||
|                 editor = await BalloonEditor.create($noteDetail[0], {}); | ||||
|  | ||||
|                 editor.document.on('change', noteChanged); | ||||
|             } | ||||
|  | ||||
|             // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 | ||||
|             editor.setData(content ? content : "<p></p>"); | ||||
|  | ||||
|             $noteDetail.show(); | ||||
|         } | ||||
|         else if (currentNote.detail.type === 'code') { | ||||
|             if (!codeEditor) { | ||||
|                 await requireLibrary(CODE_MIRROR); | ||||
|  | ||||
|                 CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||
|                 CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||
|  | ||||
|                 CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||
|  | ||||
|                 codeEditor = CodeMirror($("#note-detail-code")[0], { | ||||
|                     value: "", | ||||
|                     viewportMargin: Infinity, | ||||
|                     indentUnit: 4, | ||||
|                     matchBrackets: true, | ||||
|                     matchTags: { bothTags: true }, | ||||
|                     highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false } | ||||
|                 }); | ||||
|  | ||||
|                 codeEditor.on('change', noteChanged); | ||||
|             } | ||||
|  | ||||
|             $noteDetailCode.show(); | ||||
|  | ||||
|             // this needs to happen after the element is shown, otherwise the editor won't be refresheds | ||||
|             codeEditor.setValue(content); | ||||
|  | ||||
|             const info = CodeMirror.findModeByMIME(currentNote.detail.mime); | ||||
|  | ||||
|             if (info) { | ||||
|                 codeEditor.setOption("mode", info.mime); | ||||
|                 CodeMirror.autoLoadMode(codeEditor, info.mode); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     async function loadNoteToEditor(noteId) { | ||||
|         currentNote = await loadNote(noteId); | ||||
|  | ||||
|         if (isNewNoteCreated) { | ||||
|             isNewNoteCreated = false; | ||||
|  | ||||
|             noteTitleEl.focus().select(); | ||||
|             $noteTitle.focus().select(); | ||||
|         } | ||||
|  | ||||
|         noteIdDisplayEl.html(noteId); | ||||
|         $noteIdDisplay.html(noteId); | ||||
|  | ||||
|         await protected_session.ensureProtectedSession(currentNote.detail.isProtected, false); | ||||
|  | ||||
| @@ -137,49 +195,36 @@ const noteEditor = (function() { | ||||
|         // to login, but we chose instead to come to another node - at that point the dialog is still visible and this will close it. | ||||
|         protected_session.ensureDialogIsClosed(); | ||||
|  | ||||
|         noteDetailWrapperEl.show(); | ||||
|         $noteDetailWrapper.show(); | ||||
|  | ||||
|         noteChangeDisabled = true; | ||||
|  | ||||
|         noteTitleEl.val(currentNote.detail.title); | ||||
|         $noteTitle.val(currentNote.detail.title); | ||||
|  | ||||
|         noteType.setNoteType(currentNote.detail.type); | ||||
|         noteType.setNoteMime(currentNote.detail.mime); | ||||
|  | ||||
|         if (currentNote.detail.type === 'text') { | ||||
|             // temporary workaround for https://github.com/ckeditor/ckeditor5-enter/issues/49 | ||||
|             editor.setData(currentNote.detail.content ? currentNote.detail.content : "<p></p>"); | ||||
|         $noteDetail.hide(); | ||||
|         $noteDetailCode.hide(); | ||||
|         $noteDetailRender.html('').hide(); | ||||
|         $noteDetailAttachment.hide(); | ||||
|  | ||||
|             noteDetailEl.show(); | ||||
|             noteDetailCodeEl.hide(); | ||||
|             noteDetailRenderEl.html('').hide(); | ||||
|         } | ||||
|         else if (currentNote.detail.type === 'code') { | ||||
|             noteDetailEl.hide(); | ||||
|             noteDetailCodeEl.show(); | ||||
|             noteDetailRenderEl.html('').hide(); | ||||
|  | ||||
|             // this needs to happen after the element is shown, otherwise the editor won't be refresheds | ||||
|             codeEditor.setValue(currentNote.detail.content); | ||||
|  | ||||
|             const info = CodeMirror.findModeByMIME(currentNote.detail.mime); | ||||
|  | ||||
|             if (info) { | ||||
|                 codeEditor.setOption("mode", info.mime); | ||||
|                 CodeMirror.autoLoadMode(codeEditor, info.mode); | ||||
|             } | ||||
|         } | ||||
|         else if (currentNote.detail.type === 'render') { | ||||
|             noteDetailEl.hide(); | ||||
|             noteDetailCodeEl.hide(); | ||||
|             noteDetailRenderEl.html('').show(); | ||||
|         if (currentNote.detail.type === 'render') { | ||||
|             $noteDetailRender.show(); | ||||
|  | ||||
|             const subTree = await server.get('script/subtree/' + getCurrentNoteId()); | ||||
|  | ||||
|             noteDetailRenderEl.html(subTree); | ||||
|             $noteDetailRender.html(subTree); | ||||
|         } | ||||
|         else if (currentNote.detail.type === 'file') { | ||||
|             $noteDetailAttachment.show(); | ||||
|  | ||||
|             $attachmentFileName.text(currentNote.attributes.original_file_name); | ||||
|             $attachmentFileSize.text(currentNote.attributes.file_size + " bytes"); | ||||
|             $attachmentFileType.text(currentNote.detail.mime); | ||||
|         } | ||||
|         else { | ||||
|             throwError("Unrecognized type " + currentNote.detail.type); | ||||
|             await setContent(currentNote.detail.content); | ||||
|         } | ||||
|  | ||||
|         noteChangeDisabled = false; | ||||
| @@ -188,7 +233,7 @@ const noteEditor = (function() { | ||||
|         noteTree.setNoteTreeBackgroundBasedOnProtectedStatus(noteId); | ||||
|  | ||||
|         // after loading new note make sure editor is scrolled to the top | ||||
|         noteDetailWrapperEl.scrollTop(0); | ||||
|         $noteDetailWrapper.scrollTop(0); | ||||
|  | ||||
|         loadAttributeList(); | ||||
|     } | ||||
| @@ -198,17 +243,17 @@ const noteEditor = (function() { | ||||
|  | ||||
|         const attributes = await server.get('notes/' + noteId + '/attributes'); | ||||
|  | ||||
|         attributeListInnerEl.html(''); | ||||
|         $attributeListInner.html(''); | ||||
|  | ||||
|         if (attributes.length > 0) { | ||||
|             for (const attr of attributes) { | ||||
|                 attributeListInnerEl.append(formatAttribute(attr) + " "); | ||||
|                 $attributeListInner.append(formatAttribute(attr) + " "); | ||||
|             } | ||||
|  | ||||
|             attributeListEl.show(); | ||||
|             $attributeList.show(); | ||||
|         } | ||||
|         else { | ||||
|             attributeListEl.hide(); | ||||
|             $attributeList.hide(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @@ -224,12 +269,12 @@ const noteEditor = (function() { | ||||
|         const note = getCurrentNote(); | ||||
|  | ||||
|         if (note.detail.type === 'text') { | ||||
|             noteDetailEl.focus(); | ||||
|             $noteDetail.focus(); | ||||
|         } | ||||
|         else if (note.detail.type === 'code') { | ||||
|             codeEditor.focus(); | ||||
|         } | ||||
|         else if (note.detail.type === 'render') { | ||||
|         else if (note.detail.type === 'render' || note.detail.type === 'file') { | ||||
|             // do nothing | ||||
|         } | ||||
|         else { | ||||
| @@ -254,45 +299,49 @@ const noteEditor = (function() { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     $attachmentDownload.click(() => { | ||||
|         if (isElectron()) { | ||||
|             const remote = require('electron').remote; | ||||
|  | ||||
|             remote.getCurrentWebContents().downloadURL(getAttachmentUrl()); | ||||
|         } | ||||
|         else { | ||||
|             window.location.href = getAttachmentUrl(); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     $attachmentOpen.click(() => { | ||||
|         if (isElectron()) { | ||||
|             const open = require("open"); | ||||
|  | ||||
|             open(getAttachmentUrl()); | ||||
|         } | ||||
|         else { | ||||
|             window.location.href = getAttachmentUrl(); | ||||
|         } | ||||
|     }); | ||||
|  | ||||
|     function getAttachmentUrl() { | ||||
|         // electron needs absolute URL so we extract current host, port, protocol | ||||
|         const url = new URL(window.location.href); | ||||
|         const host = url.protocol + "//" + url.hostname + ":" + url.port; | ||||
|  | ||||
|         const downloadUrl = "/api/attachments/download/" + getCurrentNoteId(); | ||||
|  | ||||
|         return host + downloadUrl; | ||||
|     } | ||||
|  | ||||
|     $(document).ready(() => { | ||||
|         noteTitleEl.on('input', () => { | ||||
|         $noteTitle.on('input', () => { | ||||
|             noteChanged(); | ||||
|  | ||||
|             const title = noteTitleEl.val(); | ||||
|             const title = $noteTitle.val(); | ||||
|  | ||||
|             noteTree.setNoteTitle(getCurrentNoteId(), title); | ||||
|         }); | ||||
|  | ||||
|         BalloonEditor | ||||
|             .create(document.querySelector('#note-detail'), { | ||||
|             }) | ||||
|             .then(edit => { | ||||
|                 editor = edit; | ||||
|  | ||||
|                 editor.document.on('change', noteChanged); | ||||
|             }) | ||||
|             .catch(error => { | ||||
|                 console.error(error); | ||||
|             }); | ||||
|  | ||||
|         CodeMirror.keyMap.default["Shift-Tab"] = "indentLess"; | ||||
|         CodeMirror.keyMap.default["Tab"] = "indentMore"; | ||||
|  | ||||
|         CodeMirror.modeURL = 'libraries/codemirror/mode/%N/%N.js'; | ||||
|  | ||||
|         codeEditor = CodeMirror($("#note-detail-code")[0], { | ||||
|             value: "", | ||||
|             viewportMargin: Infinity, | ||||
|             indentUnit: 4, | ||||
|             matchBrackets: true, | ||||
|             matchTags: { bothTags: true }, | ||||
|             highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: false } | ||||
|         }); | ||||
|  | ||||
|         codeEditor.on('change', noteChanged); | ||||
|  | ||||
|         // so that tab jumps from note title (which has tabindex 1) | ||||
|         noteDetailEl.attr("tabindex", 2); | ||||
|         $noteDetail.attr("tabindex", 2); | ||||
|     }); | ||||
|  | ||||
|     $(document).bind('keydown', "ctrl+return", executeCurrentNote); | ||||
| @@ -314,6 +363,7 @@ const noteEditor = (function() { | ||||
|         getEditor, | ||||
|         focus, | ||||
|         executeCurrentNote, | ||||
|         loadAttributeList | ||||
|         loadAttributeList, | ||||
|         setContent | ||||
|     }; | ||||
| })(); | ||||
| @@ -1,9 +1,9 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const noteTree = (function() { | ||||
|     const treeEl = $("#tree"); | ||||
|     const parentListEl = $("#parent-list"); | ||||
|     const parentListListEl = $("#parent-list-inner"); | ||||
|     const $tree = $("#tree"); | ||||
|     const $parentList = $("#parent-list"); | ||||
|     const $parentListList = $("#parent-list-inner"); | ||||
|  | ||||
|     let startNotePath = null; | ||||
|     let notesTreeMap = {}; | ||||
| @@ -14,6 +14,8 @@ const noteTree = (function() { | ||||
|     let parentChildToNoteTreeId = {}; | ||||
|     let noteIdToTitle = {}; | ||||
|  | ||||
|     let hiddenInAutocomplete = {}; | ||||
|  | ||||
|     function getNoteTreeId(parentNoteId, childNoteId) { | ||||
|         assertArguments(parentNoteId, childNoteId); | ||||
|  | ||||
| @@ -50,7 +52,7 @@ const noteTree = (function() { | ||||
|  | ||||
|     // note that if you want to access data like noteId or isProtected, you need to go into "data" property | ||||
|     function getCurrentNode() { | ||||
|         return treeEl.fancytree("getActiveNode"); | ||||
|         return $tree.fancytree("getActiveNode"); | ||||
|     } | ||||
|  | ||||
|     function getCurrentNotePath() { | ||||
| @@ -312,11 +314,11 @@ const noteTree = (function() { | ||||
|         } | ||||
|  | ||||
|         if (parents.length <= 1) { | ||||
|             parentListEl.hide(); | ||||
|             $parentList.hide(); | ||||
|         } | ||||
|         else { | ||||
|             parentListEl.show(); | ||||
|             parentListListEl.empty(); | ||||
|             $parentList.show(); | ||||
|             $parentListList.empty(); | ||||
|  | ||||
|             for (const parentNoteId of parents) { | ||||
|                 const parentNotePath = getSomeNotePath(parentNoteId); | ||||
| @@ -333,7 +335,7 @@ const noteTree = (function() { | ||||
|                     item = link.createNoteLink(notePath, title); | ||||
|                 } | ||||
|  | ||||
|                 parentListListEl.append($("<li/>").append(item)); | ||||
|                 $parentListList.append($("<li/>").append(item)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -541,7 +543,7 @@ const noteTree = (function() { | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         treeEl.fancytree({ | ||||
|         $tree.fancytree({ | ||||
|             autoScroll: true, | ||||
|             keyboard: false, // we takover keyboard handling in the hotkeys plugin | ||||
|             extensions: ["hotkeys", "filter", "dnd", "clones"], | ||||
| @@ -622,11 +624,11 @@ const noteTree = (function() { | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         treeEl.contextmenu(contextMenu.contextMenuSettings); | ||||
|         $tree.contextmenu(contextMenu.contextMenuSettings); | ||||
|     } | ||||
|  | ||||
|     function getTree() { | ||||
|         return treeEl.fancytree('getTree'); | ||||
|         return $tree.fancytree('getTree'); | ||||
|     } | ||||
|  | ||||
|     async function reload() { | ||||
| @@ -640,23 +642,28 @@ const noteTree = (function() { | ||||
|         return document.location.hash.substr(1); // strip initial # | ||||
|     } | ||||
|  | ||||
|     function loadTree() { | ||||
|         return server.get('tree').then(resp => { | ||||
|             startNotePath = resp.start_note_path; | ||||
|     async function loadTree() { | ||||
|         const resp = await server.get('tree'); | ||||
|         startNotePath = resp.start_note_path; | ||||
|  | ||||
|             if (document.location.hash) { | ||||
|                 startNotePath = getNotePathFromAddress(); | ||||
|             } | ||||
|         if (document.location.hash) { | ||||
|             startNotePath = getNotePathFromAddress(); | ||||
|         } | ||||
|  | ||||
|             return prepareNoteTree(resp.notes); | ||||
|         }); | ||||
|         hiddenInAutocomplete = {}; | ||||
|  | ||||
|         for (const noteId of resp.hiddenInAutocomplete) { | ||||
|             hiddenInAutocomplete[noteId] = true; | ||||
|         } | ||||
|  | ||||
|         return prepareNoteTree(resp.notes); | ||||
|     } | ||||
|  | ||||
|     $(() => loadTree().then(noteTree => initFancyTree(noteTree))); | ||||
|  | ||||
|     function collapseTree(node = null) { | ||||
|         if (!node) { | ||||
|             node = treeEl.fancytree("getRootNode"); | ||||
|             node = $tree.fancytree("getRootNode"); | ||||
|         } | ||||
|  | ||||
|         node.setExpanded(false); | ||||
| @@ -706,6 +713,10 @@ const noteTree = (function() { | ||||
|         const autocompleteItems = []; | ||||
|  | ||||
|         for (const childNoteId of parentToChildren[parentNoteId]) { | ||||
|             if (hiddenInAutocomplete[childNoteId]) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             const childNotePath = (notePath ? (notePath + '/') : '') + childNoteId; | ||||
|             const childTitlePath = (titlePath ? (titlePath + ' / ') : '') + getNoteTitle(childNoteId, parentNoteId); | ||||
|  | ||||
| @@ -733,7 +744,7 @@ const noteTree = (function() { | ||||
|     } | ||||
|  | ||||
|     async function createNewTopLevelNote() { | ||||
|         const rootNode = treeEl.fancytree("getRootNode"); | ||||
|         const rootNode = $tree.fancytree("getRootNode"); | ||||
|  | ||||
|         await createNote(rootNode, "root", "into"); | ||||
|     } | ||||
| @@ -775,7 +786,7 @@ const noteTree = (function() { | ||||
|         }; | ||||
|  | ||||
|         if (target === 'after') { | ||||
|             node.appendSibling(newNode).setActive(true); | ||||
|             await node.appendSibling(newNode).setActive(true); | ||||
|         } | ||||
|         else if (target === 'into') { | ||||
|             if (!node.getChildren() && node.isFolder()) { | ||||
| @@ -785,7 +796,7 @@ const noteTree = (function() { | ||||
|                 node.addChildren(newNode); | ||||
|             } | ||||
|  | ||||
|             node.getLastChild().setActive(true); | ||||
|             await node.getLastChild().setActive(true); | ||||
|  | ||||
|             node.folder = true; | ||||
|             node.renderTitle(); | ||||
| @@ -794,6 +805,8 @@ const noteTree = (function() { | ||||
|             throwError("Unrecognized target: " + target); | ||||
|         } | ||||
|  | ||||
|         clearSelectedNodes(); // to unmark previously active node | ||||
|  | ||||
|         showMessage("Created!"); | ||||
|     } | ||||
|  | ||||
| @@ -803,6 +816,10 @@ const noteTree = (function() { | ||||
|         await reload(); | ||||
|     } | ||||
|  | ||||
|     function noteExists(noteId) { | ||||
|         return !!childToParents[noteId]; | ||||
|     } | ||||
|  | ||||
|     $(document).bind('keydown', 'ctrl+o', e => { | ||||
|         const node = getCurrentNode(); | ||||
|         const parentNoteId = node.data.parentNoteId; | ||||
| @@ -876,6 +893,7 @@ const noteTree = (function() { | ||||
|         removeParentChildRelation, | ||||
|         setParentChildRelation, | ||||
|         getSelectedNodes, | ||||
|         sortAlphabetically | ||||
|         sortAlphabetically, | ||||
|         noteExists | ||||
|     }; | ||||
| })(); | ||||
| @@ -1,7 +1,7 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const noteType = (function() { | ||||
|     const executeScriptButton = $("#execute-script-button"); | ||||
|     const $executeScriptButton = $("#execute-script-button"); | ||||
|     const noteTypeModel = new NoteTypeModel(); | ||||
|  | ||||
|     function NoteTypeModel() { | ||||
| @@ -65,11 +65,18 @@ const noteType = (function() { | ||||
|             else if (type === 'render') { | ||||
|                 return 'Render HTML note'; | ||||
|             } | ||||
|             else if (type === 'file') { | ||||
|                 return 'Attachment'; | ||||
|             } | ||||
|             else { | ||||
|                 throwError('Unrecognized type: ' + type); | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         this.isDisabled = function() { | ||||
|             return self.type() === "file"; | ||||
|         }; | ||||
|  | ||||
|         async function save() { | ||||
|             const note = noteEditor.getCurrentNote(); | ||||
|  | ||||
| @@ -114,7 +121,7 @@ const noteType = (function() { | ||||
|         }; | ||||
|  | ||||
|         this.updateExecuteScriptButtonVisibility = function() { | ||||
|             executeScriptButton.toggle(self.mime() === 'application/javascript'); | ||||
|             $executeScriptButton.toggle(self.mime() === 'application/javascript'); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const protected_session = (function() { | ||||
|     const dialogEl = $("#protected-session-password-dialog"); | ||||
|     const passwordFormEl = $("#protected-session-password-form"); | ||||
|     const passwordEl = $("#protected-session-password"); | ||||
|     const noteDetailWrapperEl = $("#note-detail-wrapper"); | ||||
|     const $dialog = $("#protected-session-password-dialog"); | ||||
|     const $passwordForm = $("#protected-session-password-form"); | ||||
|     const $password = $("#protected-session-password"); | ||||
|     const $noteDetailWrapper = $("#note-detail-wrapper"); | ||||
|  | ||||
|     let protectedSessionDeferred = null; | ||||
|     let lastProtectedSessionOperationDate = null; | ||||
| @@ -25,9 +25,9 @@ const protected_session = (function() { | ||||
|         if (requireProtectedSession && !isProtectedSessionAvailable()) { | ||||
|             protectedSessionDeferred = dfd; | ||||
|  | ||||
|             noteDetailWrapperEl.hide(); | ||||
|             $noteDetailWrapper.hide(); | ||||
|  | ||||
|             dialogEl.dialog({ | ||||
|             $dialog.dialog({ | ||||
|                 modal: modal, | ||||
|                 width: 400, | ||||
|                 open: () => { | ||||
| @@ -46,8 +46,8 @@ const protected_session = (function() { | ||||
|     } | ||||
|  | ||||
|     async function setupProtectedSession() { | ||||
|         const password = passwordEl.val(); | ||||
|         passwordEl.val(""); | ||||
|         const password = $password.val(); | ||||
|         $password.val(""); | ||||
|  | ||||
|         const response = await enterProtectedSession(password); | ||||
|  | ||||
| @@ -58,15 +58,15 @@ const protected_session = (function() { | ||||
|  | ||||
|         protectedSessionId = response.protectedSessionId; | ||||
|  | ||||
|         dialogEl.dialog("close"); | ||||
|         $dialog.dialog("close"); | ||||
|  | ||||
|         noteEditor.reload(); | ||||
|         noteTree.reload(); | ||||
|  | ||||
|         if (protectedSessionDeferred !== null) { | ||||
|             ensureDialogIsClosed(dialogEl, passwordEl); | ||||
|             ensureDialogIsClosed($dialog, $password); | ||||
|  | ||||
|             noteDetailWrapperEl.show(); | ||||
|             $noteDetailWrapper.show(); | ||||
|  | ||||
|             protectedSessionDeferred.resolve(); | ||||
|  | ||||
| @@ -77,11 +77,11 @@ const protected_session = (function() { | ||||
|     function ensureDialogIsClosed() { | ||||
|         // this may fal if the dialog has not been previously opened | ||||
|         try { | ||||
|             dialogEl.dialog('close'); | ||||
|             $dialog.dialog('close'); | ||||
|         } | ||||
|         catch (e) {} | ||||
|  | ||||
|         passwordEl.val(''); | ||||
|         $password.val(''); | ||||
|     } | ||||
|  | ||||
|     async function enterProtectedSession(password) { | ||||
| @@ -155,7 +155,7 @@ const protected_session = (function() { | ||||
|         noteEditor.reload(); | ||||
|     } | ||||
|  | ||||
|     passwordFormEl.submit(() => { | ||||
|     $passwordForm.submit(() => { | ||||
|         setupProtectedSession(); | ||||
|  | ||||
|         return false; | ||||
|   | ||||
| @@ -1,40 +1,40 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const searchTree = (function() { | ||||
|     const treeEl = $("#tree"); | ||||
|     const searchInputEl = $("input[name='search-text']"); | ||||
|     const resetSearchButton = $("button#reset-search-button"); | ||||
|     const searchBoxEl = $("#search-box"); | ||||
|     const $tree = $("#tree"); | ||||
|     const $searchInput = $("input[name='search-text']"); | ||||
|     const $resetSearchButton = $("button#reset-search-button"); | ||||
|     const $searchBox = $("#search-box"); | ||||
|  | ||||
|     resetSearchButton.click(resetSearch); | ||||
|     $resetSearchButton.click(resetSearch); | ||||
|  | ||||
|     function toggleSearch() { | ||||
|         if (searchBoxEl.is(":hidden")) { | ||||
|             searchBoxEl.show(); | ||||
|             searchInputEl.focus(); | ||||
|         if ($searchBox.is(":hidden")) { | ||||
|             $searchBox.show(); | ||||
|             $searchInput.focus(); | ||||
|         } | ||||
|         else { | ||||
|             resetSearch(); | ||||
|  | ||||
|             searchBoxEl.hide(); | ||||
|             $searchBox.hide(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     function resetSearch() { | ||||
|         searchInputEl.val(""); | ||||
|         $searchInput.val(""); | ||||
|  | ||||
|         getTree().clearFilter(); | ||||
|     } | ||||
|  | ||||
|     function getTree() { | ||||
|         return treeEl.fancytree('getTree'); | ||||
|         return $tree.fancytree('getTree'); | ||||
|     } | ||||
|  | ||||
|     searchInputEl.keyup(async e => { | ||||
|         const searchText = searchInputEl.val(); | ||||
|     $searchInput.keyup(async e => { | ||||
|         const searchText = $searchInput.val(); | ||||
|  | ||||
|         if (e && e.which === $.ui.keyCode.ESCAPE || $.trim(searchText) === "") { | ||||
|             resetSearchButton.click(); | ||||
|             $resetSearchButton.click(); | ||||
|             return; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -31,12 +31,23 @@ const server = (function() { | ||||
|         return await call('DELETE', url); | ||||
|     } | ||||
|  | ||||
|     function prepareParams(params) { | ||||
|         return params.map(p => { | ||||
|             if (typeof p === "function") { | ||||
|                 return "!@#Function: " + p.toString(); | ||||
|             } | ||||
|             else { | ||||
|                 return p; | ||||
|             } | ||||
|         }); | ||||
|     } | ||||
|  | ||||
|     async function exec(params, script) { | ||||
|         if (typeof script === "function") { | ||||
|             script = script.toString(); | ||||
|         } | ||||
|  | ||||
|         const ret = await post('script/exec', { script: script, params: params }); | ||||
|         const ret = await post('script/exec', { script: script, params: prepareParams(params) }); | ||||
|  | ||||
|         return ret.executionResult; | ||||
|     } | ||||
| @@ -105,6 +116,7 @@ const server = (function() { | ||||
|         put, | ||||
|         remove, | ||||
|         exec, | ||||
|         ajax, | ||||
|         // don't remove, used from CKEditor image upload! | ||||
|         getHeaders | ||||
|     } | ||||
|   | ||||
| @@ -1,14 +1,14 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const treeUtils = (function() { | ||||
|     const treeEl = $("#tree"); | ||||
|     const $tree = $("#tree"); | ||||
|  | ||||
|     function getParentProtectedStatus(node) { | ||||
|         return isTopLevelNode(node) ? 0 : node.getParent().data.isProtected; | ||||
|     } | ||||
|  | ||||
|     function getNodeByKey(key) { | ||||
|         return treeEl.fancytree('getNodeByKey', key); | ||||
|         return $tree.fancytree('getNodeByKey', key); | ||||
|     } | ||||
|  | ||||
|     function getNoteIdFromNotePath(notePath) { | ||||
|   | ||||
| @@ -116,8 +116,7 @@ async function stopWatch(what, func) { | ||||
| } | ||||
|  | ||||
| function executeScript(script) { | ||||
|     // last \r\n is necessary if script contains line comment on its last line | ||||
|     eval("(async function() {" + script + "\r\n})()"); | ||||
|     eval(script); | ||||
| } | ||||
|  | ||||
| function formatValueWithWhitespace(val) { | ||||
| @@ -132,4 +131,57 @@ function formatAttribute(attr) { | ||||
|     } | ||||
|  | ||||
|     return str; | ||||
| } | ||||
|  | ||||
| const CKEDITOR = { "js": ["libraries/ckeditor/ckeditor.js"] }; | ||||
|  | ||||
| const CODE_MIRROR = { | ||||
|     js: [ | ||||
|         "libraries/codemirror/codemirror.js", | ||||
|         "libraries/codemirror/addon/mode/loadmode.js", | ||||
|         "libraries/codemirror/addon/fold/xml-fold.js", | ||||
|         "libraries/codemirror/addon/edit/matchbrackets.js", | ||||
|         "libraries/codemirror/addon/edit/matchtags.js", | ||||
|         "libraries/codemirror/addon/search/match-highlighter.js", | ||||
|         "libraries/codemirror/mode/meta.js" | ||||
|     ], | ||||
|     css: [ | ||||
|         "libraries/codemirror/codemirror.css" | ||||
|     ] | ||||
| }; | ||||
|  | ||||
| async function requireLibrary(library) { | ||||
|     if (library.css) { | ||||
|         library.css.map(cssUrl => requireCss(cssUrl)); | ||||
|     } | ||||
|  | ||||
|     if (library.js) { | ||||
|         for (const scriptUrl of library.js) { | ||||
|             await requireScript(scriptUrl); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function requireScript(url) { | ||||
|     const scripts = Array | ||||
|         .from(document.querySelectorAll('script')) | ||||
|         .map(scr => scr.src); | ||||
|  | ||||
|     if (!scripts.includes(url)) { | ||||
|         return $.ajax({ | ||||
|             url: url, | ||||
|             dataType: "script", | ||||
|             cache: true | ||||
|         }) | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function requireCss(url) { | ||||
|     const css = Array | ||||
|         .from(document.querySelectorAll('link')) | ||||
|         .map(scr => scr.href); | ||||
|  | ||||
|     if (!css.includes(url)) { | ||||
|         $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', url)); | ||||
|     } | ||||
| } | ||||
| @@ -270,6 +270,7 @@ div.ui-tooltip { | ||||
|     margin-right: 5px; | ||||
| } | ||||
|  | ||||
| .recent-notes-autocomplete { | ||||
|     border: 0 !important; | ||||
| #attachment-table th, #attachment-table td { | ||||
|     padding: 10px; | ||||
|     font-size: large; | ||||
| } | ||||
							
								
								
									
										61
									
								
								src/routes/api/attachments.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/routes/api/attachments.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| "use strict"; | ||||
|  | ||||
| const express = require('express'); | ||||
| const router = express.Router(); | ||||
| const sql = require('../../services/sql'); | ||||
| const auth = require('../../services/auth'); | ||||
| const notes = require('../../services/notes'); | ||||
| const attributes = require('../../services/attributes'); | ||||
| const multer = require('multer')(); | ||||
| const wrap = require('express-promise-wrap').wrap; | ||||
|  | ||||
| router.post('/upload/:parentNoteId', auth.checkApiAuthOrElectron, multer.single('upload'), wrap(async (req, res, next) => { | ||||
|     const sourceId = req.headers.source_id; | ||||
|     const parentNoteId = req.params.parentNoteId; | ||||
|     const file = req.file; | ||||
|     const originalName = file.originalname; | ||||
|     const size = file.size; | ||||
|  | ||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [parentNoteId]); | ||||
|  | ||||
|     if (!note) { | ||||
|         return res.status(404).send(`Note ${parentNoteId} doesn't exist.`); | ||||
|     } | ||||
|  | ||||
|     await sql.doInTransaction(async () => { | ||||
|         const noteId = (await notes.createNewNote(parentNoteId, { | ||||
|             title: originalName, | ||||
|             content: file.buffer, | ||||
|             target: 'into', | ||||
|             isProtected: false, | ||||
|             type: 'file', | ||||
|             mime: file.mimetype | ||||
|         }, req, sourceId)).noteId; | ||||
|  | ||||
|         await attributes.createAttribute(noteId, "original_file_name", originalName, sourceId); | ||||
|         await attributes.createAttribute(noteId, "file_size", size, sourceId); | ||||
|  | ||||
|         res.send({ | ||||
|             noteId: noteId | ||||
|         }); | ||||
|     }); | ||||
| })); | ||||
|  | ||||
| router.get('/download/:noteId', auth.checkApiAuthOrElectron, wrap(async (req, res, next) => { | ||||
|     const noteId = req.params.noteId; | ||||
|     const note = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||
|  | ||||
|     if (!note) { | ||||
|         return res.status(404).send(`Note ${parentNoteId} doesn't exist.`); | ||||
|     } | ||||
|  | ||||
|     const attributeMap = await attributes.getNoteAttributeMap(noteId); | ||||
|     const fileName = attributeMap.original_file_name ? attributeMap.original_file_name : note.title; | ||||
|  | ||||
|     res.setHeader('Content-Disposition', 'attachment; filename=' + fileName); | ||||
|     res.setHeader('Content-Type', note.mime); | ||||
|  | ||||
|     res.send(note.content); | ||||
| })); | ||||
|  | ||||
| module.exports = router; | ||||
| @@ -5,6 +5,7 @@ const router = express.Router(); | ||||
| const auth = require('../../services/auth'); | ||||
| const sql = require('../../services/sql'); | ||||
| const notes = require('../../services/notes'); | ||||
| const attributes = require('../../services/attributes'); | ||||
| const log = require('../../services/log'); | ||||
| const utils = require('../../services/utils'); | ||||
| const protected_session = require('../../services/protected_session'); | ||||
| @@ -25,8 +26,19 @@ router.get('/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|  | ||||
|     protected_session.decryptNote(req, detail); | ||||
|  | ||||
|     let attributeMap = null; | ||||
|  | ||||
|     if (detail.type === 'file') { | ||||
|         // no need to transfer attachment payload for this request | ||||
|         detail.content = null; | ||||
|  | ||||
|         // attributes contain important attachment metadata - filename and size | ||||
|         attributeMap = await attributes.getNoteAttributeMap(noteId); | ||||
|     } | ||||
|  | ||||
|     res.send({ | ||||
|         detail: detail | ||||
|         detail: detail, | ||||
|         attributes: attributeMap | ||||
|     }); | ||||
| })); | ||||
|  | ||||
|   | ||||
| @@ -31,26 +31,28 @@ router.get('/startup', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
| })); | ||||
|  | ||||
| router.get('/subtree/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const repository = new Repository(req); | ||||
|     const noteId = req.params.noteId; | ||||
|  | ||||
|     const repository = new Repository(req); | ||||
|  | ||||
|     const noteScript = (await repository.getNote(noteId)).content; | ||||
|  | ||||
|     const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository); | ||||
|  | ||||
|     res.send(subTreeScripts + noteScript); | ||||
|     res.send(await getNoteWithSubtreeScript(noteId, repository)); | ||||
| })); | ||||
|  | ||||
| async function getNoteWithSubtreeScript(noteId, repository) { | ||||
|     const noteScript = (await repository.getNote(noteId)).content; | ||||
|     const note = await repository.getNote(noteId); | ||||
|  | ||||
|     const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository); | ||||
|     let noteScript = note.content; | ||||
|  | ||||
|     if (note.isJavaScript()) { | ||||
|         // last \r\n is necessary if script contains line comment on its last line | ||||
|         noteScript = "(async function() {" + noteScript + "\r\n})()"; | ||||
|     } | ||||
|  | ||||
|     const subTreeScripts = await getSubTreeScripts(noteId, [noteId], repository, note.isJavaScript()); | ||||
|  | ||||
|     return subTreeScripts + noteScript; | ||||
| } | ||||
|  | ||||
| async function getSubTreeScripts(parentId, includedNoteIds, repository) { | ||||
| async function getSubTreeScripts(parentId, includedNoteIds, repository, isJavaScript) { | ||||
|     const children = await repository.getEntities(` | ||||
|                                       SELECT notes.*  | ||||
|                                       FROM notes JOIN note_tree USING(noteId) | ||||
| @@ -69,7 +71,7 @@ async function getSubTreeScripts(parentId, includedNoteIds, repository) { | ||||
|  | ||||
|         script += await getSubTreeScripts(child.noteId, includedNoteIds, repository); | ||||
|  | ||||
|         if (child.mime === 'application/javascript') { | ||||
|         if (!isJavaScript && child.mime === 'application/javascript') { | ||||
|             child.content = '<script>' + child.content + '</script>'; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -79,9 +79,12 @@ router.get('/changed', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|  | ||||
| router.get('/notes/:noteId', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|     const noteId = req.params.noteId; | ||||
|     const entity = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]); | ||||
|  | ||||
|     sync.serializeNoteContentBuffer(entity); | ||||
|  | ||||
|     res.send({ | ||||
|         entity: await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [noteId]) | ||||
|         entity: entity | ||||
|     }); | ||||
| })); | ||||
|  | ||||
|   | ||||
| @@ -29,8 +29,20 @@ router.get('/', auth.checkApiAuth, wrap(async (req, res, next) => { | ||||
|  | ||||
|     protected_session.decryptNotes(req, notes); | ||||
|  | ||||
|     const hiddenInAutocomplete = await sql.getColumn(` | ||||
|       SELECT  | ||||
|         DISTINCT noteId  | ||||
|       FROM  | ||||
|         attributes | ||||
|         JOIN notes USING(noteId) | ||||
|       WHERE | ||||
|         attributes.name = 'hide_in_autocomplete'  | ||||
|         AND attributes.isDeleted = 0 | ||||
|         AND notes.isDeleted = 0`); | ||||
|  | ||||
|     res.send({ | ||||
|         notes: notes, | ||||
|         hiddenInAutocomplete: hiddenInAutocomplete, | ||||
|         start_note_path: await options.getOption('start_note_path') | ||||
|     }); | ||||
| })); | ||||
|   | ||||
| @@ -61,10 +61,8 @@ router.put('/:noteTreeId/move-before/:beforeNoteTreeId', auth.checkApiAuth, wrap | ||||
|  | ||||
|         await sync_table.addNoteReorderingSync(beforeNote.parentNoteId, sourceId); | ||||
|  | ||||
|         const now = utils.nowDate(); | ||||
|  | ||||
|         await sql.execute("UPDATE note_tree SET parentNoteId = ?, notePosition = ?, dateModified = ? WHERE noteTreeId = ?", | ||||
|             [beforeNote.parentNoteId, beforeNote.notePosition, now, noteTreeId]); | ||||
|             [beforeNote.parentNoteId, beforeNote.notePosition, utils.nowDate(), noteTreeId]); | ||||
|  | ||||
|         await sync_table.addNoteTreeSync(noteTreeId, sourceId); | ||||
|     }); | ||||
|   | ||||
| @@ -29,6 +29,7 @@ const imageRoute = require('./api/image'); | ||||
| const attributesRoute = require('./api/attributes'); | ||||
| const scriptRoute = require('./api/script'); | ||||
| const senderRoute = require('./api/sender'); | ||||
| const attachmentsRoute = require('./api/attachments'); | ||||
|  | ||||
| function register(app) { | ||||
|     app.use('/', indexRoute); | ||||
| @@ -61,6 +62,7 @@ function register(app) { | ||||
|     app.use('/api/images', imageRoute); | ||||
|     app.use('/api/script', scriptRoute); | ||||
|     app.use('/api/sender', senderRoute); | ||||
|     app.use('/api/attachments', attachmentsRoute); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -8,4 +8,6 @@ window.goToday = async function() { | ||||
|     }); | ||||
|  | ||||
|     api.activateNote(todayNoteId); | ||||
| }; | ||||
| }; | ||||
|  | ||||
| $(document).bind('keydown', "alt+t", window.goToday); | ||||
| @@ -1,4 +1,4 @@ | ||||
| <form id="weight-form" style="display: flex; width: 500px; justify-content: space-around; align-items: flex-end;"> | ||||
| <form id="weight-form" style="display: flex; width: 700px; justify-content: space-around; align-items: flex-end;"> | ||||
|     <div> | ||||
|         <label for="weight-date">Date</label> | ||||
|         <input type="text" id="weight-date" class="form-control" style="width: 150px; text-align: center;" /> | ||||
| @@ -7,6 +7,10 @@ | ||||
|         <label for="weight">Weight</label> | ||||
|         <input type="number" id="weight" value="80.0" step="0.1" class="form-control" style="text-align: center; width: 100px;" /> | ||||
|     </div> | ||||
|     <div> | ||||
|         <label for="comment">Comment</label> | ||||
|         <input type="text" id="comment" class="form-control" style="width: 200px;" /> | ||||
|     </div> | ||||
|  | ||||
|     <button type="submit" class="btn btn-primary">Add</button> | ||||
| </form> | ||||
| @@ -16,84 +20,127 @@ | ||||
| <canvas id="canvas"></canvas> | ||||
|  | ||||
| <script> | ||||
| (async function() { | ||||
|     const dateEl = $("#weight-date"); | ||||
|     const weightEl = $("#weight"); | ||||
|     (async function() { | ||||
|         const $form = $("#weight-form"); | ||||
|         const $date = $("#weight-date"); | ||||
|         const $weight = $("#weight"); | ||||
|         const $comment = $("#comment"); | ||||
|         let chart; | ||||
|  | ||||
|     dateEl.datepicker(); | ||||
|     dateEl.datepicker('option', 'dateFormat', 'yy-mm-dd'); | ||||
|     dateEl.datepicker('setDate', new Date()); | ||||
|         $date.datepicker(); | ||||
|         $date.datepicker('option', 'dateFormat', 'yy-mm-dd'); | ||||
|         $date.datepicker('setDate', new Date()); | ||||
|  | ||||
|     async function saveWeight() { | ||||
|         await server.exec([dateEl.val(), weightEl.val()], async (date, weight) => { | ||||
|             const dataNote = await this.getNoteWithAttribute('date_data', date); | ||||
|         async function saveWeight() { | ||||
|             await server.exec([$date.val(), parseFloat($weight.val()), $comment.val()], async (date, weight, comment) => { | ||||
|                 const dataNote = await this.getNoteWithAttribute('date_data', date); | ||||
|  | ||||
|             if (dataNote) { | ||||
|                 dataNote.jsonContent.weight = weight; | ||||
|                 if (dataNote) { | ||||
|                     dataNote.jsonContent.weight = weight; | ||||
|  | ||||
|                 await this.updateEntity(dataNote); | ||||
|             } | ||||
|             else { | ||||
|                 const parentNoteId = await this.getDateNoteId(date); | ||||
|                 const jsonContent = { weight: weight }; | ||||
|  | ||||
|                 await this.createNote(parentNoteId, 'data', jsonContent, { | ||||
|                     json: true, | ||||
|                     attributes: { | ||||
|                         date_data: date | ||||
|                     if (comment) { | ||||
|                         dataNote.jsonContent.weight_comment = comment; | ||||
|                     } | ||||
|                 }); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         showMessage("Weight has been saved"); | ||||
|                     await this.updateEntity(dataNote); | ||||
|                 } | ||||
|                 else { | ||||
|                     const parentNoteId = await this.getDateNoteId(date); | ||||
|                     const jsonContent = { weight: weight }; | ||||
|  | ||||
|                     if (comment) { | ||||
|                         jsonContent.weight_comment = comment; | ||||
|                     } | ||||
|  | ||||
|                     await this.createNote(parentNoteId, 'data', jsonContent, { | ||||
|                         json: true, | ||||
|                         attributes: { | ||||
|                             date_data: date, | ||||
|                             hide_in_autocomplete: null | ||||
|                         } | ||||
|                     }); | ||||
|                 } | ||||
|             }); | ||||
|  | ||||
|             showMessage("Weight has been saved"); | ||||
|  | ||||
|             chart.data = await getData(); | ||||
|             chart.update(); | ||||
|         } | ||||
|  | ||||
|         async function drawChart() { | ||||
|             const data = await getData(); | ||||
|  | ||||
|             const ctx = $("#canvas")[0].getContext("2d"); | ||||
|  | ||||
|             chart = new Chart(ctx, { | ||||
|                 type: 'line', | ||||
|                 data: data, | ||||
|                 options: { | ||||
|                     tooltips: { | ||||
|                         enabled: true, | ||||
|                         mode: 'single', | ||||
|                         callbacks: { | ||||
|                             label: function (tooltipItem, data) { | ||||
|                                 const multistringText = [tooltipItem.yLabel]; | ||||
|                                 const comment = data.comments[tooltipItem['index']]; | ||||
|  | ||||
|                                 if (comment) { | ||||
|                                     multistringText.push(comment); | ||||
|                                 } | ||||
|  | ||||
|                                 return multistringText; | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|  | ||||
|         async function getData() { | ||||
|             const data = await server.exec([], async () => { | ||||
|                 const notes = await this.getNotesWithAttribute('date_data'); | ||||
|                 const data = []; | ||||
|  | ||||
|                 for (const note of notes) { | ||||
|                     const dateAttr = await note.getAttribute('date_data'); | ||||
|  | ||||
|                     data.push({ | ||||
|                         date: dateAttr.value, | ||||
|                         weight: note.jsonContent.weight, | ||||
|                         comment: note.jsonContent.weight_comment | ||||
|                     }); | ||||
|                 } | ||||
|  | ||||
|                 data.sort((a, b) => a.date < b.date ? -1 : +1); | ||||
|  | ||||
|                 return data; | ||||
|             }); | ||||
|  | ||||
|             const datasets = [{ | ||||
|                 label: "Weight", | ||||
|                 backgroundColor: 'red', | ||||
|                 borderColor: 'red', | ||||
|                 data: data.map(row => row.weight), | ||||
|                 fill: false | ||||
|             }]; | ||||
|  | ||||
|             const labels = data.map(row => row.date); | ||||
|             const comments = data.map(row => row.comment); | ||||
|  | ||||
|             return { | ||||
|                 labels: labels, | ||||
|                 datasets: datasets, | ||||
|                 comments: comments | ||||
|             }; | ||||
|         } | ||||
|  | ||||
|         $form.submit(event => { | ||||
|             saveWeight(); | ||||
|  | ||||
|             event.preventDefault(); | ||||
|         }); | ||||
|  | ||||
|         drawChart(); | ||||
|     } | ||||
|  | ||||
|     async function drawChart() { | ||||
|         const data = await server.exec([], async () => { | ||||
|             const notes = await this.getNotesWithAttribute('date_data'); | ||||
|             const data = []; | ||||
|  | ||||
|             for (const note of notes) { | ||||
|                 const dateAttr = await note.getAttribute('date_data'); | ||||
|  | ||||
|                 data.push({ | ||||
|                     date: dateAttr.value, | ||||
|                     weight: note.jsonContent.weight | ||||
|                 }); | ||||
|             } | ||||
|  | ||||
|             data.sort((a, b) => a.date < b.date ? -1 : +1); | ||||
|  | ||||
|             return data; | ||||
|         }); | ||||
|  | ||||
|         var config = { | ||||
|             type: 'line', | ||||
|             data: { | ||||
|                 labels: data.map(row => row.date), | ||||
|                 datasets: [{ | ||||
|                     label: "Weight", | ||||
|                     backgroundColor: 'red', | ||||
|                     borderColor: 'red', | ||||
|                     data: data.map(row => row.weight), | ||||
|                     fill: false | ||||
|                 }] | ||||
|             } | ||||
|         }; | ||||
|  | ||||
|         var ctx = $("#canvas")[0].getContext("2d"); | ||||
|         new Chart(ctx, config); | ||||
|     } | ||||
|  | ||||
|     $("#weight-form").submit(event => { | ||||
|         saveWeight(); | ||||
|  | ||||
|         event.preventDefault(); | ||||
|     }); | ||||
|  | ||||
|     drawChart(); | ||||
| })(); | ||||
|     })(); | ||||
| </script> | ||||
| @@ -3,7 +3,7 @@ | ||||
| const build = require('./build'); | ||||
| const packageJson = require('../../package'); | ||||
|  | ||||
| const APP_DB_VERSION = 75; | ||||
| const APP_DB_VERSION = 77; | ||||
|  | ||||
| module.exports = { | ||||
|     app_version: packageJson.version, | ||||
|   | ||||
| @@ -5,10 +5,15 @@ const utils = require('./utils'); | ||||
| const sync_table = require('./sync_table'); | ||||
| const Repository = require('./repository'); | ||||
|  | ||||
| const BUILTIN_ATTRIBUTES = [ 'run_on_startup', 'disable_versioning', 'calendar_root' ]; | ||||
| const BUILTIN_ATTRIBUTES = [ | ||||
|     'run_on_startup', | ||||
|     'disable_versioning', | ||||
|     'calendar_root', | ||||
|     'hide_in_autocomplete' | ||||
| ]; | ||||
|  | ||||
| async function getNoteAttributeMap(noteId) { | ||||
|     return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ?`, [noteId]); | ||||
|     return await sql.getMap(`SELECT name, value FROM attributes WHERE noteId = ? AND isDeleted = 0`, [noteId]); | ||||
| } | ||||
|  | ||||
| async function getNoteIdWithAttribute(name, value) { | ||||
| @@ -47,7 +52,11 @@ async function getNoteIdsWithAttribute(name) { | ||||
|           WHERE notes.isDeleted = 0 AND attributes.isDeleted = 0 AND attributes.name = ? AND attributes.isDeleted = 0`, [name]); | ||||
| } | ||||
|  | ||||
| async function createAttribute(noteId, name, value = null, sourceId = null) { | ||||
| async function createAttribute(noteId, name, value = "", sourceId = null) { | ||||
|     if (value === null || value === undefined) { | ||||
|         value = ""; | ||||
|     } | ||||
|  | ||||
|     const now = utils.nowDate(); | ||||
|     const attributeId = utils.newAttributeId(); | ||||
|  | ||||
|   | ||||
| @@ -214,7 +214,7 @@ async function runAllChecks() { | ||||
|           FROM  | ||||
|             notes | ||||
|           WHERE  | ||||
|             type != 'text' AND type != 'code' AND type != 'render'`, | ||||
|             type != 'text' AND type != 'code' AND type != 'render' AND type != 'file'`, | ||||
|         "Note has invalid type", errorList); | ||||
|  | ||||
|     await runSyncRowChecks("notes", "noteId", errorList); | ||||
|   | ||||
| @@ -22,7 +22,7 @@ function info(message) { | ||||
|  | ||||
| function error(message) { | ||||
|     // we're using .info() instead of .error() because simple-node-logger emits weird error for showError() | ||||
|     info(message); | ||||
|     info("ERROR: " + message); | ||||
| } | ||||
|  | ||||
| const requestBlacklist = [ "/libraries", "/javascripts", "/images", "/stylesheets" ]; | ||||
|   | ||||
| @@ -19,7 +19,14 @@ async function executeScript(dataKey, script, params) { | ||||
| } | ||||
|  | ||||
| function getParams(params) { | ||||
|     return params.map(p => JSON.stringify(p)).join(","); | ||||
|     return params.map(p => { | ||||
|         if (typeof p === "string" && p.startsWith("!@#Function: ")) { | ||||
|             return p.substr(13); | ||||
|         } | ||||
|         else { | ||||
|             return JSON.stringify(p); | ||||
|         } | ||||
|     }).join(","); | ||||
| } | ||||
|  | ||||
| module.exports = { | ||||
|   | ||||
| @@ -54,6 +54,8 @@ function ScriptContext(noteId, dataKey) { | ||||
|         return noteId; | ||||
|     }; | ||||
|  | ||||
|     this.createAttribute = attributes.createAttribute; | ||||
|  | ||||
|     this.updateEntity = this.repository.updateEntity; | ||||
|  | ||||
|     this.log = function(message) { | ||||
|   | ||||
| @@ -204,6 +204,8 @@ async function pushEntity(sync, syncContext) { | ||||
|  | ||||
|     if (sync.entityName === 'notes') { | ||||
|         entity = await sql.getRow('SELECT * FROM notes WHERE noteId = ?', [sync.entityId]); | ||||
|  | ||||
|         serializeNoteContentBuffer(entity); | ||||
|     } | ||||
|     else if (sync.entityName === 'note_tree') { | ||||
|         entity = await sql.getRow('SELECT * FROM note_tree WHERE noteTreeId = ?', [sync.entityId]); | ||||
| @@ -258,6 +260,12 @@ async function pushEntity(sync, syncContext) { | ||||
|     await syncRequest(syncContext, 'PUT', '/api/sync/' + sync.entityName, payload); | ||||
| } | ||||
|  | ||||
| function serializeNoteContentBuffer(note) { | ||||
|     if (note.type === 'file') { | ||||
|         note.content = note.content.toString("binary"); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function checkContentHash(syncContext) { | ||||
|     const resp = await syncRequest(syncContext, 'GET', '/api/sync/check'); | ||||
|  | ||||
| @@ -350,5 +358,6 @@ sql.dbReady.then(() => { | ||||
| }); | ||||
|  | ||||
| module.exports = { | ||||
|     sync | ||||
|     sync, | ||||
|     serializeNoteContentBuffer | ||||
| }; | ||||
| @@ -1,10 +1,17 @@ | ||||
| const sql = require('./sql'); | ||||
| const log = require('./log'); | ||||
| const eventLog = require('./event_log'); | ||||
| const notes = require('./notes'); | ||||
| const sync_table = require('./sync_table'); | ||||
|  | ||||
| function deserializeNoteContentBuffer(note) { | ||||
|     if (note.type === 'file') { | ||||
|         note.content = new Buffer(note.content, 'binary'); | ||||
|     } | ||||
| } | ||||
|  | ||||
| async function updateNote(entity, sourceId) { | ||||
|     deserializeNoteContentBuffer(entity); | ||||
|  | ||||
|     const origNote = await sql.getRow("SELECT * FROM notes WHERE noteId = ?", [entity.noteId]); | ||||
|  | ||||
|     if (!origNote || origNote.dateModified <= entity.dateModified) { | ||||
|   | ||||
| @@ -43,6 +43,14 @@ function nowDate() { | ||||
|     return dateStr(new Date()); | ||||
| } | ||||
|  | ||||
| function localDate() { | ||||
|     const date = new Date(); | ||||
|  | ||||
|     return date.getFullYear() + "-" | ||||
|         + (date.getMonth() < 9 ? "0" : "") + (date.getMonth() + 1) + "-" | ||||
|         + (date.getDate() < 10 ? "0" : "") + date.getDate(); | ||||
| } | ||||
|  | ||||
| function dateStr(date) { | ||||
|     return date.toISOString(); | ||||
| } | ||||
| @@ -125,6 +133,7 @@ module.exports = { | ||||
|     randomSecureToken, | ||||
|     randomString, | ||||
|     nowDate, | ||||
|     localDate, | ||||
|     dateStr, | ||||
|     parseDate, | ||||
|     parseDateTime, | ||||
|   | ||||
| @@ -105,7 +105,7 @@ | ||||
|                   onclick="noteEditor.executeCurrentNote()">Execute <kbd>Ctrl+Enter</kbd></button> | ||||
|  | ||||
|           <div class="dropdown" id="note-type"> | ||||
|             <button id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm"> | ||||
|             <button data-bind="disable: isDisabled()" id="dLabel" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm"> | ||||
|               Type: <span data-bind="text: typeString()"></span> | ||||
|               <span class="caret"></span> | ||||
|             </button> | ||||
| @@ -130,6 +130,7 @@ | ||||
|               <li><a onclick="noteHistory.showCurrentNoteHistory();"><kbd>Alt+H</kbd> History</a></li> | ||||
|               <li><a onclick="attributesDialog.showDialog();"><kbd>Alt+A</kbd> Attributes</a></li> | ||||
|               <li><a onclick="noteSource.showDialog();"><kbd>Ctrl+U</kbd> HTML source</a></li> | ||||
|               <li><a onclick="uploadAttachment();">Upload attachment</a></li> | ||||
|             </ul> | ||||
|           </div> | ||||
|         </div> | ||||
| @@ -141,6 +142,32 @@ | ||||
|         <div id="note-detail-code"></div> | ||||
|  | ||||
|         <div id="note-detail-render"></div> | ||||
|  | ||||
|         <div id="note-detail-attachment"> | ||||
|           <table id="attachment-table"> | ||||
|             <tr> | ||||
|               <th>File name:</th> | ||||
|               <td id="attachment-filename"></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th>File type:</th> | ||||
|               <td id="attachment-filetype"></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <th>File size:</th> | ||||
|               <td id="attachment-filesize"></td> | ||||
|             </tr> | ||||
|             <tr> | ||||
|               <td> | ||||
|                 <button id="attachment-download" class="btn btn-primary" type="button">Download</button> | ||||
|                   | ||||
|                 <button id="attachment-open" class="btn btn-primary" type="button">Open</button> | ||||
|               </td> | ||||
|             </tr> | ||||
|           </table> | ||||
|         </div> | ||||
|  | ||||
|         <input type="file" id="file-upload" style="display: none" /> | ||||
|       </div> | ||||
|  | ||||
|       <div id="attribute-list"> | ||||
| @@ -449,8 +476,6 @@ | ||||
|     <link href="libraries/fancytree/skin-win8/ui.fancytree.css" rel="stylesheet"> | ||||
|     <script src="libraries/fancytree/jquery.fancytree-all.min.js"></script> | ||||
|  | ||||
|     <script src="libraries/ckeditor/ckeditor.js"></script> | ||||
|  | ||||
|     <script src="libraries/jquery.hotkeys.js"></script> | ||||
|     <script src="libraries/jquery.fancytree.hotkeys.js"></script> | ||||
|  | ||||
| @@ -458,15 +483,6 @@ | ||||
|  | ||||
|     <script src="libraries/knockout.min.js"></script> | ||||
|  | ||||
|     <script src="libraries/codemirror/codemirror.js"></script> | ||||
|     <link rel="stylesheet" href="libraries/codemirror/codemirror.css"> | ||||
|     <script src="libraries/codemirror/addon/mode/loadmode.js"></script> | ||||
|     <script src="libraries/codemirror/addon/fold/xml-fold.js"></script> | ||||
|     <script src="libraries/codemirror/addon/edit/matchbrackets.js"></script> | ||||
|     <script src="libraries/codemirror/addon/edit/matchtags.js"></script> | ||||
|     <script src="libraries/codemirror/addon/search/match-highlighter.js"></script> | ||||
|     <script src="libraries/codemirror/mode/meta.js"></script> | ||||
|  | ||||
|     <link href="stylesheets/style.css" rel="stylesheet"> | ||||
|  | ||||
|     <script src="javascripts/utils.js"></script> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user