From 8efb849391be5890e2db70d75894022141b82b81 Mon Sep 17 00:00:00 2001 From: Elian Doran Date: Thu, 4 Dec 2025 15:44:10 +0200 Subject: [PATCH] refactor(react/launch_bar): port history navigation --- .../src/widgets/buttons/button_from_note.ts | 63 ------------- .../src/widgets/buttons/history_navigation.ts | 90 ------------------- .../src/widgets/containers/launcher.tsx | 6 +- .../widgets/launch_bar/HistoryNavigation.tsx | 86 ++++++++++++++++++ 4 files changed, 89 insertions(+), 156 deletions(-) delete mode 100644 apps/client/src/widgets/buttons/button_from_note.ts delete mode 100644 apps/client/src/widgets/buttons/history_navigation.ts create mode 100644 apps/client/src/widgets/launch_bar/HistoryNavigation.tsx diff --git a/apps/client/src/widgets/buttons/button_from_note.ts b/apps/client/src/widgets/buttons/button_from_note.ts deleted file mode 100644 index 64e680257..000000000 --- a/apps/client/src/widgets/buttons/button_from_note.ts +++ /dev/null @@ -1,63 +0,0 @@ -import froca from "../../services/froca.js"; -import attributeService from "../../services/attributes.js"; -import CommandButtonWidget from "./command_button.js"; -import type { EventData } from "../../components/app_context.js"; - -export type ButtonNoteIdProvider = () => string; - -export default class ButtonFromNoteWidget extends CommandButtonWidget { - - constructor() { - super(); - - this.settings.buttonNoteIdProvider = null; - } - - buttonNoteIdProvider(provider: ButtonNoteIdProvider) { - this.settings.buttonNoteIdProvider = provider; - return this; - } - - doRender() { - super.doRender(); - - this.updateIcon(); - } - - updateIcon() { - if (!this.settings.buttonNoteIdProvider) { - console.error(`buttonNoteId for '${this.componentId}' is not defined.`); - return; - } - - const buttonNoteId = this.settings.buttonNoteIdProvider(); - - if (!buttonNoteId) { - console.error(`buttonNoteId for '${this.componentId}' is not defined.`); - return; - } - - froca.getNote(buttonNoteId).then((note) => { - const icon = note?.getIcon(); - if (icon) { - this.settings.icon = icon; - } - - this.refreshIcon(); - }); - } - - entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) { - // TODO: this seems incorrect - //@ts-ignore - const buttonNote = froca.getNoteFromCache(this.buttonNoteIdProvider()); - - if (!buttonNote) { - return; - } - - if (loadResults.getAttributeRows(this.componentId).find((attr) => attr.type === "label" && attr.name === "iconClass" && attributeService.isAffecting(attr, buttonNote))) { - this.updateIcon(); - } - } -} diff --git a/apps/client/src/widgets/buttons/history_navigation.ts b/apps/client/src/widgets/buttons/history_navigation.ts deleted file mode 100644 index 74eaf6acc..000000000 --- a/apps/client/src/widgets/buttons/history_navigation.ts +++ /dev/null @@ -1,90 +0,0 @@ -import utils from "../../services/utils.js"; -import contextMenu, { MenuCommandItem } from "../../menus/context_menu.js"; -import treeService from "../../services/tree.js"; -import ButtonFromNoteWidget from "./button_from_note.js"; -import type FNote from "../../entities/fnote.js"; -import type { CommandNames } from "../../components/app_context.js"; -import type { WebContents } from "electron"; -import link from "../../services/link.js"; - -export default class HistoryNavigationButton extends ButtonFromNoteWidget { - private webContents?: WebContents; - - constructor(launcherNote: FNote, command: string) { - super(); - - this.title(() => launcherNote.title) - .icon(() => launcherNote.getIcon()) - .command(() => command as CommandNames) - .titlePlacement("right") - .buttonNoteIdProvider(() => launcherNote.noteId) - .onContextMenu((e) => { if (e) this.showContextMenu(e); }) - .class("launcher-button"); - } - - doRender() { - super.doRender(); - - if (utils.isElectron()) { - this.webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents(); - - // without this, the history is preserved across frontend reloads - this.webContents?.clearHistory(); - - this.refresh(); - } - } - - async showContextMenu(e: JQuery.ContextMenuEvent) { - e.preventDefault(); - - if (!this.webContents || this.webContents.navigationHistory.length() < 2) { - return; - } - - let items: MenuCommandItem[] = []; - - const history = this.webContents.navigationHistory.getAllEntries(); - const activeIndex = this.webContents.navigationHistory.getActiveIndex(); - - for (const idx in history) { - const { notePath } = link.parseNavigationStateFromUrl(history[idx].url); - if (!notePath) continue; - - const title = await treeService.getNotePathTitle(notePath); - - items.push({ - title, - command: idx, - uiIcon: - parseInt(idx) === activeIndex - ? "bx bx-radio-circle-marked" // compare with type coercion! - : parseInt(idx) < activeIndex - ? "bx bx-left-arrow-alt" - : "bx bx-right-arrow-alt" - }); - } - - items.reverse(); - - if (items.length > 20) { - items = items.slice(0, 50); - } - - contextMenu.show({ - x: e.pageX, - y: e.pageY, - items, - selectMenuItemHandler: (item: MenuCommandItem) => { - if (item && item.command && this.webContents) { - const idx = parseInt(item.command, 10); - this.webContents.navigationHistory.goToIndex(idx); - } - } - }); - } - - activeNoteChangedEvent() { - this.refresh(); - } -} diff --git a/apps/client/src/widgets/containers/launcher.tsx b/apps/client/src/widgets/containers/launcher.tsx index 4ae82bbcd..fbc40fe2c 100644 --- a/apps/client/src/widgets/containers/launcher.tsx +++ b/apps/client/src/widgets/containers/launcher.tsx @@ -7,13 +7,13 @@ import ScriptLauncher from "../buttons/launcher/script_launcher.js"; import CommandButtonWidget from "../buttons/command_button.js"; import utils from "../../services/utils.js"; import TodayLauncher from "../buttons/launcher/today_launcher.js"; -import HistoryNavigationButton from "../buttons/history_navigation.js"; import QuickSearchLauncherWidget from "../quick_search_launcher.js"; import type FNote from "../../entities/fnote.js"; import type { CommandNames } from "../../components/app_context.js"; import AiChatButton from "../buttons/ai_chat_button.js"; import BookmarkButtons from "../launch_bar/BookmarkButtons.jsx"; import SpacerWidget from "../launch_bar/SpacerWidget.jsx"; +import HistoryNavigationButton from "../launch_bar/HistoryNavigation.jsx"; interface InnerWidget extends BasicWidget { settings?: { @@ -117,9 +117,9 @@ export default class LauncherWidget extends BasicWidget { case "syncStatus": return new SyncStatusWidget(); case "backInHistoryButton": - return new HistoryNavigationButton(note, "backInNoteHistory"); + return case "forwardInHistoryButton": - return new HistoryNavigationButton(note, "forwardInNoteHistory"); + return case "todayInJournal": return new TodayLauncher(note); case "quickSearch": diff --git a/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx new file mode 100644 index 000000000..0b5ba4b45 --- /dev/null +++ b/apps/client/src/widgets/launch_bar/HistoryNavigation.tsx @@ -0,0 +1,86 @@ +import { useEffect, useRef } from "preact/hooks"; +import FNote from "../../entities/fnote"; +import { dynamicRequire, escapeHtml, isElectron } from "../../services/utils"; +import { useNoteLabel, useNoteProperty } from "../react/hooks"; +import { LaunchBarActionButton } from "./launch_bar_widgets"; +import type { WebContents } from "electron"; +import contextMenu, { MenuCommandItem } from "../../menus/context_menu"; +import tree from "../../services/tree"; +import link from "../../services/link"; + +interface HistoryNavigationProps { + launcherNote: FNote; + command: "backInNoteHistory" | "forwardInNoteHistory"; +} + +export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) { + const [ iconClass ] = useNoteLabel(launcherNote, "iconClass"); + const title = useNoteProperty(launcherNote, "title"); + const webContentsRef = useRef(null); + + useEffect(() => { + if (isElectron()) { + const webContents = dynamicRequire("@electron/remote").getCurrentWebContents(); + // without this, the history is preserved across frontend reloads + webContents?.clearHistory(); + webContentsRef.current = webContents; + } + }, []); + + return iconClass && title && ( + { + e.preventDefault(); + + const webContents = webContentsRef.current; + if (!webContents || webContents.navigationHistory.length() < 2) { + return; + } + + let items: MenuCommandItem[] = []; + + const history = webContents.navigationHistory.getAllEntries(); + const activeIndex = webContents.navigationHistory.getActiveIndex(); + + for (const idx in history) { + const { notePath } = link.parseNavigationStateFromUrl(history[idx].url); + if (!notePath) continue; + + const title = await tree.getNotePathTitle(notePath); + + items.push({ + title, + command: idx, + uiIcon: + parseInt(idx) === activeIndex + ? "bx bx-radio-circle-marked" // compare with type coercion! + : parseInt(idx) < activeIndex + ? "bx bx-left-arrow-alt" + : "bx bx-right-arrow-alt" + }); + } + + items.reverse(); + + if (items.length > 20) { + items = items.slice(0, 50); + } + + contextMenu.show({ + x: e.pageX, + y: e.pageY, + items, + selectMenuItemHandler: (item: MenuCommandItem) => { + if (item && item.command && webContents) { + const idx = parseInt(item.command, 10); + webContents.navigationHistory.goToIndex(idx); + } + } + }); + }} + /> + ) +}