diff --git a/spec/search/parser.spec.js b/spec/search/parser.spec.js index 6eadd2ec1..1d1e49d24 100644 --- a/spec/search/parser.spec.js +++ b/spec/search/parser.spec.js @@ -31,7 +31,7 @@ describe("Parser", () => { const rootExp = parse({ fulltextTokens: tokens(["hello", "hi"]), expressionTokens: [], - searchContext: new SearchContext({includeNoteContent: false, excludeArchived: true}) + searchContext: new SearchContext({excludeArchived: true}) }); expect(rootExp.constructor.name).toEqual("AndExp"); @@ -45,7 +45,7 @@ describe("Parser", () => { const rootExp = parse({ fulltextTokens: tokens(["hello", "hi"]), expressionTokens: [], - searchContext: new SearchContext({includeNoteContent: true}) + searchContext: new SearchContext() }); expect(rootExp.constructor.name).toEqual("AndExp"); diff --git a/src/becca/becca.js b/src/becca/becca.js index ee22d03b5..c40a08c37 100644 --- a/src/becca/becca.js +++ b/src/becca/becca.js @@ -153,16 +153,25 @@ class Becca { } /** @returns {BAttachment|null} */ - getAttachment(attachmentId) { - const row = sql.getRow("SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0", [attachmentId]); + getAttachment(attachmentId, opts = {}) { + opts.includeContentLength = !!opts.includeContentLength; + + const query = opts.includeContentLength + ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength + FROM attachments + JOIN blobs USING (blobId) + WHERE attachmentId = ? AND isDeleted = 0` + : `SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`; const BAttachment = require("./entities/battachment"); // avoiding circular dependency problems - return row ? new BAttachment(row) : null; + + return sql.getRows(query, [attachmentId]) + .map(row => new BAttachment(row))[0]; } /** @returns {BAttachment} */ - getAttachmentOrThrow(attachmentId) { - const attachment = this.getAttachment(attachmentId); + getAttachmentOrThrow(attachmentId, opts = {}) { + const attachment = this.getAttachment(attachmentId, opts); if (!attachment) { throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`); } diff --git a/src/becca/entities/battachment.js b/src/becca/entities/battachment.js index cc2e30011..cc2eadf00 100644 --- a/src/becca/entities/battachment.js +++ b/src/becca/entities/battachment.js @@ -59,6 +59,9 @@ class BAttachment extends AbstractBeccaEntity { /** @type {string} */ this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince; + /** @type {integer} optionally added to the entity */ + this.contentLength = row.contentLength; + this.decrypt(); } @@ -206,12 +209,14 @@ class BAttachment extends AbstractBeccaEntity { isDeleted: false, dateModified: this.dateModified, utcDateModified: this.utcDateModified, - utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince + utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince, + contentLength: this.contentLength }; } getPojoToSave() { const pojo = this.getPojo(); + delete pojo.contentLength; if (pojo.isProtected) { if (this.isDecrypted) { diff --git a/src/becca/entities/bnote.js b/src/becca/entities/bnote.js index 40bdbf308..bd22acb4e 100644 --- a/src/becca/entities/bnote.js +++ b/src/becca/entities/bnote.js @@ -1114,24 +1114,33 @@ class BNote extends AbstractBeccaEntity { } /** @returns {BAttachment[]} */ - getAttachments() { - return sql.getRows(` - SELECT attachments.* - FROM attachments - WHERE parentId = ? - AND isDeleted = 0 - ORDER BY position`, [this.noteId]) + getAttachments(opts = {}) { + opts.includeContentLength = !!opts.includeContentLength; + + const query = opts.includeContentLength + ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength + FROM attachments + JOIN blobs USING (blobId) + WHERE parentId = ? AND isDeleted = 0 + ORDER BY position` + : `SELECT * FROM attachments WHERE parentId = ? AND isDeleted = 0 ORDER BY position`; + + return sql.getRows(query, [this.noteId]) .map(row => new BAttachment(row)); } /** @returns {BAttachment|null} */ - getAttachmentById(attachmentId) { - return sql.getRows(` - SELECT attachments.* - FROM attachments - WHERE parentId = ? - AND attachmentId = ? - AND isDeleted = 0`, [this.noteId, attachmentId]) + getAttachmentById(attachmentId, opts = {}) { + opts.includeContentLength = !!opts.includeContentLength; + + const query = opts.includeContentLength + ? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength + FROM attachments + JOIN blobs USING (blobId) + WHERE parentId = ? AND attachmentId = ? AND isDeleted = 0` + : `SELECT * FROM attachments WHERE parentId = ? AND attachmentId = ? AND isDeleted = 0`; + + return sql.getRows(query, [this.noteId, attachmentId]) .map(row => new BAttachment(row))[0]; } diff --git a/src/public/app/entities/fattachment.js b/src/public/app/entities/fattachment.js index becd6a24f..e565efcb3 100644 --- a/src/public/app/entities/fattachment.js +++ b/src/public/app/entities/fattachment.js @@ -23,6 +23,9 @@ class FAttachment { /** @type {string} */ this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince; + /** @type {integer} optionally added to the entity */ + this.contentLength = row.contentLength; + this.froca.attachments[this.attachmentId] = this; } diff --git a/src/public/app/services/content_renderer.js b/src/public/app/services/content_renderer.js index 97496431c..3d1523dcd 100644 --- a/src/public/app/services/content_renderer.js +++ b/src/public/app/services/content_renderer.js @@ -26,7 +26,7 @@ async function getRenderedContent(entity, options = {}) { const type = getRenderingType(entity); // attachment supports only image and file/pdf/audio/video - const $renderedContent = $('
'); + const $renderedContent = $('
'); if (type === 'text') { await renderText(entity, options, $renderedContent); @@ -118,9 +118,17 @@ async function renderCode(note, options, $renderedContent) { function renderImage(entity, $renderedContent) { const sanitizedTitle = entity.title.replace(/[^a-z0-9-.]/gi, ""); + let url; + + if (entity instanceof FNote) { + url = `api/images/${entity.noteId}/${sanitizedTitle}?${entity.utcDateModified}`; + } else if (entity instanceof FAttachment) { + url = `api/attachments/${entity.attachmentId}/image/${sanitizedTitle}?${entity.utcDateModified}">`; + } + $renderedContent.append( $("") - .attr("src", `api/images/${entity.noteId}/${sanitizedTitle}`) + .attr("src", url) .css("max-width", "100%") ); } diff --git a/src/public/app/widgets/attachment_detail.js b/src/public/app/widgets/attachment_detail.js index a134fc8fc..55c87caaf 100644 --- a/src/public/app/widgets/attachment_detail.js +++ b/src/public/app/widgets/attachment_detail.js @@ -8,10 +8,16 @@ import linkService from "../services/link.js"; import contentRenderer from "../services/content_renderer.js"; const TPL = ` -
+
@@ -65,7 +91,7 @@ const TPL = `
-
+
`; @@ -141,11 +167,11 @@ export default class AttachmentDetailWidget extends BasicWidget { this.$wrapper.find('.attachment-details') .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-content').append(contentRenderer.getRenderedContent(this.attachment)); + this.$wrapper.find('.attachment-content-wrapper').append((await contentRenderer.getRenderedContent(this.attachment)).$renderedContent); } copyAttachmentReferenceToClipboard() { - imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content')); + imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content-wrapper')); } async entitiesReloadedEvent({loadResults}) { diff --git a/src/public/app/widgets/note_detail.js b/src/public/app/widgets/note_detail.js index bee29e998..833ee93da 100644 --- a/src/public/app/widgets/note_detail.js +++ b/src/public/app/widgets/note_detail.js @@ -167,9 +167,13 @@ export default class NoteDetailWidget extends NoteContextAwareWidget { checkFullHeight() { // https://github.com/zadam/trilium/issues/2522 this.$widget.toggleClass("full-height", - !this.noteContext.hasNoteList() - && ['editableText', 'editableCode', 'canvas', 'webView', 'noteMap'].includes(this.type) - && this.mime !== 'text/x-sqlite;schema=trilium'); + ( + !this.noteContext.hasNoteList() + && ['editableText', 'editableCode', 'canvas', 'webView', 'noteMap'].includes(this.type) + && this.mime !== 'text/x-sqlite;schema=trilium' + ) + || this.noteContext.viewScope.viewMode === 'attachments' + ); } getTypeWidget() { diff --git a/src/public/app/widgets/type_widgets/attachment_detail.js b/src/public/app/widgets/type_widgets/attachment_detail.js index 679b5de27..8abff539b 100644 --- a/src/public/app/widgets/type_widgets/attachment_detail.js +++ b/src/public/app/widgets/type_widgets/attachment_detail.js @@ -1,19 +1,26 @@ import TypeWidget from "./type_widget.js"; -import server from "../../services/server.js"; import AttachmentDetailWidget from "../attachment_detail.js"; import linkService from "../../services/link.js"; +import froca from "../../services/froca.js"; const TPL = `
@@ -50,7 +57,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { }) ); - const attachment = await server.get(`attachments/${this.attachmentId}`); + const attachment = await froca.getAttachment(this.attachmentId); if (!attachment) { this.$wrapper.html("This attachment has been deleted."); diff --git a/src/public/app/widgets/type_widgets/attachment_list.js b/src/public/app/widgets/type_widgets/attachment_list.js index 707b6b5fa..f3af78253 100644 --- a/src/public/app/widgets/type_widgets/attachment_list.js +++ b/src/public/app/widgets/type_widgets/attachment_list.js @@ -51,7 +51,7 @@ export default class AttachmentListTypeWidget extends TypeWidget { this.children = []; this.renderedAttachmentIds = new Set(); - const attachments = await server.get(`notes/${this.noteId}/attachments`); + const attachments = await note.getAttachments(); if (attachments.length === 0) { this.$list.html('
This note has no attachments.
'); diff --git a/src/public/stylesheets/style.css b/src/public/stylesheets/style.css index 9a2150ecb..9603dd032 100644 --- a/src/public/stylesheets/style.css +++ b/src/public/stylesheets/style.css @@ -736,11 +736,11 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href overflow: auto; } -.include-note.box-size-medium .include-note-content.type-pdf .rendered-note-content { +.include-note.box-size-medium .include-note-content.type-pdf .rendered-content { height: 20em; /* PDF is rendered in iframe and must be sized absolutely */ } -.include-note.box-size-full .include-note-content.type-pdf .rendered-note-content { +.include-note.box-size-full .include-note-content.type-pdf .rendered-content { height: 50em; /* PDF is rendered in iframe and it's not possible to put full height so at least a large height */ } diff --git a/src/routes/api/attachments.js b/src/routes/api/attachments.js index 352a9c51a..9acaa00ce 100644 --- a/src/routes/api/attachments.js +++ b/src/routes/api/attachments.js @@ -10,13 +10,13 @@ function getAttachmentBlob(req) { function getAttachments(req) { const note = becca.getNoteOrThrow(req.params.noteId); - return note.getAttachments(); + return note.getAttachments({includeContentLength: true}); } function getAttachment(req) { const {attachmentId} = req.params; - return becca.getAttachmentOrThrow(attachmentId); + return becca.getAttachmentOrThrow(attachmentId, {includeContentLength: true}); } function saveAttachment(req) {