mirror of
https://github.com/zadam/trilium.git
synced 2026-01-07 15:14:24 +01:00
chore(react/type_widget): list attachments with content
This commit is contained in:
parent
58b14ae31c
commit
dc73467d34
@ -11,89 +11,8 @@ import type FAttachment from "../entities/fattachment.js";
|
|||||||
import type { EventData } from "../components/app_context.js";
|
import type { EventData } from "../components/app_context.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<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 {
|
|
||||||
display: flex;
|
|
||||||
align-items: baseline;
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-details {
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-content-wrapper {
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-content-wrapper .rendered-content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-content-wrapper pre {
|
|
||||||
padding: 10px;
|
|
||||||
margin-top: 10px;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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-wrapper img {
|
|
||||||
margin: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-detail-wrapper.list-view .attachment-content-wrapper img, .attachment-detail-wrapper.list-view .attachment-content-wrapper video {
|
|
||||||
max-height: 300px;
|
|
||||||
max-width: 90%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-detail-wrapper.full-detail .attachment-content-wrapper img {
|
|
||||||
max-width: 90%;
|
|
||||||
object-fit: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.attachment-detail-wrapper.scheduled-for-deletion .attachment-content-wrapper img {
|
|
||||||
filter: contrast(10%);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="attachment-detail-wrapper">
|
|
||||||
<div class="attachment-title-line">
|
|
||||||
<div class="attachment-actions-container"></div>
|
|
||||||
<h4 class="attachment-title"></h4>
|
|
||||||
<div class="attachment-details"></div>
|
|
||||||
<div style="flex: 1 1;"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="attachment-deletion-warning alert alert-info" style="margin-top: 15px;"></div>
|
<div class="attachment-deletion-warning alert alert-info" style="margin-top: 15px;"></div>
|
||||||
|
|
||||||
<div class="attachment-content-wrapper"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
@ -125,21 +44,6 @@ export default class AttachmentDetailWidget extends BasicWidget {
|
|||||||
this.$wrapper = this.$widget.find(".attachment-detail-wrapper");
|
this.$wrapper = this.$widget.find(".attachment-detail-wrapper");
|
||||||
this.$wrapper.addClass(this.isFullDetail ? "full-detail" : "list-view");
|
this.$wrapper.addClass(this.isFullDetail ? "full-detail" : "list-view");
|
||||||
|
|
||||||
if (!this.isFullDetail) {
|
|
||||||
const $link = await linkService.createLink(this.attachment.ownerId, {
|
|
||||||
title: this.attachment.title,
|
|
||||||
viewScope: {
|
|
||||||
viewMode: "attachments",
|
|
||||||
attachmentId: this.attachment.attachmentId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
$link.addClass("use-tn-links");
|
|
||||||
|
|
||||||
this.$wrapper.find(".attachment-title").append($link);
|
|
||||||
} else {
|
|
||||||
this.$wrapper.find(".attachment-title").text(this.attachment.title);
|
|
||||||
}
|
|
||||||
|
|
||||||
const $deletionWarning = this.$wrapper.find(".attachment-deletion-warning");
|
const $deletionWarning = this.$wrapper.find(".attachment-deletion-warning");
|
||||||
const { utcDateScheduledForErasureSince } = this.attachment;
|
const { utcDateScheduledForErasureSince } = this.attachment;
|
||||||
|
|
||||||
@ -166,10 +70,9 @@ export default class AttachmentDetailWidget extends BasicWidget {
|
|||||||
$deletionWarning.hide();
|
$deletionWarning.hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$wrapper.find(".attachment-details").text(t("attachment_detail_2.role_and_size", { 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());
|
||||||
|
|
||||||
const { $renderedContent } = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail });
|
const { $renderedContent } = await );
|
||||||
this.$wrapper.find(".attachment-content-wrapper").append($renderedContent);
|
this.$wrapper.find(".attachment-content-wrapper").append($renderedContent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
import link from "../../services/link";
|
import link, { ViewScope } from "../../services/link";
|
||||||
import { useImperativeSearchHighlighlighting } from "./hooks";
|
import { useImperativeSearchHighlighlighting } from "./hooks";
|
||||||
|
|
||||||
interface NoteLinkOpts {
|
interface NoteLinkOpts {
|
||||||
@ -11,18 +11,25 @@ interface NoteLinkOpts {
|
|||||||
noPreview?: boolean;
|
noPreview?: boolean;
|
||||||
noTnLink?: boolean;
|
noTnLink?: boolean;
|
||||||
highlightedTokens?: string[] | null | undefined;
|
highlightedTokens?: string[] | null | undefined;
|
||||||
|
// Override the text of the link, otherwise the note title is used.
|
||||||
|
title?: string;
|
||||||
|
viewScope?: ViewScope;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens }: NoteLinkOpts) {
|
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope }: NoteLinkOpts) {
|
||||||
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
||||||
const ref = useRef<HTMLSpanElement>(null);
|
const ref = useRef<HTMLSpanElement>(null);
|
||||||
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
|
||||||
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
link.createLink(stringifiedNotePath, { showNotePath, showNoteIcon })
|
link.createLink(stringifiedNotePath, {
|
||||||
.then(setJqueryEl);
|
title,
|
||||||
}, [ stringifiedNotePath, showNotePath ]);
|
showNotePath,
|
||||||
|
showNoteIcon,
|
||||||
|
viewScope
|
||||||
|
}).then(setJqueryEl);
|
||||||
|
}, [ stringifiedNotePath, showNotePath, title, viewScope ]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !jqueryEl) return;
|
if (!ref.current || !jqueryEl) return;
|
||||||
|
|||||||
@ -12,3 +12,74 @@
|
|||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
}
|
}
|
||||||
/* #endregion */
|
/* #endregion */
|
||||||
|
|
||||||
|
/* #region Attachment detail */
|
||||||
|
.attachment-detail-widget {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-detail-wrapper {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-title-line {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-details {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-content-wrapper {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-content-wrapper .rendered-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-content-wrapper pre {
|
||||||
|
padding: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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-wrapper img {
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-detail-wrapper.list-view .attachment-content-wrapper img, .attachment-detail-wrapper.list-view .attachment-content-wrapper video {
|
||||||
|
max-height: 300px;
|
||||||
|
max-width: 90%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-detail-wrapper.full-detail .attachment-content-wrapper img {
|
||||||
|
max-width: 90%;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-detail-wrapper.scheduled-for-deletion .attachment-content-wrapper img {
|
||||||
|
filter: contrast(10%);
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
@ -3,14 +3,36 @@ import { TypeWidgetProps } from "./type_widget";
|
|||||||
import "./Attachment.css";
|
import "./Attachment.css";
|
||||||
import NoteLink from "../react/NoteLink";
|
import NoteLink from "../react/NoteLink";
|
||||||
import Button from "../react/Button";
|
import Button from "../react/Button";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent } from "../react/react_utils";
|
||||||
import HelpButton from "../react/HelpButton";
|
import HelpButton from "../react/HelpButton";
|
||||||
|
import FAttachment from "../../entities/fattachment";
|
||||||
|
import Alert from "../react/Alert";
|
||||||
|
import utils from "../../services/utils";
|
||||||
|
import content_renderer from "../../services/content_renderer";
|
||||||
|
|
||||||
export function AttachmentList({ note }: TypeWidgetProps) {
|
export function AttachmentList({ note }: TypeWidgetProps) {
|
||||||
|
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
note.getAttachments().then(setAttachments);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(refresh, [ note ]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="attachment-list note-detail-printable">
|
<div className="attachment-list note-detail-printable">
|
||||||
<AttachmentListHeader noteId={note.noteId} />
|
<AttachmentListHeader noteId={note.noteId} />
|
||||||
|
|
||||||
|
<div className="attachment-list-wrapper">
|
||||||
|
{attachments.length ? (
|
||||||
|
attachments.map(attachment => <AttachmentDetail attachment={attachment} />)
|
||||||
|
) : (
|
||||||
|
<Alert type="info">
|
||||||
|
{t("attachment_list.no_attachments")}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -39,3 +61,42 @@ function AttachmentListHeader({ noteId }: { noteId: string }) {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AttachmentDetail({ attachment, isFullDetail }: { attachment: FAttachment, isFullDetail: boolean }) {
|
||||||
|
const contentWrapper = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
content_renderer.getRenderedContent(attachment, { imageHasZoom: isFullDetail })
|
||||||
|
.then(({ $renderedContent }) => {
|
||||||
|
contentWrapper.current?.replaceChildren(...$renderedContent);
|
||||||
|
})
|
||||||
|
}, [ attachment ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="attachment-detail-widget">
|
||||||
|
<div className="attachment-detail-wrapper">
|
||||||
|
<div className="attachment-title-line">
|
||||||
|
<div className="attachment-actions-container"></div>
|
||||||
|
<h4 className="attachment-title">
|
||||||
|
{!isFullDetail ? (
|
||||||
|
<NoteLink
|
||||||
|
notePath={attachment.ownerId}
|
||||||
|
title={attachment.title}
|
||||||
|
viewScope={{
|
||||||
|
viewMode: "attachments",
|
||||||
|
attachmentId: attachment.attachmentId
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (attachment.title)}
|
||||||
|
</h4>
|
||||||
|
<div className="attachment-details">
|
||||||
|
{t("attachment_detail_2.role_and_size", { role: attachment.role, size: utils.formatSize(attachment.contentLength) })}
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1 1;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div ref={contentWrapper} className="attachment-content-wrapper" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import { t } from "../../services/i18n.js";
|
|||||||
import type { EventData } from "../../components/app_context.js";
|
import type { EventData } from "../../components/app_context.js";
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="attachment-list-wrapper"></div>
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export default class AttachmentListTypeWidget extends TypeWidget {
|
export default class AttachmentListTypeWidget extends TypeWidget {
|
||||||
@ -27,28 +26,12 @@ export default class AttachmentListTypeWidget extends TypeWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async doRefresh(note: Parameters<TypeWidget["doRefresh"]>[0]) {
|
async doRefresh(note: Parameters<TypeWidget["doRefresh"]>[0]) {
|
||||||
const $helpButton = $(`
|
|
||||||
<button class="attachment-help-button icon-action bx bx-help-circle"
|
|
||||||
type="button" data-help-page="attachments.html"
|
|
||||||
title="${}">
|
|
||||||
</button>
|
|
||||||
`);
|
|
||||||
utils.initHelpButtons($helpButton);
|
|
||||||
|
|
||||||
const noteLink = await linkService.createLink(this.noteId); // do separately to avoid race condition between empty() and .append()
|
|
||||||
noteLink.addClass("use-tn-links");
|
|
||||||
|
|
||||||
this.$list.empty();
|
this.$list.empty();
|
||||||
this.children = [];
|
this.children = [];
|
||||||
this.renderedAttachmentIds = new Set();
|
this.renderedAttachmentIds = new Set();
|
||||||
|
|
||||||
const attachments = await note.getAttachments();
|
const attachments = await note.getAttachments();
|
||||||
|
|
||||||
if (attachments.length === 0) {
|
|
||||||
this.$list.html('<div class="alert alert-info">' + t("attachment_list.no_attachments") + "</div>");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const attachment of attachments) {
|
for (const attachment of attachments) {
|
||||||
const attachmentDetailWidget = new AttachmentDetailWidget(attachment, false);
|
const attachmentDetailWidget = new AttachmentDetailWidget(attachment, false);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user