feat(react/ribbon): port image properties

This commit is contained in:
Elian Doran
2025-08-22 21:04:04 +03:00
parent 21683db0b8
commit 8287063aab
8 changed files with 123 additions and 154 deletions

View File

@@ -296,7 +296,7 @@ function isHtmlEmpty(html: string) {
); );
} }
async function clearBrowserCache() { export async function clearBrowserCache() {
if (isElectron()) { if (isElectron()) {
const win = dynamicRequire("@electron/remote").getCurrentWindow(); const win = dynamicRequire("@electron/remote").getCurrentWindow();
await win.webContents.session.clearCache(); await win.webContents.session.clearCache();

View File

@@ -10,6 +10,7 @@ import NoteContext from "../../components/note_context";
import { ReactWrappedWidget } from "../basic_widget"; import { ReactWrappedWidget } from "../basic_widget";
import FNote from "../../entities/fnote"; import FNote from "../../entities/fnote";
import attributes from "../../services/attributes"; import attributes from "../../services/attributes";
import FBlob from "../../entities/fblob";
type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void; type TriliumEventHandler<T extends EventNames> = (data: EventData<T>) => void;
const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map(); const registeredHandlers: Map<Component, Map<EventNames, TriliumEventHandler<any>[]>> = new Map();
@@ -389,3 +390,24 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: s
return [ labelValue, setter ] as const; return [ labelValue, setter ] as const;
} }
export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] {
if (!note) {
return [ undefined ];
}
const [ blob, setBlob ] = useState<FBlob | null>();
function refresh() {
note?.getBlob().then(setBlob);
}
useEffect(refresh, [ note?.noteId ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.hasRevisionForNote(note.noteId)) {
refresh();
}
});
return [ blob ] as const;
}

View File

@@ -1,5 +1,7 @@
import { ComponentChild, createContext, render, type JSX, type RefObject } from "preact"; import { ComponentChild, createContext, render, type JSX, type RefObject } from "preact";
import Component from "../../components/component"; import Component from "../../components/component";
import { EventData, EventNames } from "../../components/app_context";
import { useContext } from "preact/hooks";
export const ParentComponent = createContext<Component | null>(null); export const ParentComponent = createContext<Component | null>(null);

View File

