wip attachment support

pull/255/head
zadam 2023-04-03 23:47:24 +07:00
parent 2bc78ccafb
commit 5d6d9ab6d6
27 changed files with 289 additions and 63 deletions

@ -7,8 +7,9 @@ CREATE TABLE IF NOT EXISTS "attachments"
title TEXT not null, title TEXT not null,
isProtected INT not null DEFAULT 0, isProtected INT not null DEFAULT 0,
blobId TEXT DEFAULT null, blobId TEXT DEFAULT null,
utcDateScheduledForDeletionSince TEXT DEFAULT NULL, dateModified TEXT NOT NULL,
utcDateModified TEXT not null, utcDateModified TEXT not null,
utcDateScheduledForDeletionSince TEXT DEFAULT NULL,
isDeleted INT not null, isDeleted INT not null,
deleteId TEXT DEFAULT NULL); deleteId TEXT DEFAULT NULL);

@ -118,8 +118,9 @@ CREATE TABLE IF NOT EXISTS "attachments"
title TEXT not null, title TEXT not null,
isProtected INT not null DEFAULT 0, isProtected INT not null DEFAULT 0,
blobId TEXT DEFAULT null, blobId TEXT DEFAULT null,
utcDateScheduledForDeletionSince TEXT DEFAULT NULL, dateModified TEXT NOT NULL,
utcDateModified TEXT not null, utcDateModified TEXT not null,
utcDateScheduledForDeletionSince TEXT DEFAULT NULL,
isDeleted INT not null, isDeleted INT not null,
deleteId TEXT DEFAULT NULL); deleteId TEXT DEFAULT NULL);
CREATE INDEX IDX_attachments_parentId_role CREATE INDEX IDX_attachments_parentId_role

@ -6,6 +6,8 @@ const becca = require('../becca');
const AbstractBeccaEntity = require("./abstract_becca_entity"); const AbstractBeccaEntity = require("./abstract_becca_entity");
/** /**
* FIXME: how to order attachments?
*
* Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for * Attachment represent data related/attached to the note. Conceptually similar to attributes, but intended for
* larger amounts of data and generally not accessible to the user. * larger amounts of data and generally not accessible to the user.
* *
@ -45,9 +47,11 @@ class BAttachment extends AbstractBeccaEntity {
/** @type {boolean} */ /** @type {boolean} */
this.isProtected = !!row.isProtected; this.isProtected = !!row.isProtected;
/** @type {string} */ /** @type {string} */
this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince; this.dateModified = row.dateModified;
/** @type {string} */ /** @type {string} */
this.utcDateModified = row.utcDateModified; this.utcDateModified = row.utcDateModified;
/** @type {string} */
this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince;
} }
getNote() { getNote() {
@ -76,6 +80,7 @@ class BAttachment extends AbstractBeccaEntity {
beforeSaving() { beforeSaving() {
super.beforeSaving(); super.beforeSaving();
this.dateModified = dateUtils.localNowDateTime();
this.utcDateModified = dateUtils.utcNowDateTime(); this.utcDateModified = dateUtils.utcNowDateTime();
} }
@ -89,8 +94,9 @@ class BAttachment extends AbstractBeccaEntity {
blobId: this.blobId, blobId: this.blobId,
isProtected: !!this.isProtected, isProtected: !!this.isProtected,
isDeleted: false, isDeleted: false,
utcDateScheduledForDeletionSince: this.utcDateScheduledForDeletionSince, dateModified: this.dateModified,
utcDateModified: this.utcDateModified utcDateModified: this.utcDateModified,
utcDateScheduledForDeletionSince: this.utcDateScheduledForDeletionSince
}; };
} }

