mirror of
https://github.com/zadam/trilium.git
synced 2024-09-20 15:45:58 +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) {
|
|||
};
|
||||
}
|
||||
|
||||
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
|
||||
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…
Reference in a new issue