@@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { t } from "../../services/i18n"; import { t } from "../../services/i18n";
import { formatSize } from "../../services/utils"; import { formatSize } from "../../services/utils";
import FormFileUpload, { FormFileUploadButton } from "../react/FormFileUpload"; import FormFileUpload, { FormFileUploadButton } from "../react/FormFileUpload";
import { useNoteLabel, useTriliumEventBeta } from "../react/hooks"; import { useNoteBlob, useNoteLabel, useTriliumEventBeta } from "../react/hooks";
import { TabContext } from "./ribbon-interface"; import { TabContext } from "./ribbon-interface";
import FBlob from "../../entities/fblob"; import FBlob from "../../entities/fblob";
import Button from "../react/Button"; import Button from "../react/Button";
@@ -13,19 +13,8 @@ import server from "../../services/server";
export default function FilePropertiesTab({ note }: TabContext) { export default function FilePropertiesTab({ note }: TabContext) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName"); const [ originalFileName ] = useNoteLabel(note, "originalFileName");
const [ blob, setBlob ] = useState<FBlob | null>();
const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable(); const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable();
const [ blob ] = useNoteBlob(note);
function refresh() {
note?.getBlob().then(setBlob);
}
useEffect(refresh, [ note?.noteId ]);
useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => {
if (note && loadResults.hasRevisionForNote(note.noteId)) {
refresh();
}
});
return ( return (
<div className="file-properties-widget"> <div className="file-properties-widget">

View File

@@ -0,0 +1,82 @@
import { t } from "../../services/i18n";
import { useNoteBlob, useNoteLabel } from "../react/hooks";
import { TabContext } from "./ribbon-interface";
import { clearBrowserCache, formatSize } from "../../services/utils";
import Button from "../react/Button";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import { ParentComponent } from "../react/react_utils";
import { useContext } from "preact/hooks";
import { FormFileUploadButton } from "../react/FormFileUpload";
import server from "../../services/server";
import toast from "../../services/toast";
export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
const [ originalFileName ] = useNoteLabel(note, "originalFileName");
const [ blob ] = useNoteBlob(note);
const parentComponent = useContext(ParentComponent);
return (
<div className="image-properties">
{note && (
<>
<div style={{ display: "flex", justifyContent: "space-evenly", margin: "10px" }}>
<span>
<strong>{t("image_properties.original_file_name")}:</strong>{" "}
<span>{originalFileName ?? "?"}</span>
</span>
<span>
<strong>{t("image_properties.file_type")}:</strong>{" "}
<span>{note.mime}</span>
</span>
<span>
<strong>{t("image_properties.file_size")}:</strong>{" "}
<span>{formatSize(blob?.contentLength)}</span>
</span>
</div>
<div style={{ display: "flex", justifyContent: "space-evenly", margin: "10px" }}>
<Button
text={t("image_properties.download")}
icon="bx bx-download"
primary
onClick={() => downloadFileNote(note.noteId)}
/>
<Button
text={t("image_properties.open")}
icon="bx bx-link-external"
onClick={() => openNoteExternally(note.noteId, note.mime)}
/>
<Button
text={t("image_properties.copy_reference_to_clipboard")}
icon="bx bx-copy"
onClick={() => parentComponent?.triggerEvent("copyImageReferenceToClipboard", { ntxId })}
/>
<FormFileUploadButton
text={t("image_properties.upload_new_revision")}
icon="bx bx-folder-open"
onChange={async (files) => {
if (!files) return;
const fileToUpload = files[0]; // copy to allow reset below
const result = await server.upload(`images/${note.noteId}`, fileToUpload);
if (result.uploaded) {
toast.showMessage(t("image_properties.upload_success"));
await clearBrowserCache();
} else {
toast.showError(t("image_properties.upload_failed", { message: result.message }));
}
}}
/>
</div>
</>
)}
</div>
)
}

View File

@@ -16,6 +16,7 @@ import NotePropertiesTab from "./NotePropertiesTab";
import NoteInfoTab from "./NoteInfoTab"; import NoteInfoTab from "./NoteInfoTab";
import SimilarNotesTab from "./SimilarNotesTab"; import SimilarNotesTab from "./SimilarNotesTab";
import FilePropertiesTab from "./FilePropertiesTab"; import FilePropertiesTab from "./FilePropertiesTab";
import ImagePropertiesTab from "./ImagePropertiesTab";
interface TitleContext { interface TitleContext {
note: FNote | null | undefined; note: FNote | null | undefined;
@@ -86,9 +87,12 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
activate: true activate: true
}, },
{ {
// ImagePropertiesWidget
title: t("image_properties.title"), title: t("image_properties.title"),
icon: "bx bx-image" icon: "bx bx-image",
content: ImagePropertiesTab,
show: ({ note }) => note?.type === "image",
toggleCommand: "toggleRibbonTabImageProperties",
activate: true,
}, },
{ {
// BasicProperties // BasicProperties
@@ -135,7 +139,7 @@ const TAB_CONFIGURATION = numberObjectsInPlace<TabConfiguration>([
]); ]);
export default function Ribbon() { export default function Ribbon() {
const { note } = useNoteContext(); const { note, ntxId } = useNoteContext();
const titleContext: TitleContext = { note }; const titleContext: TitleContext = { note };
const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>(); const [ activeTabIndex, setActiveTabIndex ] = useState<number | undefined>();
const filteredTabs = useMemo(() => TAB_CONFIGURATION.filter(tab => tab.show?.(titleContext)), [ titleContext, note ]); const filteredTabs = useMemo(() => TAB_CONFIGURATION.filter(tab => tab.show?.(titleContext)), [ titleContext, note ]);
@@ -171,7 +175,11 @@ export default function Ribbon() {
return; return;
} }
return tab?.content && tab.content({ note, hidden: !isActive }); return tab?.content && tab.content({
note,
hidden: !isActive,
ntxId
});
})} })}
</div> </div>
</div> </div>

View File

@@ -3,4 +3,5 @@ import FNote from "../../entities/fnote";
export interface TabContext { export interface TabContext {
note: FNote | null | undefined; note: FNote | null | undefined;
hidden: boolean; hidden: boolean;
ntxId?: string | null | undefined;
} }

