mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 11:26:15 +01:00
Port small widgets to React (#6830)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import froca from "../services/froca.js";
|
||||
import RootCommandExecutor from "./root_command_executor.js";
|
||||
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
|
||||
import Entrypoints from "./entrypoints.js";
|
||||
import options from "../services/options.js";
|
||||
import utils, { hasTouchBar } from "../services/utils.js";
|
||||
import zoomComponent from "./zoom.js";
|
||||
@@ -32,6 +32,7 @@ import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||
@@ -89,6 +90,11 @@ export type CommandMappings = {
|
||||
closeTocCommand: CommandData;
|
||||
closeHlt: CommandData;
|
||||
showLaunchBarSubtree: CommandData;
|
||||
showHiddenSubtree: CommandData;
|
||||
showSQLConsoleHistory: CommandData;
|
||||
logout: CommandData;
|
||||
switchToMobileVersion: CommandData;
|
||||
switchToDesktopVersion: CommandData;
|
||||
showRevisions: CommandData & {
|
||||
noteId?: string | null;
|
||||
};
|
||||
@@ -134,6 +140,7 @@ export type CommandMappings = {
|
||||
showLeftPane: CommandData;
|
||||
showAttachments: CommandData;
|
||||
showSearchHistory: CommandData;
|
||||
showShareSubtree: CommandData;
|
||||
hoistNote: CommandData & { noteId: string };
|
||||
leaveProtectedSession: CommandData;
|
||||
enterProtectedSession: CommandData;
|
||||
|
||||
@@ -10,22 +10,7 @@ import bundleService from "../services/bundle.js";
|
||||
import froca from "../services/froca.js";
|
||||
import linkService from "../services/link.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
// TODO: Move somewhere else nicer.
|
||||
export type SqlExecuteResults = string[][][];
|
||||
|
||||
// TODO: Deduplicate with server.
|
||||
interface SqlExecuteResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
results: SqlExecuteResults;
|
||||
}
|
||||
|
||||
// TODO: Deduplicate with server.
|
||||
interface CreateChildrenResponse {
|
||||
note: FNote;
|
||||
}
|
||||
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||
|
||||
export default class Entrypoints extends Component {
|
||||
constructor() {
|
||||
@@ -34,7 +19,7 @@ export default class Entrypoints extends Component {
|
||||
|
||||
openDevToolsCommand() {
|
||||
if (utils.isElectron()) {
|
||||
utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools();
|
||||
utils.dynamicRequire("@electron/remote").getCurrentWindow().webContents.toggleDevTools();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +109,7 @@ export default class Entrypoints extends Component {
|
||||
if (utils.isElectron()) {
|
||||
// standard JS version does not work completely correctly in electron
|
||||
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
webContents.goToIndex(activeIndex - 1);
|
||||
} else {
|
||||
@@ -136,7 +121,7 @@ export default class Entrypoints extends Component {
|
||||
if (utils.isElectron()) {
|
||||
// standard JS version does not work completely correctly in electron
|
||||
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
webContents.goToIndex(activeIndex + 1);
|
||||
} else {
|
||||
|
||||
@@ -1,24 +1,18 @@
|
||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
import SqlResultWidget from "../widgets/sql_result.js";
|
||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||
import SearchResultWidget from "../widgets/search_result.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||
import SpacerWidget from "../widgets/spacer.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
|
||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||
@@ -29,19 +23,25 @@ import TocWidget from "../widgets/toc.js";
|
||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import ApiLogWidget from "../widgets/api_log.js";
|
||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import options from "../services/options.js";
|
||||
import utils from "../services/utils.js";
|
||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||
import type { AppContext } from "../components/app_context.js";
|
||||
import type { WidgetsByParent } from "../services/bundle.js";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||
import SqlResults from "../widgets/sql_result.js";
|
||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||
import ApiLog from "../widgets/api_log.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@@ -76,9 +76,9 @@ export default class DesktopLayout {
|
||||
new FlexContainer("row")
|
||||
.class("tab-row-container")
|
||||
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
||||
.optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
|
||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||
.child(new TabRowWidget().class("full-width"))
|
||||
.optChild(customTitleBarButtons, new TitleBarButtonsWidget())
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px")
|
||||
.css("background-color", "var(--launcher-pane-background-color)")
|
||||
.setParent(appContext)
|
||||
@@ -99,7 +99,7 @@ export default class DesktopLayout {
|
||||
new FlexContainer("column")
|
||||
.id("rest-pane")
|
||||
.css("flex-grow", "1")
|
||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px"))
|
||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
.filling()
|
||||
@@ -136,14 +136,14 @@ export default class DesktopLayout {
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
.child(new PromotedAttributesWidget())
|
||||
.child(new SqlTableSchemasWidget())
|
||||
.child(<SqlTableSchemas />)
|
||||
.child(new NoteDetailWidget())
|
||||
.child(new NoteListWidget(false))
|
||||
.child(new SearchResultWidget())
|
||||
.child(new SqlResultWidget())
|
||||
.child(new ScrollPaddingWidget())
|
||||
.child(<SearchResult />)
|
||||
.child(<SqlResults />)
|
||||
.child(<ScrollPadding />)
|
||||
)
|
||||
.child(new ApiLogWidget())
|
||||
.child(<ApiLog />)
|
||||
.child(new FindWidget())
|
||||
.child(
|
||||
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||
@@ -162,7 +162,7 @@ export default class DesktopLayout {
|
||||
)
|
||||
)
|
||||
)
|
||||
.child(new CloseZenButton())
|
||||
.child(<CloseZenModeButton />)
|
||||
|
||||
// Desktop-specific dialogs.
|
||||
.child(<PasswordNoteSetDialog />)
|
||||
@@ -176,14 +176,18 @@ export default class DesktopLayout {
|
||||
let launcherPane;
|
||||
|
||||
if (isHorizontal) {
|
||||
launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true));
|
||||
launcherPane = new FlexContainer("row")
|
||||
.css("height", "53px")
|
||||
.class("horizontal")
|
||||
.child(new LauncherContainer(true))
|
||||
.child(<GlobalMenu isHorizontalLayout={true} />);
|
||||
} else {
|
||||
launcherPane = new FlexContainer("column")
|
||||
.css("width", "53px")
|
||||
.class("vertical")
|
||||
.child(new GlobalMenuWidget(false))
|
||||
.child(<GlobalMenu isHorizontalLayout={false} />)
|
||||
.child(new LauncherContainer(false))
|
||||
.child(new LeftPaneToggleWidget(false));
|
||||
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
||||
}
|
||||
|
||||
launcherPane.id("launcher-pane");
|
||||
|
||||
@@ -3,8 +3,6 @@ import NoteTitleWidget from "../widgets/note_title.js";
|
||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_button.js";
|
||||
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import NoteListWidget from "../widgets/note_list.js";
|
||||
@@ -18,11 +16,13 @@ import type AppContext from "../components/app_context.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||
import { applyModals } from "./layout_commons.js";
|
||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -139,9 +139,9 @@ export default class MobileLayout {
|
||||
.contentSized()
|
||||
.css("font-size", "larger")
|
||||
.css("align-items", "center")
|
||||
.child(new ToggleSidebarButtonWidget().contentSized())
|
||||
.child(<ToggleSidebarButton />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(new MobileDetailMenuWidget(true).contentSized())
|
||||
.child(<MobileDetailMenu />)
|
||||
)
|
||||
.child(new SharedInfoWidget())
|
||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||
@@ -154,7 +154,7 @@ export default class MobileLayout {
|
||||
.child(new NoteListWidget(false))
|
||||
.child(<FilePropertiesWrapper />)
|
||||
)
|
||||
.child(new MobileEditorToolbar())
|
||||
.child(<MobileEditorToolbar />)
|
||||
)
|
||||
)
|
||||
.child(
|
||||
@@ -162,9 +162,14 @@ export default class MobileLayout {
|
||||
.contentSized()
|
||||
.id("mobile-bottom-bar")
|
||||
.child(new TabRowWidget().css("height", "40px"))
|
||||
.child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane"))
|
||||
.child(new FlexContainer("row")
|
||||
.class("horizontal")
|
||||
.css("height", "53px")
|
||||
.child(new LauncherContainer(true))
|
||||
.child(<GlobalMenuWidget isHorizontalLayout />)
|
||||
.id("launcher-pane"))
|
||||
)
|
||||
.child(new CloseZenButton());
|
||||
.child(<CloseZenModeButton />);
|
||||
applyModals(rootContainer);
|
||||
return rootContainer;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,10 @@ async function getAction(actionName: string, silent = false) {
|
||||
return action;
|
||||
}
|
||||
|
||||
export function getActionSync(actionName: string) {
|
||||
return keyboardActionRepo[actionName];
|
||||
}
|
||||
|
||||
function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
||||
//@ts-ignore
|
||||
//TODO: each() does not support async callbacks.
|
||||
|
||||
@@ -218,7 +218,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
|
||||
if (utils.isElectron()) {
|
||||
const ipc = utils.dynamicRequire("electron").ipcRenderer;
|
||||
|
||||
ipc.on("server-response", async (event: string, arg: Arg) => {
|
||||
ipc.on("server-response", async (_, arg: Arg) => {
|
||||
if (arg.statusCode >= 200 && arg.statusCode < 300) {
|
||||
handleSuccessfulResponse(arg);
|
||||
} else {
|
||||
|
||||
@@ -311,7 +311,13 @@ function copySelectionToClipboard() {
|
||||
}
|
||||
}
|
||||
|
||||
export function dynamicRequire(moduleName: string) {
|
||||
type dynamicRequireMappings = {
|
||||
"@electron/remote": typeof import("@electron/remote"),
|
||||
"electron": typeof import("electron"),
|
||||
"child_process": typeof import("child_process")
|
||||
};
|
||||
|
||||
export function dynamicRequire<T extends keyof dynamicRequireMappings>(moduleName: T): Awaited<dynamicRequireMappings[T]>{
|
||||
if (typeof __non_webpack_require__ !== "undefined") {
|
||||
return __non_webpack_require__(moduleName);
|
||||
} else {
|
||||
|
||||
@@ -442,14 +442,20 @@ body #context-menu-container .dropdown-item > span {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dropdown-menu kbd {
|
||||
.dropdown-item span.keyboard-shortcut {
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdown-menu kbd {
|
||||
color: var(--muted-text-color);
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
padding-bottom: 0;
|
||||
padding: 0;
|
||||
flex-grow: 1;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dropdown-item,
|
||||
|
||||
@@ -197,13 +197,17 @@ html body .dropdown-item[disabled] {
|
||||
|
||||
/* Menu item keyboard shortcut */
|
||||
.dropdown-item kbd {
|
||||
margin-left: 16px;
|
||||
font-family: unset !important;
|
||||
font-size: unset !important;
|
||||
color: var(--menu-item-keyboard-shortcut-color) !important;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.dropdown-item span.keyboard-shortcut {
|
||||
color: var(--menu-item-keyboard-shortcut-color) !important;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.dropdown-divider {
|
||||
position: relative;
|
||||
border-color: transparent !important;
|
||||
|
||||
@@ -96,7 +96,6 @@
|
||||
background: var(--background) !important;
|
||||
color: var(--color) !important;
|
||||
line-height: unset;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.sql-table-schemas-widget .sql-table-schemas button:hover,
|
||||
@@ -106,18 +105,6 @@
|
||||
--color: var(--main-text-color);
|
||||
}
|
||||
|
||||
/* Tooltip */
|
||||
|
||||
.tooltip .table-schema {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
/* Data type */
|
||||
.tooltip .table-schema td:nth-child(2) {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
|
||||
/*
|
||||
* NOTE MAP
|
||||
*/
|
||||
|
||||
@@ -1682,7 +1682,8 @@
|
||||
"hoist-this-note-workspace": "Hoist this note (workspace)",
|
||||
"refresh-saved-search-results": "Refresh saved search results",
|
||||
"create-child-note": "Create child note",
|
||||
"unhoist": "Unhoist"
|
||||
"unhoist": "Unhoist",
|
||||
"toggle-sidebar": "Toggle sidebar"
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep Window on Top"
|
||||
|
||||
28
apps/client/src/widgets/api_log.css
Normal file
28
apps/client/src/widgets/api_log.css
Normal file
@@ -0,0 +1,28 @@
|
||||
.api-log-widget {
|
||||
flex-grow: 1;
|
||||
max-height: 40%;
|
||||
position: relative;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
background-color: var(--accented-background-color);
|
||||
}
|
||||
|
||||
.api-log-container {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: 0.8em;
|
||||
white-space: pre;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.close-api-log-button {
|
||||
padding: 5px;
|
||||
border: 1px solid var(--button-border-color);
|
||||
background-color: var(--button-background-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -1,80 +0,0 @@
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="api-log-widget">
|
||||
<style>
|
||||
.api-log-widget {
|
||||
padding: 15px;
|
||||
flex-grow: 1;
|
||||
max-height: 40%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.hidden-api-log {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.api-log-container {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.close-api-log-button {
|
||||
padding: 5px;
|
||||
border: 1px solid var(--button-border-color);
|
||||
background-color: var(--button-background-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="bx bx-x close-api-log-button" title="${t("api_log.close")}"></div>
|
||||
|
||||
<div class="api-log-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class ApiLogWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $logContainer!: JQuery<HTMLElement>;
|
||||
private $closeButton!: JQuery<HTMLElement>;
|
||||
|
||||
isEnabled() {
|
||||
return !!this.note && this.note.mime.startsWith("application/javascript;env=") && super.isEnabled();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.toggle(false);
|
||||
|
||||
this.$logContainer = this.$widget.find(".api-log-container");
|
||||
this.$closeButton = this.$widget.find(".close-api-log-button");
|
||||
this.$closeButton.on("click", () => this.toggle(false));
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
this.$logContainer.empty();
|
||||
}
|
||||
|
||||
apiLogMessagesEvent({ messages, noteId }: EventData<"apiLogMessages">) {
|
||||
if (!this.isNote(noteId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggle(true);
|
||||
|
||||
for (const message of messages) {
|
||||
this.$logContainer.append(message).append($("<br>"));
|
||||
}
|
||||
}
|
||||
|
||||
toggle(show: boolean) {
|
||||
this.$widget.toggleClass("hidden-api-log", !show);
|
||||
}
|
||||
}
|
||||
41
apps/client/src/widgets/api_log.tsx
Normal file
41
apps/client/src/widgets/api_log.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "./api_log.css";
|
||||
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import { t } from "../services/i18n";
|
||||
|
||||
/**
|
||||
* Displays the messages that are logged by the current note via `api.log`, for frontend and backend scripts.
|
||||
*/
|
||||
export default function ApiLog() {
|
||||
const { note, noteId } = useNoteContext();
|
||||
const [ messages, setMessages ] = useState<string[]>();
|
||||
|
||||
useTriliumEvent("apiLogMessages", ({ messages, noteId: eventNoteId }) => {
|
||||
if (eventNoteId !== noteId) return;
|
||||
setMessages(messages);
|
||||
});
|
||||
|
||||
// Clear when navigating away.
|
||||
useEffect(() => setMessages(undefined), [ note ]);
|
||||
|
||||
const isEnabled = note?.mime.startsWith("application/javascript;env=") && messages?.length;
|
||||
return (
|
||||
<div className={`api-log-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
<>
|
||||
<ActionButton
|
||||
icon="bx bx-x"
|
||||
className="close-api-log-button"
|
||||
text={t("api_log.close")}
|
||||
onClick={() => setMessages(undefined)}
|
||||
/>
|
||||
|
||||
<div className="api-log-container">
|
||||
{messages.join("\n")}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
||||
* @param components the components to be added as children to this component provided the condition is truthy.
|
||||
* @returns self for chaining.
|
||||
*/
|
||||
optChild(condition: boolean, ...components: T[]) {
|
||||
optChild(condition: boolean, ...components: (T | VNode)[]) {
|
||||
if (condition) {
|
||||
return this.child(...components);
|
||||
} else {
|
||||
|
||||
102
apps/client/src/widgets/buttons/global_menu.css
Normal file
102
apps/client/src/widgets/buttons/global_menu.css
Normal file
@@ -0,0 +1,102 @@
|
||||
.global-menu {
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-menu {
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.global-menu-button {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.global-menu-button svg path {
|
||||
fill: var(--launcher-pane-text-color);
|
||||
}
|
||||
|
||||
.global-menu-button:hover { border: 0; }
|
||||
.global-menu-button:hover svg path {
|
||||
transition: 200ms ease-in-out fill;
|
||||
}
|
||||
.global-menu-button:hover svg path.st0 { fill:#95C980; }
|
||||
.global-menu-button:hover svg path.st1 { fill:#72B755; }
|
||||
.global-menu-button:hover svg path.st2 { fill:#4FA52B; }
|
||||
.global-menu-button:hover svg path.st3 { fill:#EE8C89; }
|
||||
.global-menu-button:hover svg path.st4 { fill:#E96562; }
|
||||
.global-menu-button:hover svg path.st5 { fill:#E33F3B; }
|
||||
.global-menu-button:hover svg path.st6 { fill:#EFB075; }
|
||||
.global-menu-button:hover svg path.st7 { fill:#E99547; }
|
||||
.global-menu-button:hover svg path.st8 { fill:#E47B19; }
|
||||
|
||||
.global-menu-button-update-available {
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
bottom: -30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons {
|
||||
margin-left: 2em;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
padding: 3px;
|
||||
margin-left: 3px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-state {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-item .bx {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* #region Update available */
|
||||
.global-menu-button-update-available-button {
|
||||
width: 21px !important;
|
||||
height: 21px !important;
|
||||
padding: 0 !important;
|
||||
|
||||
border-radius: var(--button-border-radius);
|
||||
transform: scale(0.9);
|
||||
border: none;
|
||||
opacity: 0.8;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
|
||||
opacity: 1;
|
||||
}
|
||||
/* #endregion */
|
||||
@@ -1,436 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import UpdateAvailableWidget from "./update_available.js";
|
||||
import options from "../../services/options.js";
|
||||
import { Tooltip, Dropdown } from "bootstrap";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="dropdown global-menu">
|
||||
<style>
|
||||
.global-menu {
|
||||
width: 53px;
|
||||
height: 53px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-menu {
|
||||
min-width: 20em;
|
||||
}
|
||||
|
||||
.global-menu-button {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
padding: 6px;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.global-menu-button > svg path {
|
||||
fill: var(--launcher-pane-text-color);
|
||||
}
|
||||
|
||||
.global-menu-button:hover { border: 0; }
|
||||
.global-menu-button:hover > svg path {
|
||||
transition: 200ms ease-in-out fill;
|
||||
}
|
||||
.global-menu-button:hover > svg path.st0 { fill:#95C980; }
|
||||
.global-menu-button:hover > svg path.st1 { fill:#72B755; }
|
||||
.global-menu-button:hover > svg path.st2 { fill:#4FA52B; }
|
||||
.global-menu-button:hover > svg path.st3 { fill:#EE8C89; }
|
||||
.global-menu-button:hover > svg path.st4 { fill:#E96562; }
|
||||
.global-menu-button:hover > svg path.st5 { fill:#E33F3B; }
|
||||
.global-menu-button:hover > svg path.st6 { fill:#EFB075; }
|
||||
.global-menu-button:hover > svg path.st7 { fill:#E99547; }
|
||||
.global-menu-button:hover > svg path.st8 { fill:#E47B19; }
|
||||
|
||||
.global-menu-button-update-available {
|
||||
position: absolute;
|
||||
right: -30px;
|
||||
bottom: -30px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a {
|
||||
display: inline-block;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
color: var(--button-text-color);
|
||||
background-color: var(--button-background-color);
|
||||
padding: 3px;
|
||||
margin-left: 3px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-buttons a:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.global-menu .zoom-state {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.global-menu .dropdown-item .bx {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false" class="icon-action global-menu-button">
|
||||
<div class="global-menu-button-update-available"></div>
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-right">
|
||||
<li class="dropdown-item" data-trigger-command="openNewWindow">
|
||||
<span class="bx bx-window-open"></span>
|
||||
${t("global_menu.open_new_window")}
|
||||
<kbd data-command="openNewWindow"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showShareSubtree">
|
||||
<span class="bx bx-share-alt"></span>
|
||||
${t("global_menu.show_shared_notes_subtree")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<span class="zoom-container dropdown-item dropdown-item-container">
|
||||
<div>
|
||||
<span class="bx bx-empty"></span>
|
||||
${t("global_menu.zoom")}
|
||||
</div>
|
||||
|
||||
<div class="zoom-buttons">
|
||||
<a data-trigger-command="toggleFullscreen" title="${t("global_menu.toggle_fullscreen")}" class="bx bx-expand-alt"></a>
|
||||
|
||||
|
||||
|
||||
<a data-trigger-command="zoomOut" title="${t("global_menu.zoom_out")}" class="bx bx-minus"></a>
|
||||
|
||||
<span data-trigger-command="zoomReset" title="${t("global_menu.reset_zoom_level")}" class="zoom-state"></span>
|
||||
|
||||
<a data-trigger-command="zoomIn" title="${t("global_menu.zoom_in")}" class="bx bx-plus"></a>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
<li class="dropdown-item toggle-pin">
|
||||
<span class="bx bx-pin"></span>
|
||||
${t("title_bar_buttons.window-on-top")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="toggleZenMode">
|
||||
<span class="bx bxs-yin-yang"></span>
|
||||
${t("global_menu.toggle-zen-mode")}
|
||||
<kbd data-command="toggleZenMode"></kbd>
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider desktop-only"></div>
|
||||
|
||||
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
|
||||
<span class="bx bx-mobile"></span>
|
||||
${t("global_menu.switch_to_mobile_version")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
|
||||
<span class="bx bx-desktop"></span>
|
||||
${t("global_menu.switch_to_desktop_version")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
|
||||
<span class="bx ${utils.isMobile() ? "bx-mobile" : "bx-sidebar"}"></span>
|
||||
${t("global_menu.configure_launchbar")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item dropdown-submenu">
|
||||
<span class="dropdown-toggle">
|
||||
<span class="bx bx-chip"></span>${t("global_menu.advanced")}
|
||||
</span>
|
||||
|
||||
<ul class="dropdown-menu">
|
||||
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
|
||||
<span class="bx bx-hide"></span>
|
||||
${t("global_menu.show_hidden_subtree")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showSearchHistory">
|
||||
<span class="bx bx-search-alt"></span>
|
||||
${t("global_menu.open_search_history")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showBackendLog">
|
||||
<span class="bx bx-detail"></span>
|
||||
${t("global_menu.show_backend_log")}
|
||||
<kbd data-command="showBackendLog"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showSQLConsole">
|
||||
<span class="bx bx-data"></span>
|
||||
${t("global_menu.open_sql_console")}
|
||||
<kbd data-command="showSQLConsole"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showSQLConsoleHistory">
|
||||
<span class="bx bx-data"></span>
|
||||
${t("global_menu.open_sql_console_history")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider"></div>
|
||||
|
||||
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
|
||||
<span class="bx bx-bug-alt"></span>
|
||||
${t("global_menu.open_dev_tools")}
|
||||
<kbd data-command="openDevTools"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="reloadFrontendApp"
|
||||
title="${t("global_menu.reload_hint")}">
|
||||
<span class="bx bx-refresh"></span>
|
||||
${t("global_menu.reload_frontend")}
|
||||
<kbd data-command="reloadFrontendApp"></kbd>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item" data-trigger-command="showOptions">
|
||||
<span class="bx bx-cog"></span>
|
||||
${t("global_menu.options")}
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider desktop-only"></div>
|
||||
|
||||
<li class="dropdown-item show-help-button" data-trigger-command="showHelp">
|
||||
<span class="bx bx-help-circle"></span>
|
||||
${t("global_menu.show_help")}
|
||||
<kbd data-command="showHelp"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item show-help-button" data-trigger-command="showCheatsheet">
|
||||
<span class="bx bxs-keyboard"></span>
|
||||
${t("global_menu.show-cheatsheet")}
|
||||
<kbd data-command="showCheatsheet"></kbd>
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item show-about-dialog-button">
|
||||
<span class="bx bx-info-circle"></span>
|
||||
${t("global_menu.about")}
|
||||
</li>
|
||||
|
||||
<li class="dropdown-item update-to-latest-version-button" style="display: none;" data-trigger-command="downloadLatestVersion">
|
||||
<span class="bx bx-sync"></span>
|
||||
|
||||
<span class="version-text"></span>
|
||||
</li>
|
||||
|
||||
<div class="dropdown-divider logout-button-separator"></div>
|
||||
|
||||
<li class="dropdown-item logout-button" data-trigger-command="logout">
|
||||
<span class="bx bx-log-out"></span>
|
||||
${t("global_menu.logout")}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class GlobalMenuWidget extends BasicWidget {
|
||||
private updateAvailableWidget: UpdateAvailableWidget;
|
||||
private isHorizontalLayout: boolean;
|
||||
private tooltip!: Tooltip;
|
||||
private dropdown!: Dropdown;
|
||||
|
||||
private $updateToLatestVersionButton!: JQuery<HTMLElement>;
|
||||
private $zoomState!: JQuery<HTMLElement>;
|
||||
private $toggleZenMode!: JQuery<HTMLElement>;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
|
||||
this.updateAvailableWidget = new UpdateAvailableWidget();
|
||||
this.isHorizontalLayout = isHorizontalLayout;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
if (!this.isHorizontalLayout) {
|
||||
this.$widget.addClass("dropend");
|
||||
}
|
||||
|
||||
const $globalMenuButton = this.$widget.find(".global-menu-button");
|
||||
if (!this.isHorizontalLayout) {
|
||||
$globalMenuButton.prepend(
|
||||
$(`\
|
||||
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t("global_menu.menu")}">
|
||||
<g>
|
||||
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
|
||||
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
|
||||
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
|
||||
|
||||
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
|
||||
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
|
||||
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
|
||||
|
||||
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
|
||||
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
|
||||
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
|
||||
</g>
|
||||
</svg>`)
|
||||
);
|
||||
|
||||
this.tooltip = new Tooltip(this.$widget.find("[data-bs-toggle='tooltip']")[0], { trigger: "hover" });
|
||||
} else {
|
||||
$globalMenuButton.toggleClass("bx bx-menu");
|
||||
}
|
||||
|
||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
|
||||
popperConfig: {
|
||||
placement: "bottom"
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find(".show-about-dialog-button").on("click", () => this.triggerCommand("openAboutDialog"));
|
||||
|
||||
const isElectron = utils.isElectron();
|
||||
|
||||
this.$widget.find(".toggle-pin").toggle(isElectron);
|
||||
if (isElectron) {
|
||||
this.$widget.on("click", ".toggle-pin", (e) => {
|
||||
const $el = $(e.target);
|
||||
const remote = utils.dynamicRequire("@electron/remote");
|
||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
const isAlwaysOnTop = focusedWindow.isAlwaysOnTop();
|
||||
if (isAlwaysOnTop) {
|
||||
focusedWindow.setAlwaysOnTop(false);
|
||||
$el.removeClass("active");
|
||||
} else {
|
||||
focusedWindow.setAlwaysOnTop(true);
|
||||
$el.addClass("active");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.$widget.find(".logout-button").toggle(!isElectron);
|
||||
this.$widget.find(".logout-button-separator").toggle(!isElectron);
|
||||
|
||||
this.$widget.find(".open-dev-tools-button").toggle(isElectron);
|
||||
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop());
|
||||
this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile());
|
||||
|
||||
this.$widget.on("click", ".dropdown-item", (e) => {
|
||||
if ($(e.target).parent(".zoom-buttons")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.dropdown.toggle();
|
||||
});
|
||||
if (utils.isMobile()) {
|
||||
this.$widget.on("click", ".dropdown-submenu .dropdown-toggle", (e) => {
|
||||
const $submenu = $(e.target).closest(".dropdown-item");
|
||||
$submenu.toggleClass("submenu-open");
|
||||
$submenu.find("ul.dropdown-menu").toggleClass("show");
|
||||
e.stopPropagation();
|
||||
return;
|
||||
});
|
||||
}
|
||||
this.$widget.on("click", ".dropdown-submenu", (e) => {
|
||||
if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass("dropdown-toggle")) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
|
||||
this.$widget.find(".global-menu-button-update-available").append(this.updateAvailableWidget.render());
|
||||
|
||||
this.$updateToLatestVersionButton = this.$widget.find(".update-to-latest-version-button");
|
||||
|
||||
if (!utils.isElectron()) {
|
||||
this.$widget.find(".zoom-container").hide();
|
||||
}
|
||||
|
||||
this.$zoomState = this.$widget.find(".zoom-state");
|
||||
this.$toggleZenMode = this.$widget.find('[data-trigger-command="toggleZenMode"');
|
||||
this.$widget.on("show.bs.dropdown", () => this.#onShown());
|
||||
if (this.tooltip) {
|
||||
this.$widget.on("hide.bs.dropdown", () => this.tooltip.enable());
|
||||
}
|
||||
|
||||
this.$widget.find(".zoom-buttons").on(
|
||||
"click",
|
||||
// delay to wait for the actual zoom change
|
||||
() => setTimeout(() => this.updateZoomState(), 300)
|
||||
);
|
||||
|
||||
this.updateVersionStatus();
|
||||
|
||||
setInterval(() => this.updateVersionStatus(), 8 * 60 * 60 * 1000);
|
||||
}
|
||||
|
||||
#onShown() {
|
||||
this.$toggleZenMode.toggleClass("active", $("body").hasClass("zen"));
|
||||
this.updateZoomState();
|
||||
if (this.tooltip) {
|
||||
this.tooltip.hide();
|
||||
this.tooltip.disable();
|
||||
}
|
||||
}
|
||||
|
||||
updateZoomState() {
|
||||
if (!utils.isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomFactor = utils.dynamicRequire("electron").webFrame.getZoomFactor();
|
||||
const zoomPercent = Math.round(zoomFactor * 100);
|
||||
|
||||
this.$zoomState.text(`${zoomPercent}%`);
|
||||
}
|
||||
|
||||
async updateVersionStatus() {
|
||||
await options.initializedPromise;
|
||||
|
||||
if (options.get("checkForUpdates") !== "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
const latestVersion = await this.fetchLatestVersion();
|
||||
this.updateAvailableWidget.updateVersionStatus(latestVersion);
|
||||
// Show "click to download" button in options menu if there's a new version available
|
||||
this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
||||
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
|
||||
}
|
||||
|
||||
async fetchLatestVersion() {
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
|
||||
|
||||
const resp = await fetch(RELEASES_API_URL);
|
||||
const data = await resp.json();
|
||||
|
||||
return data?.tag_name?.substring(1);
|
||||
}
|
||||
|
||||
downloadLatestVersionCommand() {
|
||||
window.open("https://github.com/TriliumNext/Trilium/releases/latest");
|
||||
}
|
||||
|
||||
activeContextChangedEvent() {
|
||||
this.dropdown.hide();
|
||||
}
|
||||
|
||||
noteSwitchedEvent() {
|
||||
this.dropdown.hide();
|
||||
}
|
||||
}
|
||||
239
apps/client/src/widgets/buttons/global_menu.tsx
Normal file
239
apps/client/src/widgets/buttons/global_menu.tsx
Normal file
@@ -0,0 +1,239 @@
|
||||
import Dropdown from "../react/Dropdown";
|
||||
import "./global_menu.css";
|
||||
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool } from "../react/hooks";
|
||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import KeyboardShortcut from "../react/KeyboardShortcut";
|
||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||
import { ComponentChildren } from "preact";
|
||||
import Component from "../../components/component";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils";
|
||||
|
||||
interface MenuItemProps<T> {
|
||||
icon: string,
|
||||
text: ComponentChildren,
|
||||
title?: string,
|
||||
command: T,
|
||||
disabled?: boolean
|
||||
active?: boolean;
|
||||
outsideChildren?: ComponentChildren;
|
||||
}
|
||||
|
||||
export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||
const isVerticalLayout = !isHorizontalLayout;
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
className="global-menu"
|
||||
buttonClassName={`global-menu-button ${isHorizontalLayout ? "bx bx-menu" : ""}`} noSelectButtonStyle iconAction hideToggleArrow
|
||||
text={<>
|
||||
{isVerticalLayout && <VerticalLayoutIcon />}
|
||||
{isUpdateAvailable && <div class="global-menu-button-update-available">
|
||||
<span className="bx bx-sync global-menu-button-update-available-button" title={t("update_available.update_available")}></span>
|
||||
</div>}
|
||||
</>}
|
||||
>
|
||||
|
||||
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||
<MenuItem command="showShareSubtree" icon="bx bx-share-alt" text={t("global_menu.show_shared_notes_subtree")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<ZoomControls parentComponent={parentComponent} />
|
||||
<ToggleWindowOnTop />
|
||||
<KeyboardActionMenuItem command="toggleZenMode" icon="bx bxs-yin-yang" text={t("global_menu.toggle-zen-mode")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<SwitchToOptions />
|
||||
<MenuItem command="showLaunchBarSubtree" icon={`bx ${isMobile() ? "bx-mobile" : "bx-sidebar"}`} text={t("global_menu.configure_launchbar")} />
|
||||
<AdvancedMenu />
|
||||
<MenuItem command="showOptions" icon="bx bx-cog" text={t("global_menu.options")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<KeyboardActionMenuItem command="showHelp" icon="bx bx-help-circle" text={t("global_menu.show_help")} />
|
||||
<KeyboardActionMenuItem command="showCheatsheet" icon="bx bxs-keyboard" text={t("global_menu.show-cheatsheet")} />
|
||||
<MenuItem command="openAboutDialog" icon="bx bx-info-circle" text={t("global_menu.about")} />
|
||||
{isUpdateAvailable && <MenuItem command={() => window.open("https://github.com/TriliumNext/Trilium/releases/latest")} icon="bx bx-sync" text={`Version ${latestVersion} is available, click to download.`} /> }
|
||||
{!isElectron() && <BrowserOnlyOptions />}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
function AdvancedMenu() {
|
||||
return (
|
||||
<FormDropdownSubmenu icon="bx bx-chip" title={t("global_menu.advanced")}>
|
||||
<MenuItem command="showHiddenSubtree" icon="bx bx-hide" text={t("global_menu.show_hidden_subtree")} />
|
||||
<MenuItem command="showSearchHistory" icon="bx bx-search-alt" text={t("global_menu.open_search_history")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
<KeyboardActionMenuItem command="showBackendLog" icon="bx bx-detail" text={t("global_menu.show_backend_log")} />
|
||||
<KeyboardActionMenuItem command="showSQLConsole" icon="bx bx-data" text={t("global_menu.open_sql_console")} />
|
||||
<MenuItem command="showSQLConsoleHistory" icon="bx bx-data" text={t("global_menu.open_sql_console_history")} />
|
||||
<FormDropdownDivider />
|
||||
|
||||
{isElectron() && <MenuItem command="openDevTools" icon="bx bx-bug-alt" text={t("global_menu.open_dev_tools")} />}
|
||||
<KeyboardActionMenuItem command="reloadFrontendApp" icon="bx bx-refresh" text={t("global_menu.reload_frontend")} title={t("global_menu.reload_hint")} />
|
||||
</FormDropdownSubmenu>
|
||||
)
|
||||
}
|
||||
|
||||
function BrowserOnlyOptions() {
|
||||
return <>
|
||||
<FormDropdownDivider />
|
||||
<MenuItem command="logout" icon="bx bx-log-out" text={t("global_menu.logout")} />
|
||||
</>;
|
||||
}
|
||||
|
||||
function SwitchToOptions() {
|
||||
if (isElectron()) {
|
||||
return;
|
||||
} else if (!isMobile()) {
|
||||
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />
|
||||
} else {
|
||||
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />
|
||||
}
|
||||
}
|
||||
|
||||
function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) {
|
||||
return <FormListItem
|
||||
icon={icon}
|
||||
title={title}
|
||||
triggerCommand={typeof command === "string" ? command : undefined}
|
||||
onClick={typeof command === "function" ? command : undefined}
|
||||
disabled={disabled}
|
||||
active={active}
|
||||
>{text}</FormListItem>
|
||||
}
|
||||
|
||||
function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<KeyboardActionNames>) {
|
||||
return <MenuItem
|
||||
{...props}
|
||||
command={command}
|
||||
text={<>{text} <KeyboardShortcut actionName={command as KeyboardActionNames} /></>}
|
||||
/>
|
||||
}
|
||||
|
||||
function VerticalLayoutIcon() {
|
||||
const logoRef = useRef<SVGSVGElement>(null);
|
||||
useStaticTooltip(logoRef);
|
||||
|
||||
return (
|
||||
<svg ref={logoRef} viewBox="0 0 256 256" title={t("global_menu.menu")}>
|
||||
<g>
|
||||
<path className="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
|
||||
<path className="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
|
||||
<path className="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
|
||||
|
||||
<path className="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
|
||||
<path className="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
|
||||
<path className="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
|
||||
|
||||
<path className="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
|
||||
<path className="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
|
||||
<path className="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) {
|
||||
const [ zoomLevel, setZoomLevel ] = useState(100);
|
||||
|
||||
function updateZoomState() {
|
||||
if (!isElectron()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const zoomFactor = dynamicRequire("electron").webFrame.getZoomFactor();
|
||||
setZoomLevel(Math.round(zoomFactor * 100));
|
||||
}
|
||||
|
||||
useEffect(updateZoomState, []);
|
||||
|
||||
function ZoomControlButton({ command, title, icon, children }: { command: KeyboardActionNames, title: string, icon?: string, children?: ComponentChildren }) {
|
||||
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||
useStaticTooltipWithKeyboardShortcut(linkRef, title, command);
|
||||
return (
|
||||
<a
|
||||
ref={linkRef}
|
||||
onClick={(e) => {
|
||||
parentComponent?.triggerCommand(command);
|
||||
setTimeout(() => updateZoomState(), 300)
|
||||
e.stopPropagation();
|
||||
}}
|
||||
className={icon}
|
||||
>{children}</a>
|
||||
)
|
||||
}
|
||||
|
||||
return isElectron() ? (
|
||||
<FormListItem
|
||||
icon="bx bx-empty"
|
||||
className="zoom-container"
|
||||
>
|
||||
{t("global_menu.zoom")}
|
||||
<>
|
||||
<div className="zoom-buttons">
|
||||
<ZoomControlButton command="toggleFullscreen" title={t("global_menu.toggle_fullscreen")} icon="bx bx-expand-alt" />
|
||||
|
||||
<ZoomControlButton command="zoomOut" title={t("global_menu.zoom_out")} icon="bx bx-minus" />
|
||||
<ZoomControlButton command="zoomReset" title={t("global_menu.reset_zoom_level")}>{zoomLevel}{t("units.percentage")}</ZoomControlButton>
|
||||
<ZoomControlButton command="zoomIn" title={t("global_menu.zoom_in")} icon="bx bx-plus" />
|
||||
</div>
|
||||
</>
|
||||
</FormListItem>
|
||||
) : (
|
||||
<MenuItem icon="bx bx-expand-alt" command="toggleFullscreen" text={t("global_menu.toggle_fullscreen")} />
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleWindowOnTop() {
|
||||
const focusedWindow = isElectron() ? dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow() : null;
|
||||
const [ isAlwaysOnTop, setIsAlwaysOnTop ] = useState(focusedWindow?.isAlwaysOnTop());
|
||||
|
||||
return (isElectron() &&
|
||||
<MenuItem
|
||||
icon="bx bx-pin"
|
||||
text={t("title_bar_buttons.window-on-top")}
|
||||
active={isAlwaysOnTop}
|
||||
command={() => {
|
||||
const newState = !isAlwaysOnTop;
|
||||
focusedWindow?.setAlwaysOnTop(newState);
|
||||
setIsAlwaysOnTop(newState);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function useTriliumUpdateStatus() {
|
||||
const [ latestVersion, setLatestVersion ] = useState<string>();
|
||||
const [ checkForUpdates ] = useTriliumOptionBool("checkForUpdates");
|
||||
const isUpdateAvailable = utils.isUpdateAvailable(latestVersion, glob.triliumVersion);
|
||||
|
||||
async function updateVersionStatus() {
|
||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
|
||||
|
||||
const resp = await fetch(RELEASES_API_URL);
|
||||
const data = await resp.json();
|
||||
const latestVersion = data?.tag_name?.substring(1);
|
||||
setLatestVersion(latestVersion);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!checkForUpdates) {
|
||||
setLatestVersion(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
updateVersionStatus();
|
||||
|
||||
const interval = setInterval(() => updateVersionStatus(), 8 * 60 * 60 * 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [ checkForUpdates ]);
|
||||
|
||||
return { isUpdateAvailable, latestVersion };
|
||||
}
|
||||
@@ -4,15 +4,7 @@ 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";
|
||||
|
||||
interface WebContents {
|
||||
history: string[];
|
||||
getActiveIndex(): number;
|
||||
clearHistory(): void;
|
||||
canGoBack(): boolean;
|
||||
canGoForward(): boolean;
|
||||
goToIndex(index: string): void;
|
||||
}
|
||||
import type { WebContents } from "electron";
|
||||
|
||||
interface ContextMenuItem {
|
||||
title: string;
|
||||
@@ -51,14 +43,14 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget {
|
||||
async showContextMenu(e: JQuery.ContextMenuEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.webContents || this.webContents.history.length < 2) {
|
||||
if (!this.webContents || this.webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: ContextMenuItem[] = [];
|
||||
|
||||
const activeIndex = this.webContents.getActiveIndex();
|
||||
const history = this.webContents.history;
|
||||
const history = this.webContents.navigationHistory;
|
||||
const activeIndex = history.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const url = history[idx];
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import options from "../../services/options.js";
|
||||
import splitService from "../../services/resizer.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
export default class LeftPaneToggleWidget extends CommandButtonWidget {
|
||||
private currentLeftPaneVisible: boolean;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
|
||||
this.currentLeftPaneVisible = options.is("leftPaneVisible");
|
||||
|
||||
this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
|
||||
|
||||
this.settings.icon = () => {
|
||||
if (options.get("layoutOrientation") === "horizontal") {
|
||||
return "bx-sidebar";
|
||||
}
|
||||
|
||||
return this.currentLeftPaneVisible ? "bx-chevrons-left" : "bx-chevrons-right";
|
||||
};
|
||||
|
||||
this.settings.title = () => (this.currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel"));
|
||||
|
||||
this.settings.command = () => (this.currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane");
|
||||
|
||||
if (isHorizontalLayout) {
|
||||
this.settings.titlePlacement = "bottom";
|
||||
}
|
||||
}
|
||||
|
||||
refreshIcon() {
|
||||
super.refreshIcon();
|
||||
splitService.setupLeftPaneResizer(this.currentLeftPaneVisible);
|
||||
}
|
||||
|
||||
setLeftPaneVisibilityEvent({ leftPaneVisible }: EventData<"setLeftPaneVisibility">) {
|
||||
this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
|
||||
this.refreshIcon();
|
||||
}
|
||||
}
|
||||
29
apps/client/src/widgets/buttons/left_pane_toggle.tsx
Normal file
29
apps/client/src/widgets/buttons/left_pane_toggle.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import options from "../../services/options";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import resizer from "../../services/resizer";
|
||||
|
||||
export default function LeftPaneToggle({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||
const [ currentLeftPaneVisible, setCurrentLeftPaneVisible ] = useState(options.is("leftPaneVisible"));
|
||||
|
||||
useTriliumEvent("setLeftPaneVisibility", ({ leftPaneVisible }) => {
|
||||
setCurrentLeftPaneVisible(leftPaneVisible ?? !currentLeftPaneVisible);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
resizer.setupLeftPaneResizer(currentLeftPaneVisible);
|
||||
}, [ currentLeftPaneVisible ]);
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
className={`${isHorizontalLayout ? "toggle-button" : "launcher-button"}`}
|
||||
text={currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel")}
|
||||
triggerCommand={currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane"}
|
||||
icon={isHorizontalLayout
|
||||
? "bx bx-sidebar"
|
||||
: (currentLeftPaneVisible ? "bx bx-chevrons-left" : "bx bx-chevrons-right" )}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div style="display: none;">
|
||||
<style>
|
||||
.global-menu-button-update-available-button {
|
||||
width: 21px !important;
|
||||
height: 21px !important;
|
||||
padding: 0 !important;
|
||||
|
||||
border-radius: var(--button-border-radius);
|
||||
transform: scale(0.9);
|
||||
border: none;
|
||||
opacity: 0.8;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span class="bx bx-sync global-menu-button-update-available-button" title="${t("update_available.update_available")}"></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class UpdateAvailableWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
}
|
||||
|
||||
updateVersionStatus(latestVersion: string) {
|
||||
this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
||||
}
|
||||
}
|
||||
26
apps/client/src/widgets/close_zen_button.css
Normal file
26
apps/client/src/widgets/close_zen_button.css
Normal file
@@ -0,0 +1,26 @@
|
||||
:root {
|
||||
--zen-button-size: 32px;
|
||||
}
|
||||
|
||||
.close-zen-container {
|
||||
width: var(--zen-button-size);
|
||||
height: var(--zen-button-size);
|
||||
}
|
||||
|
||||
body.zen .close-zen-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
z-index: 9999;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
body.zen.mobile .close-zen-container {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
body.zen.electron:not(.platform-darwin):not(.native-titlebar) .close-zen-container {
|
||||
left: calc(env(titlebar-area-width) - var(--zen-button-size) - 2px);
|
||||
right: unset;
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="close-zen-container">
|
||||
<button class="button-widget bx icon-action bxs-yin-yang"
|
||||
data-trigger-command="toggleZenMode"
|
||||
title="${t("zen_mode.button_exit")}"
|
||||
/>
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--zen-button-size: 32px;
|
||||
}
|
||||
|
||||
.close-zen-container {
|
||||
display: none;
|
||||
width: var(--zen-button-size);
|
||||
height: var(--zen-button-size);
|
||||
}
|
||||
|
||||
body.zen .close-zen-container {
|
||||
display: block;
|
||||
position: fixed;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
z-index: 9999;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
body.zen.mobile .close-zen-container {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
body.zen.electron:not(.platform-darwin):not(.native-titlebar) .close-zen-container {
|
||||
left: calc(env(titlebar-area-width) - var(--zen-button-size) - 2px);
|
||||
right: unset;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
||||
export default class CloseZenButton extends BasicWidget {
|
||||
|
||||
doRender(): void {
|
||||
this.$widget = $(TPL);
|
||||
}
|
||||
|
||||
zenChangedEvent() {
|
||||
this.toggleInt(true);
|
||||
}
|
||||
|
||||
}
|
||||
25
apps/client/src/widgets/close_zen_button.tsx
Normal file
25
apps/client/src/widgets/close_zen_button.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { t } from "../services/i18n";
|
||||
import ActionButton from "./react/ActionButton";
|
||||
import { useTriliumEvent } from "./react/hooks";
|
||||
import "./close_zen_button.css";
|
||||
|
||||
export default function CloseZenModeButton() {
|
||||
const [ zenModeEnabled, setZenModeEnabled ] = useState(false);
|
||||
|
||||
useTriliumEvent("zenModeChanged", ({ isEnabled }) => {
|
||||
setZenModeEnabled(isEnabled);
|
||||
});
|
||||
|
||||
return (
|
||||
<div class={`close-zen-container ${!zenModeEnabled ? "hidden-ext" : ""}`}>
|
||||
{zenModeEnabled && (
|
||||
<ActionButton
|
||||
icon="bx bxs-yin-yang"
|
||||
triggerCommand="toggleZenMode"
|
||||
text={t("zen_mode.button_exit")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import contextMenu from "../../menus/context_menu.js";
|
||||
import noteCreateService from "../../services/note_create.js";
|
||||
import branchService from "../../services/branches.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
|
||||
const TPL = /*html*/`<button type="button" class="action-button bx"></button>`;
|
||||
|
||||
class MobileDetailMenuWidget extends BasicWidget {
|
||||
private isHorizontalLayout: boolean;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
this.isHorizontalLayout = isHorizontalLayout;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$widget.addClass(this.isHorizontalLayout ? "bx-dots-vertical-rounded" : "bx-menu");
|
||||
|
||||
this.$widget.on("click", async (e) => {
|
||||
const note = appContext.tabManager.getActiveContextNote();
|
||||
|
||||
contextMenu.show<"insertChildNote" | "delete" | "showRevisions">({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
||||
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
||||
{ title: "----" },
|
||||
{ title: "Note revisions", command: "showRevisions", uiIcon: "bx bx-history" }
|
||||
],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
noteCreateService.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
throw new Error("Cannot get note path to delete.");
|
||||
}
|
||||
|
||||
const branchId = await treeService.getBranchIdFromUrl(notePath);
|
||||
|
||||
if (!branchId) {
|
||||
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
|
||||
}
|
||||
|
||||
if (await branchService.deleteNotes([branchId])) {
|
||||
this.triggerCommand("setActiveScreen", { screen: "tree" });
|
||||
}
|
||||
} else if (command) {
|
||||
this.triggerCommand(command);
|
||||
}
|
||||
},
|
||||
forcePositionOnMobile: true
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default MobileDetailMenuWidget;
|
||||
@@ -0,0 +1,57 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import appContext from "../../components/app_context";
|
||||
import contextMenu from "../../menus/context_menu";
|
||||
import branches from "../../services/branches";
|
||||
import { t } from "../../services/i18n";
|
||||
import note_create from "../../services/note_create";
|
||||
import tree from "../../services/tree";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
|
||||
export default function MobileDetailMenu() {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon="bx bx-dots-vertical-rounded"
|
||||
text=""
|
||||
onClick={(e) => {
|
||||
const note = appContext.tabManager.getActiveContextNote();
|
||||
|
||||
contextMenu.show<"insertChildNote" | "delete" | "showRevisions">({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
||||
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
||||
{ title: "----" },
|
||||
{ title: "Note revisions", command: "showRevisions", uiIcon: "bx bx-history" }
|
||||
],
|
||||
selectMenuItemHandler: async ({ command }) => {
|
||||
if (command === "insertChildNote") {
|
||||
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||
} else if (command === "delete") {
|
||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||
if (!notePath) {
|
||||
throw new Error("Cannot get note path to delete.");
|
||||
}
|
||||
|
||||
const branchId = await tree.getBranchIdFromUrl(notePath);
|
||||
|
||||
if (!branchId) {
|
||||
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
|
||||
}
|
||||
|
||||
if (await branches.deleteNotes([branchId]) && parentComponent) {
|
||||
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
|
||||
}
|
||||
} else if (command && parentComponent) {
|
||||
parentComponent.triggerCommand(command);
|
||||
}
|
||||
},
|
||||
forcePositionOnMobile: true
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<button type="button" class="action-button bx bx-sidebar"></button>`;
|
||||
|
||||
class ToggleSidebarButtonWidget extends BasicWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$widget.on("click", () =>
|
||||
this.triggerCommand("setActiveScreen", {
|
||||
screen: "tree"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default ToggleSidebarButtonWidget;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { useContext } from "preact/hooks";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
export default function ToggleSidebarButton() {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon="bx bx-sidebar"
|
||||
text={t("note_tree.toggle-sidebar")}
|
||||
onClick={() => parentComponent?.triggerCommand("setActiveScreen", {
|
||||
screen: "tree"
|
||||
})}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import keyboard_actions from "../../services/keyboard_actions";
|
||||
|
||||
export interface ActionButtonProps {
|
||||
text: string;
|
||||
titlePosition?: "bottom" | "left"; // TODO: Use it
|
||||
titlePosition?: "bottom" | "left";
|
||||
icon: string;
|
||||
className?: string;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
@@ -25,7 +25,7 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
|
||||
|
||||
useEffect(() => {
|
||||
if (triggerCommand) {
|
||||
keyboard_actions.getAction(triggerCommand).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
||||
keyboard_actions.getAction(triggerCommand, true).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
||||
}
|
||||
}, [triggerCommand]);
|
||||
|
||||
|
||||
@@ -4,11 +4,12 @@ interface AlertProps {
|
||||
type: "info" | "danger" | "warning";
|
||||
title?: string;
|
||||
children: ComponentChildren;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Alert({ title, type, children }: AlertProps) {
|
||||
export default function Alert({ title, type, children, className }: AlertProps) {
|
||||
return (
|
||||
<div className={`alert alert-${type}`}>
|
||||
<div className={`alert alert-${type} ${className ?? ""}`}>
|
||||
{title && <h4>{title}</h4>}
|
||||
|
||||
{children}
|
||||
|
||||
@@ -18,9 +18,10 @@ export interface DropdownProps {
|
||||
noSelectButtonStyle?: boolean;
|
||||
disabled?: boolean;
|
||||
text?: ComponentChildren;
|
||||
forceShown?: boolean;
|
||||
}
|
||||
|
||||
export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle }: DropdownProps) {
|
||||
export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, forceShown }: DropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
@@ -30,8 +31,12 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre
|
||||
if (!triggerRef.current) return;
|
||||
|
||||
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current);
|
||||
if (forceShown) {
|
||||
dropdown.show();
|
||||
setShown(true);
|
||||
}
|
||||
return () => dropdown.dispose();
|
||||
}, []); // Add dependency array
|
||||
}, []);
|
||||
|
||||
const onShown = useCallback(() => {
|
||||
setShown(true);
|
||||
@@ -75,13 +80,13 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre
|
||||
<span className="caret"></span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
<ul
|
||||
class={`dropdown-menu ${isStatic ? "static" : ""} ${dropdownContainerClassName ?? ""} tn-dropdown-list`}
|
||||
style={dropdownContainerStyle}
|
||||
aria-labelledby={ariaId}
|
||||
>
|
||||
{shown && children}
|
||||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
|
||||
import { ComponentChildren } from "preact";
|
||||
import Icon from "./Icon";
|
||||
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
||||
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact/compat";
|
||||
import "./FormList.css";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
import { isMobile } from "../../services/utils";
|
||||
|
||||
interface FormListOpts {
|
||||
children: ComponentChildren;
|
||||
@@ -89,14 +91,24 @@ interface FormListItemOpts {
|
||||
rtl?: boolean;
|
||||
}
|
||||
|
||||
export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand }: FormListItemOpts) {
|
||||
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
|
||||
placement: "right",
|
||||
fallbackPlacements: [ "right" ]
|
||||
}
|
||||
|
||||
export function FormListItem({ className, icon, value, title, active, disabled, checked, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
|
||||
const itemRef = useRef<HTMLLIElement>(null);
|
||||
|
||||
if (checked) {
|
||||
icon = "bx bx-check";
|
||||
}
|
||||
|
||||
useStaticTooltip(itemRef, TOOLTIP_CONFIG);
|
||||
|
||||
return (
|
||||
<a
|
||||
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""}`}
|
||||
<li
|
||||
ref={itemRef}
|
||||
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""} ${className ?? ""}`}
|
||||
data-value={value} title={title}
|
||||
tabIndex={0}
|
||||
onClick={onClick}
|
||||
@@ -104,17 +116,27 @@ export function FormListItem({ children, icon, value, title, active, badges, dis
|
||||
dir={rtl ? "rtl" : undefined}
|
||||
>
|
||||
<Icon icon={icon} />
|
||||
<div>
|
||||
{children}
|
||||
{badges && badges.map(({ className, text }) => (
|
||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||
))}
|
||||
{description && <div className="description">{description}</div>}
|
||||
</div>
|
||||
</a>
|
||||
{description ? (
|
||||
<div>
|
||||
<FormListContent description={description} {...contentProps} />
|
||||
</div>
|
||||
) : (
|
||||
<FormListContent description={description} {...contentProps} />
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
function FormListContent({ children, badges, description }: Pick<FormListItemOpts, "children" | "badges" | "description">) {
|
||||
return <>
|
||||
{children}
|
||||
{badges && badges.map(({ className, text }) => (
|
||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||
))}
|
||||
{description && <div className="description">{description}</div>}
|
||||
</>;
|
||||
}
|
||||
|
||||
interface FormListHeaderOpts {
|
||||
text: string;
|
||||
}
|
||||
@@ -129,4 +151,30 @@ export function FormListHeader({ text }: FormListHeaderOpts) {
|
||||
|
||||
export function FormDropdownDivider() {
|
||||
return <div className="dropdown-divider" />;
|
||||
}
|
||||
|
||||
export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
|
||||
const [ openOnMobile, setOpenOnMobile ] = useState(false);
|
||||
|
||||
return (
|
||||
<li className={`dropdown-item dropdown-submenu ${openOnMobile ? "submenu-open" : ""}`}>
|
||||
<span
|
||||
className="dropdown-toggle"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (isMobile()) {
|
||||
setOpenOnMobile(!openOnMobile);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Icon icon={icon} />{" "}
|
||||
{title}
|
||||
</span>
|
||||
|
||||
<ul className={`dropdown-menu ${openOnMobile ? "show" : ""}`}>
|
||||
{children}
|
||||
</ul>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,14 @@ import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/common
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
import { joinElements } from "./react_utils";
|
||||
import utils from "../../services/utils";
|
||||
|
||||
interface KeyboardShortcutProps {
|
||||
actionName: KeyboardActionNames;
|
||||
}
|
||||
|
||||
const isMobile = utils.isMobile();
|
||||
|
||||
export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps) {
|
||||
|
||||
const [ action, setAction ] = useState<ActionKeyboardShortcut>();
|
||||
@@ -18,17 +21,14 @@ export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps)
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{action.effectiveShortcuts?.map((shortcut) => {
|
||||
return (!isMobile &&
|
||||
<span className="keyboard-shortcut">
|
||||
{joinElements(action.effectiveShortcuts?.map((shortcut) => {
|
||||
const keys = shortcut.split("+");
|
||||
return joinElements(keys
|
||||
.map((key, i) => (
|
||||
<>
|
||||
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
||||
</>
|
||||
)))
|
||||
})}
|
||||
</>
|
||||
return joinElements(
|
||||
keys.map((key, i) => <kbd>{key}</kbd>)
|
||||
, "+");
|
||||
}))}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, use
|
||||
import { EventData, EventNames } from "../../components/app_context";
|
||||
import { ParentComponent } from "./react_utils";
|
||||
import SpacedUpdate from "../../services/spaced_update";
|
||||
import { OptionNames } from "@triliumnext/commons";
|
||||
import { KeyboardActionNames, OptionNames } from "@triliumnext/commons";
|
||||
import options, { type OptionValue } from "../../services/options";
|
||||
import utils, { reloadFrontendApp } from "../../services/utils";
|
||||
import NoteContext from "../../components/note_context";
|
||||
@@ -14,6 +14,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
import { RefObject, VNode } from "preact";
|
||||
import { Tooltip } from "bootstrap";
|
||||
import { CSSProperties } from "preact/compat";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
|
||||
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
@@ -502,9 +503,10 @@ export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Toolti
|
||||
* @param elRef the element to bind the tooltip to.
|
||||
* @param config optionally, the tooltip configuration.
|
||||
*/
|
||||
export function useStaticTooltip(elRef: RefObject<HTMLElement>, config?: Partial<Tooltip.Options>) {
|
||||
export function useStaticTooltip(elRef: RefObject<Element>, config?: Partial<Tooltip.Options>) {
|
||||
useEffect(() => {
|
||||
if (!elRef?.current) return;
|
||||
const hasTooltip = config?.title || elRef.current?.getAttribute("title");
|
||||
if (!elRef?.current || !hasTooltip) return;
|
||||
|
||||
const $el = $(elRef.current);
|
||||
$el.tooltip(config);
|
||||
@@ -514,6 +516,19 @@ export function useStaticTooltip(elRef: RefObject<HTMLElement>, config?: Partial
|
||||
}, [ elRef, config ]);
|
||||
}
|
||||
|
||||
export function useStaticTooltipWithKeyboardShortcut(elRef: RefObject<Element>, title: string, actionName: KeyboardActionNames | undefined) {
|
||||
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
||||
useStaticTooltip(elRef, {
|
||||
title: keyboardShortcut?.length ? `${title} (${keyboardShortcut?.join(",")})` : title
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (actionName) {
|
||||
keyboard_actions.getAction(actionName).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
||||
}
|
||||
}, [actionName]);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export function useLegacyImperativeHandlers(handlers: Record<string, Function>) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
@@ -41,7 +41,9 @@ export function disposeReactWidget(container: Element) {
|
||||
render(null, container);
|
||||
}
|
||||
|
||||
export function joinElements(components: ComponentChild[], separator = ", ") {
|
||||
export function joinElements(components: ComponentChild[] | undefined, separator = ", ") {
|
||||
if (!components) return <></>;
|
||||
|
||||
const joinedComponents: ComponentChild[] = [];
|
||||
for (let i=0; i<components.length; i++) {
|
||||
joinedComponents.push(components[i]);
|
||||
@@ -50,5 +52,5 @@ export function joinElements(components: ComponentChild[], separator = ", ") {
|
||||
}
|
||||
}
|
||||
|
||||
return joinedComponents;
|
||||
return <>{joinedComponents}</>;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
import { useNoteContext, useNoteProperty, useStaticTooltip, useTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
|
||||
import { useNoteContext, useNoteProperty, useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
|
||||
import "./style.css";
|
||||
import { VNode } from "preact";
|
||||
import BasicPropertiesTab from "./BasicPropertiesTab";
|
||||
@@ -252,16 +252,7 @@ export default function Ribbon() {
|
||||
|
||||
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
|
||||
const iconRef = useRef<HTMLDivElement>(null);
|
||||
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
||||
useStaticTooltip(iconRef, {
|
||||
title: keyboardShortcut?.length ? `${title} (${keyboardShortcut?.join(",")})` : title
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (toggleCommand) {
|
||||
keyboard_actions.getAction(toggleCommand).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
||||
}
|
||||
}, [toggleCommand]);
|
||||
useStaticTooltipWithKeyboardShortcut(iconRef, title, toggleCommand);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`<div class="scroll-padding-widget"></div>`;
|
||||
|
||||
export default class ScrollPaddingWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $scrollingContainer!: JQuery<HTMLElement>;
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && ["text", "code"].includes(this.note?.type ?? "");
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
this.$widget.on("click", () => this.triggerCommand("scrollToEnd", { ntxId: this.ntxId }));
|
||||
}
|
||||
|
||||
initialRenderCompleteEvent() {
|
||||
this.$scrollingContainer = this.$widget.closest(".scrolling-container");
|
||||
|
||||
new ResizeObserver(() => this.refreshHeight()).observe(this.$scrollingContainer[0]);
|
||||
|
||||
this.refreshHeight();
|
||||
}
|
||||
|
||||
refreshHeight() {
|
||||
const containerHeight = this.$scrollingContainer.height();
|
||||
|
||||
this.$widget.css("height", Math.round((containerHeight ?? 0) / 2));
|
||||
}
|
||||
}
|
||||
42
apps/client/src/widgets/scroll_padding.tsx
Normal file
42
apps/client/src/widgets/scroll_padding.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
|
||||
export default function ScrollPadding() {
|
||||
const { note, parentComponent, ntxId } = useNoteContext();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [height, setHeight] = useState<number>(10);
|
||||
const isEnabled = ["text", "code"].includes(note?.type ?? "");
|
||||
|
||||
const refreshHeight = () => {
|
||||
if (!ref.current) return;
|
||||
const container = ref.current.closest(".scrolling-container") as HTMLElement | null;
|
||||
if (!container) return;
|
||||
setHeight(Math.round(container.offsetHeight / 2));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEnabled) return;
|
||||
|
||||
const container = ref.current?.closest(".scrolling-container") as HTMLElement | null;
|
||||
if (!container) return;
|
||||
|
||||
// Observe container resize
|
||||
const observer = new ResizeObserver(() => refreshHeight());
|
||||
observer.observe(container);
|
||||
|
||||
// Initial resize
|
||||
refreshHeight();
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [note]); // re-run when note changes
|
||||
|
||||
return (isEnabled ?
|
||||
<div
|
||||
ref={ref}
|
||||
className="scroll-padding-widget"
|
||||
style={{ height }}
|
||||
onClick={() => parentComponent.triggerCommand("scrollToEnd", { ntxId })}
|
||||
/>
|
||||
: <div></div>
|
||||
)
|
||||
}
|
||||
16
apps/client/src/widgets/search_result.css
Normal file
16
apps/client/src/widgets/search_result.css
Normal file
@@ -0,0 +1,16 @@
|
||||
.search-result-widget {
|
||||
flex-grow: 100000;
|
||||
flex-shrink: 100000;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
.search-result-widget .note-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.search-no-results, .search-not-executed-yet {
|
||||
margin: 20px;
|
||||
padding: 20px !important;
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="search-result-widget">
|
||||
<style>
|
||||
.search-result-widget {
|
||||
flex-grow: 100000;
|
||||
flex-shrink: 100000;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.search-result-widget .note-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.search-no-results, .search-not-executed-yet {
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="search-no-results alert alert-info">
|
||||
${t("search_result.no_notes_found")}
|
||||
</div>
|
||||
|
||||
<div class="search-not-executed-yet alert alert-info">
|
||||
${t("search_result.search_not_executed")}
|
||||
</div>
|
||||
|
||||
<div class="search-result-widget-content">
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class SearchResultWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
private $noResults!: JQuery<HTMLElement>;
|
||||
private $notExecutedYet!: JQuery<HTMLElement>;
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && this.note?.type === "search";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
this.$content = this.$widget.find(".search-result-widget-content");
|
||||
this.$noResults = this.$widget.find(".search-no-results");
|
||||
this.$notExecutedYet = this.$widget.find(".search-not-executed-yet");
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
const noResults = note.getChildNoteIds().length === 0 && !!note.searchResultsLoaded;
|
||||
|
||||
this.$content.empty();
|
||||
this.$noResults.toggle(noResults);
|
||||
this.$notExecutedYet.toggle(!note.searchResultsLoaded);
|
||||
|
||||
if (noResults || !note.searchResultsLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: this.$content,
|
||||
parentNote: note,
|
||||
showNotePath: true
|
||||
});
|
||||
await noteListRenderer.renderList();
|
||||
}
|
||||
|
||||
searchRefreshedEvent({ ntxId }: EventData<"searchRefreshed">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
notesReloadedEvent({ noteIds }: EventData<"notesReloaded">) {
|
||||
if (this.noteId && noteIds.includes(this.noteId)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
65
apps/client/src/widgets/search_result.tsx
Normal file
65
apps/client/src/widgets/search_result.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { t } from "../services/i18n";
|
||||
import Alert from "./react/Alert";
|
||||
import { useNoteContext, useNoteProperty, useTriliumEvent } from "./react/hooks";
|
||||
import "./search_result.css";
|
||||
import NoteListRenderer from "../services/note_list_renderer";
|
||||
|
||||
enum SearchResultState {
|
||||
NO_RESULTS,
|
||||
NOT_EXECUTED,
|
||||
GOT_RESULTS
|
||||
}
|
||||
|
||||
export default function SearchResult() {
|
||||
const { note, ntxId } = useNoteContext();
|
||||
const [ state, setState ] = useState<SearchResultState>();
|
||||
const searchContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
function refresh() {
|
||||
searchContainerRef.current?.replaceChildren();
|
||||
|
||||
if (note?.type !== "search") {
|
||||
setState(undefined);
|
||||
} else if (!note?.searchResultsLoaded) {
|
||||
setState(SearchResultState.NOT_EXECUTED);
|
||||
} else if (note.getChildNoteIds().length === 0) {
|
||||
setState(SearchResultState.NO_RESULTS);
|
||||
} else if (searchContainerRef.current) {
|
||||
setState(SearchResultState.GOT_RESULTS);
|
||||
|
||||
const noteListRenderer = new NoteListRenderer({
|
||||
$parent: $(searchContainerRef.current),
|
||||
parentNote: note,
|
||||
showNotePath: true
|
||||
});
|
||||
noteListRenderer.renderList();
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => refresh(), [ note ]);
|
||||
useTriliumEvent("searchRefreshed", ({ ntxId: eventNtxId }) => {
|
||||
if (eventNtxId === ntxId) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
useTriliumEvent("notesReloaded", ({ noteIds }) => {
|
||||
if (note?.noteId && noteIds.includes(note.noteId)) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="search-result-widget">
|
||||
{state === SearchResultState.NOT_EXECUTED && (
|
||||
<Alert type="info" className="search-not-executed-yet">{t("search_result.search_not_executed")}</Alert>
|
||||
)}
|
||||
|
||||
{state === SearchResultState.NO_RESULTS && (
|
||||
<Alert type="info" className="search-no-results">{t("search_result.no_notes_found")}</Alert>
|
||||
)}
|
||||
|
||||
<div ref={searchContainerRef} className="search-result-widget-content" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
apps/client/src/widgets/sql_result.css
Normal file
7
apps/client/src/widgets/sql_result.css
Normal file
@@ -0,0 +1,7 @@
|
||||
.sql-result-widget {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sql-console-result-container td {
|
||||
white-space: preserve;
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="sql-result-widget">
|
||||
<style>
|
||||
.sql-result-widget {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.sql-console-result-container td {
|
||||
white-space: preserve;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="sql-query-no-rows alert alert-info" style="display: none;">
|
||||
${t("sql_result.no_rows")}
|
||||
</div>
|
||||
|
||||
<div class="sql-console-result-container"></div>
|
||||
</div>`;
|
||||
|
||||
export default class SqlResultWidget extends NoteContextAwareWidget {
|
||||
|
||||
private $resultContainer!: JQuery<HTMLElement>;
|
||||
private $noRowsAlert!: JQuery<HTMLElement>;
|
||||
|
||||
isEnabled() {
|
||||
return this.note && this.note.mime === "text/x-sqlite;schema=trilium" && super.isEnabled();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
|
||||
this.$resultContainer = this.$widget.find(".sql-console-result-container");
|
||||
this.$noRowsAlert = this.$widget.find(".sql-query-no-rows");
|
||||
}
|
||||
|
||||
async sqlQueryResultsEvent({ ntxId, results }: EventData<"sqlQueryResults">) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$noRowsAlert.toggle(results.length === 1 && results[0].length === 0);
|
||||
this.$resultContainer.toggle(results.length > 1 || results[0].length > 0);
|
||||
|
||||
this.$resultContainer.empty();
|
||||
|
||||
for (const rows of results) {
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
// inserts, updates
|
||||
this.$resultContainer
|
||||
.empty()
|
||||
.show()
|
||||
.append($("<pre>").text(JSON.stringify(rows, null, "\t")));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!rows.length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const $table = $('<table class="table table-striped">');
|
||||
this.$resultContainer.append($table);
|
||||
|
||||
const result = rows[0];
|
||||
const $row = $("<tr>");
|
||||
|
||||
for (const key in result) {
|
||||
$row.append($("<th>").text(key));
|
||||
}
|
||||
|
||||
$table.append($row);
|
||||
|
||||
for (const result of rows) {
|
||||
const $row = $("<tr>");
|
||||
|
||||
for (const key in result) {
|
||||
$row.append($("<td>").text(result[key]));
|
||||
}
|
||||
|
||||
$table.append($row);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
62
apps/client/src/widgets/sql_result.tsx
Normal file
62
apps/client/src/widgets/sql_result.tsx
Normal file
@@ -0,0 +1,62 @@
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||
import "./sql_result.css";
|
||||
import { useState } from "preact/hooks";
|
||||
import Alert from "./react/Alert";
|
||||
import { t } from "../services/i18n";
|
||||
|
||||
export default function SqlResults() {
|
||||
const { note, ntxId } = useNoteContext();
|
||||
const [ results, setResults ] = useState<SqlExecuteResults>();
|
||||
|
||||
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
setResults(results);
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="sql-result-widget">
|
||||
{note?.mime === "text/x-sqlite;schema=trilium" && (
|
||||
results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? (
|
||||
<Alert type="info">
|
||||
{t("sql_result.no_rows")}
|
||||
</Alert>
|
||||
) : (
|
||||
<div class="sql-console-result-container">
|
||||
{results?.map(rows => {
|
||||
// inserts, updates
|
||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||
return <pre>{JSON.stringify(rows, null, "\t")}</pre>
|
||||
}
|
||||
|
||||
// selects
|
||||
return <SqlResultTable rows={rows} />
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SqlResultTable({ rows }: { rows: object[] }) {
|
||||
if (!rows.length) return;
|
||||
|
||||
return (
|
||||
<table className="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
{Object.keys(rows[0]).map(key => <th>{key}</th>)}
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{rows.map(row => (
|
||||
<tr>
|
||||
{Object.values(row).map(cell => <td>{cell}</td>)}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)
|
||||
}
|
||||
43
apps/client/src/widgets/sql_table_schemas.css
Normal file
43
apps/client/src/widgets/sql_table_schemas.css
Normal file
@@ -0,0 +1,43 @@
|
||||
.sql-table-schemas-widget {
|
||||
padding: 12px;
|
||||
padding-right: 10%;
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
.sql-table-schemas > .dropdown {
|
||||
display: inline-block !important;
|
||||
}
|
||||
|
||||
.sql-table-schemas button.btn {
|
||||
padding: 0.25rem 0.4rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0.5;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sql-console-result-container {
|
||||
width: 100%;
|
||||
font-size: smaller;
|
||||
margin-top: 10px;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-schema td {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.dropdown .table-schema {
|
||||
font-family: var(--monospace-font-family);
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
/* Data type */
|
||||
.dropdown .table-schema td:nth-child(2) {
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="sql-table-schemas-widget">
|
||||
<style>
|
||||
.sql-table-schemas-widget {
|
||||
padding: 12px;
|
||||
padding-right: 10%;
|
||||
}
|
||||
|
||||
.sql-table-schemas button {
|
||||
padding: 0.25rem 0.4rem;
|
||||
font-size: 0.875rem;
|
||||
line-height: 0.5;
|
||||
border: 1px solid var(--button-border-color);
|
||||
border-radius: var(--button-border-radius);
|
||||
background: var(--button-background-color);
|
||||
color: var(--button-text-color);
|
||||
}
|
||||
|
||||
.sql-console-result-container {
|
||||
width: 100%;
|
||||
font-size: smaller;
|
||||
margin-top: 10px;
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.table-schema td {
|
||||
padding: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
${t("sql_table_schemas.tables")}:
|
||||
<span class="sql-table-schemas"></span>
|
||||
</div>`;
|
||||
|
||||
interface SchemaResponse {
|
||||
name: string;
|
||||
columns: {
|
||||
name: string;
|
||||
type: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export default class SqlTableSchemasWidget extends NoteContextAwareWidget {
|
||||
|
||||
private tableSchemasShown?: boolean;
|
||||
private $sqlConsoleTableSchemas!: JQuery<HTMLElement>;
|
||||
|
||||
isEnabled() {
|
||||
return this.note && this.note.mime === "text/x-sqlite;schema=trilium" && super.isEnabled();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
this.$sqlConsoleTableSchemas = this.$widget.find(".sql-table-schemas");
|
||||
}
|
||||
|
||||
async refreshWithNote(note: FNote) {
|
||||
if (this.tableSchemasShown) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.tableSchemasShown = true;
|
||||
|
||||
const tableSchema = await server.get<SchemaResponse[]>("sql/schema");
|
||||
|
||||
for (const table of tableSchema) {
|
||||
const $tableLink = $('<button class="btn">').text(table.name);
|
||||
|
||||
const $table = $('<table class="table-schema">');
|
||||
|
||||
for (const column of table.columns) {
|
||||
$table.append($("<tr>").append($("<td>").text(column.name)).append($("<td>").text(column.type)));
|
||||
}
|
||||
|
||||
this.$sqlConsoleTableSchemas.append($tableLink).append(" ");
|
||||
|
||||
$tableLink.tooltip({
|
||||
html: true,
|
||||
placement: "bottom",
|
||||
title: $table[0].outerHTML,
|
||||
sanitize: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
46
apps/client/src/widgets/sql_table_schemas.tsx
Normal file
46
apps/client/src/widgets/sql_table_schemas.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { t } from "../services/i18n";
|
||||
import { useNoteContext } from "./react/hooks";
|
||||
import "./sql_table_schemas.css";
|
||||
import { SchemaResponse } from "@triliumnext/commons";
|
||||
import server from "../services/server";
|
||||
import Dropdown from "./react/Dropdown";
|
||||
|
||||
export default function SqlTableSchemas() {
|
||||
const { note } = useNoteContext();
|
||||
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
|
||||
|
||||
useEffect(() => {
|
||||
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
|
||||
}, []);
|
||||
|
||||
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas;
|
||||
return (
|
||||
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||
{isEnabled && (
|
||||
<>
|
||||
{t("sql_table_schemas.tables")}{": "}
|
||||
|
||||
<span class="sql-table-schemas">
|
||||
{schemas.map(({ name, columns }) => (
|
||||
<>
|
||||
<Dropdown text={name} noSelectButtonStyle hideToggleArrow
|
||||
>
|
||||
<table className="table-schema">
|
||||
{columns.map(column => (
|
||||
<tr>
|
||||
<td>{column.name}</td>
|
||||
<td>{column.type}</td>
|
||||
</tr>
|
||||
))}
|
||||
</table>
|
||||
</Dropdown>
|
||||
{" "}
|
||||
</>
|
||||
))}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
30
apps/client/src/widgets/title_bar_buttons.css
Normal file
30
apps/client/src/widgets/title_bar_buttons.css
Normal file
@@ -0,0 +1,30 @@
|
||||
.component.title-bar-buttons {
|
||||
flex-shrink: 0;
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.title-bar-buttons div button {
|
||||
border: none !important;
|
||||
border-radius: 0;
|
||||
background: none !important;
|
||||
font-size: 150%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-bar-buttons div:hover button {
|
||||
background-color: var(--accented-background-color) !important;
|
||||
}
|
||||
|
||||
.title-bar-buttons div {
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import options from "../services/options.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="title-bar-buttons">
|
||||
<style>
|
||||
.title-bar-buttons {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title-bar-buttons div button {
|
||||
border: none !important;
|
||||
border-radius: 0;
|
||||
background: none !important;
|
||||
font-size: 150%;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.title-bar-buttons div:hover button {
|
||||
background-color: var(--accented-background-color) !important;
|
||||
}
|
||||
|
||||
.title-bar-buttons div {
|
||||
display: inline-block;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- divs act as a hitbox for the buttons, making them clickable on corners -->
|
||||
<div class="minimize-btn"><button class="btn bx bx-minus"></button></div>
|
||||
<div class="maximize-btn"><button class="btn bx bx-checkbox"></button></div>
|
||||
<div class="close-btn"><button class="btn bx bx-x"></button></div>
|
||||
</div>`;
|
||||
|
||||
export default class TitleBarButtonsWidget extends BasicWidget {
|
||||
doRender() {
|
||||
if (!utils.isElectron() || options.is("nativeTitleBarVisible")) {
|
||||
return (this.$widget = $("<div>"));
|
||||
}
|
||||
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
const $minimizeBtn = this.$widget.find(".minimize-btn");
|
||||
const $maximizeBtn = this.$widget.find(".maximize-btn");
|
||||
const $closeBtn = this.$widget.find(".close-btn");
|
||||
|
||||
$minimizeBtn.on("click", () => {
|
||||
$minimizeBtn.trigger("blur");
|
||||
const remote = utils.dynamicRequire("@electron/remote");
|
||||
remote.BrowserWindow.getFocusedWindow().minimize();
|
||||
});
|
||||
|
||||
$maximizeBtn.on("click", () => {
|
||||
$maximizeBtn.trigger("blur");
|
||||
const remote = utils.dynamicRequire("@electron/remote");
|
||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
|
||||
if (focusedWindow.isMaximized()) {
|
||||
focusedWindow.unmaximize();
|
||||
} else {
|
||||
focusedWindow.maximize();
|
||||
}
|
||||
});
|
||||
|
||||
$closeBtn.on("click", () => {
|
||||
$closeBtn.trigger("blur");
|
||||
const remote = utils.dynamicRequire("@electron/remote");
|
||||
remote.BrowserWindow.getFocusedWindow().close();
|
||||
});
|
||||
}
|
||||
}
|
||||
63
apps/client/src/widgets/title_bar_buttons.tsx
Normal file
63
apps/client/src/widgets/title_bar_buttons.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { dynamicRequire, isElectron } from "../services/utils";
|
||||
import { useTriliumOption } from "./react/hooks";
|
||||
import "./title_bar_buttons.css";
|
||||
import type { BrowserWindow } from "electron";
|
||||
|
||||
interface TitleBarButtonProps {
|
||||
className: string;
|
||||
icon: string;
|
||||
onClick: (context: {
|
||||
window: BrowserWindow
|
||||
}) => void;
|
||||
}
|
||||
|
||||
export default function TitleBarButtons() {
|
||||
const [ nativeTitleBarVisible ] = useTriliumOption("nativeTitleBarVisible");
|
||||
const isEnabled = (isElectron() && nativeTitleBarVisible);
|
||||
|
||||
return (
|
||||
<div className="title-bar-buttons">
|
||||
{isEnabled && (
|
||||
<>
|
||||
<TitleBarButton
|
||||
className="minimize-btn"
|
||||
icon="bx bx-minus"
|
||||
onClick={({ window }) => window.minimize()}
|
||||
/>
|
||||
|
||||
<TitleBarButton
|
||||
className="maximize-btn"
|
||||
icon="bx bx-checkbox"
|
||||
onClick={({ window }) => {
|
||||
if (window.isMaximized()) {
|
||||
window.unmaximize();
|
||||
} else {
|
||||
window.maximize();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<TitleBarButton
|
||||
className="close-btn"
|
||||
icon="bx bx-x"
|
||||
onClick={({ window }) => window.close()}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TitleBarButton({ className, icon, onClick }: TitleBarButtonProps) {
|
||||
// divs act as a hitbox for the buttons, making them clickable on corners
|
||||
return (
|
||||
<div className={className}>
|
||||
<button className={`btn ${icon}`} onClick={() => {
|
||||
const remote = dynamicRequire("@electron/remote");
|
||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||
if (!focusedWindow) return;
|
||||
onClick({ window: focusedWindow });
|
||||
}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
.classic-toolbar-outer-container {
|
||||
contain: none !important;
|
||||
}
|
||||
|
||||
.classic-toolbar-outer-container.visible {
|
||||
height: 38px;
|
||||
background-color: var(--main-background-color);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#root-widget.virtual-keyboard-opened .classic-toolbar-outer-container.ios {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 38px;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget::-webkit-scrollbar {
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget.dropdown-active {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget .ck.ck-toolbar {
|
||||
--ck-color-toolbar-background: transparent;
|
||||
--ck-color-button-default-background: transparent;
|
||||
--ck-color-button-default-disabled-background: transparent;
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget .ck.ck-button.ck-disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
@@ -1,130 +0,0 @@
|
||||
import { isIOS } from "../../../services/utils.js";
|
||||
import NoteContextAwareWidget from "../../note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`\
|
||||
<div class="classic-toolbar-outer-container">
|
||||
<div class="classic-toolbar-widget"></div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.classic-toolbar-outer-container.visible {
|
||||
height: 38px;
|
||||
background-color: var(--main-background-color);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
#root-widget.virtual-keyboard-opened .classic-toolbar-outer-container.ios {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 38px;
|
||||
overflow: scroll;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget::-webkit-scrollbar {
|
||||
height: 0 !important;
|
||||
width: 0 !important;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget.dropdown-active {
|
||||
height: 50vh;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget .ck.ck-toolbar {
|
||||
--ck-color-toolbar-background: transparent;
|
||||
--ck-color-button-default-background: transparent;
|
||||
--ck-color-button-default-disabled-background: transparent;
|
||||
position: absolute;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget .ck.ck-button.ck-disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
/**
|
||||
* Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism:
|
||||
*
|
||||
* - On iOS, because it does not respect the viewport meta value `interactive-widget=resizes-content`, we need to listen to window resizes and scroll and reposition the toolbar using absolute positioning.
|
||||
* - On Android, the viewport change makes the keyboard resize the content area, all we have to do is to hide the tab bar and global menu (handled in the global style).
|
||||
*/
|
||||
export default class MobileEditorToolbar extends NoteContextAwareWidget {
|
||||
|
||||
private observer: MutationObserver;
|
||||
private $innerWrapper!: JQuery<HTMLElement>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.observer = new MutationObserver((e) => this.#onDropdownStateChanged(e));
|
||||
}
|
||||
|
||||
get name() {
|
||||
return "classicEditor";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$innerWrapper = this.$widget.find(".classic-toolbar-widget");
|
||||
this.contentSized();
|
||||
|
||||
// Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable.
|
||||
this.observer.disconnect();
|
||||
this.observer.observe(this.$widget[0], {
|
||||
attributeFilter: ["aria-expanded"],
|
||||
subtree: true
|
||||
});
|
||||
|
||||
if (isIOS()) {
|
||||
this.#handlePositioningOniOS();
|
||||
}
|
||||
}
|
||||
|
||||
#handlePositioningOniOS() {
|
||||
const adjustPosition = () => {
|
||||
let bottom = window.innerHeight - (window.visualViewport?.height || 0);
|
||||
this.$widget.css("bottom", `${bottom}px`);
|
||||
}
|
||||
|
||||
this.$widget.addClass("ios");
|
||||
window.visualViewport?.addEventListener("resize", adjustPosition);
|
||||
window.addEventListener("scroll", adjustPosition);
|
||||
}
|
||||
|
||||
#onDropdownStateChanged(e: MutationRecord[]) {
|
||||
const dropdownActive = e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e);
|
||||
this.$innerWrapper.toggleClass("dropdown-active", dropdownActive);
|
||||
}
|
||||
|
||||
async #shouldDisplay() {
|
||||
if (!this.note || this.note.type !== "text") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await this.noteContext?.isReadOnly()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async refreshWithNote() {
|
||||
this.toggleExt(await this.#shouldDisplay());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useNoteContext } from "../../react/hooks";
|
||||
import "./mobile_editor_toolbar.css";
|
||||
import { isIOS } from "../../../services/utils";
|
||||
|
||||
/**
|
||||
* Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism:
|
||||
*
|
||||
* - On iOS, because it does not respect the viewport meta value `interactive-widget=resizes-content`, we need to listen to window resizes and scroll and reposition the toolbar using absolute positioning.
|
||||
* - On Android, the viewport change makes the keyboard resize the content area, all we have to do is to hide the tab bar and global menu (handled in the global style).
|
||||
*/
|
||||
export default function MobileEditorToolbar() {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const { note, noteContext } = useNoteContext();
|
||||
const [ shouldDisplay, setShouldDisplay ] = useState(false);
|
||||
const [ dropdownActive, setDropdownActive ] = useState(false);
|
||||
|
||||
usePositioningOniOS(wrapperRef);
|
||||
|
||||
useEffect(() => {
|
||||
noteContext?.isReadOnly().then(isReadOnly => {
|
||||
setShouldDisplay(note?.type === "text" && !isReadOnly);
|
||||
});
|
||||
}, [ note ]);
|
||||
|
||||
// Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable.
|
||||
useEffect(() => {
|
||||
if (!wrapperRef.current) return;
|
||||
|
||||
const observer = new MutationObserver(e => {
|
||||
setDropdownActive(e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e));
|
||||
});
|
||||
|
||||
observer.observe(wrapperRef.current, {
|
||||
attributeFilter: ["aria-expanded"],
|
||||
subtree: true
|
||||
});
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={`classic-toolbar-outer-container ${!shouldDisplay ? "hidden-ext" : "visible"} ${isIOS() ? "ios" : ""}`}>
|
||||
<div ref={wrapperRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`}></div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function usePositioningOniOS(wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||
const adjustPosition = useCallback(() => {
|
||||
if (!wrapperRef.current) return;
|
||||
let bottom = window.innerHeight - (window.visualViewport?.height || 0);
|
||||
wrapperRef.current.style.bottom = `${bottom}px`;
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isIOS()) return;
|
||||
|
||||
window.visualViewport?.addEventListener("resize", adjustPosition);
|
||||
window.addEventListener("scroll", adjustPosition);
|
||||
|
||||
return () => {
|
||||
window.visualViewport?.removeEventListener("resize", adjustPosition);
|
||||
window.removeEventListener("scroll", adjustPosition);
|
||||
};
|
||||
}, []);
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import ValidationError from "../../errors/validation_error.js";
|
||||
import blobService from "../../services/blob.js";
|
||||
import type { Request } from "express";
|
||||
import type BBranch from "../../becca/entities/bbranch.js";
|
||||
import type { AttributeRow, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
|
||||
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
@@ -123,7 +123,7 @@ function createNote(req: Request) {
|
||||
return {
|
||||
note,
|
||||
branch
|
||||
};
|
||||
} satisfies CreateChildrenResponse;
|
||||
}
|
||||
|
||||
function updateNoteData(req: Request) {
|
||||
|
||||
@@ -18,7 +18,7 @@ function getSchema() {
|
||||
for (const tableName of tableNames) {
|
||||
tables.push({
|
||||
name: tableName,
|
||||
columns: sql.getRows(`PRAGMA table_info(${tableName})`)
|
||||
columns: sql.getRows<{ name: string; type: string; }>(`PRAGMA table_info(${tableName})`)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AttachmentRow, AttributeRow, NoteType } from "./rows.js";
|
||||
import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from "./rows.js";
|
||||
|
||||
type Response = {
|
||||
success: true,
|
||||
@@ -220,3 +220,25 @@ export type BacklinksResponse = ({
|
||||
noteId: string;
|
||||
excerpts: string[]
|
||||
})[];
|
||||
|
||||
|
||||
export type SqlExecuteResults = (object[] | object)[];
|
||||
|
||||
export interface SqlExecuteResponse {
|
||||
success: boolean;
|
||||
error?: string;
|
||||
results: SqlExecuteResults;
|
||||
}
|
||||
|
||||
export interface CreateChildrenResponse {
|
||||
note: NoteRow;
|
||||
branch: BranchRow;
|
||||
}
|
||||
|
||||
export interface SchemaResponse {
|
||||
name: string;
|
||||
columns: {
|
||||
name: string;
|
||||
type: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user