From 8287063aab34585c60cb8b5afff305d3a754fa70 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Fri, 22 Aug 2025 21:04:04 +0300 Subject: [PATCH] feat(react/ribbon): port image properties --- apps/client/src/services/utils.ts | 2 +- apps/client/src/widgets/react/hooks.tsx | 22 +++ apps/client/src/widgets/react/react_utils.tsx | 4 +- .../src/widgets/ribbon/FilePropertiesTab.tsx | 15 +- .../src/widgets/ribbon/ImagePropertiesTab.tsx | 82 +++++++++++ apps/client/src/widgets/ribbon/Ribbon.tsx | 16 ++- .../src/widgets/ribbon/ribbon-interface.ts | 1 + .../ribbon_widgets/image_properties.ts | 135 ------------------ 8 files changed, 123 insertions(+), 154 deletions(-) create mode 100644 apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx delete mode 100644 apps/client/src/widgets/ribbon_widgets/image_properties.ts diff --git a/apps/client/src/services/utils.ts b/apps/client/src/services/utils.ts index 7ae1899fe..e58958f3c 100644 --- a/apps/client/src/services/utils.ts +++ b/apps/client/src/services/utils.ts @@ -296,7 +296,7 @@ function isHtmlEmpty(html: string) { ); } -async function clearBrowserCache() { +export async function clearBrowserCache() { if (isElectron()) { const win = dynamicRequire("@electron/remote").getCurrentWindow(); await win.webContents.session.clearCache(); diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 66100105f..9868e9f55 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -10,6 +10,7 @@ import NoteContext from "../../components/note_context"; import { ReactWrappedWidget } from "../basic_widget"; import FNote from "../../entities/fnote"; import attributes from "../../services/attributes"; +import FBlob from "../../entities/fblob"; type TriliumEventHandler = (data: EventData) => void; const registeredHandlers: Map[]>> = new Map(); @@ -388,4 +389,25 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: s }, [note]); return [ labelValue, setter ] as const; +} + +export function useNoteBlob(note: FNote | null | undefined): [ FBlob | null | undefined ] { + if (!note) { + return [ undefined ]; + } + + const [ blob, setBlob ] = useState(); + + function refresh() { + note?.getBlob().then(setBlob); + } + + useEffect(refresh, [ note?.noteId ]); + useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { + if (note && loadResults.hasRevisionForNote(note.noteId)) { + refresh(); + } + }); + + return [ blob ] as const; } \ No newline at end of file diff --git a/apps/client/src/widgets/react/react_utils.tsx b/apps/client/src/widgets/react/react_utils.tsx index db451b21a..490dd2094 100644 --- a/apps/client/src/widgets/react/react_utils.tsx +++ b/apps/client/src/widgets/react/react_utils.tsx @@ -1,5 +1,7 @@ import { ComponentChild, createContext, render, type JSX, type RefObject } from "preact"; import Component from "../../components/component"; +import { EventData, EventNames } from "../../components/app_context"; +import { useContext } from "preact/hooks"; export const ParentComponent = createContext(null); @@ -44,4 +46,4 @@ export function disposeReactWidget(container: Element) { export function separateByCommas(components: ComponentChild[]) { return components.reduce((acc, item) => (acc.length ? [...acc, ", ", item] : [item]), []); -} \ No newline at end of file +} diff --git a/apps/client/src/widgets/ribbon/FilePropertiesTab.tsx b/apps/client/src/widgets/ribbon/FilePropertiesTab.tsx index ad7a9c43f..916f17ff9 100644 --- a/apps/client/src/widgets/ribbon/FilePropertiesTab.tsx +++ b/apps/client/src/widgets/ribbon/FilePropertiesTab.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from "preact/hooks"; import { t } from "../../services/i18n"; import { formatSize } from "../../services/utils"; 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 FBlob from "../../entities/fblob"; import Button from "../react/Button"; @@ -13,19 +13,8 @@ import server from "../../services/server"; export default function FilePropertiesTab({ note }: TabContext) { const [ originalFileName ] = useNoteLabel(note, "originalFileName"); - const [ blob, setBlob ] = useState(); const canAccessProtectedNote = !note?.isProtected || protected_session_holder.isProtectedSessionAvailable(); - - function refresh() { - note?.getBlob().then(setBlob); - } - - useEffect(refresh, [ note?.noteId ]); - useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { - if (note && loadResults.hasRevisionForNote(note.noteId)) { - refresh(); - } - }); + const [ blob ] = useNoteBlob(note); return (
diff --git a/apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx b/apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx new file mode 100644 index 000000000..824040b8a --- /dev/null +++ b/apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx @@ -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 ( +
+ {note && ( + <> +
+ + {t("image_properties.original_file_name")}:{" "} + {originalFileName ?? "?"} + + + + {t("image_properties.file_type")}:{" "} + {note.mime} + + + + {t("image_properties.file_size")}:{" "} + {formatSize(blob?.contentLength)} + +
+ +
+
+ + )} +
+ ) +} \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon/Ribbon.tsx b/apps/client/src/widgets/ribbon/Ribbon.tsx index bfff47091..4d6c84be7 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -16,6 +16,7 @@ import NotePropertiesTab from "./NotePropertiesTab"; import NoteInfoTab from "./NoteInfoTab"; import SimilarNotesTab from "./SimilarNotesTab"; import FilePropertiesTab from "./FilePropertiesTab"; +import ImagePropertiesTab from "./ImagePropertiesTab"; interface TitleContext { note: FNote | null | undefined; @@ -86,9 +87,12 @@ const TAB_CONFIGURATION = numberObjectsInPlace([ activate: true }, { - // ImagePropertiesWidget 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 @@ -135,7 +139,7 @@ const TAB_CONFIGURATION = numberObjectsInPlace([ ]); export default function Ribbon() { - const { note } = useNoteContext(); + const { note, ntxId } = useNoteContext(); const titleContext: TitleContext = { note }; const [ activeTabIndex, setActiveTabIndex ] = useState(); const filteredTabs = useMemo(() => TAB_CONFIGURATION.filter(tab => tab.show?.(titleContext)), [ titleContext, note ]); @@ -171,7 +175,11 @@ export default function Ribbon() { return; } - return tab?.content && tab.content({ note, hidden: !isActive }); + return tab?.content && tab.content({ + note, + hidden: !isActive, + ntxId + }); })}
diff --git a/apps/client/src/widgets/ribbon/ribbon-interface.ts b/apps/client/src/widgets/ribbon/ribbon-interface.ts index 030361fce..bc8186734 100644 --- a/apps/client/src/widgets/ribbon/ribbon-interface.ts +++ b/apps/client/src/widgets/ribbon/ribbon-interface.ts @@ -3,4 +3,5 @@ import FNote from "../../entities/fnote"; export interface TabContext { note: FNote | null | undefined; hidden: boolean; + ntxId?: string | null | undefined; } diff --git a/apps/client/src/widgets/ribbon_widgets/image_properties.ts b/apps/client/src/widgets/ribbon_widgets/image_properties.ts deleted file mode 100644 index dfa67d7db..000000000 --- a/apps/client/src/widgets/ribbon_widgets/image_properties.ts +++ /dev/null @@ -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*/` -
-
- - ${t("image_properties.original_file_name")}: - - - - - ${t("image_properties.file_type")}: - - - - - ${t("image_properties.file_size")}: - - -
- -
- - - - - - - -
- - -
`; - -export default class ImagePropertiesWidget extends NoteContextAwareWidget { - - private $copyReferenceToClipboardButton!: JQuery; - private $uploadNewRevisionButton!: JQuery; - private $uploadNewRevisionInput!: JQuery; - private $fileName!: JQuery; - private $fileType!: JQuery; - private $fileSize!: JQuery; - private $openButton!: JQuery; - private $imageDownloadButton!: JQuery; - - 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); - } -}