Merge 4571b956830d33b7e0e60e8ac314486dd8c4cc4b into 1195cbd772ffd68f7757855f272a7c5c2c29eb78

This commit is contained in:
Elian Doran 2025-12-04 22:59:10 +02:00 committed by GitHub
commit 1b32a67938
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 957 additions and 1051 deletions

View File

@ -30,7 +30,6 @@ import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx";
import SpacerWidget from "../widgets/spacer.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js";
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
@ -43,8 +42,8 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import utils from "../services/utils.js";
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
export default class DesktopLayout {
@ -125,7 +124,7 @@ export default class DesktopLayout {
.cssBlock(".title-row > * { margin: 5px; }")
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(new SpacerWidget(0, 1))
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
.child(<MovePaneButton direction="left" />)
.child(<MovePaneButton direction="right" />)
.child(<ClosePaneButton />)

View File

@ -150,7 +150,7 @@ export function isMac() {
export const hasTouchBar = (isMac() && isElectron());
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
}

View File

@ -212,7 +212,7 @@ body[dir=ltr] #launcher-container {
}
#launcher-pane .launcher-button,
#launcher-pane .dropdown {
#launcher-pane .right-dropdown-widget {
width: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
height: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
margin: var(--launcher-pane-button-gap) var(--launcher-pane-button-margin);

View File

@ -1,78 +0,0 @@
import FlexContainer from "./containers/flex_container.js";
import OpenNoteButtonWidget from "./buttons/open_note_button_widget.js";
import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
import froca from "../services/froca.js";
import utils from "../services/utils.js";
import type { EventData } from "../components/app_context.js";
import type Component from "../components/component.js";
interface BookmarkButtonsSettings {
titlePlacement?: string;
}
export default class BookmarkButtons extends FlexContainer<Component> {
private settings: BookmarkButtonsSettings;
private noteIds: string[];
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "row" : "column");
this.contentSized();
this.settings = {};
this.noteIds = [];
}
async refresh(): Promise<void> {
this.$widget.empty();
this.children = [];
this.noteIds = [];
const bookmarkParentNote = await froca.getNote("_lbBookmarks");
if (!bookmarkParentNote) {
return;
}
for (const note of await bookmarkParentNote.getChildNotes()) {
this.noteIds.push(note.noteId);
let buttonWidget: OpenNoteButtonWidget | BookmarkFolderWidget = note.isLabelTruthy("bookmarkFolder")
? new BookmarkFolderWidget(note)
: new OpenNoteButtonWidget(note).class("launcher-button");
if (this.settings.titlePlacement) {
if (!("settings" in buttonWidget)) {
(buttonWidget as any).settings = {};
}
(buttonWidget as any).settings.titlePlacement = this.settings.titlePlacement;
}
this.child(buttonWidget);
this.$widget.append(buttonWidget.render());
buttonWidget.refreshIcon();
}
utils.reloadTray();
}
initialRenderCompleteEvent(): void {
this.refresh();
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">): void {
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === "_lbBookmarks")) {
this.refresh();
}
if (loadResults.getAttributeRows().find((attr) =>
attr.type === "label" &&
attr.name && ["iconClass", "workspaceIconClass", "bookmarkFolder"].includes(attr.name) &&
attr.noteId && this.noteIds.includes(attr.noteId)
)) {
this.refresh();
}
}
}

View File

@ -1,26 +0,0 @@
import type { EventData } from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
import options from "../../services/options.js";
import CommandButtonWidget from "./command_button.js";
export default class AiChatButton extends CommandButtonWidget {
constructor(note: FNote) {
super();
this.command("createAiChat")
.title(() => note.title)
.icon(() => note.getIcon())
.class("launcher-button");
}
isEnabled() {
return options.get("aiEnabled") === "true";
}
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
if (loadResults.isOptionReloaded("aiEnabled")) {
this.refresh();
}
}
}

View File

@ -1,88 +0,0 @@
import RightDropdownButtonWidget from "./right_dropdown_button.js";
import linkService from "../../services/link.js";
import utils from "../../services/utils.js";
import type FNote from "../../entities/fnote.js";
const DROPDOWN_TPL = `
<div class="bookmark-folder-widget">
<style>
.bookmark-folder-widget {
min-width: 400px;
max-height: 500px;
padding: 7px 15px 0 15px;
font-size: 1.2rem;
overflow: auto;
}
.bookmark-folder-widget ul {
padding: 0;
list-style-type: none;
}
.bookmark-folder-widget .note-link {
display: block;
padding: 5px 10px 5px 5px;
}
.bookmark-folder-widget .note-link:hover {
background-color: var(--accented-background-color);
text-decoration: none;
}
.dropdown-menu .bookmark-folder-widget a:hover {
text-decoration: none;
background: transparent !important;
}
.bookmark-folder-widget li .note-link {
padding-inline-start: 35px;
}
</style>
<div class="parent-note"></div>
<ul class="children-notes"></ul>
</div>`;
interface LinkOptions {
showTooltip: boolean;
showNoteIcon: boolean;
}
export default class BookmarkFolderWidget extends RightDropdownButtonWidget {
private note: FNote;
private $parentNote!: JQuery<HTMLElement>;
private $childrenNotes!: JQuery<HTMLElement>;
declare $dropdownContent: JQuery<HTMLElement>;
constructor(note: FNote) {
super(utils.escapeHtml(note.title), note.getIcon(), DROPDOWN_TPL);
this.note = note;
}
doRender(): void {
super.doRender();
this.$parentNote = this.$dropdownContent.find(".parent-note");
this.$childrenNotes = this.$dropdownContent.find(".children-notes");
}
async dropdownShown(): Promise<void> {
this.$parentNote.empty();
this.$childrenNotes.empty();
const linkOptions: LinkOptions = {
showTooltip: false,
showNoteIcon: true
};
this.$parentNote.append((await linkService.createLink(this.note.noteId, linkOptions)).addClass("note-link"));
for (const childNote of await this.note.getChildNotes()) {
this.$childrenNotes.append($("<li>").append((await linkService.createLink(childNote.noteId, linkOptions)).addClass("note-link")));
}
}
refreshIcon(): void {}
}

