mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 03:16:11 +01:00
feat(react/ribbon): port image properties
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -388,4 +389,25 @@ export function useNoteLabelBoolean(note: FNote | undefined | null, labelName: s
|
|||||||
}, [note]);
|
}, [note]);
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
|
|
||||||
@@ -44,4 +46,4 @@ export function disposeReactWidget(container: Element) {
|
|||||||
export function separateByCommas(components: ComponentChild[]) {
|
export function separateByCommas(components: ComponentChild[]) {
|
||||||
return components.reduce<any>((acc, item) =>
|
return components.reduce<any>((acc, item) =>
|
||||||
(acc.length ? [...acc, ", ", item] : [item]), []);
|
(acc.length ? [...acc, ", ", item] : [item]), []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
82
apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx
Normal file
82
apps/client/src/widgets/ribbon/ImagePropertiesTab.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user