2025-12-12 00:13:25 +02:00
|
|
|
import "./StatusBar.css";
|
|
|
|
|
|
2025-12-12 19:31:00 +02:00
|
|
|
import { Locale } from "@triliumnext/commons";
|
|
|
|
|
import clsx from "clsx";
|
2025-12-12 20:17:07 +02:00
|
|
|
import { type ComponentChildren } from "preact";
|
2025-12-12 19:31:00 +02:00
|
|
|
import { createPortal } from "preact/compat";
|
2025-12-12 20:55:54 +02:00
|
|
|
import { useContext, useRef, useState } from "preact/hooks";
|
2025-12-12 19:31:00 +02:00
|
|
|
|
2025-12-12 20:59:57 +02:00
|
|
|
import { CommandNames } from "../../components/app_context";
|
2025-12-12 20:30:15 +02:00
|
|
|
import NoteContext from "../../components/note_context";
|
2025-12-12 00:34:47 +02:00
|
|
|
import FNote from "../../entities/fnote";
|
2025-12-12 21:29:40 +02:00
|
|
|
import attributes from "../../services/attributes";
|
2025-12-12 00:34:47 +02:00
|
|
|
import { t } from "../../services/i18n";
|
2025-12-12 20:30:15 +02:00
|
|
|
import { ViewScope } from "../../services/link";
|
2025-12-12 00:34:47 +02:00
|
|
|
import { openInAppHelpFromUrl } from "../../services/utils";
|
2025-12-12 19:31:00 +02:00
|
|
|
import { formatDateTime } from "../../utils/formatters";
|
2025-12-12 20:30:15 +02:00
|
|
|
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
|
2025-12-12 19:31:00 +02:00
|
|
|
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
2025-12-12 18:29:40 +02:00
|
|
|
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
2025-12-12 21:19:16 +02:00
|
|
|
import { useActiveNoteContext, useStaticTooltip, useTriliumEvent } from "../react/hooks";
|
2025-12-12 18:46:34 +02:00
|
|
|
import Icon from "../react/Icon";
|
2025-12-12 20:59:57 +02:00
|
|
|
import { ParentComponent } from "../react/react_utils";
|
2025-12-12 19:31:00 +02:00
|
|
|
import { ContentLanguagesModal, useLanguageSwitcher } from "../ribbon/BasicPropertiesTab";
|
2025-12-12 21:29:40 +02:00
|
|
|
import AttributeEditor, { AttributeEditorImperativeHandlers } from "../ribbon/components/AttributeEditor";
|
2025-12-12 19:31:00 +02:00
|
|
|
import { NoteSizeWidget, useNoteMetadata } from "../ribbon/NoteInfoTab";
|
2025-12-12 20:59:57 +02:00
|
|
|
import { useAttachments } from "../type_widgets/Attachment";
|
2025-12-12 19:31:00 +02:00
|
|
|
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
|
|
|
|
|
import Breadcrumb from "./Breadcrumb";
|
2025-12-12 00:34:47 +02:00
|
|
|
|
|
|
|
|
interface StatusBarContext {
|
|
|
|
|
note: FNote;
|
2025-12-12 20:17:07 +02:00
|
|
|
noteContext: NoteContext;
|
2025-12-12 20:59:57 +02:00
|
|
|
viewScope?: ViewScope;
|
2025-12-12 00:34:47 +02:00
|
|
|
}
|
|
|
|
|
|
2025-12-12 00:13:25 +02:00
|
|
|
export default function StatusBar() {
|
2025-12-12 20:30:15 +02:00
|
|
|
const { note, noteContext, viewScope } = useActiveNoteContext();
|
|
|
|
|
const context = note && noteContext && { note, noteContext, viewScope } satisfies StatusBarContext;
|
2025-12-12 00:34:47 +02:00
|
|
|
|
2025-12-12 00:13:25 +02:00
|
|
|
return (
|
|
|
|
|
<div className="status-bar">
|
2025-12-12 21:29:40 +02:00
|
|
|
{context && <AttributesPane {...context} />}
|
|
|
|
|
|
|
|
|
|
<div className="status-bar-main-row">
|
|
|
|
|
{context && <>
|
|
|
|
|
<div className="breadcrumb-row">
|
|
|
|
|
<Breadcrumb {...context} />
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="actions-row">
|
|
|
|
|
<AttributesButton {...context} />
|
|
|
|
|
<AttachmentCount {...context} />
|
|
|
|
|
<BacklinksBadge {...context} />
|
|
|
|
|
<LanguageSwitcher {...context} />
|
|
|
|
|
<NoteInfoBadge {...context} />
|
|
|
|
|
</div>
|
|
|
|
|
</>}
|
|
|
|
|
</div>
|
2025-12-12 00:13:25 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-12 00:34:47 +02:00
|
|
|
|
2025-12-12 18:58:54 +02:00
|
|
|
function StatusBarDropdown({ children, icon, text, buttonClassName, titleOptions, ...dropdownProps }: Omit<DropdownProps, "hideToggleArrow" | "title" | "titlePosition"> & {
|
|
|
|
|
title: string;
|
2025-12-12 18:46:34 +02:00
|
|
|
icon?: string;
|
|
|
|
|
}) {
|
|
|
|
|
return (
|
|
|
|
|
<Dropdown
|
|
|
|
|
buttonClassName={clsx("status-bar-dropdown-button", buttonClassName)}
|
2025-12-12 18:58:54 +02:00
|
|
|
titlePosition="top"
|
|
|
|
|
titleOptions={{
|
|
|
|
|
...titleOptions,
|
|
|
|
|
popperConfig: {
|
|
|
|
|
...titleOptions?.popperConfig,
|
|
|
|
|
strategy: "fixed"
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-12-12 18:46:34 +02:00
|
|
|
text={<>
|
|
|
|
|
{icon && (<><Icon icon={icon} /> </>)}
|
|
|
|
|
{text}
|
|
|
|
|
</>}
|
|
|
|
|
{...dropdownProps}
|
|
|
|
|
>
|
|
|
|
|
{children}
|
|
|
|
|
</Dropdown>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 21:29:40 +02:00
|
|
|
interface StatusBarButtonBaseProps {
|
2025-12-12 20:55:54 +02:00
|
|
|
className?: string;
|
|
|
|
|
icon: string;
|
|
|
|
|
title: string;
|
|
|
|
|
text?: string | number;
|
|
|
|
|
disabled?: boolean;
|
2025-12-12 21:29:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type StatusBarButtonWithCommand = StatusBarButtonBaseProps & { triggerCommand: CommandNames; };
|
|
|
|
|
type StatusBarButtonWithClick = StatusBarButtonBaseProps & { onClick: () => void; };
|
|
|
|
|
|
|
|
|
|
function StatusBarButton({ className, icon, text, title, ...restProps }: StatusBarButtonWithCommand | StatusBarButtonWithClick) {
|
2025-12-12 20:55:54 +02:00
|
|
|
const parentComponent = useContext(ParentComponent);
|
|
|
|
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
|
|
|
useStaticTooltip(buttonRef, {
|
|
|
|
|
placement: "top",
|
|
|
|
|
fallbackPlacements: [ "top" ],
|
|
|
|
|
popperConfig: { strategy: "fixed" },
|
|
|
|
|
title
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
ref={buttonRef}
|
|
|
|
|
className={clsx("btn select-button", className)}
|
|
|
|
|
type="button"
|
2025-12-12 21:29:40 +02:00
|
|
|
onClick={() => {
|
|
|
|
|
if ("triggerCommand" in restProps) {
|
|
|
|
|
parentComponent?.triggerCommand(restProps.triggerCommand);
|
|
|
|
|
} else {
|
|
|
|
|
restProps.onClick();
|
|
|
|
|
}
|
|
|
|
|
}}
|
2025-12-12 20:55:54 +02:00
|
|
|
>
|
|
|
|
|
<Icon icon={icon} /> {text}
|
|
|
|
|
</button>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-12 19:31:00 +02:00
|
|
|
//#region Language Switcher
|
2025-12-12 00:34:47 +02:00
|
|
|
function LanguageSwitcher({ note }: StatusBarContext) {
|
2025-12-12 18:29:40 +02:00
|
|
|
const [ modalShown, setModalShown ] = useState(false);
|
|
|
|
|
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
|
|
|
|
|
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
|
|
|
|
|
|
2025-12-12 00:34:47 +02:00
|
|
|
return (
|
2025-12-12 18:29:40 +02:00
|
|
|
<>
|
2025-12-12 20:18:50 +02:00
|
|
|
{note.type === "text" && <StatusBarDropdown
|
2025-12-12 18:58:54 +02:00
|
|
|
icon="bx bx-globe"
|
|
|
|
|
title={t("status_bar.language_title")}
|
|
|
|
|
text={<span dir={activeLocale?.rtl ? "rtl" : "ltr"}>{getLocaleName(activeLocale)}</span>}
|
|
|
|
|
>
|
2025-12-12 20:59:57 +02:00
|
|
|
{processedLocales.map((locale, index) => {
|
2025-12-12 18:29:40 +02:00
|
|
|
if (typeof locale === "object") {
|
|
|
|
|
return <FormListItem
|
2025-12-12 20:59:57 +02:00
|
|
|
key={locale.id}
|
2025-12-12 18:29:40 +02:00
|
|
|
rtl={locale.rtl}
|
|
|
|
|
checked={locale.id === currentNoteLanguage}
|
|
|
|
|
onClick={() => setCurrentNoteLanguage(locale.id)}
|
2025-12-12 20:59:57 +02:00
|
|
|
>{locale.name}</FormListItem>;
|
2025-12-12 18:29:40 +02:00
|
|
|
} else {
|
2025-12-12 20:59:57 +02:00
|
|
|
return <FormDropdownDivider key={`divider-${index}`} />;
|
2025-12-12 18:29:40 +02:00
|
|
|
}
|
|
|
|
|
})}
|
|
|
|
|
<FormDropdownDivider />
|
2025-12-12 00:34:47 +02:00
|
|
|
<FormListItem
|
|
|
|
|
onClick={() => openInAppHelpFromUrl("veGu4faJErEM")}
|
|
|
|
|
icon="bx bx-help-circle"
|
|
|
|
|
>{t("note_language.help-on-languages")}</FormListItem>
|
2025-12-12 18:29:40 +02:00
|
|
|
<FormListItem
|
|
|
|
|
onClick={() => setModalShown(true)}
|
|
|
|
|
icon="bx bx-cog"
|
|
|
|
|
>{t("note_language.configure-languages")}</FormListItem>
|
2025-12-12 20:18:50 +02:00
|
|
|
</StatusBarDropdown>}
|
2025-12-12 18:29:40 +02:00
|
|
|
{createPortal(
|
|
|
|
|
<ContentLanguagesModal modalShown={modalShown} setModalShown={setModalShown} />,
|
|
|
|
|
document.body
|
2025-12-12 00:34:47 +02:00
|
|
|
)}
|
2025-12-12 18:29:40 +02:00
|
|
|
</>
|
2025-12-12 00:34:47 +02:00
|
|
|
);
|
|
|
|
|
}
|
2025-12-12 18:29:40 +02:00
|
|
|
|
|
|
|
|
export function getLocaleName(locale: Locale | null | undefined) {
|
|
|
|
|
if (!locale) return "";
|
|
|
|
|
if (!locale.id) return "-";
|
2025-12-12 18:53:54 +02:00
|
|
|
if (locale.name.length <= 4 || locale.rtl) return locale.name; // Some locales like Japanese and Chinese look better than their ID.
|
|
|
|
|
return locale.id
|
|
|
|
|
.replace("_", "-")
|
|
|
|
|
.toLocaleUpperCase();
|
2025-12-12 18:29:40 +02:00
|
|
|
}
|
2025-12-12 19:31:00 +02:00
|
|
|
//#endregion
|
|
|
|
|
|
|
|
|
|
//#region Note info
|
|
|
|
|
export function NoteInfoBadge({ note }: { note: FNote | null | undefined }) {
|
|
|
|
|
const { metadata, ...sizeProps } = useNoteMetadata(note);
|
|
|
|
|
|
|
|
|
|
return (note &&
|
|
|
|
|
<StatusBarDropdown
|
|
|
|
|
icon="bx bx-info-circle"
|
|
|
|
|
title={t("status_bar.note_info_title")}
|
|
|
|
|
dropdownContainerClassName="dropdown-note-info"
|
|
|
|
|
dropdownOptions={{ autoClose: "outside" }}
|
|
|
|
|
>
|
|
|
|
|
<ul>
|
|
|
|
|
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
|
|
|
|
|
<NoteInfoValue text={t("note_info_widget.modified")} value={formatDateTime(metadata?.dateModified)} />
|
|
|
|
|
<NoteInfoValue text={t("note_info_widget.type")} value={<span>{note.type} {note.mime && <span>({note.mime})</span>}</span>} />
|
|
|
|
|
<NoteInfoValue text={t("note_info_widget.note_id")} value={<code>{note.noteId}</code>} />
|
|
|
|
|
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
|
|
|
|
|
</ul>
|
|
|
|
|
</StatusBarDropdown>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function NoteInfoValue({ text, title, value }: { text: string; title?: string, value: ComponentChildren }) {
|
|
|
|
|
return (
|
|
|
|
|
<li>
|
|
|
|
|
<strong title={title}>{text}{": "}</strong>
|
|
|
|
|
<span>{value}</span>
|
|
|
|
|
</li>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
//#endregion
|
2025-12-12 20:30:15 +02:00
|
|
|
|
|
|
|
|
//#region Backlinks
|
|
|
|
|
function BacklinksBadge({ note, viewScope }: StatusBarContext) {
|
|
|
|
|
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
|
|
|
|
|
return (note && count > 0 &&
|
|
|
|
|
<StatusBarDropdown
|
|
|
|
|
className="backlinks-badge backlinks-widget"
|
|
|
|
|
icon="bx bx-revision"
|
2025-12-12 20:55:54 +02:00
|
|
|
text={count}
|
2025-12-12 20:30:15 +02:00
|
|
|
title={t("status_bar.backlinks_title", { count })}
|
|
|
|
|
dropdownContainerClassName="backlinks-items"
|
|
|
|
|
>
|
|
|
|
|
<BacklinksList note={note} />
|
|
|
|
|
</StatusBarDropdown>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
//#endregion
|
2025-12-12 20:55:54 +02:00
|
|
|
|
|
|
|
|
//#region Attachment count
|
|
|
|
|
function AttachmentCount({ note }: StatusBarContext) {
|
|
|
|
|
const attachments = useAttachments(note);
|
|
|
|
|
const count = attachments.length;
|
|
|
|
|
|
|
|
|
|
return (note && count > 0 &&
|
|
|
|
|
<StatusBarButton
|
2025-12-12 21:19:16 +02:00
|
|
|
className="attachment-count-button"
|
2025-12-12 20:55:54 +02:00
|
|
|
icon="bx bx-paperclip"
|
|
|
|
|
text={count}
|
|
|
|
|
title={t("status_bar.attachments_title", { count })}
|
|
|
|
|
triggerCommand="showAttachments"
|
|
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
//#endregion
|
2025-12-12 21:19:16 +02:00
|
|
|
|
|
|
|
|
//#region Attributes
|
|
|
|
|
function AttributesButton({ note }: StatusBarContext) {
|
|
|
|
|
const [ count, setCount ] = useState(note.attributes.length);
|
|
|
|
|
|
|
|
|
|
// React to changes in count.
|
|
|
|
|
useTriliumEvent("entitiesReloaded", (({loadResults}) => {
|
|
|
|
|
if (loadResults.getAttributeRows().some(attr => attributes.isAffecting(attr, note))) {
|
|
|
|
|
setCount(note.attributes.length);
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<StatusBarButton
|
|
|
|
|
className="attributes-button"
|
|
|
|
|
icon="bx bx-list-check"
|
|
|
|
|
title={t("status_bar.attributes_title")}
|
|
|
|
|
text={t("status_bar.attributes", { count })}
|
2025-12-12 21:29:40 +02:00
|
|
|
onClick={() => {
|
|
|
|
|
alert("Hi");
|
|
|
|
|
}}
|
2025-12-12 21:19:16 +02:00
|
|
|
/>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-12 21:29:40 +02:00
|
|
|
|
|
|
|
|
function AttributesPane({ note, noteContext }: StatusBarContext) {
|
|
|
|
|
const parentComponent = useContext(ParentComponent);
|
|
|
|
|
const api = useRef<AttributeEditorImperativeHandlers>(null);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="attribute-list">
|
|
|
|
|
{parentComponent && <AttributeEditor
|
|
|
|
|
componentId={parentComponent.componentId}
|
|
|
|
|
api={api}
|
|
|
|
|
ntxId={noteContext.ntxId}
|
|
|
|
|
note={note}
|
|
|
|
|
hidden={!note}
|
|
|
|
|
/>}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-12-12 21:19:16 +02:00
|
|
|
//#endregion
|