@ -38,7 +38,7 @@ export default class Entrypoints extends Component {
await ws.waitForMaxKnownEntityChangeId(); await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, true); await appContext.tabManager.openTabWithNoteWithHoisting(note.noteId, {activate: true});
appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true}); appContext.triggerEvent('focusAndSelectTitle', {isNewNote: true});
} }
@ -135,7 +135,7 @@ export default class Entrypoints extends Component {
utils.reloadFrontendApp("Switching to mobile version"); utils.reloadFrontendApp("Switching to mobile version");
} }
async openInWindowCommand({notePath, hoistedNoteId}) { async openInWindowCommand({notePath, hoistedNoteId, viewScope}) {
if (!hoistedNoteId) { if (!hoistedNoteId) {
hoistedNoteId = 'root'; hoistedNoteId = 'root';
} }
@ -143,10 +143,10 @@ export default class Entrypoints extends Component {
if (utils.isElectron()) { if (utils.isElectron()) {
const {ipcRenderer} = utils.dynamicRequire('electron'); const {ipcRenderer} = utils.dynamicRequire('electron');
ipcRenderer.send('create-extra-window', {notePath, hoistedNoteId}); ipcRenderer.send('create-extra-window', {notePath, hoistedNoteId, viewScope});
} }
else { else {
const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extra=1#${notePath}`; const url = `${window.location.protocol}//${window.location.host}${window.location.pathname}?extraWindow=1&extraHoistedNoteId=${hoistedNoteId}&extraViewScope=${JSON.stringify(viewScope)}#${notePath}`;
window.open(url, '', 'width=1000,height=800'); window.open(url, '', 'width=1000,height=800');
} }

@ -53,8 +53,8 @@ class NoteContext extends Component {
this.notePath = resolvedNotePath; this.notePath = resolvedNotePath;
({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(resolvedNotePath)); ({noteId: this.noteId, parentNoteId: this.parentNoteId} = treeService.getNoteIdAndParentIdFromNotePath(resolvedNotePath));
this.resetViewScope(); this.viewScope = opts.viewScope || {};
this.viewScope.viewMode = opts.viewMode || "default"; this.viewScope.viewMode = this.viewScope.viewMode || "default";
this.saveToRecentNotes(resolvedNotePath); this.saveToRecentNotes(resolvedNotePath);
@ -187,7 +187,7 @@ class NoteContext extends Component {
notePath: this.notePath, notePath: this.notePath,
hoistedNoteId: this.hoistedNoteId, hoistedNoteId: this.hoistedNoteId,
active: this.isActive(), active: this.isActive(),
viewMode: this.viewScope.viewMode viewScope: this.viewScope
} }
} }

@ -117,7 +117,12 @@ export default class RootCommandExecutor extends Component {
const notePath = appContext.tabManager.getActiveContextNotePath(); const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) { if (notePath) {
await appContext.tabManager.openContextWithNote(notePath, { activate: true, viewMode: 'source' }); await appContext.tabManager.openContextWithNote(notePath, {
activate: true,
viewScope: {
viewMode: 'source'
}
});
} }
} }
@ -125,7 +130,25 @@ export default class RootCommandExecutor extends Component {
const notePath = appContext.tabManager.getActiveContextNotePath(); const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) { if (notePath) {
await appContext.tabManager.openContextWithNote(notePath, { activate: true, viewMode: 'attachments' }); await appContext.tabManager.openContextWithNote(notePath, {
activate: true,
viewScope: {
viewMode: 'attachments'
}
});
}
}
async showAttachmentDetailCommand() {
const notePath = appContext.tabManager.getActiveContextNotePath();
if (notePath) {
await appContext.tabManager.openContextWithNote(notePath, {
activate: true,
viewScope: {
viewMode: 'attachments'
}
});
} }
} }
} }

