attachment content rendering

This commit is contained in:
zadam 2023-05-21 18:14:17 +02:00
parent e20fac19ba
commit 579ed7e194
12 changed files with 115 additions and 44 deletions

View File

@ -31,7 +31,7 @@ describe("Parser", () => {
const rootExp = parse({ const rootExp = parse({
fulltextTokens: tokens(["hello", "hi"]), fulltextTokens: tokens(["hello", "hi"]),
expressionTokens: [], expressionTokens: [],
searchContext: new SearchContext({includeNoteContent: false, excludeArchived: true}) searchContext: new SearchContext({excludeArchived: true})
}); });
expect(rootExp.constructor.name).toEqual("AndExp"); expect(rootExp.constructor.name).toEqual("AndExp");
@ -45,7 +45,7 @@ describe("Parser", () => {
const rootExp = parse({ const rootExp = parse({
fulltextTokens: tokens(["hello", "hi"]), fulltextTokens: tokens(["hello", "hi"]),
expressionTokens: [], expressionTokens: [],
searchContext: new SearchContext({includeNoteContent: true}) searchContext: new SearchContext()
}); });
expect(rootExp.constructor.name).toEqual("AndExp"); expect(rootExp.constructor.name).toEqual("AndExp");

View File

@ -153,16 +153,25 @@ class Becca {
} }
/** @returns {BAttachment|null} */ /** @returns {BAttachment|null} */
getAttachment(attachmentId) { getAttachment(attachmentId, opts = {}) {
const row = sql.getRow("SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0", [attachmentId]); 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 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} */ /** @returns {BAttachment} */
getAttachmentOrThrow(attachmentId) { getAttachmentOrThrow(attachmentId, opts = {}) {
const attachment = this.getAttachment(attachmentId); const attachment = this.getAttachment(attachmentId, opts);
if (!attachment) { if (!attachment) {
throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`); throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`);
} }

View File

@ -59,6 +59,9 @@ class BAttachment extends AbstractBeccaEntity {
/** @type {string} */ /** @type {string} */
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince; this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
/** @type {integer} optionally added to the entity */
this.contentLength = row.contentLength;
this.decrypt(); this.decrypt();
} }
@ -206,12 +209,14 @@ class BAttachment extends AbstractBeccaEntity {
isDeleted: false, isDeleted: false,
dateModified: this.dateModified, dateModified: this.dateModified,
utcDateModified: this.utcDateModified, utcDateModified: this.utcDateModified,
utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince utcDateScheduledForErasureSince: this.utcDateScheduledForErasureSince,
contentLength: this.contentLength
}; };
} }
getPojoToSave() { getPojoToSave() {
const pojo = this.getPojo(); const pojo = this.getPojo();
delete pojo.contentLength;
if (pojo.isProtected) { if (pojo.isProtected) {
if (this.isDecrypted) { if (this.isDecrypted) {

View File

@ -1114,24 +1114,33 @@ class BNote extends AbstractBeccaEntity {
} }
/** @returns {BAttachment[]} */ /** @returns {BAttachment[]} */
getAttachments() { getAttachments(opts = {}) {
return sql.getRows(` opts.includeContentLength = !!opts.includeContentLength;
SELECT attachments.*
const query = opts.includeContentLength
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments FROM attachments
WHERE parentId = ? JOIN blobs USING (blobId)
AND isDeleted = 0 WHERE parentId = ? AND isDeleted = 0
ORDER BY position`, [this.noteId]) 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)); .map(row => new BAttachment(row));
} }
/** @returns {BAttachment|null} */ /** @returns {BAttachment|null} */
getAttachmentById(attachmentId) { getAttachmentById(attachmentId, opts = {}) {
return sql.getRows(` opts.includeContentLength = !!opts.includeContentLength;
SELECT attachments.*
const query = opts.includeContentLength
? `SELECT attachments.*, LENGTH(blobs.content) AS contentLength
FROM attachments FROM attachments
WHERE parentId = ? JOIN blobs USING (blobId)
AND attachmentId = ? WHERE parentId = ? AND attachmentId = ? AND isDeleted = 0`
AND isDeleted = 0`, [this.noteId, attachmentId]) : `SELECT * FROM attachments WHERE parentId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows(query, [this.noteId, attachmentId])
.map(row => new BAttachment(row))[0]; .map(row => new BAttachment(row))[0];
} }

View File

@ -23,6 +23,9 @@ class FAttachment {
/** @type {string} */ /** @type {string} */
this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince; this.utcDateScheduledForErasureSince = row.utcDateScheduledForErasureSince;
/** @type {integer} optionally added to the entity */
this.contentLength = row.contentLength;
this.froca.attachments[this.attachmentId] = this; this.froca.attachments[this.attachmentId] = this;
} }

View File

@ -26,7 +26,7 @@ async function getRenderedContent(entity, options = {}) {
const type = getRenderingType(entity); const type = getRenderingType(entity);
// attachment supports only image and file/pdf/audio/video // attachment supports only image and file/pdf/audio/video
const $renderedContent = $('<div class="rendered-note-content">'); const $renderedContent = $('<div class="rendered-content">');
if (type === 'text') { if (type === 'text') {
await renderText(entity, options, $renderedContent); await renderText(entity, options, $renderedContent);
@ -118,9 +118,17 @@ async function renderCode(note, options, $renderedContent) {
function renderImage(entity, $renderedContent) { function renderImage(entity, $renderedContent) {
const sanitizedTitle = entity.title.replace(/[^a-z0-9-.]/gi, ""); 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( $renderedContent.append(
$("<img>") $("<img>")
.attr("src", `api/images/${entity.noteId}/${sanitizedTitle}`) .attr("src", url)
.css("max-width", "100%") .css("max-width", "100%")
); );
} }

View File

@ -8,10 +8,16 @@ import linkService from "../services/link.js";
import contentRenderer from "../services/content_renderer.js"; import contentRenderer from "../services/content_renderer.js";
const TPL = ` const TPL = `
<div class="attachment-detail"> <div class="attachment-detail-widget">
<style> <style>
.attachment-detail-widget {
height: 100%;
}
.attachment-detail-wrapper { .attachment-detail-wrapper {
margin-bottom: 20px; margin-bottom: 20px;
display: flex;
flex-direction: column;
} }
.attachment-title-line { .attachment-title-line {
@ -24,33 +30,53 @@ const TPL = `
margin-left: 10px; margin-left: 10px;
} }
.attachment-content pre { .attachment-content-wrapper {
flex-grow: 1;
}
.attachment-content-wrapper .rendered-content {
height: 100%;
}
.attachment-content-wrapper pre {
background: var(--accented-background-color); background: var(--accented-background-color);
padding: 10px; padding: 10px;
margin-top: 10px; margin-top: 10px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.attachment-detail-wrapper.list-view .attachment-content pre { .attachment-detail-wrapper.list-view .attachment-content-wrapper {
max-height: 300px;
}
.attachment-detail-wrapper.full-detail {
height: 100%;
}
.attachment-detail-wrapper.full-detail .attachment-content-wrapper {
height: 100%;
}
.attachment-detail-wrapper.list-view .attachment-content-wrapper pre {
max-height: 400px; max-height: 400px;
} }
.attachment-content img { .attachment-content-wrapper img {
margin: 10px; margin: 10px;
} }
.attachment-detail-wrapper.list-view .attachment-content img { .attachment-detail-wrapper.list-view .attachment-content-wrapper img {
max-height: 300px; max-height: 300px;
max-width: 90%; max-width: 90%;
object-fit: contain; object-fit: contain;
} }
.attachment-detail-wrapper.full-detail .attachment-content img { .attachment-detail-wrapper.full-detail .attachment-content-wrapper img {
max-width: 90%; max-width: 90%;
object-fit: contain; object-fit: contain;
} }
.attachment-detail-wrapper.scheduled-for-deletion .attachment-content img { .attachment-detail-wrapper.scheduled-for-deletion .attachment-content-wrapper img {
filter: contrast(10%); filter: contrast(10%);
} }
</style> </style>
@ -65,7 +91,7 @@ const TPL = `
<div class="attachment-deletion-warning alert alert-info"></div> <div class="attachment-deletion-warning alert alert-info"></div>
<div class="attachment-content"></div> <div class="attachment-content-wrapper"></div>
</div> </div>
</div>`; </div>`;
@ -141,11 +167,11 @@ export default class AttachmentDetailWidget extends BasicWidget {
this.$wrapper.find('.attachment-details') this.$wrapper.find('.attachment-details')
.text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`); .text(`Role: ${this.attachment.role}, Size: ${utils.formatSize(this.attachment.contentLength)}`);
this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render()); this.$wrapper.find('.attachment-actions-container').append(this.attachmentActionsWidget.render());
this.$wrapper.find('.attachment-content').append(contentRenderer.getRenderedContent(this.attachment)); this.$wrapper.find('.attachment-content-wrapper').append((await contentRenderer.getRenderedContent(this.attachment)).$renderedContent);
} }
copyAttachmentReferenceToClipboard() { copyAttachmentReferenceToClipboard() {
imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content')); imageService.copyImageReferenceToClipboard(this.$wrapper.find('.attachment-content-wrapper'));
} }
async entitiesReloadedEvent({loadResults}) { async entitiesReloadedEvent({loadResults}) {

View File

@ -167,9 +167,13 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
checkFullHeight() { checkFullHeight() {
// https://github.com/zadam/trilium/issues/2522 // https://github.com/zadam/trilium/issues/2522
this.$widget.toggleClass("full-height", this.$widget.toggleClass("full-height",
(
!this.noteContext.hasNoteList() !this.noteContext.hasNoteList()
&& ['editableText', 'editableCode', 'canvas', 'webView', 'noteMap'].includes(this.type) && ['editableText', 'editableCode', 'canvas', 'webView', 'noteMap'].includes(this.type)
&& this.mime !== 'text/x-sqlite;schema=trilium'); && this.mime !== 'text/x-sqlite;schema=trilium'
)
|| this.noteContext.viewScope.viewMode === 'attachments'
);
} }
getTypeWidget() { getTypeWidget() {

View File

@ -1,19 +1,26 @@
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import server from "../../services/server.js";
import AttachmentDetailWidget from "../attachment_detail.js"; import AttachmentDetailWidget from "../attachment_detail.js";
import linkService from "../../services/link.js"; import linkService from "../../services/link.js";
import froca from "../../services/froca.js";
const TPL = ` const TPL = `
<div class="attachment-detail note-detail-printable"> <div class="attachment-detail note-detail-printable">
<style> <style>
.attachment-detail { .attachment-detail {
padding: 15px; padding: 15px;
height: 100%;
display: flex;
flex-direction: column;
} }
.attachment-detail .links-wrapper { .attachment-detail .links-wrapper {
padding: 16px; padding: 16px;
font-size: larger; font-size: larger;
} }
.attachment-detail .attachment-wrapper {
flex-grow: 1;
}
</style> </style>
<div class="links-wrapper"></div> <div class="links-wrapper"></div>
@ -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) { if (!attachment) {
this.$wrapper.html("<strong>This attachment has been deleted.</strong>"); this.$wrapper.html("<strong>This attachment has been deleted.</strong>");

View File

@ -51,7 +51,7 @@ export default class AttachmentListTypeWidget extends TypeWidget {
this.children = []; this.children = [];
this.renderedAttachmentIds = new Set(); this.renderedAttachmentIds = new Set();
const attachments = await server.get(`notes/${this.noteId}/attachments`); const attachments = await note.getAttachments();
if (attachments.length === 0) { if (attachments.length === 0) {
this.$list.html('<div class="alert alert-info">This note has no attachments.</div>'); this.$list.html('<div class="alert alert-info">This note has no attachments.</div>');

View File

@ -736,11 +736,11 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
overflow: auto; 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 */ 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 */ height: 50em; /* PDF is rendered in iframe and it's not possible to put full height so at least a large height */
} }

View File

@ -10,13 +10,13 @@ function getAttachmentBlob(req) {
function getAttachments(req) { function getAttachments(req) {
const note = becca.getNoteOrThrow(req.params.noteId); const note = becca.getNoteOrThrow(req.params.noteId);
return note.getAttachments(); return note.getAttachments({includeContentLength: true});
} }
function getAttachment(req) { function getAttachment(req) {
const {attachmentId} = req.params; const {attachmentId} = req.params;
return becca.getAttachmentOrThrow(attachmentId); return becca.getAttachmentOrThrow(attachmentId, {includeContentLength: true});
} }
function saveAttachment(req) { function saveAttachment(req) {