diff --git a/.vscode/snippets.code-snippets b/.vscode/snippets.code-snippets index 77b251a4a..c7255af76 100644 --- a/.vscode/snippets.code-snippets +++ b/.vscode/snippets.code-snippets @@ -20,5 +20,10 @@ "scope": "typescript", "prefix": "jqf", "body": ["private $${1:name}!: JQuery;"] + }, + "region": { + "scope": "css", + "prefix": "region", + "body": ["/* #region ${1:name} */\n$0\n/* #endregion */"] } } diff --git a/apps/client/src/services/open.ts b/apps/client/src/services/open.ts index 7dab08a1a..0bcda805e 100644 --- a/apps/client/src/services/open.ts +++ b/apps/client/src/services/open.ts @@ -35,7 +35,7 @@ function download(url: string) { } } -function downloadFileNote(noteId: string) { +export function downloadFileNote(noteId: string) { const url = `${getFileUrl("notes", noteId)}?${Date.now()}`; // don't use cache download(url); @@ -163,7 +163,7 @@ async function openExternally(type: string, entityId: string, mime: string) { } } -const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime); +export const openNoteExternally = async (noteId: string, mime: string) => await openExternally("notes", noteId, mime); const openAttachmentExternally = async (attachmentId: string, mime: string) => await openExternally("attachments", attachmentId, mime); function getHost() { diff --git a/apps/client/src/widgets/react/FormFileUpload.tsx b/apps/client/src/widgets/react/FormFileUpload.tsx index c2f53a5a9..0274aff8e 100644 --- a/apps/client/src/widgets/react/FormFileUpload.tsx +++ b/apps/client/src/widgets/react/FormFileUpload.tsx @@ -1,12 +1,22 @@ +import { Ref } from "preact"; + interface FormFileUploadProps { + name?: string; onChange: (files: FileList | null) => void; multiple?: boolean; + hidden?: boolean; + inputRef?: Ref; } -export default function FormFileUpload({ onChange, multiple }: FormFileUploadProps) { +export default function FormFileUpload({ inputRef, name, onChange, multiple, hidden }: FormFileUploadProps) { return ( - ) diff --git a/apps/client/src/widgets/ribbon/FilePropertiesTab.tsx b/apps/client/src/widgets/ribbon/FilePropertiesTab.tsx new file mode 100644 index 000000000..2af652135 --- /dev/null +++ b/apps/client/src/widgets/ribbon/FilePropertiesTab.tsx @@ -0,0 +1,97 @@ +import { useEffect, useRef, useState } from "preact/hooks"; +import { t } from "../../services/i18n"; +import { formatSize } from "../../services/utils"; +import FormFileUpload from "../react/FormFileUpload"; +import { useNoteLabel, useTriliumEventBeta } from "../react/hooks"; +import { TabContext } from "./ribbon-interface"; +import FBlob from "../../entities/fblob"; +import Button from "../react/Button"; +import protected_session_holder from "../../services/protected_session_holder"; +import { downloadFileNote, openNoteExternally } from "../../services/open"; +import toast from "../../services/toast"; +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(); + const inputRef = useRef(null); + + function refresh() { + note?.getBlob().then(setBlob); + } + + useEffect(refresh, [ note?.noteId ]); + useTriliumEventBeta("entitiesReloaded", ({ loadResults }) => { + if (note && loadResults.hasRevisionForNote(note.noteId)) { + refresh(); + } + }); + + return ( +
+ {note && ( + + + + + + + + + + + + + + + + + +
{t("file_properties.note_id")}:{note.noteId}{t("file_properties.original_file_name")}:{originalFileName ?? "?"}
{t("file_properties.file_type")}:{note.mime}{t("file_properties.file_size")}:{formatSize(blob?.contentLength ?? 0)}
+
+
+
+ )} +
+ ); +} \ 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 14d448d45..bfff47091 100644 --- a/apps/client/src/widgets/ribbon/Ribbon.tsx +++ b/apps/client/src/widgets/ribbon/Ribbon.tsx @@ -15,6 +15,7 @@ import EditedNotesTab from "./EditedNotesTab"; import NotePropertiesTab from "./NotePropertiesTab"; import NoteInfoTab from "./NoteInfoTab"; import SimilarNotesTab from "./SimilarNotesTab"; +import FilePropertiesTab from "./FilePropertiesTab"; interface TitleContext { note: FNote | null | undefined; @@ -77,9 +78,12 @@ const TAB_CONFIGURATION = numberObjectsInPlace([ activate: true }, { - // FilePropertiesWidget title: t("file_properties.title"), - icon: "bx bx-file" + icon: "bx bx-file", + content: FilePropertiesTab, + show: ({ note }) => note?.type === "file", + toggleCommand: "toggleRibbonTabFileProperties", + activate: true }, { // ImagePropertiesWidget diff --git a/apps/client/src/widgets/ribbon/style.css b/apps/client/src/widgets/ribbon/style.css index 7200e6a00..87baedfc2 100644 --- a/apps/client/src/widgets/ribbon/style.css +++ b/apps/client/src/widgets/ribbon/style.css @@ -203,4 +203,22 @@ white-space: nowrap; overflow: hidden; } +/* #endregion */ + +/* #region File Properties */ +.file-table { + width: 100%; + margin-top: 10px; +} + +.file-table th, .file-table td { + padding: 5px; + overflow-wrap: anywhere; +} + +.file-buttons { + padding: 10px; + display: flex; + justify-content: space-evenly; +} /* #endregion */ \ No newline at end of file diff --git a/apps/client/src/widgets/ribbon_widgets/file_properties.ts b/apps/client/src/widgets/ribbon_widgets/file_properties.ts deleted file mode 100644 index 4d4b419c3..000000000 --- a/apps/client/src/widgets/ribbon_widgets/file_properties.ts +++ /dev/null @@ -1,155 +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 protectedSessionHolder from "../../services/protected_session_holder.js"; -import { t } from "../../services/i18n.js"; -import type FNote from "../../entities/fnote.js"; - -const TPL = /*html*/` -
- - - - - - - - - - - - - - - - - - - -
${t("file_properties.note_id")}:${t("file_properties.original_file_name")}:
${t("file_properties.file_type")}:${t("file_properties.file_size")}:
-
- - - - - - - -
-
-
`; - -export default class FilePropertiesWidget extends NoteContextAwareWidget { - - private $fileNoteId!: JQuery; - private $fileName!: JQuery; - private $fileType!: JQuery; - private $fileSize!: JQuery; - private $downloadButton!: JQuery; - private $openButton!: JQuery; - private $uploadNewRevisionButton!: JQuery; - private $uploadNewRevisionInput!: JQuery; - - get name() { - return "fileProperties"; - } - - get toggleCommand() { - return "toggleRibbonTabFileProperties"; - } - - isEnabled() { - return this.note && this.note.type === "file"; - } - - getTitle() { - return { - show: this.isEnabled(), - activate: true, - - }; - } - - doRender() { - this.$widget = $(TPL); - this.contentSized(); - this.$fileNoteId = this.$widget.find(".file-note-id"); - this.$fileName = this.$widget.find(".file-filename"); - this.$fileType = this.$widget.find(".file-filetype"); - this.$fileSize = this.$widget.find(".file-filesize"); - this.$downloadButton = this.$widget.find(".file-download"); - this.$openButton = this.$widget.find(".file-open"); - this.$uploadNewRevisionButton = this.$widget.find(".file-upload-new-revision"); - this.$uploadNewRevisionInput = this.$widget.find(".file-upload-new-revision-input"); - - this.$downloadButton.on("click", () => this.noteId && openService.downloadFileNote(this.noteId)); - this.$openButton.on("click", () => this.noteId && this.note && openService.openNoteExternally(this.noteId, this.note.mime)); - - 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(`notes/${this.noteId}/file`, fileToUpload); - - if (result.uploaded) { - toastService.showMessage(t("file_properties.upload_success")); - - this.refresh(); - } else { - toastService.showError(t("file_properties.upload_failed")); - } - }); - } - - async refreshWithNote(note: FNote) { - this.$widget.show(); - - if (!this.note) { - return; - } - - this.$fileNoteId.text(note.noteId); - this.$fileName.text(note.getLabelValue("originalFileName") || "?"); - this.$fileType.text(note.mime); - - const blob = await this.note.getBlob(); - - this.$fileSize.text(utils.formatSize(blob?.contentLength ?? 0)); - - // open doesn't work for protected notes since it works through a browser which isn't in protected session - this.$openButton.toggle(!note.isProtected); - this.$downloadButton.toggle(!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()); - this.$uploadNewRevisionButton.toggle(!note.isProtected || protectedSessionHolder.isProtectedSessionAvailable()); - } -}