@ -86,7 +86,8 @@ export default class TabManager extends Component {
filteredTabs.push({ filteredTabs.push({
notePath: notePathInUrl || 'root', notePath: notePathInUrl || 'root',
active: true, active: true,
hoistedNoteId: glob.extraHoistedNoteId || 'root' hoistedNoteId: glob.extraHoistedNoteId || 'root',
viewScope: glob.extraViewScope || {}
}); });
} }
@ -101,7 +102,7 @@ export default class TabManager extends Component {
ntxId: tab.ntxId, ntxId: tab.ntxId,
mainNtxId: tab.mainNtxId, mainNtxId: tab.mainNtxId,
hoistedNoteId: tab.hoistedNoteId, hoistedNoteId: tab.hoistedNoteId,
viewMode: tab.viewMode viewScope: tab.viewScope
}); });
} }
}); });
@ -271,7 +272,7 @@ export default class TabManager extends Component {
/** /**
* If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab. * If the requested notePath is within current note hoisting scope then keep the note hoisting also for the new tab.
*/ */
async openTabWithNoteWithHoisting(notePath, activate = false) { async openTabWithNoteWithHoisting(notePath, opts = {}) {
const noteContext = this.getActiveContext(); const noteContext = this.getActiveContext();
let hoistedNoteId = 'root'; let hoistedNoteId = 'root';
@ -283,7 +284,9 @@ export default class TabManager extends Component {
} }
} }
return this.openContextWithNote(notePath, { activate, hoistedNoteId }); opts.hoistedNoteId = hoistedNoteId;
return this.openContextWithNote(notePath, opts);
} }
async openContextWithNote(notePath, opts = {}) { async openContextWithNote(notePath, opts = {}) {
@ -291,7 +294,7 @@ export default class TabManager extends Component {
const ntxId = opts.ntxId || null; const ntxId = opts.ntxId || null;
const mainNtxId = opts.mainNtxId || null; const mainNtxId = opts.mainNtxId || null;
const hoistedNoteId = opts.hoistedNoteId || 'root'; const hoistedNoteId = opts.hoistedNoteId || 'root';
const viewMode = opts.viewMode || "default"; const viewScope = opts.viewScope || { viewMode: "default" };
const noteContext = await this.openEmptyTab(ntxId, hoistedNoteId, mainNtxId); const noteContext = await this.openEmptyTab(ntxId, hoistedNoteId, mainNtxId);
@ -299,7 +302,7 @@ export default class TabManager extends Component {
await noteContext.setNote(notePath, { await noteContext.setNote(notePath, {
// if activate is false then send normal noteSwitched event // if activate is false then send normal noteSwitched event
triggerSwitchEvent: !activate, triggerSwitchEvent: !activate,
viewMode: viewMode viewScope: viewScope
}); });
} }

@ -0,0 +1,33 @@
class FAttachment {
constructor(froca, row) {
this.froca = froca;
this.update(row);
}
update(row) {
/** @type {string} */
this.attachmentId = row.attachmentId;
/** @type {string} */
this.parentId = row.parentId;
/** @type {string} */
this.role = row.role;
/** @type {string} */
this.mime = row.mime;
/** @type {string} */
this.title = row.title;
/** @type {string} */
this.dateModified = row.dateModified;
/** @type {string} */
this.utcDateModified = row.utcDateModified;
/** @type {string} */
this.utcDateScheduledForDeletionSince = row.utcDateScheduledForDeletionSince;
this.froca.attachments[this.attachmentId] = this;
}
/** @returns {FNote} */
getNote() {
return this.froca.notes[this.parentId];
}
}

@ -51,6 +51,9 @@ class FNote {
/** @type {Object.<string, string>} */ /** @type {Object.<string, string>} */
this.childToBranch = {}; this.childToBranch = {};
/** @type {FAttachment[]|null} */
this.attachments = null; // lazy loaded
this.update(row); this.update(row);
} }
@ -225,6 +228,23 @@ class FNote {
return await this.froca.getNotes(this.children); return await this.froca.getNotes(this.children);
} }
/** @returns {Promise<FAttachment[]>} */
async getAttachments() {
if (!this.attachments) {
this.attachments = (await server.get(`notes/${this.noteId}/attachments`))
.map(row => new FAttachment(froca, row));
}
return this.attachments;
}
/** @returns {Promise<FAttachment>} */
async getAttachmentById(attachmentId) {
const attachments = await this.getAttachments();
return attachments.find(att => att.attachmentId === attachmentId);
}
/** /**
* @param {string} [type] - (optional) attribute type to filter * @param {string} [type] - (optional) attribute type to filter
* @param {string} [name] - (optional) attribute name to filter * @param {string} [name] - (optional) attribute name to filter

@ -1,4 +1,5 @@
/** /**
* FIXME: probably make it a FBlob
* Complements the FNote with the main note content and other extra attributes * Complements the FNote with the main note content and other extra attributes
*/ */
class FNoteComplement { class FNoteComplement {

@ -1,7 +1,7 @@
import contextMenu from "./context_menu.js"; import contextMenu from "./context_menu.js";
import appContext from "../components/app_context.js"; import appContext from "../components/app_context.js";
function openContextMenu(notePath, hoistedNoteId, e) { function openContextMenu(notePath, e, viewScope = {}, hoistedNoteId = null) {
contextMenu.show({ contextMenu.show({
x: e.pageX, x: e.pageX,
y: e.pageY, y: e.pageY,
@ -16,16 +16,16 @@ function openContextMenu(notePath, hoistedNoteId, e) {
} }
if (command === 'openNoteInNewTab') { if (command === 'openNoteInNewTab') {
appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId }); appContext.tabManager.openContextWithNote(notePath, { hoistedNoteId, viewScope });
} }
else if (command === 'openNoteInNewSplit') { else if (command === 'openNoteInNewSplit') {
const subContexts = appContext.tabManager.getActiveContext().getSubContexts(); const subContexts = appContext.tabManager.getActiveContext().getSubContexts();
const {ntxId} = subContexts[subContexts.length - 1]; const {ntxId} = subContexts[subContexts.length - 1];
appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath, hoistedNoteId}); appContext.triggerCommand("openNewNoteSplit", {ntxId, notePath, hoistedNoteId, viewScope});
} }
else if (command === 'openNoteInNewWindow') { else if (command === 'openNoteInNewWindow') {
appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId}); appContext.triggerCommand('openInWindow', {notePath, hoistedNoteId, viewScope});
} }
} }
}); });

