chore(react/type_widget): list attachments with content

This commit is contained in:
Elian Doran 2025-09-21 10:32:12 +03:00
parent 58b14ae31c
commit dc73467d34
No known key found for this signature in database
5 changed files with 146 additions and 121 deletions

View File

@ -11,89 +11,8 @@ import type FAttachment from "../entities/fattachment.js";
import type { EventData } from "../components/app_context.js";
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-content-wrapper"></div>
</div>
</div>`;
@ -125,21 +44,6 @@ export default class AttachmentDetailWidget extends BasicWidget {
this.$wrapper = this.$widget.find(".attachment-detail-wrapper");
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 { utcDateScheduledForErasureSince } = this.attachment;
@ -166,10 +70,9 @@ export default class AttachmentDetailWidget extends BasicWidget {
$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());
const { $renderedContent } = await contentRenderer.getRenderedContent(this.attachment, { imageHasZoom: this.isFullDetail });
const { $renderedContent } = await );
this.$wrapper.find(".attachment-content-wrapper").append($renderedContent);
}

View File

@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from "preact/hooks";
import link from "../../services/link";
import link, { ViewScope } from "../../services/link";
import { useImperativeSearchHighlighlighting } from "./hooks";
interface NoteLinkOpts {
@ -11,18 +11,25 @@ interface NoteLinkOpts {
noPreview?: boolean;
noTnLink?: boolean;
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 ref = useRef<HTMLSpanElement>(null);
const [ jqueryEl, setJqueryEl ] = useState<JQuery<HTMLElement>>();
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
useEffect(() => {
link.createLink(stringifiedNotePath, { showNotePath, showNoteIcon })
.then(setJqueryEl);
}, [ stringifiedNotePath, showNotePath ]);
link.createLink(stringifiedNotePath, {
title,
showNotePath,
showNoteIcon,
viewScope
}).then(setJqueryEl);
}, [ stringifiedNotePath, showNotePath, title, viewScope ]);
useEffect(() => {
if (!ref.current || !jqueryEl) return;

View File

@ -11,4 +11,75 @@
justify-content: space-between;
align-items: baseline;
}
/* #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 */

View File

@ -3,14 +3,36 @@ import { TypeWidgetProps } from "./type_widget";
import "./Attachment.css";
import NoteLink from "../react/NoteLink";
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 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) {
const [ attachments, setAttachments ] = useState<FAttachment[]>([]);
function refresh() {
note.getAttachments().then(setAttachments);
}
useEffect(refresh, [ note ]);
return (
<div className="attachment-list note-detail-printable">
<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>
)
}
@ -39,3 +61,42 @@ function AttachmentListHeader({ noteId }: { noteId: string }) {
</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>
)
}

View File

@ -6,7 +6,6 @@ import { t } from "../../services/i18n.js";
import type { EventData } from "../../components/app_context.js";
const TPL = /*html*/`
<div class="attachment-list-wrapper"></div>
`;
export default class AttachmentListTypeWidget extends TypeWidget {
@ -27,28 +26,12 @@ export default class AttachmentListTypeWidget extends TypeWidget {
}
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.children = [];
this.renderedAttachmentIds = new Set();
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) {
const attachmentDetailWidget = new AttachmentDetailWidget(attachment, false);