chore(react/launch_bar): port launcher container & launcher

This commit is contained in:
Elian Doran 2025-12-05 11:31:10 +02:00
parent caaa3583a7
commit d511085db3
No known key found for this signature in database
7 changed files with 206 additions and 282 deletions

View File

@ -10,7 +10,6 @@ import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
import HighlightsListWidget from "../widgets/highlights_list.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
@ -44,6 +43,7 @@ import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
export default class DesktopLayout {
@ -184,14 +184,14 @@ export default class DesktopLayout {
launcherPane = new FlexContainer("row")
.css("height", "53px")
.class("horizontal")
.child(new LauncherContainer(true))
.child(<LauncherContainer isHorizontalLayout={true} />)
.child(<GlobalMenu isHorizontalLayout={true} />);
} else {
launcherPane = new FlexContainer("column")
.css("width", "53px")
.class("vertical")
.child(<GlobalMenu isHorizontalLayout={false} />)
.child(new LauncherContainer(false))
.child(<LauncherContainer isHorizontalLayout={false} />)
.child(<LeftPaneToggle isHorizontalLayout={false} />);
}

View File

@ -6,7 +6,6 @@ import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import FlexContainer from "../widgets/containers/flex_container.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import LauncherContainer from "../widgets/containers/launcher_container.js";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
@ -30,6 +29,7 @@ import NoteDetail from "../widgets/NoteDetail.jsx";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
const MOBILE_CSS = `
<style>
@ -183,7 +183,7 @@ export default class MobileLayout {
.child(new FlexContainer("row")
.class("horizontal")
.css("height", "53px")
.child(new LauncherContainer(true))
.child(<LauncherContainer isHorizontalLayout />)
.child(<GlobalMenuWidget isHorizontalLayout />)
.id("launcher-pane"))
)

View File

@ -236,7 +236,7 @@ export function isIOS() {
return /iPad|iPhone|iPod/.test(navigator.userAgent);
}
function isDesktop() {
export function isDesktop() {
return (
window.glob?.device === "desktop" ||
// window.glob.device is not available in setup

View File

@ -1,198 +0,0 @@
import BasicWidget, { wrapReactWidgets } from "../basic_widget.js";
import utils, { isMobile } from "../../services/utils.js";
import type FNote from "../../entities/fnote.js";
import BookmarkButtons from "../launch_bar/BookmarkButtons.jsx";
import SpacerWidget from "../launch_bar/SpacerWidget.jsx";
import HistoryNavigationButton from "../launch_bar/HistoryNavigation.jsx";
import AiChatButton from "../launch_bar/AiChatButton.jsx";
import ProtectedSessionStatusWidget from "../launch_bar/ProtectedSessionStatusWidget.jsx";
import { VNode } from "preact";
import { CommandButton, CustomNoteLauncher, NoteLauncher } from "../launch_bar/GenericButtons.jsx";
import date_notes from "../../services/date_notes.js";
import { useLegacyWidget, useNoteContext, useNoteRelation, useNoteRelationTarget } from "../react/hooks.jsx";
import QuickSearchWidget from "../quick_search.js";
import { ParentComponent } from "../react/react_utils.jsx";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "../launch_bar/launch_bar_widgets.jsx";
import CalendarWidget from "../launch_bar/CalendarWidget.jsx";
import SyncStatus from "../launch_bar/SyncStatus.jsx";
interface InnerWidget extends BasicWidget {
settings?: {
titlePlacement: "bottom";
};
}
export default class LauncherWidget extends BasicWidget {
private innerWidget!: InnerWidget;
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
isEnabled() {
return this.innerWidget.isEnabled();
}
doRender() {
this.$widget = this.innerWidget.render();
}
async initLauncher(note: FNote) {
if (note.type !== "launcher") {
throw new Error(`Note '${note.noteId}' '${note.title}' is not a launcher even though it's in the launcher subtree`);
}
if (!utils.isDesktop() && note.isLabelTruthy("desktopOnly")) {
return false;
}
const launcherType = note.getLabelValue("launcherType");
if (glob.TRILIUM_SAFE_MODE && launcherType === "customWidget") {
return false;
}
let widget: BasicWidget | VNode;
if (launcherType === "command") {
widget = wrapReactWidgets<BasicWidget>([ <CommandButton launcherNote={note} /> ])[0];
} else if (launcherType === "note") {
widget = wrapReactWidgets<BasicWidget>([ <NoteLauncher launcherNote={note} /> ])[0];
} else if (launcherType === "script") {
widget = wrapReactWidgets<BasicWidget>([ <ScriptLauncher launcherNote={note} /> ])[0];
} else if (launcherType === "customWidget") {
widget = wrapReactWidgets<BasicWidget>([ <CustomWidget launcherNote={note} /> ])[0];
} else if (launcherType === "builtinWidget") {
widget = wrapReactWidgets<BasicWidget>([ this.initBuiltinWidget(note) ])[0];
} else {
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title '${note.title}'`);
}
if (!widget) {
throw new Error(`Unknown initialization error for note '${note.noteId}', title '${note.title}'`);
}
this.child(widget);
this.innerWidget = widget as InnerWidget;
if (this.isHorizontalLayout && this.innerWidget.settings) {
this.innerWidget.settings.titlePlacement = "bottom";
}
return true;
}
async initCustomWidget(note: FNote) {
const widget = await note.getRelationTarget("widget");
if (widget) {
return await widget.executeScript();
} else {
throw new Error(`Custom widget of launcher '${note.noteId}' '${note.title}' is not defined.`);
}
}
initBuiltinWidget(note: FNote) {
const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
case "calendar":
return <CalendarWidget launcherNote={note} />
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return <SpacerWidget baseSize={baseSize} growthFactor={growthFactor} />;
case "bookmarks":
return <BookmarkButtons isHorizontalLayout={this.isHorizontalLayout} />;
case "protectedSession":
return <ProtectedSessionStatusWidget />;
case "syncStatus":
return <SyncStatus />;
case "backInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
case "forwardInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
case "todayInJournal":
return <TodayLauncher launcherNote={note} />
case "quickSearch":
return <QuickSearchLauncherWidget isHorizontalLayout={this.isHorizontalLayout} />
case "aiChatLauncher":
return <AiChatButton launcherNote={note} />
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}
}
}
function ScriptLauncher({ launcherNote }: { launcherNote: FNote }) {
const { icon, title } = useLauncherIconAndTitle(launcherNote);
return (
<LaunchBarActionButton
icon={icon}
text={title}
onClick={async () => {
if (launcherNote.isLabelTruthy("scriptInLauncherContent")) {
await launcherNote.executeScript();
} else {
const script = await launcherNote.getRelationTarget("script");
if (script) {
await script.executeScript();
}
}
}}
/>
)
}
function TodayLauncher({ launcherNote }: { launcherNote: FNote }) {
return (
<CustomNoteLauncher
launcherNote={launcherNote}
getTargetNoteId={async () => {
const todayNote = await date_notes.getTodayNote();
return todayNote?.noteId ?? null;
}}
/>
);
}
function QuickSearchLauncherWidget({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
const widget = useMemo(() => new QuickSearchWidget(), []);
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
const isEnabled = isHorizontalLayout && !isMobile();
parentComponent?.contentSized();
return (
<div>
{isEnabled && <LegacyWidgetRenderer widget={widget} />}
</div>
)
}
function CustomWidget({ launcherNote }: { launcherNote: FNote }) {
const [ widgetNote ] = useNoteRelationTarget(launcherNote, "widget");
const [ widget, setWidget ] = useState<BasicWidget>();
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
parentComponent?.contentSized();
useEffect(() => {
widgetNote?.executeScript().then(setWidget);
}, [ widgetNote ]);
return (
<div>
{widget && <LegacyWidgetRenderer widget={widget} />}
</div>
)
}
function LegacyWidgetRenderer({ widget }: { widget: BasicWidget }) {
const { noteContext } = useNoteContext();
const [ widgetEl ] = useLegacyWidget(() => widget, {
noteContext
});
return widgetEl;
}

View File

@ -1,78 +0,0 @@
import FlexContainer from "./flex_container.js";
import froca from "../../services/froca.js";
import appContext, { type EventData } from "../../components/app_context.js";
import LauncherWidget from "./launcher.js";
import utils from "../../services/utils.js";
export default class LauncherContainer extends FlexContainer<LauncherWidget> {
private isHorizontalLayout: boolean;
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "row" : "column");
this.id("launcher-container");
this.filling();
this.isHorizontalLayout = isHorizontalLayout;
this.load();
}
async load() {
await froca.initializedPromise;
const visibleLaunchersRootId = utils.isMobile() ? "_lbMobileVisibleLaunchers" : "_lbVisibleLaunchers";
const visibleLaunchersRoot = await froca.getNote(visibleLaunchersRootId, true);
if (!visibleLaunchersRoot) {
console.log("Visible launchers root note doesn't exist.");
return;
}
const newChildren: LauncherWidget[] = [];
for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
try {
const launcherWidget = new LauncherWidget(this.isHorizontalLayout);
const success = await launcherWidget.initLauncher(launcherNote);
if (success) {
newChildren.push(launcherWidget);
}
} catch (e) {
console.error(e);
}
}
this.children = [];
this.child(...newChildren);
this.$widget.empty();
this.renderChildren();
await this.handleEventInChildren("initialRenderComplete", {});
const activeContext = appContext.tabManager.getActiveContext();
if (activeContext) {
await this.handleEvent("setNoteContext", {
noteContext: activeContext
});
if (activeContext.notePath) {
await this.handleEvent("noteSwitched", {
noteContext: activeContext,
notePath: activeContext.notePath
});
}
}
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId && froca.getNoteFromCache(branch.parentNoteId)?.isLaunchBarConfig())) {
// changes in note placement require reload of all launchers, all other changes are handled by individual
// launchers
this.load();
}
}
}

View File

@ -0,0 +1,120 @@
import { useLayoutEffect, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import froca from "../../services/froca";
import { isDesktop, isMobile } from "../../services/utils";
import CalendarWidget from "./CalendarWidget";
import SpacerWidget from "./SpacerWidget";
import BookmarkButtons from "./BookmarkButtons";
import ProtectedSessionStatusWidget from "./ProtectedSessionStatusWidget";
import SyncStatus from "./SyncStatus";
import HistoryNavigationButton from "./HistoryNavigation";
import { CustomWidget, QuickSearchLauncherWidget, ScriptLauncher, TodayLauncher } from "./LauncherDefinitions";
import AiChatButton from "./AiChatButton";
import { CommandButton, NoteLauncher } from "./GenericButtons";
import { useTriliumEvent } from "../react/hooks";
export default function LauncherContainer({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
const childNotes = useLauncherChildNotes();
return (
<div
id="launcher-container"
style={{
display: "flex",
flexGrow: 1,
flexDirection: isHorizontalLayout ? "row" : "column"
}}
>
{childNotes?.map(childNote => {
if (childNote.type !== "launcher") {
throw new Error(`Note '${childNote.noteId}' '${childNote.title}' is not a launcher even though it's in the launcher subtree`);
}
if (!isDesktop() && childNote.isLabelTruthy("desktopOnly")) {
return false;
}
return <Launcher key={childNote.noteId} note={childNote} isHorizontalLayout={isHorizontalLayout} />
})}
</div>
)
}
function Launcher({ note, isHorizontalLayout }: { note: FNote, isHorizontalLayout: boolean }) {
const launcherType = note.getLabelValue("launcherType");
if (glob.TRILIUM_SAFE_MODE && launcherType === "customWidget") return;
switch (launcherType) {
case "command":
return <CommandButton launcherNote={note} />;
case "note":
return <NoteLauncher launcherNote={note} />;
case "script":
return <ScriptLauncher launcherNote={note} />;
case "customWidget":
return <CustomWidget launcherNote={note} />;
case "builtinWidget":
return initBuiltinWidget(note, isHorizontalLayout);
default:
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title '${note.title}'`);
}
}
function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
case "calendar":
return <CalendarWidget launcherNote={note} />
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return <SpacerWidget baseSize={baseSize} growthFactor={growthFactor} />;
case "bookmarks":
return <BookmarkButtons isHorizontalLayout={isHorizontalLayout} />;
case "protectedSession":
return <ProtectedSessionStatusWidget />;
case "syncStatus":
return <SyncStatus />;
case "backInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
case "forwardInHistoryButton":
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
case "todayInJournal":
return <TodayLauncher launcherNote={note} />
case "quickSearch":
return <QuickSearchLauncherWidget isHorizontalLayout={isHorizontalLayout} />
case "aiChatLauncher":
return <AiChatButton launcherNote={note} />
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}
}
function useLauncherChildNotes() {
const [ visibleLaunchersRoot, setVisibleLaunchersRoot ] = useState<FNote | undefined | null>();
const [ childNotes, setChildNotes ] = useState<FNote[]>();
// Load the root note.
useLayoutEffect(() => {
const visibleLaunchersRootId = isMobile() ? "_lbMobileVisibleLaunchers" : "_lbVisibleLaunchers";
froca.getNote(visibleLaunchersRootId, true).then(setVisibleLaunchersRoot);
}, []);
// Load the children.
function refresh() {
if (!visibleLaunchersRoot) return;
visibleLaunchersRoot.getChildNotes().then(setChildNotes);
}
useLayoutEffect(refresh, [ visibleLaunchersRoot ]);
// React to position changes.
useTriliumEvent("entitiesReloaded", ({loadResults}) => {
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId && froca.getNoteFromCache(branch.parentNoteId)?.isLaunchBarConfig())) {
refresh();
}
});
return childNotes;
}

View File

@ -0,0 +1,80 @@
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { useLegacyWidget, useNoteContext, useNoteRelationTarget } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import BasicWidget from "../basic_widget";
import FNote from "../../entities/fnote";
import QuickSearchWidget from "../quick_search";
import { isMobile } from "../../services/utils";
import date_notes from "../../services/date_notes";
import { CustomNoteLauncher } from "./GenericButtons";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
export function ScriptLauncher({ launcherNote }: { launcherNote: FNote }) {
const { icon, title } = useLauncherIconAndTitle(launcherNote);
return (
<LaunchBarActionButton
icon={icon}
text={title}
onClick={async () => {
if (launcherNote.isLabelTruthy("scriptInLauncherContent")) {
await launcherNote.executeScript();
} else {
const script = await launcherNote.getRelationTarget("script");
if (script) {
await script.executeScript();
}
}
}}
/>
)
}
export function TodayLauncher({ launcherNote }: { launcherNote: FNote }) {
return (
<CustomNoteLauncher
launcherNote={launcherNote}
getTargetNoteId={async () => {
const todayNote = await date_notes.getTodayNote();
return todayNote?.noteId ?? null;
}}
/>
);
}
export function QuickSearchLauncherWidget({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
const widget = useMemo(() => new QuickSearchWidget(), []);
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
const isEnabled = isHorizontalLayout && !isMobile();
parentComponent?.contentSized();
return (
<div>
{isEnabled && <LegacyWidgetRenderer widget={widget} />}
</div>
)
}
export function CustomWidget({ launcherNote }: { launcherNote: FNote }) {
const [ widgetNote ] = useNoteRelationTarget(launcherNote, "widget");
const [ widget, setWidget ] = useState<BasicWidget>();
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
parentComponent?.contentSized();
useEffect(() => {
widgetNote?.executeScript().then(setWidget);
}, [ widgetNote ]);
return (
<div>
{widget && <LegacyWidgetRenderer widget={widget} />}
</div>
)
}
export function LegacyWidgetRenderer({ widget }: { widget: BasicWidget }) {
const { noteContext } = useNoteContext();
const [ widgetEl ] = useLegacyWidget(() => widget, {
noteContext
});
return widgetEl;
}