@ -34,6 +34,10 @@ class Froca {
/** @type {Object.<string, FAttribute>} */ /** @type {Object.<string, FAttribute>} */
this.attributes = {}; this.attributes = {};
/** @type {Object.<string, FAttachment>} */
this.attachments = {};
// FIXME
/** @type {Object.<string, Promise<FNoteComplement>>} */ /** @type {Object.<string, Promise<FNoteComplement>>} */
this.blobPromises = {}; this.blobPromises = {};
@ -311,6 +315,7 @@ class Froca {
} }
/** /**
* // FIXME
* @returns {Promise<FNoteComplement>} * @returns {Promise<FNoteComplement>}
*/ */
async getNoteComplement(noteId) { async getNoteComplement(noteId) {

@ -34,7 +34,7 @@ async function processEntityChanges(entityChanges) {
loadResults.addOption(ec.entity.name); loadResults.addOption(ec.entity.name);
} else if (ec.entityName === 'attachments') { } else if (ec.entityName === 'attachments') {
loadResults.addAttachment(ec.entity); processAttachment(loadResults, ec);
} else if (ec.entityName === 'etapi_tokens') { } else if (ec.entityName === 'etapi_tokens') {
// NOOP // NOOP
} }
@ -231,6 +231,43 @@ function processAttributeChange(loadResults, ec) {
} }
} }
function processAttachment(loadResults, ec) {
if (ec.isErased && ec.entityId in froca.attachments) {
utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
return;
}
const attachment = froca.attachments[ec.entityId];
if (ec.isErased || ec.entity?.isDeleted) {
if (attachment) {
const note = attachment.getNote();
if (note && note.attachments) {
note.attachments = note.attachments.filter(att => att.attachmentId !== attachment.attachmentId);
}
loadResults.addAttachment(ec.entity);
delete froca.attachments[ec.entityId];
}
return;
}
if (attachment) {
attachment.update(ec.entity);
} else {
const note = froca.notes[ec.entity.parentId];
if (note && note.attachments) {
note.attachments.push(new FAttachment(froca, ec.entity));
}
}
loadResults.addAttachment(ec.entity);
}
export default { export default {
processEntityChanges processEntityChanges
} }