View File

@ -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();
}
}
}

View File

@ -8,76 +8,8 @@ import options from "../../services/options.js";
import { Dropdown } from "bootstrap";
import type { EventData } from "../../components/app_context.js";
import { dayjs, type Dayjs } from "@triliumnext/commons";
import "../../stylesheets/calendar.css";
import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons";
const MONTHS = [
t("calendar.january"),
t("calendar.february"),
t("calendar.march"),
t("calendar.april"),
t("calendar.may"),
t("calendar.june"),
t("calendar.july"),
t("calendar.august"),
t("calendar.september"),
t("calendar.october"),
t("calendar.november"),
t("calendar.december")
];
const DROPDOWN_TPL = `
<div class="calendar-dropdown-widget">
<style>
.calendar-dropdown-widget {
width: 400px;
}
</style>
<div class="calendar-header">
<div class="calendar-month-selector">
<button class="calendar-btn tn-tool-button bx bx-chevron-left" data-calendar-toggle="previous"></button>
<button class="btn dropdown-toggle select-button" type="button"
data-bs-toggle="dropdown" data-bs-auto-close="true"
aria-expanded="false"
data-calendar-input="month"></button>
<ul class="dropdown-menu" data-calendar-input="month-list">
${Object.entries(MONTHS)
.map(([i, month]) => `<li><button class="dropdown-item" data-value=${i}>${month}</button></li>`)
.join("")}
</ul>
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="next"></button>
</div>
<div class="calendar-year-selector">
<button class="calendar-btn tn-tool-button bx bx-chevron-left" data-calendar-toggle="previousYear"></button>
<input type="number" min="1900" max="2999" step="1" data-calendar-input="year" />
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="nextYear"></button>
</div>
</div>
<div class="calendar-week"></div>
<div class="calendar-body" data-calendar-area="month"></div>
</div>`;
const DAYS_OF_WEEK = [
t("calendar.sun"),
t("calendar.mon"),
t("calendar.tue"),
t("calendar.wed"),
t("calendar.thu"),
t("calendar.fri"),
t("calendar.sat")
];
interface DateNotesForMonth {
[date: string]: string;
}
interface WeekCalculationOptions {
firstWeekType: number;
minDaysInFirstWeek: number;
@ -110,7 +42,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
super.doRender();
this.$month = this.$dropdownContent.find('[data-calendar-area="month"]');
this.$weekHeader = this.$dropdownContent.find(".calendar-week");
this.manageFirstDayOfWeek();
this.initWeekCalculation();
@ -131,37 +62,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
}
});
this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]');
this.$next.on("click", () => {
this.date = this.date.add(1, 'month');
this.createMonth();
});
this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]');
this.$previous.on("click", () => {
this.date = this.date.subtract(1, 'month');
this.createMonth();
});
// Year navigation
this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]');
this.$yearSelect.on("input", (e) => {
const target = e.target as HTMLInputElement;
this.date = this.date.year(parseInt(target.value));
this.createMonth();
});
this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]');
this.$nextYear.on("click", () => {
this.date = this.date.add(1, 'year');
this.createMonth();
});
this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]');
this.$previousYear.on("click", () => {
this.date = this.date.subtract(1, 'year');
this.createMonth();
});
// Date click
this.$dropdownContent.on("click", ".calendar-date", async (ev) => {
const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date");
@ -226,17 +126,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
this.weekNoteEnable = noteAttributes.some(a => a.name === 'enableWeekNote');
}
// Store firstDayOfWeek as ISO (17)
manageFirstDayOfWeek() {
const rawFirstDayOfWeek = options.getInt("firstDayOfWeek") || 0;
this.firstDayOfWeekISO = rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek;
let localeDaysOfWeek = [...DAYS_OF_WEEK];
const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek);
localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted];
this.$weekHeader.html(localeDaysOfWeek.map((el) => `<span>${el}</span>`).join(''));
}
initWeekCalculation() {
this.weekCalculationOptions = {
firstWeekType: options.getInt("firstWeekOfYear") || 0,
@ -244,34 +133,13 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
};
}
getWeekStartDate(date: Dayjs): Dayjs {
const currentISO = date.isoWeekday();
const diff = (currentISO - this.firstDayOfWeekISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
}
getWeekNumber(date: Dayjs): number {
const weekStart = this.getWeekStartDate(date);
return weekStart.isoWeek();
}
async dropdownShown() {
await this.getWeekNoteEnable();
this.weekNotes = await server.get<string[]>(`attribute-values/weekNote`);
this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null);
this.init( ?? null);
}
init(activeDate: string | null) {
this.activeDate = activeDate ? dayjs(`${activeDate}T12:00:00`) : null;
this.todaysDate = dayjs();
this.date = dayjs(this.activeDate || this.todaysDate).startOf('month');
this.createMonth();
}
createDay(dateNotesForMonth: DateNotesForMonth, num: number) {
const $newDay = $("<a>")
.addClass("calendar-date")
.attr("data-calendar-date", this.date.local().format('YYYY-MM-DD'));
createDay() {
const $date = $("<span>").html(String(num));
const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')];
@ -296,113 +164,13 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
$newWeekNumber.addClass("calendar-date-exists").attr("data-href", `#root/${weekNoteId}`);
}
} else {
$newWeekNumber = $("<span>").addClass("calendar-week-number-disabled");
}
$newWeekNumber.addClass("calendar-week-number").attr("data-calendar-week-number", weekNoteId);
$newWeekNumber.append($("<span>").html(String(weekNumber)));
return $newWeekNumber;
}
// Use isoWeekday() consistently
private getPrevMonthDays(firstDayISO: number): { weekNumber: number, dates: Dayjs[] } {
const prevMonthLastDay = this.date.subtract(1, 'month').endOf('month');
const daysToAdd = (firstDayISO - this.firstDayOfWeekISO + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = this.date.startOf('month');
const weekNumber = this.getWeekNumber(firstDay);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
return { weekNumber, dates };
}
private getNextMonthDays(lastDayISO: number): Dayjs[] {
const nextMonthFirstDay = this.date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
for (let i = 0; i < daysToAdd; i++) {
dates.push(nextMonthFirstDay.add(i, 'day'));
}
return dates;
}
async createMonth() {
const month = this.date.format('YYYY-MM');
const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`);
this.$month.empty();
const firstDay = this.date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
// Previous month filler
if (firstDayISO !== this.firstDayOfWeekISO) {
const { weekNumber, dates } = this.getPrevMonthDays(firstDayISO);
const prevMonth = this.date.subtract(1, 'month').format('YYYY-MM');
const dateNotesForPrevMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${prevMonth}`);
const $weekNumber = this.createWeekNumber(weekNumber);
this.$month.append($weekNumber);
dates.forEach(date => {
const tempDate = this.date;
this.date = date;
const $day = this.createDay(dateNotesForPrevMonth, date.date());
$day.addClass('calendar-date-prev-month');
this.$month.append($day);
this.date = tempDate;
});
}
const currentMonth = this.date.month();
// Main month
while (this.date.month() === currentMonth) {
const weekNumber = this.getWeekNumber(this.date);
if (this.date.isoWeekday() === this.firstDayOfWeekISO) {
const $weekNumber = this.createWeekNumber(weekNumber);
this.$month.append($weekNumber);
}
const $day = this.createDay(dateNotesForMonth, this.date.date());
this.$month.append($day);
this.date = this.date.add(1, 'day');
}
// while loop trips over and day is at 30/31, bring it back
this.date = this.date.startOf('month').subtract(1, 'month');
// Add dates from next month
const lastDayOfMonth = this.date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1;
if (lastDayISO !== lastDayOfUserWeek) {
const dates = this.getNextMonthDays(lastDayISO);
const nextMonth = this.date.add(1, 'month').format('YYYY-MM');
const dateNotesForNextMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${nextMonth}`);
dates.forEach(date => {
const tempDate = this.date;
this.date = date;
const $day = this.createDay(dateNotesForNextMonth, date.date());
$day.addClass('calendar-date-next-month');
this.$month.append($day);
this.date = tempDate;
});
}
this.$monthSelect.text(MONTHS[this.date.month()]);
this.$yearSelect.val(this.date.year());
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
const WEEK_OPTIONS: (keyof OptionDefinitions)[] = [
"firstDayOfWeek",

View File

@ -1,73 +0,0 @@
import { ActionKeyboardShortcut } from "@triliumnext/commons";
import type { CommandNames } from "../../components/app_context.js";
import keyboardActionsService from "../../services/keyboard_actions.js";
import AbstractButtonWidget, { type AbstractButtonWidgetSettings } from "./abstract_button.js";
import type { ButtonNoteIdProvider } from "./button_from_note.js";
let actions: ActionKeyboardShortcut[];
keyboardActionsService.getActions().then((as) => (actions = as));
// TODO: Is this actually used?
export type ClickHandler = (widget: CommandButtonWidget, e: JQuery.ClickEvent<any, any, any, any>) => void;
type CommandOrCallback = CommandNames | (() => CommandNames);
interface CommandButtonWidgetSettings extends AbstractButtonWidgetSettings {
command?: CommandOrCallback;
onClick?: ClickHandler;
buttonNoteIdProvider?: ButtonNoteIdProvider | null;
}
export default class CommandButtonWidget extends AbstractButtonWidget<CommandButtonWidgetSettings> {
constructor() {
super();
this.settings = {
titlePlacement: "right",
title: null,
icon: null,
onContextMenu: null
};
}
doRender() {
super.doRender();
if (this.settings.command) {
this.$widget.on("click", () => {
this.tooltip.hide();
if (this._command) {
this.triggerCommand(this._command);
}
});
} else {
console.warn(`Button widget '${this.componentId}' has no defined command`, this.settings);
}
}
getTitle() {
const title = super.getTitle();
const action = actions.find((act) => act.actionName === this._command);
if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) {
return `${title} (${action.effectiveShortcuts.join(", ")})`;
} else {
return title;
}
}
onClick(handler: ClickHandler) {
this.settings.onClick = handler;
return this;
}
command(command: CommandOrCallback) {
this.settings.command = command;
return this;
}
get _command() {
return typeof this.settings.command === "function" ? this.settings.command() : this.settings.command;
}
}

View File

@ -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<string>[] = [];
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<string>) => {
if (item && item.command && this.webContents) {
const idx = parseInt(item.command, 10);
this.webContents.navigationHistory.goToIndex(idx);
}
}
});
}
activeNoteChangedEvent() {
this.refresh();
}
}

View File

@ -1,99 +0,0 @@
import { t } from "../../../services/i18n.js";
import AbstractLauncher from "./abstract_launcher.js";
import dialogService from "../../../services/dialog.js";
import appContext from "../../../components/app_context.js";
import utils from "../../../services/utils.js";
import linkContextMenuService from "../../../menus/link_context_menu.js";
import type FNote from "../../../entities/fnote.js";
// we're intentionally displaying the launcher title and icon instead of the target,
// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok),
// but on the launchpad you want them distinguishable.
// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad
// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons).
// The only downside is more work in setting up the typical case
// where you actually want to have both title and icon in sync, but for those cases there are bookmarks
export default class NoteLauncher extends AbstractLauncher {
constructor(launcherNote: FNote) {
super(launcherNote);
this.title(() => this.launcherNote.title)
.icon(() => this.launcherNote.getIcon())
.onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu(async (evt) => {
let targetNoteId = await Promise.resolve(this.getTargetNoteId());
if (!targetNoteId || !evt) {
return;
}
const hoistedNoteId = this.getHoistedNoteId();
linkContextMenuService.openContextMenu(targetNoteId, evt, {}, hoistedNoteId);
});
}
async launch(evt?: JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
// await because subclass overrides can be async
const targetNoteId = await this.getTargetNoteId();
if (!targetNoteId || evt?.which === 3) {
return;
}
const hoistedNoteId = await this.getHoistedNoteId();
if (!hoistedNoteId) {
return;
}
if (!evt) {
// keyboard shortcut
// TODO: Fix once tabManager is ported.
//@ts-ignore
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteId);
} else {
const ctrlKey = utils.isCtrlKey(evt);
const activate = evt.shiftKey ? true : false;
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
// TODO: Fix once tabManager is ported.
//@ts-ignore
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteId, activate);
} else {
// TODO: Fix once tabManager is ported.
//@ts-ignore
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteId);
}
}
}
getTargetNoteId(): void | string | Promise<string | undefined> {
const targetNoteId = this.launcherNote.getRelationValue("target");
if (!targetNoteId) {
dialogService.info(t("note_launcher.this_launcher_doesnt_define_target_note"));
return;
}
return targetNoteId;
}
getHoistedNoteId() {
return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
}
getTitle() {
const shortcuts = this.launcherNote
.getLabels("keyboardShortcut")
.map((l) => l.value)
.filter((v) => !!v)
.join(", ");
let title = super.getTitle();
if (shortcuts) {
title += ` (${shortcuts})`;
}
return title;
}
}

View File

@ -1,23 +0,0 @@
import type FNote from "../../../entities/fnote.js";
import AbstractLauncher from "./abstract_launcher.js";
export default class ScriptLauncher extends AbstractLauncher {
constructor(launcherNote: FNote) {
super(launcherNote);
this.title(() => this.launcherNote.title)
.icon(() => this.launcherNote.getIcon())
.onClick(() => this.launch());
}
async launch() {
if (this.launcherNote.isLabelTruthy("scriptInLauncherContent")) {
await this.launcherNote.executeScript();
} else {
const script = await this.launcherNote.getRelationTarget("script");
if (script) {
await script.executeScript();
}
}
}
}

View File

@ -1,15 +0,0 @@
import NoteLauncher from "./note_launcher.js";
import dateNotesService from "../../../services/date_notes.js";
import appContext from "../../../components/app_context.js";
export default class TodayLauncher extends NoteLauncher {
async getTargetNoteId() {
const todayNote = await dateNotesService.getTodayNote();
return todayNote?.noteId;
}
getHoistedNoteId() {
return appContext.tabManager.getActiveContext()?.hoistedNoteId;
}
}

View File

@ -1,49 +0,0 @@
import OnClickButtonWidget from "./onclick_button.js";
import linkContextMenuService from "../../menus/link_context_menu.js";
import utils from "../../services/utils.js";
import appContext from "../../components/app_context.js";
import type FNote from "../../entities/fnote.js";
export default class OpenNoteButtonWidget extends OnClickButtonWidget {
private noteToOpen: FNote;
constructor(noteToOpen: FNote) {
super();
this.noteToOpen = noteToOpen;
this.title(() => utils.escapeHtml(this.noteToOpen.title))
.icon(() => this.noteToOpen.getIcon())
.onClick((widget, evt) => this.launch(evt))
.onAuxClick((widget, evt) => this.launch(evt))
.onContextMenu((evt) => {
if (evt) {
linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt);
}
});
}
async launch(evt: JQuery.ClickEvent | JQuery.TriggeredEvent | JQuery.ContextMenuEvent) {
if (evt.which === 3) {
return;
}
const hoistedNoteId = this.getHoistedNoteId();
const ctrlKey = utils.isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = evt.shiftKey ? true : false;
await appContext.tabManager.openInNewTab(this.noteToOpen.noteId, hoistedNoteId, activate);
} else {
await appContext.tabManager.openInSameTab(this.noteToOpen.noteId);
}
}
getHoistedNoteId() {
return this.noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
}
initialRenderCompleteEvent() {
// we trigger refresh above
}
}

View File

@ -1,21 +0,0 @@
import { t } from "../../services/i18n.js";
import protectedSessionHolder from "../../services/protected_session_holder.js";
import CommandButtonWidget from "./command_button.js";
export default class ProtectedSessionStatusWidget extends CommandButtonWidget {
constructor() {
super();
this.class("launcher-button");
this.settings.icon = () => (protectedSessionHolder.isProtectedSessionAvailable() ? "bx-check-shield" : "bx-shield-quarter");
this.settings.title = () => (protectedSessionHolder.isProtectedSessionAvailable() ? t("protected_session_status.active") : t("protected_session_status.inactive"));
this.settings.command = () => (protectedSessionHolder.isProtectedSessionAvailable() ? "leaveProtectedSession" : "enterProtectedSession");
}
protectedSessionStartedEvent() {
this.refreshIcon();
}
}

View File

@ -1,133 +0,0 @@
import CalendarWidget from "../buttons/calendar.js";
import SpacerWidget from "../spacer.js";
import BookmarkButtons from "../bookmark_buttons.js";
import ProtectedSessionStatusWidget from "../buttons/protected_session_status.js";
import SyncStatusWidget from "../sync_status.js";
import BasicWidget from "../basic_widget.js";
import NoteLauncher from "../buttons/launcher/note_launcher.js";
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";
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;
if (launcherType === "command") {
widget = this.initCommandLauncherWidget(note).class("launcher-button");
} else if (launcherType === "note") {
widget = new NoteLauncher(note).class("launcher-button");
} else if (launcherType === "script") {
widget = new ScriptLauncher(note).class("launcher-button");
} else if (launcherType === "customWidget") {
widget = await this.initCustomWidget(note);
} else if (launcherType === "builtinWidget") {
widget = this.initBuiltinWidget(note);
} 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;
}
initCommandLauncherWidget(note: FNote) {
return new CommandButtonWidget()
.title(() => note.title)
.icon(() => note.getIcon())
.command(() => note.getLabelValue("command") as CommandNames);
}
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 new CalendarWidget(note.title, note.getIcon());
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 new SpacerWidget(baseSize, growthFactor);
case "bookmarks":
return new BookmarkButtons(this.isHorizontalLayout);
case "protectedSession":
return new ProtectedSessionStatusWidget();
case "syncStatus":
return new SyncStatusWidget();
case "backInHistoryButton":
return new HistoryNavigationButton(note, "backInNoteHistory");
case "forwardInHistoryButton":
return new HistoryNavigationButton(note, "forwardInNoteHistory");
case "todayInJournal":
return new TodayLauncher(note);
case "quickSearch":
return new QuickSearchLauncherWidget(this.isHorizontalLayout);
case "aiChatLauncher":
return new AiChatButton(note);
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}
}
}

View File

@ -0,0 +1,198 @@
import SyncStatusWidget from "../sync_status.js";
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";
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 new SyncStatusWidget();
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

@ -0,0 +1,16 @@
import FNote from "../../entities/fnote";
import { useTriliumOptionBool } from "../react/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
export default function AiChatButton({ launcherNote }: { launcherNote: FNote }) {
const [ aiEnabled ] = useTriliumOptionBool("aiEnabled");
const { icon, title } = useLauncherIconAndTitle(launcherNote);
return aiEnabled && (
<LaunchBarActionButton
icon={icon}
text={title}
triggerCommand="createAiChat"
/>
)
}

View File

@ -0,0 +1,31 @@
.bookmark-folder-widget {
min-width: 400px;
max-height: 500px;
padding: 7px 15px 0 15px;
font-size: 1.2rem;
overflow: auto;
}
.bookmark-folder-widget ul {
padding: 0;
list-style-type: none;
}
.bookmark-folder-widget .note-link {
display: block;
padding: 5px 10px 5px 5px;
}
.bookmark-folder-widget .note-link:hover {
background-color: var(--accented-background-color);
text-decoration: none;
}
.dropdown-menu .bookmark-folder-widget a:hover:not(.disabled) {
text-decoration: none;
background-color: transparent !important;
}
.bookmark-folder-widget li .note-link {
padding-inline-start: 35px;
}

View File

@ -0,0 +1,58 @@
import { useMemo } from "preact/hooks";
import { LaunchBarDropdownButton, useLauncherIconAndTitle, type LaunchBarWidgetProps } from "./launch_bar_widgets";
import { CSSProperties } from "preact";
import type FNote from "../../entities/fnote";
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
import "./BookmarkButtons.css";
import NoteLink from "../react/NoteLink";
import { CustomNoteLauncher } from "./GenericButtons";
const PARENT_NOTE_ID = "_lbBookmarks";
export default function BookmarkButtons({ isHorizontalLayout }: LaunchBarWidgetProps) {
const style = useMemo<CSSProperties>(() => ({
display: "flex",
flexDirection: isHorizontalLayout ? "row" : "column",
contain: "none"
}), [ isHorizontalLayout ]);
const childNotes = useChildNotes(PARENT_NOTE_ID);
return (
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark note={childNote} />)}
</div>
)
}
function SingleBookmark({ note }: { note: FNote }) {
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
return bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
}
function BookmarkFolder({ note }: { note: FNote }) {
const { icon, title } = useLauncherIconAndTitle(note);
const childNotes = useChildNotes(note.noteId);
return (
<LaunchBarDropdownButton
icon={icon}
title={title}
>
<div className="bookmark-folder-widget">
<div className="parent-note">
<NoteLink notePath={note.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
</div>
<ul className="children-notes">
{childNotes.map(childNote => (
<li>
<NoteLink notePath={childNote.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
</li>
))}
</ul>
</div>
</LaunchBarDropdownButton>
)
}

View File

@ -0,0 +1,175 @@
import { useTriliumOptionInt } from "../react/hooks";
import clsx from "clsx";
import server from "../../services/server";
import { VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Dayjs } from "@triliumnext/commons";
import { t } from "../../services/i18n";
interface DateNotesForMonth {
[date: string]: string;
}
const DAYS_OF_WEEK = [
t("calendar.sun"),
t("calendar.mon"),
t("calendar.tue"),
t("calendar.wed"),
t("calendar.thu"),
t("calendar.fri"),
t("calendar.sat")
];
interface DateRangeInfo {
weekNumbers: number[];
dates: Dayjs[];
}
export default function Calendar({ date }: { date: Dayjs }) {
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0;
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
const month = date.format('YYYY-MM');
const firstDay = date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
return (
<>
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
<div className="calendar-body" data-calendar-area="month">
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays date={date} info={monthInfo.prevMonth} />}
<CurrentMonthDays date={date} firstDayOfWeekISO={firstDayOfWeekISO} />
<NextMonthDays date={date} dates={monthInfo.nextMonth.dates} />
</div>
</>
)
}
function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }) {
let localeDaysOfWeek = [...DAYS_OF_WEEK];
const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek);
localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted];
return (
<div className="calendar-week">
{localeDaysOfWeek.map(dayOfWeek => <span>{dayOfWeek}</span>)}
</div>
)
}
function PreviousMonthDays({ date, info: { dates, weekNumbers } }: { date: Dayjs, info: DateRangeInfo }) {
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth);
}, [ date ]);
return (
<>
<CalendarWeek weekNumber={weekNumbers[0]} />
{dates.map(date => <CalendarDay date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" />)}
</>
)
}
function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) {
let dateCursor = date;
const currentMonth = date.month();
const items: VNode[] = [];
while (dateCursor.month() === currentMonth) {
const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
items.push(<CalendarWeek weekNumber={weekNumber} />)
}
items.push(<CalendarDay date={dateCursor} dateNotesForMonth={{}} />)
dateCursor = dateCursor.add(1, "day");
}
return items;
}
function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) {
const nextMonth = date.add(1, 'month').format('YYYY-MM');
const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState<DateNotesForMonth>();
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth);
}, [ date ]);
return dates.map(date => (
<CalendarDay date={date} dateNotesForMonth={dateNotesForNextMonth} className="calendar-date-next-month" />
));
}
function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string }) {
return (
<a
className={clsx("calendar-date", className)}
data-calendar-date={date.local().format("YYYY-MM-DD")}
>
<span>
{date.date()}
</span>
</a>
);
}
function CalendarWeek({ weekNumber }: { weekNumber: number }) {
return (
<span className="calendar-week-number calendar-week-number-disabled">{weekNumber}</span>
)
}
export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
return {
prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
}
}
function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo {
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = date.startOf('month');
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
return { weekNumbers: [ weekNumber ], dates };
}
function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo {
const lastDayOfMonth = date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
if (lastDayISO !== lastDayOfUserWeek) {
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
for (let i = 0; i < daysToAdd; i++) {
dates.push(nextMonthFirstDay.add(i, 'day'));
}
}
return { weekNumbers: [], dates };
}
export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
return weekStart.isoWeek();
}
function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
const currentISO = date.isoWeekday();
const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
}

View File

@ -174,4 +174,8 @@
background-color: var(--hover-item-background-color);
color: var(--hover-item-text-color);
text-decoration: underline;
}
.calendar-dropdown-widget .form-control {
padding: 0;
}

View File

@ -0,0 +1,129 @@
import { Dispatch, StateUpdater, useEffect, useMemo, useState } from "preact/hooks";
import FNote from "../../entities/fnote";
import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import { Dayjs, dayjs } from "@triliumnext/commons";
import appContext from "../../components/app_context";
import "./CalendarWidget.css";
import Calendar from "./Calendar";
import ActionButton from "../react/ActionButton";
import { t } from "../../services/i18n";
import FormDropdownList from "../react/FormDropdownList";
import FormTextBox from "../react/FormTextBox";
const MONTHS = [
t("calendar.january"),
t("calendar.february"),
t("calendar.march"),
t("calendar.april"),
t("calendar.may"),
t("calendar.june"),
t("calendar.july"),
t("calendar.august"),
t("calendar.september"),
t("calendar.october"),
t("calendar.november"),
t("calendar.december")
];
export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) {
const { title, icon } = useLauncherIconAndTitle(launcherNote);
const [ date, setDate ] = useState<Dayjs>();
return (
<LaunchBarDropdownButton
icon={icon} title={title}
onShown={() => {
const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote");
const activeDate = dateNote ? dayjs(`${dateNote}T12:00:00`) : null;
const todaysDate = dayjs();
const date = dayjs(activeDate || todaysDate).startOf('month');
setDate(date);
}}
dropdownOptions={{
autoClose: "outside"
}}
>
{date && <div className="calendar-dropdown-widget" style={{ width: 400 }}>
<CalendarHeader date={date} setDate={setDate} />
<Calendar date={date} />
</div>}
</LaunchBarDropdownButton>
)
}
interface CalendarHeaderProps {
date: Dayjs;
setDate: Dispatch<StateUpdater<Dayjs | undefined>>;
}
function CalendarHeader(props: CalendarHeaderProps) {
return (
<div className="calendar-header">
<CalendarMonthSelector {...props} />
<CalendarYearSelector {...props} />
</div>
)
}
function CalendarMonthSelector({ date, setDate }: CalendarHeaderProps) {
const months = useMemo(() => (
Array.from(MONTHS.entries().map(([ index, text ]) => ({
index: index.toString(), text
})))
), []);
return (
<div className="calendar-month-selector">
<AdjustDateButton date={date} setDate={setDate} direction="prev" unit="month" />
<FormDropdownList
values={months} currentValue={date.month().toString()}
keyProperty="index" titleProperty="text"
onChange={value => {
}}
buttonProps={{ "data-calendar-input": "month" }}
/>
<AdjustDateButton date={date} setDate={setDate} direction="next" unit="month" />
</div>
);
}
function CalendarYearSelector({ date, setDate }: CalendarHeaderProps) {
return (
<div className="calendar-year-selector">
<AdjustDateButton date={date} setDate={setDate} direction="prev" unit="year" />
<FormTextBox
type="number"
min="1900" max="2999" step="1"
currentValue={date.year().toString()}
onChange={(newValue) => {
const year = parseInt(newValue, 10);
if (!Number.isNaN(year)) {
setDate(date.set("year", year));
}
}}
data-calendar-input="year"
/>
<AdjustDateButton date={date} setDate={setDate} direction="next" unit="year" />
</div>
)
}
function AdjustDateButton({ date, setDate, unit, direction }: CalendarHeaderProps & {
direction: "prev" | "next",
unit: "month" | "year"
}) {
return (
<ActionButton
icon={direction === "prev" ? "bx bx-chevron-left" : "bx bx-chevron-right" }
className="calendar-btn tn-tool-button"
noIconActionClass
text=""
onClick={(e) => {
e.stopPropagation();
const newDate = direction === "prev" ? date.subtract(1, unit) : date.add(1, unit);
setDate(newDate);
}}
/>
)
}

View File

@ -0,0 +1,89 @@
import appContext, { CommandNames } from "../../components/app_context";
import FNote from "../../entities/fnote";
import link_context_menu from "../../menus/link_context_menu";
import { escapeHtml, isCtrlKey } from "../../services/utils";
import { useNoteLabel } from "../react/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import dialog from "../../services/dialog";
import { t } from "../../services/i18n";
export function CommandButton({ launcherNote }: { launcherNote: FNote }) {
const { icon, title } = useLauncherIconAndTitle(launcherNote);
const [ command ] = useNoteLabel(launcherNote, "command");
return command && (
<LaunchBarActionButton
icon={icon}
text={title}
triggerCommand={command as CommandNames}
/>
)
}
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
launcherNote: FNote,
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>,
getHoistedNoteId?: (launcherNote: FNote) => string | null
}) {
const { icon, title } = useLauncherIconAndTitle(launcherNote);
async function launch(evt: MouseEvent) {
if (evt.which === 3) {
return;
}
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = evt.shiftKey ? true : false;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId);
}
}
return (
<LaunchBarActionButton
icon={icon}
text={escapeHtml(title)}
onClick={launch}
onAuxClick={launch}
onContextMenu={async evt => {
evt.preventDefault();
const targetNoteId = await getTargetNoteId(launcherNote);
if (targetNoteId) {
link_context_menu.openContextMenu(targetNoteId, evt);
}
}}
/>
)
}
// we're intentionally displaying the launcher title and icon instead of the target,
// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok),
// but on the launchpad you want them distinguishable.
// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad
// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons).
// The only downside is more work in setting up the typical case
// where you actually want to have both title and icon in sync, but for those cases there are bookmarks
export function NoteLauncher({ launcherNote, ...restProps }: { launcherNote: FNote, hoistedNoteId?: string }) {
return (
<CustomNoteLauncher
launcherNote={launcherNote}
getTargetNoteId={(launcherNote) => {
const targetNoteId = launcherNote.getRelationValue("target");
if (!targetNoteId) {
dialog.info(t("note_launcher.this_launcher_doesnt_define_target_note"));
return null;
}
return targetNoteId;
}}
getHoistedNoteId={launcherNote => launcherNote.getRelationValue("hoistedNote")}
{...restProps}
/>
);
}

View File

@ -0,0 +1,84 @@
import { useEffect, useRef } from "preact/hooks";
import FNote from "../../entities/fnote";
import { dynamicRequire, isElectron } from "../../services/utils";
import { LaunchBarActionButton, useLauncherIconAndTitle } 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 { icon, title } = useLauncherIconAndTitle(launcherNote);
const webContentsRef = useRef<WebContents>(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 (
<LaunchBarActionButton
icon={icon}
text={title}
triggerCommand={command}
onContextMenu={async (e) => {
e.preventDefault();
const webContents = webContentsRef.current;
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 { 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<string>) => {
if (item && item.command && webContents) {
const idx = parseInt(item.command, 10);
webContents.navigationHistory.goToIndex(idx);
}
}
});
}}
/>
)
}

View File

@ -0,0 +1,33 @@
import { useState } from "preact/hooks";
import protected_session_holder from "../../services/protected_session_holder";
import { LaunchBarActionButton } from "./launch_bar_widgets";
import { useTriliumEvent } from "../react/hooks";
import { t } from "../../services/i18n";
export default function ProtectedSessionStatusWidget() {
const protectedSessionAvailable = useProtectedSessionAvailable();
return (
protectedSessionAvailable ? (
<LaunchBarActionButton
icon="bx bx-check-shield"
text={t("protected_session_status.active")}
triggerCommand="leaveProtectedSession"
/>
) : (
<LaunchBarActionButton
icon="bx bx-shield-quarter"
text={t("protected_session_status.inactive")}
triggerCommand="enterProtectedSession"
/>
)
)
}
function useProtectedSessionAvailable() {
const [ protectedSessionAvailable, setProtectedSessionAvailable ] = useState(protected_session_holder.isProtectedSessionAvailable());
useTriliumEvent("protectedSessionStarted", () => {
setProtectedSessionAvailable(protected_session_holder.isProtectedSessionAvailable());
});
return protectedSessionAvailable;
}

View File

@ -0,0 +1,3 @@
export default function RightDropdownButton() {
return <p>Button goes here.</p>;
}

View File

@ -0,0 +1,35 @@
import appContext, { CommandNames } from "../../components/app_context";
import contextMenu from "../../menus/context_menu";
import { t } from "../../services/i18n";
import { isMobile } from "../../services/utils";
interface SpacerWidgetProps {
baseSize?: number;
growthFactor?: number;
}
export default function SpacerWidget({ baseSize, growthFactor }: SpacerWidgetProps) {
return (
<div
className="spacer"
style={{
flexBasis: baseSize ?? 0,
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);
}
}
});
}}
/>
)
}

View File

@ -0,0 +1,46 @@
import FNote from "../../entities/fnote";
import { escapeHtml } from "../../services/utils";
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
import Dropdown, { DropdownProps } from "../react/Dropdown";
import { useNoteLabel, useNoteProperty } from "../react/hooks";
import Icon from "../react/Icon";
export interface LaunchBarWidgetProps {
isHorizontalLayout: boolean;
}
export function LaunchBarActionButton(props: Omit<ActionButtonProps, "className" | "noIconActionClass" | "titlePosition">) {
return (
<ActionButton
className="button-widget launcher-button"
noIconActionClass
titlePosition="right"
{...props}
/>
)
}
export function LaunchBarDropdownButton({ children, icon, ...props }: Pick<DropdownProps, "title" | "children" | "onShown" | "dropdownOptions"> & { icon: string }) {
return (
<Dropdown
className="right-dropdown-widget"
buttonClassName="right-dropdown-button launcher-button"
hideToggleArrow
text={<Icon icon={icon} />}
{...props}
>{children}</Dropdown>
)
}
export function useLauncherIconAndTitle(note: FNote) {
const title = useNoteProperty(note, "title");
// React to changes.
useNoteLabel(note, "iconClass");
useNoteLabel(note, "workspaceIconClass");
return {
icon: note.getIcon(),
title: escapeHtml(title ?? "")
};
}

View File

@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from "preact/hooks";
import { CommandNames } from "../../components/app_context";
import { useStaticTooltip } from "./hooks";
import keyboard_actions from "../../services/keyboard_actions";
import { HTMLAttributes } from "preact";
export interface ActionButtonProps {
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu"> {
text: string;
titlePosition?: "top" | "right" | "bottom" | "left";
icon: string;
className?: string;
onClick?: (e: MouseEvent) => void;
triggerCommand?: CommandNames;
noIconActionClass?: boolean;
frame?: boolean;
@ -16,7 +16,7 @@ export interface ActionButtonProps {
disabled?: boolean;
}
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled }: ActionButtonProps) {
export default function ActionButton({ text, icon, className, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled, ...restProps }: ActionButtonProps) {
const buttonRef = useRef<HTMLButtonElement>(null);
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
@ -35,8 +35,8 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
return <button
ref={buttonRef}
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""} ${disabled ? "disabled" : ""} ${active ? "active" : ""}`}
onClick={onClick}
data-trigger-command={triggerCommand}
disabled={disabled}
{...restProps}
/>;
}