View File

@@ -1,135 +0,0 @@
import server from "../../services/server.js";
import NoteContextAwareWidget from "../note_context_aware_widget.js";
import toastService from "../../services/toast.js";
import openService from "../../services/open.js";
import utils from "../../services/utils.js";
import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
const TPL = /*html*/`
<div class="image-properties">
<div style="display: flex; justify-content: space-evenly; margin: 10px;">
<span>
<strong>${t("image_properties.original_file_name")}:</strong>
<span class="image-filename"></span>
</span>
<span>
<strong>${t("image_properties.file_type")}:</strong>
<span class="image-filetype"></span>
</span>
<span>
<strong>${t("image_properties.file_size")}:</strong>
<span class="image-filesize"></span>
</span>
</div>
<div class="no-print" style="display: flex; justify-content: space-evenly; margin: 10px;">
<button class="image-download btn btn-sm btn-primary" type="button">
<span class="bx bx-download"></span>
${t("image_properties.download")}
</button>
<button class="image-open btn btn-sm btn-primary" type="button">
<span class="bx bx-link-external"></span>
${t("image_properties.open")}
</button>
<button class="image-copy-reference-to-clipboard btn btn-sm btn-primary" type="button">
<span class="bx bx-copy"></span>
${t("image_properties.copy_reference_to_clipboard")}
</button>
<button class="image-upload-new-revision btn btn-sm btn-primary" type="button">
<span class="bx bx-folder-open"></span>
${t("image_properties.upload_new_revision")}
</button>
</div>
<input type="file" class="image-upload-new-revision-input" style="display: none">
</div>`;
export default class ImagePropertiesWidget extends NoteContextAwareWidget {
private $copyReferenceToClipboardButton!: JQuery<HTMLElement>;
private $uploadNewRevisionButton!: JQuery<HTMLElement>;
private $uploadNewRevisionInput!: JQuery<HTMLFormElement>;
private $fileName!: JQuery<HTMLElement>;
private $fileType!: JQuery<HTMLElement>;
private $fileSize!: JQuery<HTMLElement>;
private $openButton!: JQuery<HTMLElement>;
private $imageDownloadButton!: JQuery<HTMLElement>;
get name() {
return "imageProperties";
}
get toggleCommand() {
return "toggleRibbonTabImageProperties";
}
isEnabled() {
return this.note && this.note.type === "image";
}
getTitle() {
return {
show: this.isEnabled(),
activate: true,
};
}
doRender() {
this.$widget = $(TPL);
this.contentSized();
this.$copyReferenceToClipboardButton = this.$widget.find(".image-copy-reference-to-clipboard");
this.$copyReferenceToClipboardButton.on("click", () => this.triggerEvent(`copyImageReferenceToClipboard`, { ntxId: this.noteContext?.ntxId }));
this.$uploadNewRevisionButton = this.$widget.find(".image-upload-new-revision");
this.$uploadNewRevisionInput = this.$widget.find(".image-upload-new-revision-input");
this.$fileName = this.$widget.find(".image-filename");
this.$fileType = this.$widget.find(".image-filetype");
this.$fileSize = this.$widget.find(".image-filesize");
this.$openButton = this.$widget.find(".image-open");
this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime));
this.$imageDownloadButton = this.$widget.find(".image-download");
this.$imageDownloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId));
this.$uploadNewRevisionButton.on("click", () => {
this.$uploadNewRevisionInput.trigger("click");
});
this.$uploadNewRevisionInput.on("change", async () => {
const fileToUpload = this.$uploadNewRevisionInput[0].files[0]; // copy to allow reset below
this.$uploadNewRevisionInput.val("");
const result = await server.upload(`images/${this.noteId}`, fileToUpload);
if (result.uploaded) {
toastService.showMessage(t("image_properties.upload_success"));
await utils.clearBrowserCache();
this.refresh();
} else {
toastService.showError(t("image_properties.upload_failed", { message: result.message }));
}
});
}
async refreshWithNote(note: FNote) {
this.$widget.show();
const blob = await this.note?.getBlob();
this.$fileName.text(note.getLabelValue("originalFileName") || "?");
this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0));
this.$fileType.text(note.mime);
}
}