mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-03 20:06:08 +01:00 
			
		
		
		
	attachment actions
This commit is contained in:
		@@ -136,6 +136,7 @@ class AbstractBeccaEntity {
 | 
				
			|||||||
        // client code asks to save entity even if blobId didn't change (something else was changed)
 | 
					        // client code asks to save entity even if blobId didn't change (something else was changed)
 | 
				
			||||||
        opts.forceSave = !!opts.forceSave;
 | 
					        opts.forceSave = !!opts.forceSave;
 | 
				
			||||||
        opts.forceCold = !!opts.forceCold;
 | 
					        opts.forceCold = !!opts.forceCold;
 | 
				
			||||||
 | 
					        opts.forceFrontendReload = !!opts.forceFrontendReload;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (content === null || content === undefined) {
 | 
					        if (content === null || content === undefined) {
 | 
				
			||||||
            throw new Error(`Cannot set null content to ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}'`);
 | 
					            throw new Error(`Cannot set null content to ${this.constructor.primaryKeyName} '${this[this.constructor.primaryKeyName]}'`);
 | 
				
			||||||
@@ -176,7 +177,7 @@ class AbstractBeccaEntity {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @protected */
 | 
					    /** @protected */
 | 
				
			||||||
    _saveBlob(content, unencryptedContentForHashCalculation, opts) {
 | 
					    _saveBlob(content, unencryptedContentForHashCalculation, opts = {}) {
 | 
				
			||||||
        let newBlobId;
 | 
					        let newBlobId;
 | 
				
			||||||
        let blobNeedsInsert;
 | 
					        let blobNeedsInsert;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -212,7 +213,10 @@ class AbstractBeccaEntity {
 | 
				
			|||||||
                hash: hash,
 | 
					                hash: hash,
 | 
				
			||||||
                isErased: false,
 | 
					                isErased: false,
 | 
				
			||||||
                utcDateChanged: pojo.utcDateModified,
 | 
					                utcDateChanged: pojo.utcDateModified,
 | 
				
			||||||
                isSynced: true
 | 
					                isSynced: true,
 | 
				
			||||||
 | 
					                // overriding componentId will cause frontend to think the change is coming from a different component
 | 
				
			||||||
 | 
					                // and thus reload
 | 
				
			||||||
 | 
					                componentId: opts.forceFrontendReload ? utils.randomString(10) : null
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            eventService.emit(eventService.ENTITY_CHANGED, {
 | 
					            eventService.emit(eventService.ENTITY_CHANGED, {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,6 +96,7 @@ class BAttachment extends AbstractBeccaEntity {
 | 
				
			|||||||
     * @param {object} [opts]
 | 
					     * @param {object} [opts]
 | 
				
			||||||
     * @param {object} [opts.forceSave=false] - will also save this BAttachment entity
 | 
					     * @param {object} [opts.forceSave=false] - will also save this BAttachment entity
 | 
				
			||||||
     * @param {object} [opts.forceCold=false] - blob has to be saved as cold
 | 
					     * @param {object} [opts.forceCold=false] - blob has to be saved as cold
 | 
				
			||||||
 | 
					     * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    setContent(content, opts) {
 | 
					    setContent(content, opts) {
 | 
				
			||||||
        this._setContent(content, opts);
 | 
					        this._setContent(content, opts);
 | 
				
			||||||
@@ -154,6 +155,12 @@ class BAttachment extends AbstractBeccaEntity {
 | 
				
			|||||||
        return { note, branch };
 | 
					        return { note, branch };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getFileName() {
 | 
				
			||||||
 | 
					        const type = this.role === 'image' ? 'image' : 'file';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return utils.formatDownloadTitle(this.title, type, this.mime);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    beforeSaving() {
 | 
					    beforeSaving() {
 | 
				
			||||||
        super.beforeSaving();
 | 
					        super.beforeSaving();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -242,6 +242,7 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
     * @param {object} [opts]
 | 
					     * @param {object} [opts]
 | 
				
			||||||
     * @param {object} [opts.forceSave=false] - will also save this BNote entity
 | 
					     * @param {object} [opts.forceSave=false] - will also save this BNote entity
 | 
				
			||||||
     * @param {object} [opts.forceCold=false] - blob has to be saved as cold
 | 
					     * @param {object} [opts.forceCold=false] - blob has to be saved as cold
 | 
				
			||||||
 | 
					     * @param {object} [opts.forceFrontendReload=false] - override frontend heuristics on when to reload, instruct to reload
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    setContent(content, opts) {
 | 
					    setContent(content, opts) {
 | 
				
			||||||
        this._setContent(content, opts);
 | 
					        this._setContent(content, opts);
 | 
				
			||||||
@@ -1642,6 +1643,10 @@ class BNote extends AbstractBeccaEntity {
 | 
				
			|||||||
        return attachment;
 | 
					        return attachment;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getFileName() {
 | 
				
			||||||
 | 
					        return utils.formatDownloadTitle(this.title, this.type, this.mime);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    beforeSaving() {
 | 
					    beforeSaving() {
 | 
				
			||||||
        super.beforeSaving();
 | 
					        super.beforeSaving();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ import NoteIconWidget from "../widgets/note_icon.js";
 | 
				
			|||||||
import SearchResultWidget from "../widgets/search_result.js";
 | 
					import SearchResultWidget from "../widgets/search_result.js";
 | 
				
			||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
 | 
					import ScrollingContainer from "../widgets/containers/scrolling_container.js";
 | 
				
			||||||
import RootContainer from "../widgets/containers/root_container.js";
 | 
					import RootContainer from "../widgets/containers/root_container.js";
 | 
				
			||||||
import NoteUpdateStatusWidget from "../widgets/note_update_status.js";
 | 
					import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
 | 
				
			||||||
import SpacerWidget from "../widgets/spacer.js";
 | 
					import SpacerWidget from "../widgets/spacer.js";
 | 
				
			||||||
import QuickSearchWidget from "../widgets/quick_search.js";
 | 
					import QuickSearchWidget from "../widgets/quick_search.js";
 | 
				
			||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
 | 
					import SplitNoteContainer from "../widgets/containers/split_note_container.js";
 | 
				
			||||||
@@ -150,7 +150,7 @@ export default class DesktopLayout {
 | 
				
			|||||||
                                            .button(new NoteActionsWidget())
 | 
					                                            .button(new NoteActionsWidget())
 | 
				
			||||||
                                    )
 | 
					                                    )
 | 
				
			||||||
                                    .child(new SharedInfoWidget())
 | 
					                                    .child(new SharedInfoWidget())
 | 
				
			||||||
                                    .child(new NoteUpdateStatusWidget())
 | 
					                                    .child(new WatchedFileUpdateStatusWidget())
 | 
				
			||||||
                                    .child(new FloatingButtons()
 | 
					                                    .child(new FloatingButtons()
 | 
				
			||||||
                                        .child(new EditButton())
 | 
					                                        .child(new EditButton())
 | 
				
			||||||
                                        .child(new CodeButtonsWidget())
 | 
					                                        .child(new CodeButtonsWidget())
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,33 @@
 | 
				
			|||||||
import ws from "./ws.js";
 | 
					import ws from "./ws.js";
 | 
				
			||||||
import appContext from "../components/app_context.js";
 | 
					import appContext from "../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fileModificationStatus = {};
 | 
					const fileModificationStatus = {
 | 
				
			||||||
 | 
					    notes: {},
 | 
				
			||||||
 | 
					    attachments: {}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getFileModificationStatus(noteId) {
 | 
					function checkType(type) {
 | 
				
			||||||
    return fileModificationStatus[noteId];
 | 
					    if (type !== 'notes' && type !== 'attachments') {
 | 
				
			||||||
 | 
					        throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function fileModificationUploaded(noteId) {
 | 
					function getFileModificationStatus(entityType, entityId) {
 | 
				
			||||||
    delete fileModificationStatus[noteId];
 | 
					    checkType(entityType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return fileModificationStatus[entityType][entityId];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function ignoreModification(noteId) {
 | 
					function fileModificationUploaded(entityType, entityId) {
 | 
				
			||||||
    delete fileModificationStatus[noteId];
 | 
					    checkType(entityType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    delete fileModificationStatus[entityType][entityId];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function ignoreModification(entityType, entityId) {
 | 
				
			||||||
 | 
					    checkType(entityType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    delete fileModificationStatus[entityType][entityId];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ws.subscribeToMessages(async message => {
 | 
					ws.subscribeToMessages(async message => {
 | 
				
			||||||
@@ -20,10 +35,13 @@ ws.subscribeToMessages(async message => {
 | 
				
			|||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fileModificationStatus[message.noteId] = message;
 | 
					    checkType(message.entityType);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fileModificationStatus[message.entityType][message.entityId] = message;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    appContext.triggerEvent('openedFileUpdated', {
 | 
					    appContext.triggerEvent('openedFileUpdated', {
 | 
				
			||||||
        noteId: message.noteId,
 | 
					        entityType: message.entityType,
 | 
				
			||||||
 | 
					        entityId: message.entityId,
 | 
				
			||||||
        lastModifiedMs: message.lastModifiedMs,
 | 
					        lastModifiedMs: message.lastModifiedMs,
 | 
				
			||||||
        filePath: message.filePath
 | 
					        filePath: message.filePath
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,7 +39,7 @@ async function processEntityChanges(entityChanges) {
 | 
				
			|||||||
                // NOOP
 | 
					                // NOOP
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else {
 | 
					            else {
 | 
				
			||||||
                throw new Error(`Unknown entityName ${ec.entityName}`);
 | 
					                throw new Error(`Unknown entityName '${ec.entityName}'`);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (e) {
 | 
					        catch (e) {
 | 
				
			||||||
@@ -92,7 +92,7 @@ function processNoteChange(loadResults, ec) {
 | 
				
			|||||||
    loadResults.addNote(ec.entityId, ec.componentId);
 | 
					    loadResults.addNote(ec.entityId, ec.componentId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (ec.isErased && ec.entityId in froca.notes) {
 | 
					    if (ec.isErased && ec.entityId in froca.notes) {
 | 
				
			||||||
        utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
 | 
					        utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -106,7 +106,7 @@ function processNoteChange(loadResults, ec) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
async function processBranchChange(loadResults, ec) {
 | 
					async function processBranchChange(loadResults, ec) {
 | 
				
			||||||
    if (ec.isErased && ec.entityId in froca.branches) {
 | 
					    if (ec.isErased && ec.entityId in froca.branches) {
 | 
				
			||||||
        utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
 | 
					        utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -192,7 +192,7 @@ function processAttributeChange(loadResults, ec) {
 | 
				
			|||||||
    let attribute = froca.attributes[ec.entityId];
 | 
					    let attribute = froca.attributes[ec.entityId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (ec.isErased && ec.entityId in froca.attributes) {
 | 
					    if (ec.isErased && ec.entityId in froca.attributes) {
 | 
				
			||||||
        utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
 | 
					        utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -241,7 +241,7 @@ function processAttributeChange(loadResults, ec) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
function processAttachment(loadResults, ec) {
 | 
					function processAttachment(loadResults, ec) {
 | 
				
			||||||
    if (ec.isErased && ec.entityId in froca.attachments) {
 | 
					    if (ec.isErased && ec.entityId in froca.attachments) {
 | 
				
			||||||
        utils.reloadFrontendApp(`${ec.entityName} ${ec.entityId} is erased, need to do complete reload.`);
 | 
					        utils.reloadFrontendApp(`${ec.entityName} '${ec.entityId}' is erased, need to do complete reload.`);
 | 
				
			||||||
        return;
 | 
					        return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										32
									
								
								src/public/app/services/image.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/public/app/services/image.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
				
			|||||||
 | 
					import toastService from "./toast.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function copyImageReferenceToClipboard($imageWrapper) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					        $imageWrapper.attr('contenteditable', 'true');
 | 
				
			||||||
 | 
					        selectImage($imageWrapper.get(0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const success = document.execCommand('copy');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (success) {
 | 
				
			||||||
 | 
					            toastService.showMessage("Image copied to the clipboard");
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            toastService.showAndLogError("Could not copy the image to clipboard.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    finally {
 | 
				
			||||||
 | 
					        window.getSelection().removeAllRanges();
 | 
				
			||||||
 | 
					        $imageWrapper.removeAttr('contenteditable');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function selectImage(element) {
 | 
				
			||||||
 | 
					    const selection = window.getSelection();
 | 
				
			||||||
 | 
					    const range = document.createRange();
 | 
				
			||||||
 | 
					    range.selectNodeContents(element);
 | 
				
			||||||
 | 
					    selection.removeAllRanges();
 | 
				
			||||||
 | 
					    selection.addRange(range);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default {
 | 
				
			||||||
 | 
					    copyImageReferenceToClipboard
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
@@ -125,8 +125,6 @@ function calculateHash({notePath, ntxId, hoistedNoteId, viewScope = {}}) {
 | 
				
			|||||||
        hash += `?${paramStr}`;
 | 
					        hash += `?${paramStr}`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    console.log(hash);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return hash;
 | 
					    return hash;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,22 @@
 | 
				
			|||||||
import utils from "./utils.js";
 | 
					import utils from "./utils.js";
 | 
				
			||||||
import server from "./server.js";
 | 
					import server from "./server.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getFileUrl(noteId) {
 | 
					function checkType(type) {
 | 
				
			||||||
    return getUrlForDownload(`api/notes/${noteId}/download`);
 | 
					    if (type !== 'notes' && type !== 'attachments') {
 | 
				
			||||||
 | 
					        throw new Error(`Unrecognized type '${type}', should be 'notes' or 'attachments'`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
function getOpenFileUrl(noteId) {
 | 
					}
 | 
				
			||||||
    return getUrlForDownload(`api/notes/${noteId}/open`);
 | 
					
 | 
				
			||||||
 | 
					function getFileUrl(type, noteId) {
 | 
				
			||||||
 | 
					    checkType(type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return getUrlForDownload(`api/${type}/${noteId}/download`);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function getOpenFileUrl(type, noteId) {
 | 
				
			||||||
 | 
					    checkType(type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return getUrlForDownload(`api/${type}/${noteId}/open`);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function download(url) {
 | 
					function download(url) {
 | 
				
			||||||
@@ -19,32 +30,15 @@ function download(url) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function downloadFileNote(noteId) {
 | 
					function downloadFileNote(noteId) {
 | 
				
			||||||
    const url = `${getFileUrl(noteId)}?${Date.now()}`; // don't use cache
 | 
					    const url = `${getFileUrl('notes', noteId)}?${Date.now()}`; // don't use cache
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    download(url);
 | 
					    download(url);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function openNoteExternally(noteId, mime) {
 | 
					function downloadAttachment(attachmentId) {
 | 
				
			||||||
    if (utils.isElectron()) {
 | 
					    const url = `${getFileUrl('attachments', attachmentId)}?${Date.now()}`; // don't use cache
 | 
				
			||||||
        const resp = await server.post(`notes/${noteId}/save-to-tmp-dir`);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const electron = utils.dynamicRequire('electron');
 | 
					    download(url);
 | 
				
			||||||
        const res = await electron.shell.openPath(resp.tmpFilePath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (res) {
 | 
					 | 
				
			||||||
            // fallback in case there's no default application for this file
 | 
					 | 
				
			||||||
            open(getFileUrl(noteId), {url: true});
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    else {
 | 
					 | 
				
			||||||
        // allow browser to handle opening common file
 | 
					 | 
				
			||||||
         if (mime === "application/pdf" ||  mime.startsWith("image") || mime.startsWith("audio") || mime.startsWith("video")){
 | 
					 | 
				
			||||||
            window.open(getOpenFileUrl(noteId));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
         else {
 | 
					 | 
				
			||||||
            window.location.href = getFileUrl(noteId);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function downloadNoteRevision(noteId, noteRevisionId) {
 | 
					function downloadNoteRevision(noteId, noteRevisionId) {
 | 
				
			||||||
@@ -67,6 +61,40 @@ function getUrlForDownload(url) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function canOpenInBrowser(mime) {
 | 
				
			||||||
 | 
					    return mime === "application/pdf"
 | 
				
			||||||
 | 
					        || mime.startsWith("image")
 | 
				
			||||||
 | 
					        || mime.startsWith("audio")
 | 
				
			||||||
 | 
					        || mime.startsWith("video");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function openExternally(type, entityId, mime) {
 | 
				
			||||||
 | 
					    checkType(type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (utils.isElectron()) {
 | 
				
			||||||
 | 
					        const resp = await server.post(`${type}/${entityId}/save-to-tmp-dir`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const electron = utils.dynamicRequire('electron');
 | 
				
			||||||
 | 
					        const res = await electron.shell.openPath(resp.tmpFilePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (res) {
 | 
				
			||||||
 | 
					            // fallback in case there's no default application for this file
 | 
				
			||||||
 | 
					            window.open(getFileUrl(type, entityId), { url: true });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    else {
 | 
				
			||||||
 | 
					        // allow browser to handle opening common file
 | 
				
			||||||
 | 
					        if (canOpenInBrowser(mime)) {
 | 
				
			||||||
 | 
					            window.open(getOpenFileUrl(type, entityId));
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            window.location.href = getFileUrl(type, entityId);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const openNoteExternally = async (noteId, mime) => await openExternally('notes', noteId, mime);
 | 
				
			||||||
 | 
					const openAttachmentExternally = async (attachmentId, mime) => await openExternally('attachments', attachmentId, mime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getHost() {
 | 
					function getHost() {
 | 
				
			||||||
    const url = new URL(window.location.href);
 | 
					    const url = new URL(window.location.href);
 | 
				
			||||||
    return `${url.protocol}//${url.hostname}:${url.port}`;
 | 
					    return `${url.protocol}//${url.hostname}:${url.port}`;
 | 
				
			||||||
@@ -75,7 +103,9 @@ function getHost() {
 | 
				
			|||||||
export default {
 | 
					export default {
 | 
				
			||||||
    download,
 | 
					    download,
 | 
				
			||||||
    downloadFileNote,
 | 
					    downloadFileNote,
 | 
				
			||||||
    openNoteExternally,
 | 
					 | 
				
			||||||
    downloadNoteRevision,
 | 
					    downloadNoteRevision,
 | 
				
			||||||
    getUrlForDownload
 | 
					    downloadAttachment,
 | 
				
			||||||
 | 
					    getUrlForDownload,
 | 
				
			||||||
 | 
					    openNoteExternally,
 | 
				
			||||||
 | 
					    openAttachmentExternally,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import AttachmentActionsWidget from "./buttons/attachments_actions.js";
 | 
				
			|||||||
import BasicWidget from "./basic_widget.js";
 | 
					import BasicWidget from "./basic_widget.js";
 | 
				
			||||||
import server from "../services/server.js";
 | 
					import server from "../services/server.js";
 | 
				
			||||||
import options from "../services/options.js";
 | 
					import options from "../services/options.js";
 | 
				
			||||||
 | 
					import imageService from "../services/image.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="attachment-detail">
 | 
					<div class="attachment-detail">
 | 
				
			||||||
@@ -148,6 +149,10 @@ export default class AttachmentDetailWidget extends BasicWidget {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    copyAttachmentReferenceToClipboard() {
 | 
				
			||||||
 | 
					        imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content'));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async entitiesReloadedEvent({loadResults}) {
 | 
					    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);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import dialogService from "../../services/dialog.js";
 | 
				
			|||||||
import toastService from "../../services/toast.js";
 | 
					import toastService from "../../services/toast.js";
 | 
				
			||||||
import ws from "../../services/ws.js";
 | 
					import ws from "../../services/ws.js";
 | 
				
			||||||
import appContext from "../../components/app_context.js";
 | 
					import appContext from "../../components/app_context.js";
 | 
				
			||||||
 | 
					import openService from "../../services/open.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="dropdown attachment-actions">
 | 
					<div class="dropdown attachment-actions">
 | 
				
			||||||
@@ -28,8 +29,15 @@ const TPL = `
 | 
				
			|||||||
        aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded"></button>
 | 
					        aria-expanded="false" class="icon-action icon-action-always-border bx bx-dots-vertical-rounded"></button>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="dropdown-menu dropdown-menu-right">
 | 
					    <div class="dropdown-menu dropdown-menu-right">
 | 
				
			||||||
        <a data-trigger-command="deleteAttachment" class="dropdown-item">Delete attachment</a>
 | 
					        <a data-trigger-command="openAttachment" class="dropdown-item">Open</a>
 | 
				
			||||||
 | 
					        <a data-trigger-command="openAttachmentExternally" class="dropdown-item"
 | 
				
			||||||
 | 
					           title="File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.">
 | 
				
			||||||
 | 
					            Open externally</a>
 | 
				
			||||||
 | 
					        <a data-trigger-command="downloadAttachment" class="dropdown-item">Download</a>
 | 
				
			||||||
 | 
					        <a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">Upload new revision</a>
 | 
				
			||||||
 | 
					        <a data-trigger-command="copyAttachmentReferenceToClipboard" class="dropdown-item">Copy reference to clipboard</a>
 | 
				
			||||||
        <a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">Convert attachment into note</a>
 | 
					        <a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">Convert attachment into note</a>
 | 
				
			||||||
 | 
					        <a data-trigger-command="deleteAttachment" class="dropdown-item">Delete attachment</a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>`;
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -40,9 +48,30 @@ export default class AttachmentActionsWidget extends BasicWidget {
 | 
				
			|||||||
        this.attachment = attachment;
 | 
					        this.attachment = attachment;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    get attachmentId() {
 | 
				
			||||||
 | 
					        return this.attachment.attachmentId;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    doRender() {
 | 
					    doRender() {
 | 
				
			||||||
        this.$widget = $(TPL);
 | 
					        this.$widget = $(TPL);
 | 
				
			||||||
        this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
 | 
					        this.$widget.on('click', '.dropdown-item', () => this.$widget.find("[data-toggle='dropdown']").dropdown('toggle'));
 | 
				
			||||||
 | 
					        this.$widget.find("[data-trigger-command='copyAttachmentReferenceToClipboard']").toggle(this.attachment.role === 'image');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async openAttachmentCommand() {
 | 
				
			||||||
 | 
					        await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async downloadAttachmentCommand() {
 | 
				
			||||||
 | 
					        await openService.downloadAttachment(this.attachmentId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async copyAttachmentReferenceToClipboardCommand() {
 | 
				
			||||||
 | 
					        this.parent.copyAttachmentReferenceToClipboard();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async openAttachmentExternallyCommand() {
 | 
				
			||||||
 | 
					        await openService.openAttachmentExternally(this.attachmentId, this.attachment.mime);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async deleteAttachmentCommand() {
 | 
					    async deleteAttachmentCommand() {
 | 
				
			||||||
@@ -50,7 +79,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await server.remove(`attachments/${this.attachment.attachmentId}`);
 | 
					        await server.remove(`attachments/${this.attachmentId}`);
 | 
				
			||||||
        toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`);
 | 
					        toastService.showMessage(`Attachment '${this.attachment.title}' has been deleted.`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -59,7 +88,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const {note: newNote} = await server.post(`attachments/${this.attachment.attachmentId}/convert-to-note`)
 | 
					        const {note: newNote} = await server.post(`attachments/${this.attachmentId}/convert-to-note`)
 | 
				
			||||||
        toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`);
 | 
					        toastService.showMessage(`Attachment '${this.attachment.title}' has been converted to note.`);
 | 
				
			||||||
        await ws.waitForMaxKnownEntityChangeId();
 | 
					        await ws.waitForMaxKnownEntityChangeId();
 | 
				
			||||||
        await appContext.tabManager.getActiveContext().setNote(newNote.noteId);
 | 
					        await appContext.tabManager.getActiveContext().setNote(newNote.noteId);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,7 +35,11 @@ const TPL = `
 | 
				
			|||||||
        <a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a>
 | 
					        <a data-trigger-command="findInText" class="dropdown-item find-in-text-button">Search in note <kbd data-command="findInText"></a>
 | 
				
			||||||
        <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
 | 
					        <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> Note source</a>
 | 
				
			||||||
        <a data-trigger-command="showAttachments" class="dropdown-item"><kbd data-command="showAttachments"></kbd> Note attachments</a>
 | 
					        <a data-trigger-command="showAttachments" class="dropdown-item"><kbd data-command="showAttachments"></kbd> Note attachments</a>
 | 
				
			||||||
        <a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"><kbd data-command="openNoteExternally"></kbd> Open note externally</a>
 | 
					        <a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"
 | 
				
			||||||
 | 
					           title="File will be open in an external application and watched for changes. You'll then be able to upload the modified version back to Trilium.">
 | 
				
			||||||
 | 
					            <kbd data-command="openNoteExternally"></kbd> 
 | 
				
			||||||
 | 
					            Open note externally
 | 
				
			||||||
 | 
					        </a>
 | 
				
			||||||
        <a class="dropdown-item import-files-button">Import files</a>
 | 
					        <a class="dropdown-item import-files-button">Import files</a>
 | 
				
			||||||
        <a class="dropdown-item export-note-button">Export note</a>
 | 
					        <a class="dropdown-item export-note-button">Export note</a>
 | 
				
			||||||
        <a class="dropdown-item delete-note-button">Delete note</a>
 | 
					        <a class="dropdown-item delete-note-button">Delete note</a>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,64 +0,0 @@
 | 
				
			|||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
					 | 
				
			||||||
import server from "../services/server.js";
 | 
					 | 
				
			||||||
import fileWatcher from "../services/file_watcher.js";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const TPL = `
 | 
					 | 
				
			||||||
<div class="dropdown note-update-status-widget alert alert-warning">
 | 
					 | 
				
			||||||
    <style>
 | 
					 | 
				
			||||||
        .note-update-status-widget {
 | 
					 | 
				
			||||||
            margin: 10px;
 | 
					 | 
				
			||||||
            contain: none;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    </style>
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    <p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p> 
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
    <div style="display: flex; flex-direction: row; justify-content: space-evenly;">
 | 
					 | 
				
			||||||
        <button class="btn btn-sm file-upload-button">Upload modified file</button>
 | 
					 | 
				
			||||||
        
 | 
					 | 
				
			||||||
        <button class="btn btn-sm ignore-this-change-button">Ignore this change</button>
 | 
					 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
</div>`;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default class NoteUpdateStatusWidget extends NoteContextAwareWidget {
 | 
					 | 
				
			||||||
    isEnabled() {
 | 
					 | 
				
			||||||
        return super.isEnabled()
 | 
					 | 
				
			||||||
            && !!fileWatcher.getFileModificationStatus(this.noteId);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    doRender() {
 | 
					 | 
				
			||||||
        this.$widget = $(TPL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$filePath = this.$widget.find(".file-path");
 | 
					 | 
				
			||||||
        this.$fileLastModified = this.$widget.find(".file-last-modified");
 | 
					 | 
				
			||||||
        this.$fileUploadButton = this.$widget.find(".file-upload-button");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$fileUploadButton.on("click", async () => {
 | 
					 | 
				
			||||||
            await server.post(`notes/${this.noteId}/upload-modified-file`, {
 | 
					 | 
				
			||||||
                filePath: this.$filePath.text()
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            fileWatcher.fileModificationUploaded(this.noteId);
 | 
					 | 
				
			||||||
            this.refresh();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
 | 
					 | 
				
			||||||
        this.$ignoreThisChangeButton.on('click', () => {
 | 
					 | 
				
			||||||
            fileWatcher.ignoreModification(this.noteId);
 | 
					 | 
				
			||||||
            this.refresh();
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    refreshWithNote(note) {
 | 
					 | 
				
			||||||
        const status = fileWatcher.getFileModificationStatus(note.noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        this.$filePath.text(status.filePath);
 | 
					 | 
				
			||||||
        this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    openedFileUpdatedEvent(data) {
 | 
					 | 
				
			||||||
        if (data.noteId === this.noteId) {
 | 
					 | 
				
			||||||
            this.refresh();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,8 +1,8 @@
 | 
				
			|||||||
import utils from "../../services/utils.js";
 | 
					import utils from "../../services/utils.js";
 | 
				
			||||||
import toastService from "../../services/toast.js";
 | 
					 | 
				
			||||||
import TypeWidget from "./type_widget.js";
 | 
					import TypeWidget from "./type_widget.js";
 | 
				
			||||||
import libraryLoader from "../../services/library_loader.js";
 | 
					import libraryLoader from "../../services/library_loader.js";
 | 
				
			||||||
import contextMenu from "../../menus/context_menu.js";
 | 
					import contextMenu from "../../menus/context_menu.js";
 | 
				
			||||||
 | 
					import imageService from "../../services/image.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="note-detail-image note-detail-printable">
 | 
					<div class="note-detail-image note-detail-printable">
 | 
				
			||||||
@@ -73,7 +73,7 @@ class ImageTypeWidget extends TypeWidget {
 | 
				
			|||||||
                    ],
 | 
					                    ],
 | 
				
			||||||
                    selectMenuItemHandler: ({command}) => {
 | 
					                    selectMenuItemHandler: ({command}) => {
 | 
				
			||||||
                        if (command === 'copyImageReferenceToClipboard') {
 | 
					                        if (command === 'copyImageReferenceToClipboard') {
 | 
				
			||||||
                            this.copyImageReferenceToClipboard();
 | 
					                            imageService.copyImageReferenceToClipboard(this.$imageWrapper);
 | 
				
			||||||
                        } else if (command === 'copyImageToClipboard') {
 | 
					                        } else if (command === 'copyImageToClipboard') {
 | 
				
			||||||
                            const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
 | 
					                            const webContents = utils.dynamicRequire('@electron/remote').getCurrentWebContents();
 | 
				
			||||||
                            utils.dynamicRequire('electron');
 | 
					                            utils.dynamicRequire('electron');
 | 
				
			||||||
@@ -98,36 +98,7 @@ class ImageTypeWidget extends TypeWidget {
 | 
				
			|||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.copyImageReferenceToClipboard();
 | 
					        imageService.copyImageReferenceToClipboard(this.$imageWrapper);
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    copyImageReferenceToClipboard() {
 | 
					 | 
				
			||||||
        this.$imageWrapper.attr('contenteditable','true');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            this.selectImage(this.$imageWrapper.get(0));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            const success = document.execCommand('copy');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (success) {
 | 
					 | 
				
			||||||
                toastService.showMessage("Image copied to the clipboard");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                toastService.showAndLogError("Could not copy the image to clipboard.");
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        finally {
 | 
					 | 
				
			||||||
            window.getSelection().removeAllRanges();
 | 
					 | 
				
			||||||
            this.$imageWrapper.removeAttr('contenteditable');
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    selectImage(element) {
 | 
					 | 
				
			||||||
        const selection = window.getSelection();
 | 
					 | 
				
			||||||
        const range = document.createRange();
 | 
					 | 
				
			||||||
        range.selectNodeContents(element);
 | 
					 | 
				
			||||||
        selection.removeAllRanges();
 | 
					 | 
				
			||||||
        selection.addRange(range);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										96
									
								
								src/public/app/widgets/watched_file_update_status.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/public/app/widgets/watched_file_update_status.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
				
			||||||
 | 
					import server from "../services/server.js";
 | 
				
			||||||
 | 
					import fileWatcher from "../services/file_watcher.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const TPL = `
 | 
				
			||||||
 | 
					<div class="dropdown watched-file-update-status-widget alert alert-warning">
 | 
				
			||||||
 | 
					    <style>
 | 
				
			||||||
 | 
					        .watched-file-update-status-widget {
 | 
				
			||||||
 | 
					            margin: 10px;
 | 
				
			||||||
 | 
					            contain: none;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    </style>
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    <p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p> 
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					    <div style="display: flex; flex-direction: row; justify-content: space-evenly;">
 | 
				
			||||||
 | 
					        <button class="btn btn-sm file-upload-button">Upload modified file</button>
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        <button class="btn btn-sm ignore-this-change-button">Ignore this change</button>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class WatchedFileUpdateStatusWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					    isEnabled() {
 | 
				
			||||||
 | 
					        const { entityType, entityId } = this.getEntity();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log(entityType, entityId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return super.isEnabled() && !!fileWatcher.getFileModificationStatus(entityType, entityId);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    doRender() {
 | 
				
			||||||
 | 
					        this.$widget = $(TPL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$filePath = this.$widget.find(".file-path");
 | 
				
			||||||
 | 
					        this.$fileLastModified = this.$widget.find(".file-last-modified");
 | 
				
			||||||
 | 
					        this.$fileUploadButton = this.$widget.find(".file-upload-button");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$fileUploadButton.on("click", async () => {
 | 
				
			||||||
 | 
					            const { entityType, entityId } = this.getEntity();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await server.post(`${entityType}/${entityId}/upload-modified-file`, {
 | 
				
			||||||
 | 
					                filePath: this.$filePath.text()
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            fileWatcher.fileModificationUploaded(entityType, entityId);
 | 
				
			||||||
 | 
					            this.refresh();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$ignoreThisChangeButton = this.$widget.find(".ignore-this-change-button");
 | 
				
			||||||
 | 
					        this.$ignoreThisChangeButton.on('click', () => {
 | 
				
			||||||
 | 
					            const { entityType, entityId } = this.getEntity();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            fileWatcher.ignoreModification(entityType, entityId);
 | 
				
			||||||
 | 
					            this.refresh();
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    refreshWithNote(note) {
 | 
				
			||||||
 | 
					        const { entityType, entityId } = this.getEntity();
 | 
				
			||||||
 | 
					        const status = fileWatcher.getFileModificationStatus(entityType, entityId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        console.log("status", status);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$filePath.text(status.filePath);
 | 
				
			||||||
 | 
					        this.$fileLastModified.text(dayjs.unix(status.lastModifiedMs / 1000).format("HH:mm:ss"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getEntity() {
 | 
				
			||||||
 | 
					        if (!this.noteContext) {
 | 
				
			||||||
 | 
					            return {};
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const { viewScope } = this.noteContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (viewScope.viewMode === 'attachments' && viewScope.attachmentId) {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                entityType: 'attachments',
 | 
				
			||||||
 | 
					                entityId: viewScope.attachmentId
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					                entityType: 'notes',
 | 
				
			||||||
 | 
					                entityId: this.noteId
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    openedFileUpdatedEvent(data) {console.log(data);
 | 
				
			||||||
 | 
					        const { entityType, entityId } = this.getEntity();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (data.entityType === entityType && data.entityId === entityId) {
 | 
				
			||||||
 | 
					            this.refresh();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -11,6 +11,7 @@ const chokidar = require('chokidar');
 | 
				
			|||||||
const ws = require('../../services/ws');
 | 
					const ws = require('../../services/ws');
 | 
				
			||||||
const becca = require("../../becca/becca");
 | 
					const becca = require("../../becca/becca");
 | 
				
			||||||
const NotFoundError = require("../../errors/not_found_error");
 | 
					const NotFoundError = require("../../errors/not_found_error");
 | 
				
			||||||
 | 
					const ValidationError = require("../../errors/validation_error.js");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function updateFile(req) {
 | 
					function updateFile(req) {
 | 
				
			||||||
    const {noteId} = req.params;
 | 
					    const {noteId} = req.params;
 | 
				
			||||||
@@ -38,61 +39,84 @@ function updateFile(req) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getFilename(note) {
 | 
					/**
 | 
				
			||||||
    // (one) reason we're not using the originFileName (available as label) is that it's not
 | 
					 * @param {BNote|BAttachment} noteOrAttachment
 | 
				
			||||||
    // available for older note revisions and thus would be inconsistent
 | 
					 * @param res
 | 
				
			||||||
    return utils.formatDownloadTitle(note.title, note.type, note.mime);
 | 
					 * @param {boolean} contentDisposition
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function downloadData(noteOrAttachment, res, contentDisposition) {
 | 
				
			||||||
 | 
					    if (noteOrAttachment.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
 | 
				
			||||||
 | 
					        return res.status(401).send("Protected session not available");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function downloadNoteFile(noteId, res, contentDisposition = true) {
 | 
					    if (contentDisposition) {
 | 
				
			||||||
 | 
					        const fileName = noteOrAttachment.getFileName();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        res.setHeader('Content-Disposition', utils.getContentDisposition(fileName));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
				
			||||||
 | 
					    res.setHeader('Content-Type', noteOrAttachment.mime);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    res.send(noteOrAttachment.getContent());
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function downloadNoteInt(noteId, res, contentDisposition = true) {
 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!note) {
 | 
					    if (!note) {
 | 
				
			||||||
        return res.setHeader("Content-Type", "text/plain")
 | 
					        return res.setHeader("Content-Type", "text/plain")
 | 
				
			||||||
            .status(404)
 | 
					            .status(404)
 | 
				
			||||||
            .send(`Note ${noteId} doesn't exist.`);
 | 
					            .send(`Note '${noteId}' doesn't exist.`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.isProtected && !protectedSessionService.isProtectedSessionAvailable()) {
 | 
					    return downloadData(note, res, contentDisposition);
 | 
				
			||||||
        return res.status(401).send("Protected session not available");
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (contentDisposition) {
 | 
					function downloadAttachmentInt(attachmentId, res, contentDisposition = true) {
 | 
				
			||||||
        const filename = getFilename(note);
 | 
					    const attachment = becca.getAttachment(attachmentId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        res.setHeader('Content-Disposition', utils.getContentDisposition(filename));
 | 
					    if (!attachment) {
 | 
				
			||||||
 | 
					        return res.setHeader("Content-Type", "text/plain")
 | 
				
			||||||
 | 
					            .status(404)
 | 
				
			||||||
 | 
					            .send(`Attachment '${attachmentId}' doesn't exist.`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
 | 
					    return downloadData(attachment, res, contentDisposition);
 | 
				
			||||||
    res.setHeader('Content-Type', note.mime);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    res.send(note.getContent());
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function downloadFile(req, res) {
 | 
					const downloadFile = (req, res) => downloadNoteInt(req.params.noteId, res, true);
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					const openFile = (req, res) => downloadNoteInt(req.params.noteId, res, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return downloadNoteFile(noteId, res);
 | 
					const downloadAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, true);
 | 
				
			||||||
}
 | 
					const openAttachment = (req, res) => downloadAttachmentInt(req.params.attachmentId, res, false);
 | 
				
			||||||
 | 
					 | 
				
			||||||
function openFile(req, res) {
 | 
					 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return downloadNoteFile(noteId, res, false);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
function fileContentProvider(req) {
 | 
					function fileContentProvider(req) {
 | 
				
			||||||
    // Read file name from route params.
 | 
					    // Read file name from route params.
 | 
				
			||||||
    const note = becca.getNote(req.params.noteId);
 | 
					    const note = becca.getNote(req.params.noteId);
 | 
				
			||||||
    const fileName = getFilename(note);
 | 
					    if (!note) {
 | 
				
			||||||
    let content = note.getContent();
 | 
					        throw new NotFoundError(`Note '${req.params.noteId}' doesn't exist.`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return streamContent(note.getContent(), note.getFileName(), note.mime);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function attachmentContentProvider(req) {
 | 
				
			||||||
 | 
					    // Read file name from route params.
 | 
				
			||||||
 | 
					    const attachment = becca.getAttachment(req.params.attachmentId);
 | 
				
			||||||
 | 
					    if (!attachment) {
 | 
				
			||||||
 | 
					        throw new NotFoundError(`Attachment '${req.params.attachmentId}' doesn't exist.`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return streamContent(attachment.getContent(), attachment.getFileName(), attachment.mime);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function streamContent(content, fileName, mimeType) {
 | 
				
			||||||
    if (typeof content === "string") {
 | 
					    if (typeof content === "string") {
 | 
				
			||||||
        content = Buffer.from(content, 'utf8');
 | 
					        content = Buffer.from(content, 'utf8');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const totalSize = content.byteLength;
 | 
					    const totalSize = content.byteLength;
 | 
				
			||||||
    const mimeType = note.mime;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const getStream = range => {
 | 
					    const getStream = range => {
 | 
				
			||||||
        if (!range) {
 | 
					        if (!range) {
 | 
				
			||||||
@@ -113,27 +137,44 @@ function fileContentProvider(req) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function saveToTmpDir(req) {
 | 
					function saveNoteToTmpDir(req) {
 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					    const note = becca.getNote(req.params.noteId);
 | 
				
			||||||
 | 
					 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!note) {
 | 
					    if (!note) {
 | 
				
			||||||
        throw new NotFoundError(`Note '${noteId}' doesn't exist.`);
 | 
					        throw new NotFoundError(`Note '${req.params.noteId}' doesn't exist.`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const tmpObj = tmp.fileSync({postfix: getFilename(note)});
 | 
					    const fileName = note.getFileName();
 | 
				
			||||||
 | 
					    const content = note.getContent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fs.writeSync(tmpObj.fd, note.getContent());
 | 
					    return saveToTmpDir(fileName, content, 'notes', note.noteId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function saveAttachmentToTmpDir(req) {
 | 
				
			||||||
 | 
					    const attachment = becca.getAttachment(req.params.attachmentId);
 | 
				
			||||||
 | 
					    if (!attachment) {
 | 
				
			||||||
 | 
					        throw new NotFoundError(`Attachment '${req.params.attachmentId}' doesn't exist.`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fileName = attachment.getFileName();
 | 
				
			||||||
 | 
					    const content = attachment.getContent();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return saveToTmpDir(fileName, content, 'attachments', attachment.attachmentId);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function saveToTmpDir(fileName, content, entityType, entityId) {
 | 
				
			||||||
 | 
					    const tmpObj = tmp.fileSync({ postfix: fileName });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fs.writeSync(tmpObj.fd, content);
 | 
				
			||||||
    fs.closeSync(tmpObj.fd);
 | 
					    fs.closeSync(tmpObj.fd);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.info(`Saved temporary file for note ${noteId} into ${tmpObj.name}`);
 | 
					    log.info(`Saved temporary file ${tmpObj.name}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (utils.isElectron()) {
 | 
					    if (utils.isElectron()) {
 | 
				
			||||||
        chokidar.watch(tmpObj.name).on('change', (path, stats) => {
 | 
					        chokidar.watch(tmpObj.name).on('change', (path, stats) => {
 | 
				
			||||||
            ws.sendMessageToAllClients({
 | 
					            ws.sendMessageToAllClients({
 | 
				
			||||||
                type: 'openedFileUpdated',
 | 
					                type: 'openedFileUpdated',
 | 
				
			||||||
                noteId: noteId,
 | 
					                entityType: entityType,
 | 
				
			||||||
 | 
					                entityId: entityId,
 | 
				
			||||||
                lastModifiedMs: stats.atimeMs,
 | 
					                lastModifiedMs: stats.atimeMs,
 | 
				
			||||||
                filePath: tmpObj.name
 | 
					                filePath: tmpObj.name
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@@ -145,11 +186,63 @@ function saveToTmpDir(req) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function uploadModifiedFileToNote(req) {
 | 
				
			||||||
 | 
					    const noteId = req.params.noteId;
 | 
				
			||||||
 | 
					    const {filePath} = req.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const note = becca.getNote(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!note) {
 | 
				
			||||||
 | 
					        throw new NotFoundError(`Note '${noteId}' has not been found`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`Updating note '${noteId}' with content from '${filePath}'`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    note.saveNoteRevision();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fileContent = fs.readFileSync(filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!fileContent) {
 | 
				
			||||||
 | 
					        throw new ValidationError(`File '${fileContent}' is empty`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    note.setContent(fileContent);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function uploadModifiedFileToAttachment(req) {
 | 
				
			||||||
 | 
					    const {attachmentId} = req.params;
 | 
				
			||||||
 | 
					    const {filePath} = req.body;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const attachment = becca.getAttachment(attachmentId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!attachment) {
 | 
				
			||||||
 | 
					        throw new NotFoundError(`Attachment '${attachmentId}' has not been found`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    log.info(`Updating attachment '${attachmentId}' with content from '${filePath}'`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attachment.getNote().saveNoteRevision();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const fileContent = fs.readFileSync(filePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!fileContent) {
 | 
				
			||||||
 | 
					        throw new ValidationError(`File '${fileContent}' is empty`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    attachment.setContent(fileContent);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
module.exports = {
 | 
					module.exports = {
 | 
				
			||||||
    updateFile,
 | 
					    updateFile,
 | 
				
			||||||
    openFile,
 | 
					    openFile,
 | 
				
			||||||
    fileContentProvider,
 | 
					    fileContentProvider,
 | 
				
			||||||
    downloadFile,
 | 
					    downloadFile,
 | 
				
			||||||
    downloadNoteFile,
 | 
					    downloadNoteInt,
 | 
				
			||||||
    saveToTmpDir
 | 
					    saveNoteToTmpDir,
 | 
				
			||||||
 | 
					    openAttachment,
 | 
				
			||||||
 | 
					    downloadAttachment,
 | 
				
			||||||
 | 
					    saveAttachmentToTmpDir,
 | 
				
			||||||
 | 
					    attachmentContentProvider,
 | 
				
			||||||
 | 
					    uploadModifiedFileToNote,
 | 
				
			||||||
 | 
					    uploadModifiedFileToAttachment
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -229,29 +229,6 @@ function getDeleteNotesPreview(req) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function uploadModifiedFile(req) {
 | 
					 | 
				
			||||||
    const noteId = req.params.noteId;
 | 
					 | 
				
			||||||
    const {filePath} = req.body;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!note) {
 | 
					 | 
				
			||||||
        throw new NotFoundError(`Note '${noteId}' has not been found`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    log.info(`Updating note '${noteId}' with content from ${filePath}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    note.saveNoteRevision();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const fileContent = fs.readFileSync(filePath);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!fileContent) {
 | 
					 | 
				
			||||||
        throw new ValidationError(`File '${fileContent}' is empty`);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    note.setContent(fileContent);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function forceSaveNoteRevision(req) {
 | 
					function forceSaveNoteRevision(req) {
 | 
				
			||||||
    const {noteId} = req.params;
 | 
					    const {noteId} = req.params;
 | 
				
			||||||
    const note = becca.getNote(noteId);
 | 
					    const note = becca.getNote(noteId);
 | 
				
			||||||
@@ -294,7 +271,6 @@ module.exports = {
 | 
				
			|||||||
    eraseDeletedNotesNow,
 | 
					    eraseDeletedNotesNow,
 | 
				
			||||||
    eraseUnusedAttachmentsNow,
 | 
					    eraseUnusedAttachmentsNow,
 | 
				
			||||||
    getDeleteNotesPreview,
 | 
					    getDeleteNotesPreview,
 | 
				
			||||||
    uploadModifiedFile,
 | 
					 | 
				
			||||||
    forceSaveNoteRevision,
 | 
					    forceSaveNoteRevision,
 | 
				
			||||||
    convertNoteToAttachment
 | 
					    convertNoteToAttachment
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
const log = require('../services/log');
 | 
					const log = require('../services/log');
 | 
				
			||||||
const fileUploadService = require('./api/files');
 | 
					const fileService = require('./api/files');
 | 
				
			||||||
const scriptService = require('../services/script');
 | 
					const scriptService = require('../services/script');
 | 
				
			||||||
const cls = require('../services/cls');
 | 
					const cls = require('../services/cls');
 | 
				
			||||||
const sql = require("../services/sql");
 | 
					const sql = require("../services/sql");
 | 
				
			||||||
@@ -26,7 +26,7 @@ function handleRequest(req, res) {
 | 
				
			|||||||
            match = path.match(regex);
 | 
					            match = path.match(regex);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (e) {
 | 
					        catch (e) {
 | 
				
			||||||
            log.error(`Testing path for label ${attr.attributeId}, regex=${attr.value} failed with error ${e.stack}`);
 | 
					            log.error(`Testing path for label '${attr.attributeId}', regex '${attr.value}' failed with error: ${e.message}, stack: ${e.stack}`);
 | 
				
			||||||
            continue;
 | 
					            continue;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -37,7 +37,7 @@ function handleRequest(req, res) {
 | 
				
			|||||||
        if (attr.name === 'customRequestHandler') {
 | 
					        if (attr.name === 'customRequestHandler') {
 | 
				
			||||||
            const note = attr.getNote();
 | 
					            const note = attr.getNote();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            log.info(`Handling custom request "${path}" with note ${note.noteId}`);
 | 
					            log.info(`Handling custom request '${path}' with note '${note.noteId}'`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                scriptService.executeNote(note, {
 | 
					                scriptService.executeNote(note, {
 | 
				
			||||||
@@ -47,7 +47,7 @@ function handleRequest(req, res) {
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            catch (e) {
 | 
					            catch (e) {
 | 
				
			||||||
                log.error(`Custom handler ${note.noteId} failed with ${e.message}`);
 | 
					                log.error(`Custom handler '${note.noteId}' failed with: ${e.message}, ${e.stack}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                res.setHeader("Content-Type", "text/plain")
 | 
					                res.setHeader("Content-Type", "text/plain")
 | 
				
			||||||
                    .status(500)
 | 
					                    .status(500)
 | 
				
			||||||
@@ -55,16 +55,16 @@ function handleRequest(req, res) {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else if (attr.name === 'customResourceProvider') {
 | 
					        else if (attr.name === 'customResourceProvider') {
 | 
				
			||||||
            fileUploadService.downloadNoteFile(attr.noteId, res);
 | 
					            fileService.downloadNoteInt(attr.noteId, res);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        else {
 | 
					        else {
 | 
				
			||||||
            throw new Error(`Unrecognized attribute name ${attr.name}`);
 | 
					            throw new Error(`Unrecognized attribute name '${attr.name}'`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return; // only first handler is executed
 | 
					        return; // only first handler is executed
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const message = `No handler matched for custom ${path} request.`;
 | 
					    const message = `No handler matched for custom '${path}' request.`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.info(message);
 | 
					    log.info(message);
 | 
				
			||||||
    res.setHeader("Content-Type", "text/plain")
 | 
					    res.setHeader("Content-Type", "text/plain")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -122,7 +122,6 @@ function register(app) {
 | 
				
			|||||||
    apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime);
 | 
					    apiRoute(PUT, '/api/notes/:noteId/type', notesApiRoute.setNoteTypeMime);
 | 
				
			||||||
    apiRoute(PUT, '/api/notes/:noteId/title', notesApiRoute.changeTitle);
 | 
					    apiRoute(PUT, '/api/notes/:noteId/title', notesApiRoute.changeTitle);
 | 
				
			||||||
    apiRoute(PST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
 | 
					    apiRoute(PST, '/api/notes/:noteId/duplicate/:parentNoteId', notesApiRoute.duplicateSubtree);
 | 
				
			||||||
    apiRoute(PST, '/api/notes/:noteId/upload-modified-file', notesApiRoute.uploadModifiedFile);
 | 
					 | 
				
			||||||
    apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
 | 
					    apiRoute(PUT, '/api/notes/:noteId/clone-to-branch/:parentBranchId', cloningApiRoute.cloneNoteToBranch);
 | 
				
			||||||
    apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent);
 | 
					    apiRoute(PUT, '/api/notes/:noteId/toggle-in-parent/:parentNoteId/:present', cloningApiRoute.toggleNoteInParent);
 | 
				
			||||||
    apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToParentNote);
 | 
					    apiRoute(PUT, '/api/notes/:noteId/clone-to-note/:parentNoteId', cloningApiRoute.cloneNoteToParentNote);
 | 
				
			||||||
@@ -137,7 +136,8 @@ function register(app) {
 | 
				
			|||||||
    route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
 | 
					    route(GET, '/api/notes/:noteId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
 | 
				
			||||||
    // this "hacky" path is used for easier referencing of CSS resources
 | 
					    // this "hacky" path is used for easier referencing of CSS resources
 | 
				
			||||||
    route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
 | 
					    route(GET, '/api/notes/download/:noteId', [auth.checkApiAuthOrElectron], filesRoute.downloadFile);
 | 
				
			||||||
    apiRoute(PST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveToTmpDir);
 | 
					    apiRoute(PST, '/api/notes/:noteId/save-to-tmp-dir', filesRoute.saveNoteToTmpDir);
 | 
				
			||||||
 | 
					    apiRoute(PST, '/api/notes/:noteId/upload-modified-file', filesRoute.uploadModifiedFileToNote);
 | 
				
			||||||
    apiRoute(PST, '/api/notes/:noteId/convert-to-attachment', notesApiRoute.convertNoteToAttachment);
 | 
					    apiRoute(PST, '/api/notes/:noteId/convert-to-attachment', notesApiRoute.convertNoteToAttachment);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
 | 
					    apiRoute(PUT, '/api/branches/:branchId/move-to/:parentBranchId', branchesApiRoute.moveBranchToParent);
 | 
				
			||||||
@@ -154,6 +154,16 @@ function register(app) {
 | 
				
			|||||||
    apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote);
 | 
					    apiRoute(PST, '/api/attachments/:attachmentId/convert-to-note', attachmentsApiRoute.convertAttachmentToNote);
 | 
				
			||||||
    apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment);
 | 
					    apiRoute(DEL, '/api/attachments/:attachmentId', attachmentsApiRoute.deleteAttachment);
 | 
				
			||||||
    route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);
 | 
					    route(GET, '/api/attachments/:attachmentId/image/:filename', [auth.checkApiAuthOrElectron], imageRoute.returnAttachedImage);
 | 
				
			||||||
 | 
					    route(GET, '/api/attachments/:attachmentId/open', [auth.checkApiAuthOrElectron], filesRoute.openAttachment);
 | 
				
			||||||
 | 
					    route(GET, '/api/attachments/:attachmentId/open-partial', [auth.checkApiAuthOrElectron],
 | 
				
			||||||
 | 
					        createPartialContentHandler(filesRoute.attachmentContentProvider, {
 | 
				
			||||||
 | 
					            debug: (string, extra) => { console.log(string, extra); }
 | 
				
			||||||
 | 
					        }));
 | 
				
			||||||
 | 
					    route(GET, '/api/attachments/:attachmentId/download', [auth.checkApiAuthOrElectron], filesRoute.downloadAttachment);
 | 
				
			||||||
 | 
					    // this "hacky" path is used for easier referencing of CSS resources
 | 
				
			||||||
 | 
					    route(GET, '/api/attachments/download/:attachmentId', [auth.checkApiAuthOrElectron], filesRoute.downloadAttachment);
 | 
				
			||||||
 | 
					    apiRoute(PST, '/api/attachments/:attachmentId/save-to-tmp-dir', filesRoute.saveAttachmentToTmpDir);
 | 
				
			||||||
 | 
					    apiRoute(PST, '/api/attachments/:attachmentId/upload-modified-file', filesRoute.uploadModifiedFileToAttachment);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
 | 
					    apiRoute(GET, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.getNoteRevisions);
 | 
				
			||||||
    apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions);
 | 
					    apiRoute(DEL, '/api/notes/:noteId/revisions', noteRevisionsApiRoute.eraseAllNoteRevisions);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -362,8 +362,9 @@ function checkImageAttachments(note, content) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const existingAttachmentIds = new Set(imageAttachments.map(att => att.attachmentId));
 | 
					    const existingAttachmentIds = new Set(imageAttachments.map(att => att.attachmentId));
 | 
				
			||||||
    const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId));
 | 
					    const unknownAttachmentIds = Array.from(foundAttachmentIds).filter(foundAttId => !existingAttachmentIds.has(foundAttId));
 | 
				
			||||||
 | 
					    const unknownAttachments = becca.getAttachments(unknownAttachmentIds);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const unknownAttachment of becca.getAttachments(unknownAttachmentIds)) {
 | 
					    for (const unknownAttachment of unknownAttachments) {
 | 
				
			||||||
        // the attachment belongs to a different note (was copy pasted), we need to make a copy for this note.
 | 
					        // the attachment belongs to a different note (was copy pasted), we need to make a copy for this note.
 | 
				
			||||||
        const newAttachment = unknownAttachment.copy();
 | 
					        const newAttachment = unknownAttachment.copy();
 | 
				
			||||||
        newAttachment.parentId = note.noteId;
 | 
					        newAttachment.parentId = note.noteId;
 | 
				
			||||||
@@ -374,7 +375,10 @@ function checkImageAttachments(note, content) {
 | 
				
			|||||||
        log.info(`Copied attachment '${unknownAttachment.attachmentId}' to new '${newAttachment.attachmentId}'`);
 | 
					        log.info(`Copied attachment '${unknownAttachment.attachmentId}' to new '${newAttachment.attachmentId}'`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return content;
 | 
					    return {
 | 
				
			||||||
 | 
					        forceFrontendReload: unknownAttachments.length > 0,
 | 
				
			||||||
 | 
					        content
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -591,6 +595,7 @@ function saveLinks(note, content) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const foundLinks = [];
 | 
					    const foundLinks = [];
 | 
				
			||||||
 | 
					    let forceFrontendReload = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (note.type === 'text') {
 | 
					    if (note.type === 'text') {
 | 
				
			||||||
        content = downloadImages(note.noteId, content);
 | 
					        content = downloadImages(note.noteId, content);
 | 
				
			||||||
@@ -599,7 +604,7 @@ function saveLinks(note, content) {
 | 
				
			|||||||
        content = findInternalLinks(content, foundLinks);
 | 
					        content = findInternalLinks(content, foundLinks);
 | 
				
			||||||
        content = findIncludeNoteLinks(content, foundLinks);
 | 
					        content = findIncludeNoteLinks(content, foundLinks);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        content = checkImageAttachments(note, content);
 | 
					        ({forceFrontendReload, content} = checkImageAttachments(note, content));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    else if (note.type === 'relationMap') {
 | 
					    else if (note.type === 'relationMap') {
 | 
				
			||||||
        findRelationMapLinks(content, foundLinks);
 | 
					        findRelationMapLinks(content, foundLinks);
 | 
				
			||||||
@@ -643,7 +648,7 @@ function saveLinks(note, content) {
 | 
				
			|||||||
        unusedLink.markAsDeleted();
 | 
					        unusedLink.markAsDeleted();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return content;
 | 
					    return { forceFrontendReload, content };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** @param {BNote} note */
 | 
					/** @param {BNote} note */
 | 
				
			||||||
@@ -677,9 +682,9 @@ function updateNoteData(noteId, content) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    saveNoteRevisionIfNeeded(note);
 | 
					    saveNoteRevisionIfNeeded(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    content = saveLinks(note, content);
 | 
					    const { forceFrontendReload, content: newContent } = saveLinks(note, content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    note.setContent(content);
 | 
					    note.setContent(newContent, { forceFrontendReload });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
@@ -780,15 +785,15 @@ function scanForLinks(note, content) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
        sql.transactional(() => {
 | 
					        sql.transactional(() => {
 | 
				
			||||||
            const newContent = saveLinks(note, content);
 | 
					            const { forceFrontendReload, content: newContent } = saveLinks(note, content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (content !== newContent) {
 | 
					            if (content !== newContent) {
 | 
				
			||||||
                note.setContent(newContent);
 | 
					                note.setContent(newContent, { forceFrontendReload });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    catch (e) {
 | 
					    catch (e) {
 | 
				
			||||||
        log.error(`Could not scan for links note ${note.noteId}: ${e.message} ${e.stack}`);
 | 
					        log.error(`Could not scan for links note '${note.noteId}': ${e.message} ${e.stack}`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,33 +199,33 @@ function replaceAll(string, replaceWhat, replaceWith) {
 | 
				
			|||||||
    return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
 | 
					    return string.replace(new RegExp(quotedReplaceWhat, "g"), replaceWith);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function formatDownloadTitle(filename, type, mime) {
 | 
					function formatDownloadTitle(fileName, type, mime) {
 | 
				
			||||||
    if (!filename) {
 | 
					    if (!fileName) {
 | 
				
			||||||
        filename = "untitled";
 | 
					        fileName = "untitled";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    filename = sanitize(filename);
 | 
					    fileName = sanitize(fileName);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (type === 'text') {
 | 
					    if (type === 'text') {
 | 
				
			||||||
        return `${filename}.html`;
 | 
					        return `${fileName}.html`;
 | 
				
			||||||
    } else if (['relationMap', 'canvas', 'search'].includes(type)) {
 | 
					    } else if (['relationMap', 'canvas', 'search'].includes(type)) {
 | 
				
			||||||
        return `${filename}.json`;
 | 
					        return `${fileName}.json`;
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
        if (!mime) {
 | 
					        if (!mime) {
 | 
				
			||||||
            return filename;
 | 
					            return fileName;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        mime = mime.toLowerCase();
 | 
					        mime = mime.toLowerCase();
 | 
				
			||||||
        const filenameLc = filename.toLowerCase();
 | 
					        const filenameLc = fileName.toLowerCase();
 | 
				
			||||||
        const extensions = mimeTypes.extensions[mime];
 | 
					        const extensions = mimeTypes.extensions[mime];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!extensions || extensions.length === 0) {
 | 
					        if (!extensions || extensions.length === 0) {
 | 
				
			||||||
            return filename;
 | 
					            return fileName;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const ext of extensions) {
 | 
					        for (const ext of extensions) {
 | 
				
			||||||
            if (filenameLc.endsWith(`.${ext}`)) {
 | 
					            if (filenameLc.endsWith(`.${ext}`)) {
 | 
				
			||||||
                return filename;
 | 
					                return fileName;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -234,10 +234,10 @@ function formatDownloadTitle(filename, type, mime) {
 | 
				
			|||||||
            // the current name without fake extension. It's possible that the title still preserves to correct
 | 
					            // the current name without fake extension. It's possible that the title still preserves to correct
 | 
				
			||||||
            // extension too
 | 
					            // extension too
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return filename;
 | 
					            return fileName;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return `${filename}.${extensions[0]}`;
 | 
					        return `${fileName}.${extensions[0]}`;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user