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({
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");

View File

@ -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.`);
}

View File

@ -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) {

View File

@ -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];
}

View File

@ -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;
}

View File

@ -26,7 +26,7 @@ async function getRenderedContent(entity, options = {}) {
const type = getRenderingType(entity);
// 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') {
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(
$("<img>")
.attr("src", `api/images/${entity.noteId}/${sanitizedTitle}`)
.attr("src", url)
.css("max-width", "100%")
);
}

View File

@ -8,10 +8,16 @@ import linkService from "../services/link.js";
import contentRenderer from "../services/content_renderer.js";
const TPL = `
<div class="attachment-detail">
<div class="attachment-detail-widget">
<style>
.attachment-detail-widget {
height: 100%;
}
.attachment-detail-wrapper {
margin-bottom: 20px;
display: flex;
flex-direction: column;
}
.attachment-title-line {
@ -24,33 +30,53 @@ const TPL = `
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);
padding: 10px;
margin-top: 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;
}
.attachment-content img {
.attachment-content-wrapper img {
margin: 10px;
}
.attachment-detail-wrapper.list-view .attachment-content img {
.attachment-detail-wrapper.list-view .attachment-content-wrapper img {
max-height: 300px;
max-width: 90%;
object-fit: contain;
}
.attachment-detail-wrapper.full-detail .attachment-content img {
.attachment-detail-wrapper.full-detail .attachment-content-wrapper img {
max-width: 90%;
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%);
}
</style>
@ -65,7 +91,7 @@ const TPL = `
<div class="attachment-deletion-warning alert alert-info"></div>
<div class="attachment-content"></div>
<div class="attachment-content-wrapper"></div>
</div>
</div>`;
@ -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}) {

View File

@ -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() {

View File

@ -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 = `
<div class="attachment-detail note-detail-printable">
<style>
.attachment-detail {
padding: 15px;
height: 100%;
display: flex;
flex-direction: column;
}
.attachment-detail .links-wrapper {
padding: 16px;
font-size: larger;
}
.attachment-detail .attachment-wrapper {
flex-grow: 1;
}
</style>
<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) {
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.renderedAttachmentIds = new Set();
const attachments = await server.get(`notes/${this.noteId}/attachments`);
const attachments = await note.getAttachments();
if (attachments.length === 0) {
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;
}
.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 */
}

View File

@ -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) {