@ -87,7 +87,16 @@ function getNotePathFromLink($link) {
const url = $link.attr('href'); const url = $link.attr('href');
return url ? getNotePathFromUrl(url) : null; const notePath = url ? getNotePathFromUrl(url) : null;
const viewScope = {
viewMode: $link.attr('data-view-mode'),
attachmentId: $link.attr('data-attachment-id'),
};
return {
notePath,
viewScope
};
} }
function goToLink(evt) { function goToLink(evt) {
@ -101,22 +110,25 @@ function goToLink(evt) {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
const notePath = getNotePathFromLink($link); const {notePath, viewScope} = getNotePathFromLink($link);
const ctrlKey = utils.isCtrlKey(evt); const ctrlKey = utils.isCtrlKey(evt);
const isLeftClick = evt.which === 1;
const isMiddleClick = evt.which === 2;
const openInNewTab = (isLeftClick && ctrlKey) || isMiddleClick;
if (notePath) { if (notePath) {
if ((evt.which === 1 && ctrlKey) || evt.which === 2) { if (openInNewTab) {
appContext.tabManager.openTabWithNoteWithHoisting(notePath); appContext.tabManager.openTabWithNoteWithHoisting(notePath, { viewScope });
} }
else if (evt.which === 1) { else if (isLeftClick) {
const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id"); const ntxId = $(evt.target).closest("[data-ntx-id]").attr("data-ntx-id");
const noteContext = ntxId const noteContext = ntxId
? appContext.tabManager.getNoteContextById(ntxId) ? appContext.tabManager.getNoteContextById(ntxId)
: appContext.tabManager.getActiveContext(); : appContext.tabManager.getActiveContext();
noteContext.setNote(notePath).then(() => { noteContext.setNote(notePath, { viewScope }).then(() => {
if (noteContext !== appContext.tabManager.getActiveContext()) { if (noteContext !== appContext.tabManager.getActiveContext()) {
appContext.tabManager.activateNoteContext(noteContext.ntxId); appContext.tabManager.activateNoteContext(noteContext.ntxId);
} }
@ -124,7 +136,7 @@ function goToLink(evt) {
} }
} }
else { else {
if ((evt.which === 1 && ctrlKey) || evt.which === 2 if (openInNewTab
|| $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices || $link.hasClass("ck-link-actions__preview") // within edit link dialog single click suffices
|| $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices || $link.closest("[contenteditable]").length === 0 // outside of CKEditor single click suffices
) { ) {
@ -147,7 +159,7 @@ function goToLink(evt) {
function linkContextMenu(e) { function linkContextMenu(e) {
const $link = $(e.target).closest("a"); const $link = $(e.target).closest("a");
const notePath = getNotePathFromLink($link); const {notePath, viewScope} = getNotePathFromLink($link);
if (!notePath) { if (!notePath) {
return; return;
@ -155,7 +167,7 @@ function linkContextMenu(e) {
e.preventDefault(); e.preventDefault();
linkContextMenuService.openContextMenu(notePath, null, e); linkContextMenuService.openContextMenu(notePath, e, viewScope, null);
} }
async function loadReferenceLinkTitle(noteId, $el) { async function loadReferenceLinkTitle(noteId, $el) {

@ -37,7 +37,7 @@ const TPL = `
<div class="attachment-detail-wrapper"> <div class="attachment-detail-wrapper">
<div class="attachment-title-line"> <div class="attachment-title-line">
<h4 class="attachment-title"></h4> <h4 class="attachment-title"><a href="javascript:" data-trigger-command="openAttachmentDetail"></a></h4>
<div class="attachment-details"></div> <div class="attachment-details"></div>
<div style="flex: 1 1;"></div> <div style="flex: 1 1;"></div>
<div class="attachment-actions-container"></div> <div class="attachment-actions-container"></div>
@ -73,7 +73,7 @@ export default class AttachmentDetailWidget extends BasicWidget {
.html() .html()
); );
this.$wrapper = this.$widget.find('.attachment-detail-wrapper'); this.$wrapper = this.$widget.find('.attachment-detail-wrapper');
this.$wrapper.find('.attachment-title').text(this.attachment.title); this.$wrapper.find('.attachment-title a').text(this.attachment.title);
this.$wrapper.find('.attachment-details') this.$wrapper.find('.attachment-details')
.text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`); .text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`);
this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render()); this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render());
@ -90,9 +90,11 @@ export default class AttachmentDetailWidget extends BasicWidget {
} }
} }
async entitiesReloadedEvent({loadResults}) { openAttachmentDetailCommand() {
console.log("AttachmentDetailWidget: entitiesReloadedEvent");
}
async entitiesReloadedEvent({loadResults}) {
const attachmentChange = loadResults.getAttachments().find(att => att.attachmentId === this.attachment.attachmentId); const attachmentChange = loadResults.getAttachments().find(att => att.attachmentId === this.attachment.attachmentId);
if (attachmentChange) { if (attachmentChange) {

@ -27,7 +27,7 @@ export default class NoteLauncher extends AbstractLauncher {
const hoistedNoteId = this.getHoistedNoteId(); const hoistedNoteId = this.getHoistedNoteId();
linkContextMenuService.openContextMenu(targetNoteId, hoistedNoteId, evt); linkContextMenuService.openContextMenu(targetNoteId, evt, {}, hoistedNoteId);
}); });
} }

@ -13,7 +13,7 @@ export default class OpenNoteButtonWidget extends OnClickButtonWidget {
.icon(() => this.noteToOpen.getIcon()) .icon(() => this.noteToOpen.getIcon())
.onClick((widget, evt) => this.launch(evt)) .onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt)) .onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu(evt => linkContextMenuService.openContextMenu(this.noteToOpen.noteId, null, evt)); .onContextMenu(evt => linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt));
} }
async launch(evt) { async launch(evt) {

@ -34,7 +34,7 @@ export default class SplitNoteContainer extends FlexContainer {
this.child(widget); this.child(widget);
} }
async openNewNoteSplitEvent({ntxId, notePath, hoistedNoteId}) { async openNewNoteSplitEvent({ntxId, notePath, hoistedNoteId, viewScope}) {
const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId; const mainNtxId = appContext.tabManager.getActiveMainContext().ntxId;
if (!ntxId) { if (!ntxId) {
@ -63,7 +63,7 @@ export default class SplitNoteContainer extends FlexContainer {
await appContext.tabManager.activateNoteContext(noteContext.ntxId); await appContext.tabManager.activateNoteContext(noteContext.ntxId);
if (notePath) { if (notePath) {
await noteContext.setNote(notePath); await noteContext.setNote(notePath, viewScope);
} }
else { else {
await noteContext.setEmpty(); await noteContext.setEmpty();

@ -61,6 +61,10 @@ export default class NoteContextAwareWidget extends BasicWidget {
} }
} }
/**
* @param {FNote} note
* @returns {Promise<void>}
*/
async refreshWithNote(note) {} async refreshWithNote(note) {}
async noteSwitchedEvent({noteContext, notePath}) { async noteSwitchedEvent({noteContext, notePath}) {

@ -27,7 +27,8 @@ import NoteMapTypeWidget from "./type_widgets/note_map.js";
import WebViewTypeWidget from "./type_widgets/web_view.js"; import WebViewTypeWidget from "./type_widgets/web_view.js";
import DocTypeWidget from "./type_widgets/doc.js"; import DocTypeWidget from "./type_widgets/doc.js";
import ContentWidgetTypeWidget from "./type_widgets/content_widget.js"; import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
import AttachmentsTypeWidget from "./type_widgets/attachments.js"; import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
const TPL = ` const TPL = `
<div class="note-detail"> <div class="note-detail">
@ -63,7 +64,8 @@ const typeWidgetClasses = {
'webView': WebViewTypeWidget, 'webView': WebViewTypeWidget,
'doc': DocTypeWidget, 'doc': DocTypeWidget,
'contentWidget': ContentWidgetTypeWidget, 'contentWidget': ContentWidgetTypeWidget,
'attachments': AttachmentsTypeWidget 'attachmentDetail': AttachmentDetailTypeWidget,
'attachmentList': AttachmentListTypeWidget
}; };
export default class NoteDetailWidget extends NoteContextAwareWidget { export default class NoteDetailWidget extends NoteContextAwareWidget {
@ -188,11 +190,12 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
} }
let type = note.type; let type = note.type;
const viewScope = this.noteContext.viewScope;
if (type === 'text' && this.noteContext.viewScope.viewMode === 'source') { if (type === 'text' && viewScope.viewMode === 'source') {
type = 'readOnlyCode'; type = 'readOnlyCode';
} else if (this.noteContext.viewScope.viewMode === 'attachments') { } else if (viewScope.viewMode === 'attachments') {
type = 'attachments'; type = viewScope.attachmentId ? 'attachmentDetail' : 'attachmentList';
} else if (type === 'text' && await this.noteContext.isReadOnly()) { } else if (type === 'text' && await this.noteContext.isReadOnly()) {
type = 'readOnlyText'; type = 'readOnlyText';
} else if ((type === 'code' || type === 'mermaid') && await this.noteContext.isReadOnly()) { } else if ((type === 'code' || type === 'mermaid') && await this.noteContext.isReadOnly()) {

@ -113,7 +113,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
.linkWidth(1) .linkWidth(1)
.linkColor(() => this.css.mutedTextColor) .linkColor(() => this.css.mutedTextColor)
.onNodeClick(node => appContext.tabManager.getActiveContext().setNote(node.id)) .onNodeClick(node => appContext.tabManager.getActiveContext().setNote(node.id))
.onNodeRightClick((node, e) => linkContextMenuService.openContextMenu(node.id, null, e)); .onNodeRightClick((node, e) => linkContextMenuService.openContextMenu(node.id, e));
if (this.mapType === 'link') { if (this.mapType === 'link') {
this.graph this.graph

@ -70,20 +70,38 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
} }
async refreshWithNote(note) { async refreshWithNote(note) {
const viewMode = this.noteContext.viewScope.viewMode; this.$noteTitle.val(await this.getTitleText(note));
this.$noteTitle.val(viewMode === 'default'
? note.title
: `${viewMode}: ${note.title}`);
this.$noteTitle.prop("readonly", this.$noteTitle.prop("readonly",
(note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable())
|| ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(note.noteId) || ['_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(note.noteId)
|| viewMode !== 'default' || this.noteContext.viewScope.viewMode !== 'default'
); );
this.setProtectedStatus(note); this.setProtectedStatus(note);
} }
/** @param {FNote} note */
async getTitleText(note) {
const viewScope = this.noteContext.viewScope;
let title = viewScope.viewMode === 'default'
? note.title
: `${note.title}: ${viewScope.viewMode}`;
if (viewScope.attachmentId) {
// assuming the attachment has been already loaded
const attachment = await note.getAttachmentById(viewScope.attachmentId);
if (attachment) {
title += `: ${attachment.title}`;
}
}
return title;
}
/** @param {FNote} note */
setProtectedStatus(note) { setProtectedStatus(note) {
this.$noteTitle.toggleClass("protected", !!note.isProtected); this.$noteTitle.toggleClass("protected", !!note.isProtected);
} }

@ -0,0 +1,54 @@
import TypeWidget from "./type_widget.js";
import server from "../../services/server.js";
import AttachmentDetailWidget from "../attachment_detail.js";
const TPL = `
<div class="attachment-detail note-detail-printable">
<style>
.attachment-detail {
padding: 15px;
}
</style>
<div class="attachment-wrapper"></div>
</div>`;
export default class AttachmentDetailTypeWidget extends TypeWidget {
static getType() {
return "attachmentDetail";
}
doRender() {
this.$widget = $(TPL);
this.$wrapper = this.$widget.find('.attachment-wrapper');
super.doRender();
}
async doRefresh(note) {
this.$wrapper.empty();
this.children = [];
this.renderedAttachmentIds = new Set();
const attachment = await server.get(`notes/${this.noteId}/attachments/${this.noteContext.viewScope.attachment.attachmentId}/?includeContent=true`);
if (!attachment) {
this.$list.html("<strong>This attachment has been deleted.</strong>");
return;
}
const attachmentDetailWidget = new AttachmentDetailWidget(attachment);
this.child(attachmentDetailWidget);
this.$list.append(attachmentDetailWidget.render());
}
async entitiesReloadedEvent({loadResults}) {
const attachmentChange = loadResults.getAttachments().find(att => att.attachmentId === this.attachment.attachmentId);
if (attachmentChange.isDeleted) {
this.refresh(); // all other updates are handled within AttachmentDetailWidget
}
}
}

@ -3,24 +3,24 @@ import server from "../../services/server.js";
import AttachmentDetailWidget from "../attachment_detail.js"; import AttachmentDetailWidget from "../attachment_detail.js";
const TPL = ` const TPL = `
<div class="attachments note-detail-printable"> <div class="attachment-list note-detail-printable">
<style> <style>
.attachments { .attachment-list {
padding: 15px; padding: 15px;
} }
</style> </style>
<div class="attachment-list"></div> <div class="attachment-list-wrapper"></div>
</div>`; </div>`;
export default class AttachmentsTypeWidget extends TypeWidget { export default class AttachmentListTypeWidget extends TypeWidget {
static getType() { static getType() {
return "attachments"; return "attachmentList";
} }
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$list = this.$widget.find('.attachment-list'); this.$list = this.$widget.find('.attachment-list-wrapper');
super.doRender(); super.doRender();
} }

@ -34,8 +34,10 @@ function index(req, res) {
instanceName: config.General ? config.General.instanceName : null, instanceName: config.General ? config.General.instanceName : null,
appCssNoteIds: getAppCssNoteIds(), appCssNoteIds: getAppCssNoteIds(),
isDev: env.isDev(), isDev: env.isDev(),
isMainWindow: !req.query.extra, isMainWindow: !req.query.extraWindow,
extraHoistedNoteId: req.query.extraHoistedNoteId, extraHoistedNoteId: req.query.extraHoistedNoteId,
// make sure only valid JSON gets rendered
extraViewScope: JSON.stringify(req.query.extraViewScope ? JSON.parse(req.query.extraViewScope) : {}),
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(), isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
maxContentWidth: parseInt(options.maxContentWidth), maxContentWidth: parseInt(options.maxContentWidth),
triliumVersion: packageJson.version, triliumVersion: packageJson.version,

@ -15,7 +15,7 @@ let mainWindow;
/** @type {Electron.BrowserWindow} */ /** @type {Electron.BrowserWindow} */
let setupWindow; let setupWindow;
async function createExtraWindow(notePath, hoistedNoteId = 'root') { async function createExtraWindow(notePath, hoistedNoteId = 'root', viewScope = {}) {
const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled'); const spellcheckEnabled = optionService.getOptionBool('spellCheckEnabled');
const {BrowserWindow} = require('electron'); const {BrowserWindow} = require('electron');
@ -35,13 +35,13 @@ async function createExtraWindow(notePath, hoistedNoteId = 'root') {
}); });
win.setMenuBarVisibility(false); win.setMenuBarVisibility(false);
win.loadURL(`http://127.0.0.1:${port}/?extra=1&extraHoistedNoteId=${hoistedNoteId}#${notePath}`); win.loadURL(`http://127.0.0.1:${port}/?extraWindow=1&extraHoistedNoteId=${hoistedNoteId}&extraViewScope=${JSON.stringify(viewScope)}#${notePath}`);
configureWebContents(win.webContents, spellcheckEnabled); configureWebContents(win.webContents, spellcheckEnabled);
} }
ipcMain.on('create-extra-window', (event, arg) => { ipcMain.on('create-extra-window', (event, arg) => {
createExtraWindow(arg.notePath, arg.hoistedNoteId); createExtraWindow(arg.notePath, arg.hoistedNoteId, arg.viewScope);
}); });
async function createMainWindow(app) { async function createMainWindow(app) {

@ -33,6 +33,7 @@
appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>, appCssNoteIds: <%- JSON.stringify(appCssNoteIds) %>,
isMainWindow: <%= isMainWindow %>, isMainWindow: <%= isMainWindow %>,
extraHoistedNoteId: '<%= extraHoistedNoteId %>', extraHoistedNoteId: '<%= extraHoistedNoteId %>',
extraViewScope: <%- extraViewScope %>,
isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>, isProtectedSessionAvailable: <%= isProtectedSessionAvailable %>,
triliumVersion: "<%= triliumVersion %>", triliumVersion: "<%= triliumVersion %>",
assetPath: "<%= assetPath %>", assetPath: "<%= assetPath %>",