mirror of
https://github.com/zadam/trilium.git
synced 2026-05-06 17:47:43 +02:00
feat(launch_bar): add a context menu to every option to easily remove them
This commit is contained in:
101
apps/client/src/menus/launcher_button_context_menu.ts
Normal file
101
apps/client/src/menus/launcher_button_context_menu.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import type { ToggleInParentResponse } from "@triliumnext/commons";
|
||||
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import branchService from "../services/branches.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import server from "../services/server.js";
|
||||
import toast from "../services/toast.js";
|
||||
import contextMenu, { type ContextMenuEvent, type MenuItem } from "./context_menu.js";
|
||||
|
||||
const VISIBLE_LAUNCHER_PARENTS = ["_lbVisibleLaunchers", "_lbMobileVisibleLaunchers"];
|
||||
|
||||
function getVisibleLauncherBranch(launcherNote: FNote) {
|
||||
return launcherNote.getParentBranches().find((b) => VISIBLE_LAUNCHER_PARENTS.includes(b.parentNoteId));
|
||||
}
|
||||
|
||||
function getBookmarkBranch(launcherNote: FNote) {
|
||||
return launcherNote.getParentBranches().find((b) => b.parentNoteId === "_lbBookmarks");
|
||||
}
|
||||
|
||||
async function removeFromLaunchBar(launcherNote: FNote) {
|
||||
const bookmarkBranch = getBookmarkBranch(launcherNote);
|
||||
if (bookmarkBranch) {
|
||||
// Individual bookmarks are represented via a branch under `_lbBookmarks`; removing them
|
||||
// from the launch bar is the same as unbookmarking the note.
|
||||
const resp = await server.put<ToggleInParentResponse>(
|
||||
`notes/${launcherNote.noteId}/toggle-in-parent/_lbBookmarks/false`
|
||||
);
|
||||
if (!resp.success && resp.message) {
|
||||
toast.showError(resp.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const launcherBranch = getVisibleLauncherBranch(launcherNote);
|
||||
if (!launcherBranch) return;
|
||||
|
||||
const isMobileLauncher = launcherBranch.parentNoteId === "_lbMobileVisibleLaunchers";
|
||||
// Branch IDs in the hidden subtree follow the `${parentNoteId}_${noteId}` convention,
|
||||
// so the branch linking `_lb(Mobile)?Root` to the "available" launchers root is predictable.
|
||||
const targetBranchId = isMobileLauncher
|
||||
? "_lbMobileRoot__lbMobileAvailableLaunchers"
|
||||
: "_lbRoot__lbAvailableLaunchers";
|
||||
await branchService.moveToParentNote([launcherBranch.branchId], targetBranchId);
|
||||
}
|
||||
|
||||
export function canRemoveFromLaunchBar(launcherNote: FNote | null | undefined) {
|
||||
if (!launcherNote) return false;
|
||||
return !!(getVisibleLauncherBranch(launcherNote) || getBookmarkBranch(launcherNote));
|
||||
}
|
||||
|
||||
export interface ShowLauncherContextMenuOptions<T extends string> {
|
||||
/** Menu items specific to this launcher (e.g. "Open in new tab" for note-based launchers). They appear above the "Remove from launch bar" item. */
|
||||
extraItems?: MenuItem<T>[];
|
||||
/** Handler for the {@link extraItems}. The "Remove from launch bar" item is handled internally and will not be forwarded. */
|
||||
onCommand?: (command: T | undefined) => void;
|
||||
}
|
||||
|
||||
const REMOVE_COMMAND = "__removeFromLaunchBar__";
|
||||
|
||||
/**
|
||||
* Displays the launch bar icon context menu. When the launcher can be removed (i.e. it is a direct
|
||||
* child of the visible launchers root or of `_lbBookmarks`), a "Remove from launch bar" entry is
|
||||
* appended. Extra items can be supplied to preserve launcher-specific actions (e.g. "Open in new tab").
|
||||
*/
|
||||
export async function showLauncherContextMenu<T extends string>(
|
||||
launcherNote: FNote | null | undefined,
|
||||
e: ContextMenuEvent,
|
||||
options: ShowLauncherContextMenuOptions<T> = {}
|
||||
) {
|
||||
e.preventDefault();
|
||||
|
||||
const items = [...(options.extraItems ?? [])] as MenuItem<string>[];
|
||||
|
||||
if (canRemoveFromLaunchBar(launcherNote)) {
|
||||
if (items.length > 0) {
|
||||
items.push({ kind: "separator" });
|
||||
}
|
||||
items.push({
|
||||
title: t("launcher_button_context_menu.remove_from_launch_bar"),
|
||||
command: REMOVE_COMMAND,
|
||||
uiIcon: "bx bx-x-circle"
|
||||
});
|
||||
}
|
||||
|
||||
if (items.length === 0) return;
|
||||
|
||||
contextMenu.show<string>({
|
||||
x: e.pageX ?? 0,
|
||||
y: e.pageY ?? 0,
|
||||
items,
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command === REMOVE_COMMAND) {
|
||||
if (launcherNote) {
|
||||
void removeFromLaunchBar(launcherNote);
|
||||
}
|
||||
return;
|
||||
}
|
||||
options.onCommand?.(command as T | undefined);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1928,6 +1928,9 @@
|
||||
"move-to-available-launchers": "Move to available launchers",
|
||||
"duplicate-launcher": "Duplicate launcher <kbd data-command=\"duplicateSubtree\">"
|
||||
},
|
||||
"launcher_button_context_menu": {
|
||||
"remove_from_launch_bar": "Remove from launch bar"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Code Blocks",
|
||||
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
|
||||
|
||||
@@ -10,11 +10,11 @@ import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../rea
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import ResponsiveContainer from "../react/ResponsiveContainer";
|
||||
import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import { LaunchBarContext, LaunchBarDropdownButton, launcherContextMenuHandler, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
const PARENT_NOTE_ID = "_lbBookmarks";
|
||||
|
||||
export default function BookmarkButtons() {
|
||||
export default function BookmarkButtons({ launcherNote }: LauncherNoteProps) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
const style = useMemo<CSSProperties>(() => ({
|
||||
display: "flex",
|
||||
@@ -22,20 +22,27 @@ export default function BookmarkButtons() {
|
||||
contain: "none"
|
||||
}), [ isHorizontalLayout ]);
|
||||
const childNotes = useChildNotes(PARENT_NOTE_ID);
|
||||
const bookmarks = childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />);
|
||||
const showContextMenu = launcherContextMenuHandler(launcherNote);
|
||||
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
desktop={
|
||||
<div style={style}>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
<div
|
||||
style={style}
|
||||
// Only trigger on empty container area; individual bookmark buttons handle their own context menu.
|
||||
onContextMenu={(e) => e.target === e.currentTarget && showContextMenu?.(e)}
|
||||
>
|
||||
{bookmarks}
|
||||
</div>
|
||||
}
|
||||
mobile={
|
||||
<LaunchBarDropdownButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-bookmark"
|
||||
title={t("bookmark_buttons.bookmarks")}
|
||||
>
|
||||
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
|
||||
{bookmarks}
|
||||
</LaunchBarDropdownButton>
|
||||
}
|
||||
/>
|
||||
@@ -90,6 +97,7 @@ function BookmarkFolder({ note }: { note: FNote }) {
|
||||
|
||||
return (
|
||||
<LaunchBarDropdownButton
|
||||
launcherNote={note}
|
||||
icon={icon}
|
||||
title={title}
|
||||
>
|
||||
|
||||
@@ -58,6 +58,7 @@ export default function CalendarWidget({ launcherNote }: LauncherNoteProps) {
|
||||
|
||||
return (
|
||||
<LaunchBarDropdownButton
|
||||
launcherNote={launcherNote}
|
||||
icon={icon} title={title}
|
||||
onShown={async () => {
|
||||
const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote");
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useCallback } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import link_context_menu from "../../menus/link_context_menu";
|
||||
import { isCtrlKey } from "../../services/utils";
|
||||
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
|
||||
@@ -13,7 +14,7 @@ export function CustomNoteLauncher(props: {
|
||||
getHoistedNoteId?: (launcherNote: FNote) => string | null;
|
||||
keyboardShortcut?: string;
|
||||
}) {
|
||||
const { launcherNote, getTargetNoteId } = props;
|
||||
const { launcherNote, getTargetNoteId, getHoistedNoteId } = props;
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
|
||||
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
|
||||
@@ -31,11 +32,17 @@ export function CustomNoteLauncher(props: {
|
||||
onClick={launch}
|
||||
onAuxClick={launch}
|
||||
onContextMenu={async evt => {
|
||||
evt.preventDefault();
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (targetNoteId) {
|
||||
link_context_menu.openContextMenu(targetNoteId, evt);
|
||||
}
|
||||
const hoistedNoteId = getHoistedNoteId?.(launcherNote) ?? null;
|
||||
const linkItems = targetNoteId ? link_context_menu.getItems(evt) : [];
|
||||
await showLauncherContextMenu<CommandNames>(launcherNote, evt, {
|
||||
extraItems: linkItems,
|
||||
onCommand: (command) => {
|
||||
if (command && targetNoteId) {
|
||||
link_context_menu.handleLinkContextMenuItem(command, evt, targetNoteId, {}, hoistedNoteId);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useMemo } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import froca from "../../services/froca";
|
||||
import link from "../../services/link";
|
||||
import tree from "../../services/tree";
|
||||
@@ -25,46 +26,61 @@ export default function HistoryNavigationButton({ launcherNote, command }: Histo
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command}
|
||||
onContextMenu={webContents ? handleHistoryContextMenu(webContents) : undefined}
|
||||
onContextMenu={async (e) => {
|
||||
const items = webContents ? await getHistoryItems(webContents) : [];
|
||||
showLauncherContextMenu<string>(launcherNote, e, {
|
||||
extraItems: items,
|
||||
onCommand: (cmd) => {
|
||||
if (cmd && webContents) {
|
||||
webContents.navigationHistory.goToIndex(parseInt(cmd, 10));
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
async function getHistoryItems(webContents: WebContents): Promise<MenuCommandItem<string>[]> {
|
||||
if (webContents.navigationHistory.length() < 2) return [];
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!noteId || !notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
const index = parseInt(idx, 10);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
checked: index === activeIndex,
|
||||
enabled: index !== activeIndex,
|
||||
uiIcon: note?.getIcon()
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > HISTORY_LIMIT) {
|
||||
items = items.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export function handleHistoryContextMenu(webContents: WebContents) {
|
||||
return async (e: MouseEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!webContents || webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { noteId, notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!noteId || !notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
const index = parseInt(idx, 10);
|
||||
const note = froca.getNoteFromCache(noteId);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
checked: index === activeIndex,
|
||||
enabled: index !== activeIndex,
|
||||
uiIcon: note?.getIcon()
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > HISTORY_LIMIT) {
|
||||
items = items.slice(0, HISTORY_LIMIT);
|
||||
}
|
||||
const items = await getHistoryItems(webContents);
|
||||
if (items.length === 0) return;
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
|
||||
@@ -83,13 +83,13 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
|
||||
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
|
||||
|
||||
return <SpacerWidget baseSize={baseSize} growthFactor={growthFactor} />;
|
||||
return <SpacerWidget launcherNote={note} baseSize={baseSize} growthFactor={growthFactor} />;
|
||||
case "bookmarks":
|
||||
return <BookmarkButtons />;
|
||||
return <BookmarkButtons launcherNote={note} />;
|
||||
case "protectedSession":
|
||||
return <ProtectedSessionStatusWidget />;
|
||||
return <ProtectedSessionStatusWidget launcherNote={note} />;
|
||||
case "syncStatus":
|
||||
return <SyncStatus />;
|
||||
return <SyncStatus launcherNote={note} />;
|
||||
case "backInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />;
|
||||
case "forwardInHistoryButton":
|
||||
@@ -97,11 +97,11 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
||||
case "todayInJournal":
|
||||
return <TodayLauncher launcherNote={note} />;
|
||||
case "quickSearch":
|
||||
return <QuickSearchLauncherWidget />;
|
||||
return <QuickSearchLauncherWidget launcherNote={note} />;
|
||||
case "mobileTabSwitcher":
|
||||
return <TabSwitcher />;
|
||||
return <TabSwitcher launcherNote={note} />;
|
||||
case "sidebarChat":
|
||||
return isExperimentalFeatureEnabled("llm") ? <SidebarChatButton /> : undefined;
|
||||
return isExperimentalFeatureEnabled("llm") ? <SidebarChatButton launcherNote={note} /> : undefined;
|
||||
default:
|
||||
console.warn(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import QuickSearchWidget from "../quick_search";
|
||||
import { useGlobalShortcut, useLegacyWidget, useNoteLabel, useNoteRelationTarget } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { CustomNoteLauncher } from "./GenericButtons";
|
||||
import { LaunchBarActionButton, LaunchBarContext, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LaunchBarContext, launcherContextMenuHandler, LauncherNoteProps, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
export function CommandButton({ launcherNote }: LauncherNoteProps) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
@@ -22,6 +22,7 @@ export function CommandButton({ launcherNote }: LauncherNoteProps) {
|
||||
|
||||
return command && (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command as CommandNames}
|
||||
@@ -74,6 +75,7 @@ export function ScriptLauncher({ launcherNote }: LauncherNoteProps) {
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon={icon}
|
||||
text={title}
|
||||
onClick={launch}
|
||||
@@ -93,7 +95,7 @@ export function TodayLauncher({ launcherNote }: LauncherNoteProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function QuickSearchLauncherWidget() {
|
||||
export function QuickSearchLauncherWidget({ launcherNote }: LauncherNoteProps) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
const widget = useMemo(() => new QuickSearchWidget(), []);
|
||||
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
|
||||
@@ -101,7 +103,7 @@ export function QuickSearchLauncherWidget() {
|
||||
parentComponent?.contentSized();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onContextMenu={launcherContextMenuHandler(launcherNote)}>
|
||||
{isEnabled && <LegacyWidgetRenderer widget={widget} />}
|
||||
</div>
|
||||
);
|
||||
@@ -136,7 +138,7 @@ export function CustomWidget({ launcherNote }: LauncherNoteProps) {
|
||||
}, [ widgetNote ]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div onContextMenu={launcherContextMenuHandler(launcherNote)}>
|
||||
{widget && (
|
||||
("type" in widget && widget.type === "preact-launcher-widget")
|
||||
? <ReactWidgetRenderer widget={widget as LauncherWidgetDefinitionWithType} />
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import { LaunchBarActionButton } from "./launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LauncherNoteProps } from "./launch_bar_widgets";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
export default function ProtectedSessionStatusWidget() {
|
||||
export default function ProtectedSessionStatusWidget({ launcherNote }: LauncherNoteProps) {
|
||||
const protectedSessionAvailable = useProtectedSessionAvailable();
|
||||
|
||||
return (
|
||||
protectedSessionAvailable ? (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-check-shield"
|
||||
text={t("protected_session_status.active")}
|
||||
triggerCommand="leaveProtectedSession"
|
||||
/>
|
||||
) : (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-shield-quarter"
|
||||
text={t("protected_session_status.inactive")}
|
||||
triggerCommand="enterProtectedSession"
|
||||
|
||||
@@ -2,13 +2,13 @@ import { useCallback } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import { t } from "../../services/i18n";
|
||||
import { LaunchBarActionButton } from "./launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LauncherNoteProps } from "./launch_bar_widgets";
|
||||
|
||||
/**
|
||||
* Launcher button to open the sidebar (which contains the chat).
|
||||
* The chat widget is always visible in the sidebar for non-chat notes.
|
||||
*/
|
||||
export default function SidebarChatButton() {
|
||||
export default function SidebarChatButton({ launcherNote }: LauncherNoteProps) {
|
||||
const handleClick = useCallback(() => {
|
||||
// Open right pane if hidden, or toggle it if visible
|
||||
appContext.triggerEvent("toggleRightPane", {});
|
||||
@@ -16,6 +16,7 @@ export default function SidebarChatButton() {
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
icon="bx bx-message-square-dots"
|
||||
text={t("sidebar_chat.launcher_title")}
|
||||
onClick={handleClick}
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import contextMenu from "../../menus/context_menu";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import { t } from "../../services/i18n";
|
||||
import { isMobile } from "../../services/utils";
|
||||
|
||||
interface SpacerWidgetProps {
|
||||
launcherNote?: FNote;
|
||||
baseSize?: number;
|
||||
growthFactor?: number;
|
||||
}
|
||||
|
||||
export default function SpacerWidget({ baseSize, growthFactor }: SpacerWidgetProps) {
|
||||
export default function SpacerWidget({ launcherNote, baseSize, growthFactor }: SpacerWidgetProps) {
|
||||
return (
|
||||
<div
|
||||
className="spacer"
|
||||
@@ -17,19 +19,16 @@ export default function SpacerWidget({ baseSize, growthFactor }: SpacerWidgetPro
|
||||
flexGrow: growthFactor ?? 1000,
|
||||
flexShrink: 1000
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command) {
|
||||
appContext.triggerCommand(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
onContextMenu={launcherNote ? (e) => showLauncherContextMenu<CommandNames>(launcherNote, e, {
|
||||
extraItems: [{
|
||||
title: t("spacer.configure_launchbar"),
|
||||
command: "showLaunchBarSubtree",
|
||||
uiIcon: "bx " + (isMobile() ? "bx-mobile" : "bx-sidebar")
|
||||
}],
|
||||
onCommand: (command) => {
|
||||
if (command) appContext.triggerCommand(command);
|
||||
}
|
||||
}) : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import sync from "../../services/sync";
|
||||
import { escapeQuotes } from "../../services/utils";
|
||||
import ws, { subscribeToMessages, unsubscribeToMessage } from "../../services/ws";
|
||||
import { useStaticTooltip, useTriliumOption } from "../react/hooks";
|
||||
import { launcherContextMenuHandler, LauncherNoteProps } from "./launch_bar_widgets";
|
||||
|
||||
type SyncState = "unknown" | "in-progress"
|
||||
| "connected-with-changes" | "connected-no-changes"
|
||||
@@ -49,7 +50,7 @@ const STATE_MAPPINGS: Record<SyncState, StateMapping> = {
|
||||
}
|
||||
};
|
||||
|
||||
export default function SyncStatus() {
|
||||
export default function SyncStatus({ launcherNote }: LauncherNoteProps) {
|
||||
const syncState = useSyncStatus();
|
||||
const { title, icon, hasChanges } = STATE_MAPPINGS[syncState];
|
||||
const spanRef = useRef<HTMLSpanElement>(null);
|
||||
@@ -60,7 +61,10 @@ export default function SyncStatus() {
|
||||
});
|
||||
|
||||
return (syncServerHost &&
|
||||
<div class="sync-status-widget launcher-button">
|
||||
<div
|
||||
class="sync-status-widget launcher-button"
|
||||
onContextMenu={launcherContextMenuHandler(launcherNote)}
|
||||
>
|
||||
<div class="sync-status">
|
||||
<span
|
||||
key={syncState} // Force re-render when state changes to update tooltip content.
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createContext } from "preact";
|
||||
import { useContext } from "preact/hooks";
|
||||
|
||||
import FNote from "../../entities/fnote";
|
||||
import { showLauncherContextMenu } from "../../menus/launcher_button_context_menu";
|
||||
import utils from "../../services/utils";
|
||||
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
|
||||
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||
@@ -22,7 +23,13 @@ export interface LauncherNoteProps {
|
||||
launcherNote: FNote;
|
||||
}
|
||||
|
||||
export function LaunchBarActionButton({ className, ...props }: Omit<ActionButtonProps, "noIconActionClass" | "titlePosition">) {
|
||||
/** Builds the default right-click handler that shows the launch-bar icon context menu (with the "Remove from launch bar" entry). Used by widgets that render a raw element rather than going through {@link LaunchBarActionButton} / {@link LaunchBarDropdownButton}. */
|
||||
export function launcherContextMenuHandler(launcherNote: FNote | null | undefined) {
|
||||
if (!launcherNote) return undefined;
|
||||
return (e: MouseEvent) => showLauncherContextMenu(launcherNote, e);
|
||||
}
|
||||
|
||||
export function LaunchBarActionButton({ className, launcherNote, onContextMenu, ...props }: Omit<ActionButtonProps, "noIconActionClass" | "titlePosition"> & { launcherNote?: FNote }) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
|
||||
return (
|
||||
@@ -30,15 +37,20 @@ export function LaunchBarActionButton({ className, ...props }: Omit<ActionButton
|
||||
className={clsx("button-widget launcher-button", className)}
|
||||
noIconActionClass
|
||||
titlePosition={getTitlePosition(isHorizontalLayout)}
|
||||
onContextMenu={onContextMenu ?? launcherContextMenuHandler(launcherNote)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...props }: Pick<DropdownProps, "title" | "children" | "onShown" | "dropdownOptions" | "dropdownRef"> & { icon: string }) {
|
||||
export function LaunchBarDropdownButton({ children, icon, dropdownOptions, launcherNote, buttonProps, ...props }: Pick<DropdownProps, "title" | "children" | "onShown" | "dropdownOptions" | "dropdownRef" | "buttonProps"> & { icon: string, launcherNote?: FNote }) {
|
||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||
const titlePosition = getTitlePosition(isHorizontalLayout);
|
||||
|
||||
const resolvedButtonProps = launcherNote && !buttonProps?.onContextMenu
|
||||
? { ...buttonProps, onContextMenu: launcherContextMenuHandler(launcherNote) }
|
||||
: buttonProps;
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="right-dropdown-widget"
|
||||
@@ -54,6 +66,7 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
|
||||
}
|
||||
}}
|
||||
mobileBackdrop
|
||||
buttonProps={resolvedButtonProps}
|
||||
{...props}
|
||||
>{children}</Dropdown>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import froca from "../../services/froca";
|
||||
import { t } from "../../services/i18n";
|
||||
import type { ViewMode, ViewScope } from "../../services/link";
|
||||
import { NoteContent } from "../collections/legacy/ListOrGridView";
|
||||
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
|
||||
import { LaunchBarActionButton, LauncherNoteProps } from "../launch_bar/launch_bar_widgets";
|
||||
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks";
|
||||
@@ -30,13 +30,14 @@ const VIEW_MODE_ICON_MAPPINGS: Record<Exclude<ViewMode, "default">, string> = {
|
||||
ocr: "bx bx-text"
|
||||
};
|
||||
|
||||
export default function TabSwitcher() {
|
||||
export default function TabSwitcher({ launcherNote }: LauncherNoteProps) {
|
||||
const [ shown, setShown ] = useState(false);
|
||||
const mainNoteContexts = useMainNoteContexts();
|
||||
|
||||
return (
|
||||
<>
|
||||
<LaunchBarActionButton
|
||||
launcherNote={launcherNote}
|
||||
className="mobile-tab-switcher"
|
||||
icon="bx bx-rectangle"
|
||||
text="Tabs"
|
||||
|
||||
Reference in New Issue
Block a user