View File

@ -1,11 +1,16 @@
import { Dropdown as BootstrapDropdown } from "bootstrap";
import { ComponentChildren } from "preact";
import { ComponentChildren, HTMLAttributes } from "preact";
import { CSSProperties, HTMLProps } from "preact/compat";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
import { useUniqueName } from "./hooks";
type DataAttributes = {
[key: `data-${string}`]: string | number | boolean | undefined;
};
export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "className"> {
buttonClassName?: string;
buttonProps?: Partial<HTMLAttributes<HTMLButtonElement> & DataAttributes>;
isStatic?: boolean;
children: ComponentChildren;
title?: string;
@ -21,9 +26,10 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
forceShown?: boolean;
onShown?: () => void;
onHidden?: () => void;
dropdownOptions?: Partial<BootstrapDropdown.Options>;
}
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden }: DropdownProps) {
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps }: DropdownProps) {
const dropdownRef = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLButtonElement | null>(null);
@ -32,7 +38,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
useEffect(() => {
if (!triggerRef.current) return;
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current);
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current, dropdownOptions);
if (forceShown) {
dropdown.show();
setShown(true);
@ -79,6 +85,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
title={title}
id={id ?? ariaId}
disabled={disabled}
{...buttonProps}
>
{text}
<span className="caret"></span>

View File

@ -4,6 +4,7 @@ import { useImperativeSearchHighlighlighting, useTriliumEvent } from "./hooks";
interface NoteLinkOpts {
className?: string;
containerClassName?: string;
notePath: string | string[];
showNotePath?: boolean;
showNoteIcon?: boolean;
@ -17,7 +18,7 @@ interface NoteLinkOpts {
noContextMenu?: boolean;
}
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
export default function NoteLink({ className, containerClassName, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
const noteId = stringifiedNotePath.split("/").at(-1);
const ref = useRef<HTMLSpanElement>(null);
@ -71,6 +72,6 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
$linkEl?.addClass(className);
}
return <span ref={ref} />
return <span className={containerClassName} ref={ref} />
}

View File

@ -23,6 +23,7 @@ import toast, { ToastOptions } from "../../services/toast";
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
import server from "../../services/server";
import { removeIndividualBinding } from "../../services/shortcuts";
import froca from "../../services/froca";
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
const parentComponent = useContext(ParentComponent);
@ -369,6 +370,16 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: Re
] as const;
}
export function useNoteRelationTarget(note: FNote, relationName: RelationNames) {
const [ targetNote, setTargetNote ] = useState<FNote | null>();
useEffect(() => {
note.getRelationTarget(relationName).then(setTargetNote);
}, [ note ]);
return [ targetNote ] as const;
}
/**
* Allows a React component to read or write a note's label while also reacting to changes in value.
*
@ -836,3 +847,15 @@ async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
return true;
}
export function useChildNotes(parentNoteId: string) {
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
async function refreshChildNotes() {
const parentNote = await froca.getNote(parentNoteId);
const childNotes = await parentNote?.getChildNotes();
setChildNotes(childNotes ?? []);
}
useEffect(() => { refreshChildNotes() }, [ parentNoteId ]);
return childNotes;
}

View File

@ -1,43 +0,0 @@
import { t } from "../services/i18n.js";
import BasicWidget from "./basic_widget.js";
import contextMenu from "../menus/context_menu.js";
import appContext, { type CommandNames } from "../components/app_context.js";
import utils from "../services/utils.js";
const TPL = /*html*/`<div class="spacer"></div>`;
export default class SpacerWidget extends BasicWidget {
private baseSize: number;
private growthFactor: number;
constructor(baseSize = 0, growthFactor = 1000) {
super();
this.baseSize = baseSize;
this.growthFactor = growthFactor;
}
doRender() {
this.$widget = $(TPL);
this.$widget.css("flex-basis", this.baseSize);
this.$widget.css("flex-grow", this.growthFactor);
this.$widget.css("flex-shrink", 1000);
this.$widget.on("contextmenu", (e) => {
this.$widget.tooltip("hide");
contextMenu.show<CommandNames>({
x: e.pageX,
y: e.pageY,
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
selectMenuItemHandler: ({ command }) => {
if (command) {
appContext.triggerCommand(command);
}
}
});
return false; // blocks default browser right click menu
});
}
}

View File

@ -24,6 +24,10 @@ type Labels = {
orderBy: string;
orderDirection: string;
// Launch bar
bookmarkFolder: boolean;
command: string;
// Collection-specific
viewType: string;
status: string;
@ -55,7 +59,11 @@ type Labels = {
*/
type Relations = [
"searchScript",
"ancestor"
"ancestor",
// Launcher-specific
"target",
"widget"
];
export type LabelNames = keyof Labels;