mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	global link map WIP
This commit is contained in:
		
							parent
							
								
									43e829ca99
								
							
						
					
					
						commit
						a0caa21458
					
				
							
								
								
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								libraries/ckeditor/ckeditor.js
									
									
									
									
										vendored
									
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @ -169,7 +169,7 @@ export default class Entrypoints extends Component { | ||||
|     async switchToDesktopVersionCommand() { | ||||
|         utils.setCookie('trilium-device', 'desktop'); | ||||
| 
 | ||||
|         utils.reloadFrontendApp(); | ||||
|         utils.reloadFrontendApp("Switching to desktop version"); | ||||
|     } | ||||
| 
 | ||||
|     async openInWindowCommand({notePath, hoistedNoteId}) { | ||||
|  | ||||
| @ -88,7 +88,7 @@ function processNoteChange(loadResults, ec) { | ||||
|     loadResults.addNote(ec.entityId, ec.sourceId); | ||||
| 
 | ||||
|     if (ec.isErased && ec.entityId in froca.notes) { | ||||
|         utils.reloadFrontendApp(); | ||||
|         utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
| @ -102,7 +102,7 @@ function processNoteChange(loadResults, ec) { | ||||
| 
 | ||||
| function processBranchChange(loadResults, ec) { | ||||
|     if (ec.isErased && ec.entityId in froca.branches) { | ||||
|         utils.reloadFrontendApp(); | ||||
|         utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
| @ -180,7 +180,7 @@ function processAttributeChange(loadResults, ec) { | ||||
|     let attribute = froca.attributes[ec.entityId]; | ||||
| 
 | ||||
|     if (ec.isErased && ec.entityId in froca.attributes) { | ||||
|         utils.reloadFrontendApp(); | ||||
|         utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -3,7 +3,6 @@ import appContext from "./app_context.js"; | ||||
| import server from "./server.js"; | ||||
| import libraryLoader from "./library_loader.js"; | ||||
| import ws from "./ws.js"; | ||||
| import protectedSessionHolder from "./protected_session_holder.js"; | ||||
| import froca from "./froca.js"; | ||||
| 
 | ||||
| function setupGlobs() { | ||||
|  | ||||
| @ -69,7 +69,7 @@ ws.subscribeToMessages(async message => { | ||||
|         toastService.showMessage("Protected session has been started."); | ||||
|     } | ||||
|     else if (message.type === 'protectedSessionLogout') { | ||||
|         utils.reloadFrontendApp(); | ||||
|         utils.reloadFrontendApp(`Protected session logout`); | ||||
|     } | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,8 @@ | ||||
| function reloadFrontendApp() { | ||||
| function reloadFrontendApp(reason) { | ||||
|     if (reason) { | ||||
|         logInfo("Frontend app reload: " + reason); | ||||
|     } | ||||
| 
 | ||||
|     window.location.reload(true); | ||||
| } | ||||
| 
 | ||||
|  | ||||
| @ -25,7 +25,19 @@ function logError(message) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function logInfo(message) { | ||||
|     console.log(utils.now(), message); | ||||
| 
 | ||||
|     if (ws && ws.readyState === 1) { | ||||
|         ws.send(JSON.stringify({ | ||||
|             type: 'log-info', | ||||
|             info: message | ||||
|         })); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| window.logError = logError; | ||||
| window.logInfo = logInfo; | ||||
| 
 | ||||
| function subscribeToMessages(messageHandler) { | ||||
|     messageHandlers.push(messageHandler); | ||||
| @ -91,7 +103,7 @@ async function handleMessage(event) { | ||||
|     } | ||||
| 
 | ||||
|     if (message.type === 'reload-frontend') { | ||||
|         utils.reloadFrontendApp(); | ||||
|         utils.reloadFrontendApp("received request from backend to reload frontend"); | ||||
|     } | ||||
|     else if (message.type === 'frontend-update') { | ||||
|         await executeFrontendUpdate(message.data.entityChanges); | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| import TypeWidget from "./type_widget.js"; | ||||
| import libraryLoader from "../../services/library_loader.js"; | ||||
| import server from "../../services/server.js"; | ||||
| import froca from "../../services/froca.js"; | ||||
| 
 | ||||
| const TPL = `<div class="note-detail-global-link-map note-detail-printable">
 | ||||
|     <style> | ||||
| @ -56,7 +55,9 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
|             .width(this.$container.width()) | ||||
|             .height(this.$container.height()) | ||||
|             .onZoom(zoom => this.setZoomLevel(zoom.k)) | ||||
|             .nodeRelSize(7) | ||||
|             .d3AlphaDecay(0.01) | ||||
|             .d3VelocityDecay(0.08) | ||||
|             .nodeRelSize(node => this.noteIdToSizeMap[node.id]) | ||||
|             .nodeCanvasObject((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx)) | ||||
|             .nodePointerAreaPaint((node, ctx) => this.paintNode(node, this.stringToColor(node.type), ctx)) | ||||
|             .nodeLabel(node => node.name) | ||||
| @ -70,19 +71,23 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
|             .linkLabel(l => `${l.source.name} - <strong>${l.name}</strong> - ${l.target.name}`) | ||||
|             .linkCanvasObject((link, ctx) => this.paintLink(link, ctx)) | ||||
|             .linkCanvasObjectMode(() => "after") | ||||
|             .linkDirectionalArrowLength(4) | ||||
|             .warmupTicks(10) | ||||
| //            .linkDirectionalArrowLength(5)
 | ||||
|             .linkDirectionalArrowRelPos(1) | ||||
|             .linkWidth(2) | ||||
|             .linkWidth(1) | ||||
|             .linkColor(() => this.css.mutedTextColor) | ||||
|             .d3VelocityDecay(0.2) | ||||
| //            .d3VelocityDecay(0.2)
 | ||||
| //            .dagMode("radialout")
 | ||||
|             .onNodeClick(node => this.nodeClicked(node)); | ||||
| 
 | ||||
|         this.graph.d3Force('link').distance(50); | ||||
| 
 | ||||
|         this.graph.d3Force('center').strength(0.9); | ||||
| 
 | ||||
|         this.graph.d3Force('link').distance(5); | ||||
|         //
 | ||||
|         this.graph.d3Force('center').strength(0.01); | ||||
|         //
 | ||||
|         this.graph.d3Force('charge').strength(-30); | ||||
|         this.graph.d3Force('charge').distanceMax(400); | ||||
| 
 | ||||
| 
 | ||||
|         this.graph.d3Force('charge').distanceMax(1000); | ||||
| 
 | ||||
|         this.renderData(await this.loadNotesAndRelations()); | ||||
|     } | ||||
| @ -113,13 +118,18 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
| 
 | ||||
|     paintNode(node, color, ctx) { | ||||
|         const {x, y} = node; | ||||
|         const size = this.noteIdToSizeMap[node.id]; | ||||
| 
 | ||||
|         ctx.fillStyle = node.id === this.noteId ? 'red' : color; | ||||
|         ctx.beginPath(); | ||||
|         ctx.arc(x, y, node.id === this.noteId ? 8 : 4, 0, 2 * Math.PI, false); | ||||
|         ctx.arc(x, y, size, 0, 2 * Math.PI, false); | ||||
|         ctx.fill(); | ||||
| 
 | ||||
|         if (this.zoomLevel < 2) { | ||||
|         const toRender = this.zoomLevel > 2 | ||||
|             || (this.zoomLevel > 1 && size > 6) | ||||
|             || (this.zoomLevel > 0.3 && size > 10); | ||||
| 
 | ||||
|         if (!toRender) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
| @ -132,7 +142,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
|         } | ||||
| 
 | ||||
|         ctx.fillStyle = this.css.textColor; | ||||
|         ctx.font = 5 + 'px ' + this.css.fontFamily; | ||||
|         ctx.font = size + 'px ' + this.css.fontFamily; | ||||
|         ctx.textAlign = 'center'; | ||||
|         ctx.textBaseline = 'middle'; | ||||
| 
 | ||||
| @ -142,7 +152,7 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
|             title = title.substr(0, 15) + "..."; | ||||
|         } | ||||
| 
 | ||||
|         ctx.fillText(title, x, y + (node.id === this.noteId ? 11 : 7)); | ||||
|         ctx.fillText(title, x, y + Math.round(size * 1.5)); | ||||
|     } | ||||
| 
 | ||||
|     paintLink(link, ctx) { | ||||
| @ -183,20 +193,16 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
|         this.linkIdToLinkMap = {}; | ||||
|         this.noteIdToLinkCountMap = {}; | ||||
| 
 | ||||
|         const resp = await server.post(`notes/root/link-map`, { | ||||
|             maxNotes: 1000, | ||||
|             maxDepth | ||||
|         }); | ||||
|         const resp = await server.post(`global-link-map`); | ||||
| 
 | ||||
|         this.noteIdToLinkCountMap = {...this.noteIdToLinkCountMap, ...resp.noteIdToLinkCountMap}; | ||||
|         this.noteIdToLinkCountMap = resp.noteIdToLinkCountMap; | ||||
| 
 | ||||
|         this.calculateSizes(resp.noteIdToDescendantCountMap); | ||||
| 
 | ||||
|         for (const link of resp.links) { | ||||
|             this.linkIdToLinkMap[link.id] = link; | ||||
|         } | ||||
| 
 | ||||
|         // preload all notes
 | ||||
|         const notes = await froca.getNotes(Object.keys(this.noteIdToLinkCountMap), true); | ||||
| 
 | ||||
|         const noteIdToLinkIdMap = {}; | ||||
|         noteIdToLinkIdMap[this.noteId] = new Set(); // for case there are no relations
 | ||||
|         const linksGroupedBySourceTarget = {}; | ||||
| @ -226,11 +232,11 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             nodes: notes.map(note => ({ | ||||
|                 id: note.noteId, | ||||
|                 name: note.title, | ||||
|                 type: note.type, | ||||
|                 expanded: this.noteIdToLinkCountMap[note.noteId] === noteIdToLinkIdMap[note.noteId].size | ||||
|             nodes: resp.notes.map(([noteId, title, type]) => ({ | ||||
|                 id: noteId, | ||||
|                 name: title, | ||||
|                 type: type, | ||||
|                 expanded: true | ||||
|             })), | ||||
|             links: Object.values(linksGroupedBySourceTarget).map(link => ({ | ||||
|                 id: link.id, | ||||
| @ -241,6 +247,20 @@ export default class GlobalLinkMapTypeWidget extends TypeWidget { | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     calculateSizes(noteIdToDescendantCountMap) { | ||||
|         this.noteIdToSizeMap = {}; | ||||
| 
 | ||||
|         for (const noteId in noteIdToDescendantCountMap) { | ||||
|             this.noteIdToSizeMap[noteId] = 4; | ||||
| 
 | ||||
|             const count = noteIdToDescendantCountMap[noteId]; | ||||
| 
 | ||||
|             if (count > 0) { | ||||
|                 this.noteIdToSizeMap[noteId] += 1 + Math.round(Math.log(count) / Math.log(1.5)); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     renderData(data, zoomToFit = true, zoomPadding = 10) { | ||||
|         this.graph.graphData(data); | ||||
| 
 | ||||
|  | ||||
| @ -79,6 +79,92 @@ function getLinkMap(req) { | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     getLinkMap | ||||
| function buildDescendantCountMap() { | ||||
|     const noteIdToCountMap = {}; | ||||
| 
 | ||||
|     function getCount(noteId) { | ||||
|         if (!(noteId in noteIdToCountMap)) { | ||||
|             const note = becca.getNote(noteId); | ||||
| 
 | ||||
|             noteIdToCountMap[noteId] = note.children.length; | ||||
| 
 | ||||
|             for (const child of note.children) { | ||||
|                 noteIdToCountMap[noteId] += getCount(child.noteId); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return noteIdToCountMap[noteId]; | ||||
|     } | ||||
| 
 | ||||
|     getCount('root'); | ||||
| 
 | ||||
|     return noteIdToCountMap; | ||||
| } | ||||
| 
 | ||||
| function getGlobalLinkMap() { | ||||
|     const relations = Object.values(becca.attributes).filter(rel => { | ||||
|         if (rel.type !== 'relation' || rel.name === 'relationMapLink' || rel.name === 'template') { | ||||
|             return false; | ||||
|         } | ||||
|         else if (rel.name === 'imageLink') { | ||||
|             const parentNote = becca.getNote(rel.noteId); | ||||
| 
 | ||||
|             return !parentNote.getChildNotes().find(childNote => childNote.noteId === rel.value); | ||||
|         } | ||||
|         else { | ||||
|             return true; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     const noteIdToLinkCountMap = {}; | ||||
| 
 | ||||
|     for (const noteId in becca.notes) { | ||||
|         noteIdToLinkCountMap[noteId] = getRelations(noteId).length; | ||||
|     } | ||||
| 
 | ||||
|     let links = Array.from(relations).map(rel => ({ | ||||
|         id: rel.noteId + "-" + rel.name + "-" + rel.value, | ||||
|         sourceNoteId: rel.noteId, | ||||
|         targetNoteId: rel.value, | ||||
|         name: rel.name | ||||
|     })); | ||||
| 
 | ||||
|     links = []; | ||||
| 
 | ||||
|     const noteIds = new Set(); | ||||
| 
 | ||||
|     const notes = Object.values(becca.notes) | ||||
|         .filter(note => !note.isArchived) | ||||
|         .map(note => [ | ||||
|             note.noteId, | ||||
|             note.isContentAvailable() ? note.title : '[protected]', | ||||
|             note.type | ||||
|         ]); | ||||
| 
 | ||||
|     notes.forEach(([noteId]) => noteIds.add(noteId)); | ||||
| 
 | ||||
|     for (const branch of Object.values(becca.branches)) { | ||||
|         if (!noteIds.has(branch.parentNoteId) || !noteIds.has(branch.noteId)) { | ||||
|             continue; | ||||
|         } | ||||
| 
 | ||||
|         links.push({ | ||||
|             id: branch.branchId, | ||||
|             sourceNoteId: branch.parentNoteId, | ||||
|             targetNoteId: branch.noteId, | ||||
|             name: 'branch' | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         notes: notes, | ||||
|         noteIdToLinkCountMap, | ||||
|         noteIdToDescendantCountMap: buildDescendantCountMap(), | ||||
|         links: links | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| module.exports = { | ||||
|     getLinkMap, | ||||
|     getGlobalLinkMap | ||||
| }; | ||||
|  | ||||
| @ -221,6 +221,7 @@ function register(app) { | ||||
|     apiRoute(GET, '/api/attributes/values/:attributeName', attributesRoute.getValuesForAttribute); | ||||
| 
 | ||||
|     apiRoute(POST, '/api/notes/:noteId/link-map', linkMapRoute.getLinkMap); | ||||
|     apiRoute(POST, '/api/global-link-map', linkMapRoute.getGlobalLinkMap); | ||||
| 
 | ||||
|     apiRoute(GET, '/api/special-notes/inbox/:date', specialNotesRoute.getInboxNote); | ||||
|     apiRoute(GET, '/api/special-notes/date/:date', specialNotesRoute.getDateNote); | ||||
|  | ||||
| @ -41,6 +41,9 @@ function init(httpServer, sessionParser) { | ||||
|             if (message.type === 'log-error') { | ||||
|                 log.info('JS Error: ' + message.error + '\r\nStack: ' + message.stack); | ||||
|             } | ||||
|             else if (message.type === 'log-info') { | ||||
|                 log.info('JS Info: ' + message.info); | ||||
|             } | ||||
|             else if (message.type === 'ping') { | ||||
|                 await syncMutexService.doExclusively(() => sendPing(ws)); | ||||
|             } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 zadam
						zadam