mirror of
https://github.com/zadam/trilium.git
synced 2026-03-06 17:17:30 +01:00
Mobile tabs v1 (#8568)
This commit is contained in:
commit
f8ab206744
@ -13,6 +13,7 @@
|
|||||||
<body id="trilium-app">
|
<body id="trilium-app">
|
||||||
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
<noscript>Trilium requires JavaScript to be enabled.</noscript>
|
||||||
|
|
||||||
|
<div id="context-menu-cover"></div>
|
||||||
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
<div class="dropdown-menu dropdown-menu-sm" id="context-menu-container" style="display: none"></div>
|
||||||
|
|
||||||
<!-- Required to match the PWA's top bar color with the theme -->
|
<!-- Required to match the PWA's top bar color with the theme -->
|
||||||
|
|||||||
@ -179,7 +179,6 @@ export default class MobileLayout {
|
|||||||
new FlexContainer("column")
|
new FlexContainer("column")
|
||||||
.contentSized()
|
.contentSized()
|
||||||
.id("mobile-bottom-bar")
|
.id("mobile-bottom-bar")
|
||||||
.child(new TabRowWidget().css("height", "40px"))
|
|
||||||
.child(new FlexContainer("row")
|
.child(new FlexContainer("row")
|
||||||
.class("horizontal")
|
.class("horizontal")
|
||||||
.css("height", "53px")
|
.css("height", "53px")
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import { KeyboardActionNames } from "@triliumnext/commons";
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||||
|
import { h, JSX, render } from "preact";
|
||||||
|
|
||||||
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
import keyboardActionService, { getActionSync } from "../services/keyboard_actions.js";
|
||||||
import note_tooltip from "../services/note_tooltip.js";
|
import note_tooltip from "../services/note_tooltip.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import { h, JSX, render } from "preact";
|
|
||||||
|
|
||||||
export interface ContextMenuOptions<T> {
|
export interface ContextMenuOptions<T> {
|
||||||
x: number;
|
x: number;
|
||||||
@ -62,17 +63,17 @@ export type ContextMenuEvent = PointerEvent | MouseEvent | JQuery.ContextMenuEve
|
|||||||
|
|
||||||
class ContextMenu {
|
class ContextMenu {
|
||||||
private $widget: JQuery<HTMLElement>;
|
private $widget: JQuery<HTMLElement>;
|
||||||
private $cover: JQuery<HTMLElement>;
|
private $cover?: JQuery<HTMLElement>;
|
||||||
private options?: ContextMenuOptions<any>;
|
private options?: ContextMenuOptions<any>;
|
||||||
private isMobile: boolean;
|
private isMobile: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.$widget = $("#context-menu-container");
|
this.$widget = $("#context-menu-container");
|
||||||
this.$cover = $("#context-menu-cover");
|
|
||||||
this.$widget.addClass("dropend");
|
this.$widget.addClass("dropend");
|
||||||
this.isMobile = utils.isMobile();
|
this.isMobile = utils.isMobile();
|
||||||
|
|
||||||
if (this.isMobile) {
|
if (this.isMobile) {
|
||||||
|
this.$cover = $("#context-menu-cover");
|
||||||
this.$cover.on("click", () => this.hide());
|
this.$cover.on("click", () => this.hide());
|
||||||
} else {
|
} else {
|
||||||
$(document).on("click", (e) => this.hide());
|
$(document).on("click", (e) => this.hide());
|
||||||
@ -91,7 +92,7 @@ class ContextMenu {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
|
this.$widget.toggleClass("mobile-bottom-menu", !this.options.forcePositionOnMobile);
|
||||||
this.$cover.addClass("show");
|
this.$cover?.addClass("show");
|
||||||
$("body").addClass("context-menu-shown");
|
$("body").addClass("context-menu-shown");
|
||||||
|
|
||||||
this.$widget.empty();
|
this.$widget.empty();
|
||||||
@ -140,16 +141,14 @@ class ContextMenu {
|
|||||||
} else {
|
} else {
|
||||||
left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET;
|
left = this.options.x - contextMenuWidth + CONTEXT_MENU_OFFSET;
|
||||||
}
|
}
|
||||||
|
} else if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
|
||||||
|
// Overflow: right
|
||||||
|
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
|
||||||
|
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
|
||||||
|
// Overflow: left
|
||||||
|
left = CONTEXT_MENU_PADDING;
|
||||||
} else {
|
} else {
|
||||||
if (contextMenuWidth && this.options.x + contextMenuWidth - CONTEXT_MENU_OFFSET > clientWidth - CONTEXT_MENU_PADDING) {
|
left = this.options.x - CONTEXT_MENU_OFFSET;
|
||||||
// Overflow: right
|
|
||||||
left = clientWidth - contextMenuWidth - CONTEXT_MENU_PADDING;
|
|
||||||
} else if (this.options.x - CONTEXT_MENU_OFFSET < CONTEXT_MENU_PADDING) {
|
|
||||||
// Overflow: left
|
|
||||||
left = CONTEXT_MENU_PADDING;
|
|
||||||
} else {
|
|
||||||
left = this.options.x - CONTEXT_MENU_OFFSET;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$widget
|
this.$widget
|
||||||
@ -261,7 +260,7 @@ class ContextMenu {
|
|||||||
.append(item.title);
|
.append(item.title);
|
||||||
|
|
||||||
if ("badges" in item && item.badges) {
|
if ("badges" in item && item.badges) {
|
||||||
for (let badge of item.badges) {
|
for (const badge of item.badges) {
|
||||||
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
const badgeElement = $(`<span class="badge">`).text(badge.title);
|
||||||
|
|
||||||
if (badge.className) {
|
if (badge.className) {
|
||||||
@ -352,7 +351,7 @@ class ContextMenu {
|
|||||||
async hide() {
|
async hide() {
|
||||||
this.options?.onHide?.();
|
this.options?.onHide?.();
|
||||||
this.$widget.removeClass("show");
|
this.$widget.removeClass("show");
|
||||||
this.$cover.removeClass("show");
|
this.$cover?.removeClass("show");
|
||||||
$("body").removeClass("context-menu-shown");
|
$("body").removeClass("context-menu-shown");
|
||||||
this.$widget.hide();
|
this.$widget.hide();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -49,7 +49,7 @@ function createClassForColor(colorString: string | null) {
|
|||||||
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
|
return clsx("use-note-color", className, colorsWithHue.has(className) && "with-hue");
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseColor(color: string) {
|
export function parseColor(color: string) {
|
||||||
try {
|
try {
|
||||||
return Color(color.toLowerCase());
|
return Color(color.toLowerCase());
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
@ -77,7 +77,7 @@ function adjustColorLightness(color: ColorInstance, lightThemeMaxLightness: numb
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the hue of the specified color, or undefined if the color is grayscale. */
|
/** Returns the hue of the specified color, or undefined if the color is grayscale. */
|
||||||
function getHue(color: ColorInstance) {
|
export function getHue(color: ColorInstance) {
|
||||||
const hslColor = color.hsl();
|
const hslColor = color.hsl();
|
||||||
if (hslColor.saturationl() > 0) {
|
if (hslColor.saturationl() > 0) {
|
||||||
return hslColor.hue();
|
return hslColor.hue();
|
||||||
|
|||||||
@ -224,10 +224,6 @@ body.mobile .modal .modal-dialog {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .modal .modal-content {
|
|
||||||
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component {
|
.component {
|
||||||
contain: size;
|
contain: size;
|
||||||
}
|
}
|
||||||
@ -1255,7 +1251,7 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
|||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 1000;
|
z-index: 2500;
|
||||||
background: rgba(0, 0, 0, 0.1);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1614,6 +1610,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
|
|
||||||
body.mobile .modal-content {
|
body.mobile .modal-content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.mobile .modal-footer {
|
body.mobile .modal-footer {
|
||||||
@ -1669,6 +1666,15 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
|
|||||||
#detail-container {
|
#detail-container {
|
||||||
background: var(--main-background-color);
|
background: var(--main-background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
margin: var(--bs-modal-margin);
|
||||||
|
max-width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 991px) {
|
@media (max-width: 991px) {
|
||||||
|
|||||||
@ -2271,5 +2271,10 @@
|
|||||||
},
|
},
|
||||||
"platform_indicator": {
|
"platform_indicator": {
|
||||||
"available_on": "Available on {{platform}}"
|
"available_on": "Available on {{platform}}"
|
||||||
|
},
|
||||||
|
"mobile_tab_switcher": {
|
||||||
|
"title_one": "{{count}} tab",
|
||||||
|
"title_other": "{{count}} tabs",
|
||||||
|
"more_options": "More options"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -155,7 +155,7 @@ function NoteAttributes({ note }: { note: FNote }) {
|
|||||||
return <span className="note-list-attributes" ref={ref} />;
|
return <span className="note-list-attributes" ref={ref} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
|
export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes }: {
|
||||||
note: FNote;
|
note: FNote;
|
||||||
trim?: boolean;
|
trim?: boolean;
|
||||||
noChildrenList?: boolean;
|
noChildrenList?: boolean;
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { useCallback, useLayoutEffect, useState } from "preact/hooks";
|
|||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import froca from "../../services/froca";
|
import froca from "../../services/froca";
|
||||||
import { isDesktop, isMobile } from "../../services/utils";
|
import { isDesktop, isMobile } from "../../services/utils";
|
||||||
|
import TabSwitcher from "../mobile_widgets/TabSwitcher";
|
||||||
import { useTriliumEvent } from "../react/hooks";
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
import { onWheelHorizontalScroll } from "../widget_utils";
|
import { onWheelHorizontalScroll } from "../widget_utils";
|
||||||
import BookmarkButtons from "./BookmarkButtons";
|
import BookmarkButtons from "./BookmarkButtons";
|
||||||
@ -97,6 +98,8 @@ function initBuiltinWidget(note: FNote, isHorizontalLayout: boolean) {
|
|||||||
return <QuickSearchLauncherWidget />;
|
return <QuickSearchLauncherWidget />;
|
||||||
case "aiChatLauncher":
|
case "aiChatLauncher":
|
||||||
return <AiChatButton launcherNote={note} />;
|
return <AiChatButton launcherNote={note} />;
|
||||||
|
case "mobileTabSwitcher":
|
||||||
|
return <TabSwitcher />;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import { createContext } from "preact";
|
import { createContext } from "preact";
|
||||||
import { useContext } from "preact/hooks";
|
import { useContext } from "preact/hooks";
|
||||||
|
|
||||||
@ -18,12 +19,12 @@ export interface LauncherNoteProps {
|
|||||||
launcherNote: FNote;
|
launcherNote: FNote;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LaunchBarActionButton(props: Omit<ActionButtonProps, "className" | "noIconActionClass" | "titlePosition">) {
|
export function LaunchBarActionButton({ className, ...props }: Omit<ActionButtonProps, "noIconActionClass" | "titlePosition">) {
|
||||||
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
const { isHorizontalLayout } = useContext(LaunchBarContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionButton
|
<ActionButton
|
||||||
className="button-widget launcher-button"
|
className={clsx("button-widget launcher-button", className)}
|
||||||
noIconActionClass
|
noIconActionClass
|
||||||
titlePosition={isHorizontalLayout ? "bottom" : "right"}
|
titlePosition={isHorizontalLayout ? "bottom" : "right"}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Tooltip } from "bootstrap";
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ComponentChild } from "preact";
|
import { ComponentChild } from "preact";
|
||||||
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useLayoutEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
|
import type React from "react";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
|
|||||||
133
apps/client/src/widgets/mobile_widgets/TabSwitcher.css
Normal file
133
apps/client/src/widgets/mobile_widgets/TabSwitcher.css
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
#launcher-container .mobile-tab-switcher {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: attr(data-tab-count);
|
||||||
|
font-family: var(--main-font-family);
|
||||||
|
font-size: 10px;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.tab-bar-modal {
|
||||||
|
.modal-dialog {
|
||||||
|
min-height: 85vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 1em;
|
||||||
|
|
||||||
|
@media (min-width: 850px) {
|
||||||
|
grid-template-columns: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-card {
|
||||||
|
background: var(--card-background-color);
|
||||||
|
border-radius: 1em;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
height: 200px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&.with-hue {
|
||||||
|
background-color: hsl(var(--bg-hue), 8.8%, 11.2%);
|
||||||
|
border-color: hsl(var(--bg-hue), 9.4%, 25.1%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
outline: 4px solid var(--more-accented-background-color);
|
||||||
|
background: var(--card-background-hover-color);
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding: 0.4em 0.5em;
|
||||||
|
border-bottom: 1px solid rgba(150, 150, 150, 0.1);
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--custom-color, inherit);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&:not(:first-of-type) {
|
||||||
|
border-top: 1px solid rgba(150, 150, 150, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
>.tn-icon {
|
||||||
|
margin-inline-end: 0.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
font-size: 0.9em;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-action {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-preview {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-size: 0.5em;
|
||||||
|
user-select: none;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.type-text {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.type-book,
|
||||||
|
&.type-contentWidget,
|
||||||
|
&.type-search,
|
||||||
|
&.type-empty {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.25em;
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-placeholder {
|
||||||
|
font-size: 500%;
|
||||||
|
}
|
||||||
|
|
||||||
|
p { margin-bottom: 0.2em;}
|
||||||
|
h2 { font-size: 1.20em; }
|
||||||
|
h3 { font-size: 1.15em; }
|
||||||
|
h4 { font-size: 1.10em; }
|
||||||
|
h5 { font-size: 1.05em}
|
||||||
|
h6 { font-size: 1em; }
|
||||||
|
}
|
||||||
|
|
||||||
|
&.with-split {
|
||||||
|
.preview-placeholder {
|
||||||
|
font-size: 250%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
.tn-link {
|
||||||
|
color: var(--main-text-color);
|
||||||
|
width: 40%;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
240
apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx
Normal file
240
apps/client/src/widgets/mobile_widgets/TabSwitcher.tsx
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
import "./TabSwitcher.css";
|
||||||
|
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { createPortal, Fragment } from "preact/compat";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
|
import appContext, { CommandNames } from "../../components/app_context";
|
||||||
|
import NoteContext from "../../components/note_context";
|
||||||
|
import FNote from "../../entities/fnote";
|
||||||
|
import contextMenu from "../../menus/context_menu";
|
||||||
|
import { getHue, parseColor } from "../../services/css_class_manager";
|
||||||
|
import froca from "../../services/froca";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import { NoteContent } from "../collections/legacy/ListOrGridView";
|
||||||
|
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
|
||||||
|
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
|
||||||
|
import ActionButton from "../react/ActionButton";
|
||||||
|
import { useActiveNoteContext, useNoteIcon, useTriliumEvents } from "../react/hooks";
|
||||||
|
import Icon from "../react/Icon";
|
||||||
|
import LinkButton from "../react/LinkButton";
|
||||||
|
import Modal from "../react/Modal";
|
||||||
|
|
||||||
|
export default function TabSwitcher() {
|
||||||
|
const [ shown, setShown ] = useState(false);
|
||||||
|
const mainNoteContexts = useMainNoteContexts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LaunchBarActionButton
|
||||||
|
className="mobile-tab-switcher"
|
||||||
|
icon="bx bx-rectangle"
|
||||||
|
text="Tabs"
|
||||||
|
onClick={() => setShown(true)}
|
||||||
|
data-tab-count={mainNoteContexts.length > 99 ? "∞" : mainNoteContexts.length}
|
||||||
|
/>
|
||||||
|
{createPortal(<TabBarModal mainNoteContexts={mainNoteContexts} shown={shown} setShown={setShown} />, document.body)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabBarModal({ mainNoteContexts, shown, setShown }: {
|
||||||
|
mainNoteContexts: NoteContext[];
|
||||||
|
shown: boolean;
|
||||||
|
setShown: (newValue: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const [ fullyShown, setFullyShown ] = useState(false);
|
||||||
|
const selectTab = useCallback((noteContextToActivate: NoteContext) => {
|
||||||
|
appContext.tabManager.activateNoteContext(noteContextToActivate.ntxId);
|
||||||
|
setShown(false);
|
||||||
|
}, [ setShown ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="tab-bar-modal"
|
||||||
|
size="xl"
|
||||||
|
title={t("mobile_tab_switcher.title", { count: mainNoteContexts.length})}
|
||||||
|
show={shown}
|
||||||
|
onShown={() => setFullyShown(true)}
|
||||||
|
customTitleBarButtons={[
|
||||||
|
{
|
||||||
|
iconClassName: "bx bx-dots-vertical-rounded",
|
||||||
|
title: t("mobile_tab_switcher.more_options"),
|
||||||
|
onClick(e) {
|
||||||
|
contextMenu.show<CommandNames>({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
items: [
|
||||||
|
{ title: t("tab_row.new_tab"), command: "openNewTab", uiIcon: "bx bx-plus" },
|
||||||
|
{ title: t("tab_row.reopen_last_tab"), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0 },
|
||||||
|
{ kind: "separator" },
|
||||||
|
{ title: t("tab_row.close_all_tabs"), command: "closeAllTabs", uiIcon: "bx bx-trash destructive-action-icon" },
|
||||||
|
],
|
||||||
|
selectMenuItemHandler: ({ command }) => {
|
||||||
|
if (command) {
|
||||||
|
appContext.triggerCommand(command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
footer={<>
|
||||||
|
<LinkButton
|
||||||
|
text={t("tab_row.new_tab")}
|
||||||
|
onClick={() => {
|
||||||
|
appContext.triggerCommand("openNewTab");
|
||||||
|
setShown(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>}
|
||||||
|
scrollable
|
||||||
|
onHidden={() => {
|
||||||
|
setShown(false);
|
||||||
|
setFullyShown(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TabBarModelContent mainNoteContexts={mainNoteContexts} selectTab={selectTab} shown={fullyShown} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabBarModelContent({ mainNoteContexts, selectTab, shown }: {
|
||||||
|
mainNoteContexts: NoteContext[];
|
||||||
|
shown: boolean;
|
||||||
|
selectTab: (noteContextToActivate: NoteContext) => void;
|
||||||
|
}) {
|
||||||
|
const activeNoteContext = useActiveNoteContext();
|
||||||
|
const tabRefs = useRef<Record<string, HTMLDivElement | null>>({});
|
||||||
|
|
||||||
|
// Scroll to active tab.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shown || !activeNoteContext?.ntxId) return;
|
||||||
|
const correspondingEl = tabRefs.current[activeNoteContext.ntxId];
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
correspondingEl?.scrollIntoView();
|
||||||
|
});
|
||||||
|
}, [ activeNoteContext, shown ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tabs">
|
||||||
|
{mainNoteContexts.map((noteContext) => (
|
||||||
|
<Tab
|
||||||
|
key={noteContext.ntxId}
|
||||||
|
noteContext={noteContext}
|
||||||
|
activeNtxId={activeNoteContext.ntxId}
|
||||||
|
selectTab={selectTab}
|
||||||
|
containerRef={el => (tabRefs.current[noteContext.ntxId ?? ""] = el)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
|
||||||
|
containerRef: (el: HTMLDivElement | null) => void;
|
||||||
|
noteContext: NoteContext;
|
||||||
|
selectTab: (noteContextToActivate: NoteContext) => void;
|
||||||
|
activeNtxId: string | null | undefined;
|
||||||
|
}) {
|
||||||
|
const { note } = noteContext;
|
||||||
|
const iconClass = useNoteIcon(note);
|
||||||
|
const colorClass = note?.getColorClass() || '';
|
||||||
|
const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext);
|
||||||
|
const subContexts = noteContext.getSubContexts();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
class={clsx("tab-card", {
|
||||||
|
active: noteContext.ntxId === activeNtxId,
|
||||||
|
"with-hue": workspaceTabBackgroundColorHue !== undefined,
|
||||||
|
"with-split": subContexts.length > 1
|
||||||
|
})}
|
||||||
|
onClick={() => selectTab(noteContext)}
|
||||||
|
style={{
|
||||||
|
"--bg-hue": workspaceTabBackgroundColorHue
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{subContexts.map(subContext => (
|
||||||
|
<Fragment key={subContext.ntxId}>
|
||||||
|
<header className={colorClass}>
|
||||||
|
{subContext.note && <Icon icon={iconClass} />}
|
||||||
|
<span className="title">{subContext.note?.title ?? t("tab_row.new_tab")}</span>
|
||||||
|
{subContext.isMainContext() && <ActionButton
|
||||||
|
icon="bx bx-x"
|
||||||
|
text={t("tab_row.close_tab")}
|
||||||
|
onClick={(e) => {
|
||||||
|
// We are closing a tab, so we need to prevent propagation for click (activate tab).
|
||||||
|
e.stopPropagation();
|
||||||
|
appContext.tabManager.removeNoteContext(subContext.ntxId);
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
|
</header>
|
||||||
|
<div className={clsx("tab-preview", `type-${subContext.note?.type ?? "empty"}`)}>
|
||||||
|
<TabPreviewContent note={subContext.note} />
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabPreviewContent({ note }: {
|
||||||
|
note: FNote | null
|
||||||
|
}) {
|
||||||
|
if (!note) {
|
||||||
|
return <PreviewPlaceholder icon="bx bx-plus" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note.type === "book") {
|
||||||
|
return <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NoteContent
|
||||||
|
note={note}
|
||||||
|
highlightedTokens={undefined}
|
||||||
|
trim
|
||||||
|
includeArchivedNotes={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function PreviewPlaceholder({ icon}: {
|
||||||
|
icon: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="preview-placeholder">
|
||||||
|
<Icon icon={icon} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWorkspaceTabBackgroundColorHue(noteContext: NoteContext) {
|
||||||
|
if (!noteContext.hoistedNoteId) return;
|
||||||
|
const hoistedNote = froca.getNoteFromCache(noteContext.hoistedNoteId);
|
||||||
|
if (!hoistedNote) return;
|
||||||
|
|
||||||
|
const workspaceTabBackgroundColor = hoistedNote.getWorkspaceTabBackgroundColor();
|
||||||
|
if (!workspaceTabBackgroundColor) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsedColor = parseColor(workspaceTabBackgroundColor);
|
||||||
|
if (!parsedColor) return;
|
||||||
|
return getHue(parsedColor);
|
||||||
|
} catch (e) {
|
||||||
|
// Colors are non-critical, simply ignore.
|
||||||
|
console.warn(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useMainNoteContexts() {
|
||||||
|
const [ noteContexts, setNoteContexts ] = useState(appContext.tabManager.getMainNoteContexts());
|
||||||
|
|
||||||
|
useTriliumEvents([ "newNoteContextCreated", "noteContextRemoved" ] , () => {
|
||||||
|
setNoteContexts(appContext.tabManager.getMainNoteContexts());
|
||||||
|
});
|
||||||
|
|
||||||
|
return noteContexts;
|
||||||
|
}
|
||||||
@ -6,10 +6,7 @@ import { useContext, useRef } from "preact/hooks";
|
|||||||
import { Fragment } from "preact/jsx-runtime";
|
import { Fragment } from "preact/jsx-runtime";
|
||||||
|
|
||||||
import FNote from "../../entities/fnote";
|
import FNote from "../../entities/fnote";
|
||||||
import { getHelpUrlForNote } from "../../services/in_app_help";
|
|
||||||
import { openInAppHelpFromUrl } from "../../services/utils";
|
|
||||||
import { ViewTypeOptions } from "../collections/interface";
|
import { ViewTypeOptions } from "../collections/interface";
|
||||||
import ActionButton from "../react/ActionButton";
|
|
||||||
import Dropdown from "../react/Dropdown";
|
import Dropdown from "../react/Dropdown";
|
||||||
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem, FormListToggleableItem } from "../react/FormList";
|
||||||
import FormTextBox from "../react/FormTextBox";
|
import FormTextBox from "../react/FormTextBox";
|
||||||
@ -19,7 +16,7 @@ import { ParentComponent } from "../react/react_utils";
|
|||||||
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
import { bookPropertiesConfig, BookProperty, ButtonProperty, CheckBoxProperty, ComboBoxItem, ComboBoxProperty, NumberProperty, SplitButtonProperty } from "../ribbon/collection-properties-config";
|
||||||
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
|
import { useViewType, VIEW_TYPE_MAPPINGS } from "../ribbon/CollectionPropertiesTab";
|
||||||
|
|
||||||
const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
export const ICON_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||||
grid: "bx bxs-grid",
|
grid: "bx bxs-grid",
|
||||||
list: "bx bx-list-ul",
|
list: "bx bx-list-ul",
|
||||||
calendar: "bx bx-calendar",
|
calendar: "bx bx-calendar",
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import clsx from "clsx";
|
|||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { CSSProperties, RefObject } from "preact";
|
import { CSSProperties, RefObject } from "preact";
|
||||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
|
import type React from "react";
|
||||||
import { CellComponentProps, Grid } from "react-window";
|
import { CellComponentProps, Grid } from "react-window";
|
||||||
|
|
||||||
import FNote from "../entities/fnote";
|
import FNote from "../entities/fnote";
|
||||||
@ -153,10 +154,10 @@ function NoteIconList({ note, dropdownRef }: {
|
|||||||
|
|
||||||
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
|
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
|
||||||
filteredIcons: IconWithName[];
|
filteredIcons: IconWithName[];
|
||||||
}>): React.JSX.Element {
|
}>) {
|
||||||
const iconIndex = rowIndex * 12 + columnIndex;
|
const iconIndex = rowIndex * 12 + columnIndex;
|
||||||
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
|
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
|
||||||
if (!iconData) return <></>;
|
if (!iconData) return <></> as React.ReactElement;
|
||||||
|
|
||||||
const { id, terms, iconPack } = iconData;
|
const { id, terms, iconPack } = iconData;
|
||||||
return (
|
return (
|
||||||
@ -166,7 +167,7 @@ function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellCompo
|
|||||||
title={t("note_icon.icon_tooltip", { name: terms?.[0] ?? id, iconPack })}
|
title={t("note_icon.icon_tooltip", { name: terms?.[0] ?? id, iconPack })}
|
||||||
style={style as CSSProperties}
|
style={style as CSSProperties}
|
||||||
/>
|
/>
|
||||||
);
|
) as React.ReactElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
|
function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { useEffect, useRef, useState } from "preact/hooks";
|
|
||||||
import { CommandNames } from "../../components/app_context";
|
|
||||||
import { useStaticTooltip } from "./hooks";
|
|
||||||
import keyboard_actions from "../../services/keyboard_actions";
|
|
||||||
import { HTMLAttributes } from "preact";
|
import { HTMLAttributes } from "preact";
|
||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
|
||||||
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu"> {
|
import { CommandNames } from "../../components/app_context";
|
||||||
|
import keyboard_actions from "../../services/keyboard_actions";
|
||||||
|
import { useStaticTooltip } from "./hooks";
|
||||||
|
|
||||||
|
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu" | "style"> {
|
||||||
text: string;
|
text: string;
|
||||||
titlePosition?: "top" | "right" | "bottom" | "left";
|
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||||
icon: string;
|
icon: string;
|
||||||
|
|||||||
@ -1,17 +1,17 @@
|
|||||||
import clsx from "clsx";
|
|
||||||
import { useEffect, useRef, useMemo } from "preact/hooks";
|
|
||||||
import { t } from "../../services/i18n";
|
|
||||||
import { ComponentChildren } from "preact";
|
|
||||||
import type { CSSProperties, RefObject } from "preact/compat";
|
|
||||||
import { openDialog } from "../../services/dialog";
|
|
||||||
import { Modal as BootstrapModal } from "bootstrap";
|
import { Modal as BootstrapModal } from "bootstrap";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { ComponentChildren, CSSProperties, RefObject } from "preact";
|
||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
|
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||||
|
|
||||||
|
import { openDialog } from "../../services/dialog";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
import { useSyncedRef } from "./hooks";
|
import { useSyncedRef } from "./hooks";
|
||||||
|
|
||||||
interface CustomTitleBarButton {
|
interface CustomTitleBarButton {
|
||||||
title: string;
|
title: string;
|
||||||
iconClassName: string;
|
iconClassName: string;
|
||||||
onClick: () => void;
|
onClick: (e: MouseEvent) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModalProps {
|
export interface ModalProps {
|
||||||
@ -80,7 +80,7 @@ export interface ModalProps {
|
|||||||
noFocus?: boolean;
|
noFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
||||||
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||||
const modalInstanceRef = useRef<BootstrapModal>();
|
const modalInstanceRef = useRef<BootstrapModal>();
|
||||||
const elementToFocus = useRef<Element | null>();
|
const elementToFocus = useRef<Element | null>();
|
||||||
@ -116,7 +116,7 @@ export default function Modal({ children, className, size, title, customTitleBar
|
|||||||
focus: !noFocus
|
focus: !noFocus
|
||||||
}).then(($widget) => {
|
}).then(($widget) => {
|
||||||
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
|
modalInstanceRef.current = BootstrapModal.getOrCreateInstance($widget[0]);
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
modalInstanceRef.current?.hide();
|
modalInstanceRef.current?.hide();
|
||||||
}
|
}
|
||||||
@ -159,13 +159,12 @@ export default function Modal({ children, className, size, title, customTitleBar
|
|||||||
|
|
||||||
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
|
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
|
||||||
<button type="button"
|
<button type="button"
|
||||||
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
|
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
|
||||||
title={titleBarButton.title}
|
title={titleBarButton.title}
|
||||||
onClick={titleBarButton.onClick}>
|
onClick={titleBarButton.onClick} />
|
||||||
</button>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")} />
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,22 @@
|
|||||||
|
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
|
||||||
|
import { useMemo } from "preact/hooks";
|
||||||
|
import type React from "react";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
import { t } from "../../../services/i18n";
|
import { t } from "../../../services/i18n";
|
||||||
|
import search from "../../../services/search";
|
||||||
import server from "../../../services/server";
|
import server from "../../../services/server";
|
||||||
import toast from "../../../services/toast";
|
import toast from "../../../services/toast";
|
||||||
|
import { isElectron } from "../../../services/utils";
|
||||||
import Button from "../../react/Button";
|
import Button from "../../react/Button";
|
||||||
import FormText from "../../react/FormText";
|
|
||||||
import OptionsSection from "./components/OptionsSection";
|
|
||||||
import TimeSelector from "./components/TimeSelector";
|
|
||||||
import { useMemo } from "preact/hooks";
|
|
||||||
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
|
||||||
import { SANITIZER_DEFAULT_ALLOWED_TAGS } from "@triliumnext/commons";
|
|
||||||
import FormCheckbox from "../../react/FormCheckbox";
|
import FormCheckbox from "../../react/FormCheckbox";
|
||||||
import FormGroup from "../../react/FormGroup";
|
import FormGroup from "../../react/FormGroup";
|
||||||
import search from "../../../services/search";
|
|
||||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
|
||||||
import FormSelect from "../../react/FormSelect";
|
import FormSelect from "../../react/FormSelect";
|
||||||
import { isElectron } from "../../../services/utils";
|
import FormText from "../../react/FormText";
|
||||||
|
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||||
|
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||||
|
import OptionsSection from "./components/OptionsSection";
|
||||||
|
import TimeSelector from "./components/TimeSelector";
|
||||||
|
|
||||||
export default function OtherSettings() {
|
export default function OtherSettings() {
|
||||||
return (
|
return (
|
||||||
@ -31,7 +33,7 @@ export default function OtherSettings() {
|
|||||||
<ShareSettings />
|
<ShareSettings />
|
||||||
<NetworkSettings />
|
<NetworkSettings />
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SearchEngineSettings() {
|
function SearchEngineSettings() {
|
||||||
@ -82,7 +84,7 @@ function SearchEngineSettings() {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TrayOptionsSettings() {
|
function TrayOptionsSettings() {
|
||||||
@ -97,7 +99,7 @@ function TrayOptionsSettings() {
|
|||||||
onChange={trayEnabled => setDisableTray(!trayEnabled)}
|
onChange={trayEnabled => setDisableTray(!trayEnabled)}
|
||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NoteErasureTimeout() {
|
function NoteErasureTimeout() {
|
||||||
@ -105,13 +107,13 @@ function NoteErasureTimeout() {
|
|||||||
<OptionsSection title={t("note_erasure_timeout.note_erasure_timeout_title")}>
|
<OptionsSection title={t("note_erasure_timeout.note_erasure_timeout_title")}>
|
||||||
<FormText>{t("note_erasure_timeout.note_erasure_description")}</FormText>
|
<FormText>{t("note_erasure_timeout.note_erasure_description")}</FormText>
|
||||||
<FormGroup name="erase-entities-after" label={t("note_erasure_timeout.erase_notes_after")}>
|
<FormGroup name="erase-entities-after" label={t("note_erasure_timeout.erase_notes_after")}>
|
||||||
<TimeSelector
|
<TimeSelector
|
||||||
name="erase-entities-after"
|
name="erase-entities-after"
|
||||||
optionValueId="eraseEntitiesAfterTimeInSeconds" optionTimeScaleId="eraseEntitiesAfterTimeScale"
|
optionValueId="eraseEntitiesAfterTimeInSeconds" optionTimeScaleId="eraseEntitiesAfterTimeScale"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<FormText>{t("note_erasure_timeout.manual_erasing_description")}</FormText>
|
<FormText>{t("note_erasure_timeout.manual_erasing_description")}</FormText>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
text={t("note_erasure_timeout.erase_deleted_notes_now")}
|
text={t("note_erasure_timeout.erase_deleted_notes_now")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -121,7 +123,7 @@ function NoteErasureTimeout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function AttachmentErasureTimeout() {
|
function AttachmentErasureTimeout() {
|
||||||
@ -145,7 +147,7 @@ function AttachmentErasureTimeout() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RevisionSnapshotInterval() {
|
function RevisionSnapshotInterval() {
|
||||||
@ -165,7 +167,7 @@ function RevisionSnapshotInterval() {
|
|||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RevisionSnapshotLimit() {
|
function RevisionSnapshotLimit() {
|
||||||
@ -176,7 +178,7 @@ function RevisionSnapshotLimit() {
|
|||||||
<FormText>{t("revisions_snapshot_limit.note_revisions_snapshot_limit_description")}</FormText>
|
<FormText>{t("revisions_snapshot_limit.note_revisions_snapshot_limit_description")}</FormText>
|
||||||
|
|
||||||
<FormGroup name="revision-snapshot-number-limit">
|
<FormGroup name="revision-snapshot-number-limit">
|
||||||
<FormTextBoxWithUnit
|
<FormTextBoxWithUnit
|
||||||
type="number" min={-1}
|
type="number" min={-1}
|
||||||
currentValue={revisionSnapshotNumberLimit}
|
currentValue={revisionSnapshotNumberLimit}
|
||||||
unit={t("revisions_snapshot_limit.snapshot_number_limit_unit")}
|
unit={t("revisions_snapshot_limit.snapshot_number_limit_unit")}
|
||||||
@ -197,7 +199,7 @@ function RevisionSnapshotLimit() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function HtmlImportTags() {
|
function HtmlImportTags() {
|
||||||
@ -236,7 +238,7 @@ function HtmlImportTags() {
|
|||||||
onClick={() => setAllowedHtmlTags(SANITIZER_DEFAULT_ALLOWED_TAGS)}
|
onClick={() => setAllowedHtmlTags(SANITIZER_DEFAULT_ALLOWED_TAGS)}
|
||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ShareSettings() {
|
function ShareSettings() {
|
||||||
@ -246,8 +248,8 @@ function ShareSettings() {
|
|||||||
return (
|
return (
|
||||||
<OptionsSection title={t("share.title")}>
|
<OptionsSection title={t("share.title")}>
|
||||||
<FormGroup name="redirectBareDomain" description={t("share.redirect_bare_domain_description")}>
|
<FormGroup name="redirectBareDomain" description={t("share.redirect_bare_domain_description")}>
|
||||||
<FormCheckbox
|
<FormCheckbox
|
||||||
label={t(t("share.redirect_bare_domain"))}
|
label={t(t("share.redirect_bare_domain"))}
|
||||||
currentValue={redirectBareDomain}
|
currentValue={redirectBareDomain}
|
||||||
onChange={async value => {
|
onChange={async value => {
|
||||||
if (value) {
|
if (value) {
|
||||||
@ -264,17 +266,17 @@ function ShareSettings() {
|
|||||||
}
|
}
|
||||||
setRedirectBareDomain(value);
|
setRedirectBareDomain(value);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
||||||
<FormGroup name="showLoginInShareTheme" description={t("share.show_login_link_description")}>
|
<FormGroup name="showLoginInShareTheme" description={t("share.show_login_link_description")}>
|
||||||
<FormCheckbox
|
<FormCheckbox
|
||||||
label={t("share.show_login_link")}
|
label={t("share.show_login_link")}
|
||||||
currentValue={showLogInShareTheme} onChange={setShowLogInShareTheme}
|
currentValue={showLogInShareTheme} onChange={setShowLogInShareTheme}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NetworkSettings() {
|
function NetworkSettings() {
|
||||||
@ -288,5 +290,5 @@ function NetworkSettings() {
|
|||||||
currentValue={checkForUpdates} onChange={setCheckForUpdates}
|
currentValue={checkForUpdates} onChange={setCheckForUpdates}
|
||||||
/>
|
/>
|
||||||
</OptionsSection>
|
</OptionsSection>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { normalizeMimeTypeForCKEditor, type OptionNames } from "@triliumnext/com
|
|||||||
import { Themes } from "@triliumnext/highlightjs";
|
import { Themes } from "@triliumnext/highlightjs";
|
||||||
import type { CSSProperties } from "preact/compat";
|
import type { CSSProperties } from "preact/compat";
|
||||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||||
|
import type React from "react";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
|
|
||||||
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
|
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
|
||||||
|
|||||||
@ -17,17 +17,17 @@ test("Can drag tabs around", async ({ page, context }) => {
|
|||||||
await app.addNewTab();
|
await app.addNewTab();
|
||||||
await app.addNewTab();
|
await app.addNewTab();
|
||||||
|
|
||||||
let tab = app.getTab(0);
|
let tab = await app.getTab(0);
|
||||||
|
|
||||||
// Drag the first tab at the end
|
// Drag the first tab at the end
|
||||||
await tab.dragTo(app.getTab(2), { targetPosition: { x: 50, y: 0 } });
|
await tab.dragTo(await app.getTab(2), { targetPosition: { x: 50, y: 0 } });
|
||||||
|
|
||||||
tab = app.getTab(2);
|
tab = await app.getTab(2);
|
||||||
await expect(tab).toContainText(NOTE_TITLE);
|
await expect(tab).toContainText(NOTE_TITLE);
|
||||||
|
|
||||||
// Drag the tab to the left
|
// Drag the tab to the left
|
||||||
await tab.dragTo(app.getTab(0), { targetPosition: { x: 50, y: 0 } });
|
await tab.dragTo(await app.getTab(0), { targetPosition: { x: 50, y: 0 } });
|
||||||
await expect(app.getTab(0)).toContainText(NOTE_TITLE);
|
await expect(await app.getTab(0)).toContainText(NOTE_TITLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can drag tab to new window", async ({ page, context }) => {
|
test("Can drag tab to new window", async ({ page, context }) => {
|
||||||
@ -36,7 +36,7 @@ test("Can drag tab to new window", async ({ page, context }) => {
|
|||||||
|
|
||||||
await app.closeAllTabs();
|
await app.closeAllTabs();
|
||||||
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
|
await app.clickNoteOnNoteTreeByTitle(NOTE_TITLE);
|
||||||
const tab = app.getTab(0);
|
const tab = await app.getTab(0);
|
||||||
await expect(tab).toContainText(NOTE_TITLE);
|
await expect(tab).toContainText(NOTE_TITLE);
|
||||||
|
|
||||||
const popupPromise = page.waitForEvent("popup");
|
const popupPromise = page.waitForEvent("popup");
|
||||||
@ -75,14 +75,14 @@ test("Tabs are restored in right order", async ({ page, context }) => {
|
|||||||
await expect(app.getActiveTab()).toContainText("Mermaid");
|
await expect(app.getActiveTab()).toContainText("Mermaid");
|
||||||
|
|
||||||
// Select the mid one.
|
// Select the mid one.
|
||||||
await app.getTab(1).click();
|
await (await app.getTab(1)).click();
|
||||||
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
||||||
|
|
||||||
// Refresh the page and check the order.
|
// Refresh the page and check the order.
|
||||||
await app.goto( { preserveTabs: true });
|
await app.goto( { preserveTabs: true });
|
||||||
await expect(app.getTab(0)).toContainText("Code notes");
|
await expect(await app.getTab(0)).toContainText("Code notes");
|
||||||
await expect(app.getTab(1)).toContainText("Text notes");
|
await expect(await app.getTab(1)).toContainText("Text notes");
|
||||||
await expect(app.getTab(2)).toContainText("Mermaid");
|
await expect(await app.getTab(2)).toContainText("Mermaid");
|
||||||
|
|
||||||
// Check the note tree has the right active node.
|
// Check the note tree has the right active node.
|
||||||
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
await expect(app.noteTreeActiveNote).toContainText("Text notes");
|
||||||
@ -118,7 +118,7 @@ test("Search works when dismissing a tab", async ({ page, context }) => {
|
|||||||
await app.addNewTab();
|
await app.addNewTab();
|
||||||
await app.goToNoteInNewTab("Sample mindmap");
|
await app.goToNoteInNewTab("Sample mindmap");
|
||||||
|
|
||||||
await app.getTab(0).click();
|
await (await app.getTab(0)).click();
|
||||||
await app.openAndClickNoteActionMenu("Search in note");
|
await app.openAndClickNoteActionMenu("Search in note");
|
||||||
await expect(app.findAndReplaceWidget.first()).toBeVisible();
|
await expect(app.findAndReplaceWidget.first()).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,6 +26,7 @@ export default class App {
|
|||||||
readonly currentNoteSplitTitle: Locator;
|
readonly currentNoteSplitTitle: Locator;
|
||||||
readonly currentNoteSplitContent: Locator;
|
readonly currentNoteSplitContent: Locator;
|
||||||
readonly sidebar: Locator;
|
readonly sidebar: Locator;
|
||||||
|
private isMobile: boolean = false;
|
||||||
|
|
||||||
constructor(page: Page, context: BrowserContext) {
|
constructor(page: Page, context: BrowserContext) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
@ -43,6 +44,8 @@ export default class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async goto({ url, isMobile, preserveTabs }: GotoOpts = {}) {
|
async goto({ url, isMobile, preserveTabs }: GotoOpts = {}) {
|
||||||
|
this.isMobile = !!isMobile;
|
||||||
|
|
||||||
await this.context.addCookies([
|
await this.context.addCookies([
|
||||||
{
|
{
|
||||||
url: BASE_URL,
|
url: BASE_URL,
|
||||||
@ -83,7 +86,12 @@ export default class App {
|
|||||||
await this.page.locator(".launcher-button.bx-cog").click();
|
await this.page.locator(".launcher-button.bx-cog").click();
|
||||||
}
|
}
|
||||||
|
|
||||||
getTab(tabIndex: number) {
|
async getTab(tabIndex: number) {
|
||||||
|
if (this.isMobile) {
|
||||||
|
await this.launcherBar.locator(".mobile-tab-switcher").click();
|
||||||
|
return this.page.locator(".modal.tab-bar-modal .tab-card").nth(tabIndex);
|
||||||
|
}
|
||||||
|
|
||||||
return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex);
|
return this.tabBar.locator(".note-tab-wrapper").nth(tabIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +105,8 @@ export default class App {
|
|||||||
async closeAllTabs() {
|
async closeAllTabs() {
|
||||||
await this.triggerCommand("closeAllTabs");
|
await this.triggerCommand("closeAllTabs");
|
||||||
// Page in Playwright is not updated somehow, need to click on the tab to make sure it's rendered
|
// Page in Playwright is not updated somehow, need to click on the tab to make sure it's rendered
|
||||||
await this.getTab(0).click();
|
const tab = await this.getTab(0);
|
||||||
|
await tab.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -356,7 +356,8 @@
|
|||||||
"visible-launchers-title": "Visible Launchers",
|
"visible-launchers-title": "Visible Launchers",
|
||||||
"user-guide": "User Guide",
|
"user-guide": "User Guide",
|
||||||
"localization": "Language & Region",
|
"localization": "Language & Region",
|
||||||
"inbox-title": "Inbox"
|
"inbox-title": "Inbox",
|
||||||
|
"tab-switcher-title": "Tab Switcher"
|
||||||
},
|
},
|
||||||
"notes": {
|
"notes": {
|
||||||
"new-note": "New note",
|
"new-note": "New note",
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export default function buildLaunchBarConfig() {
|
|||||||
id: "_lbBackInHistory",
|
id: "_lbBackInHistory",
|
||||||
...sharedLaunchers.backInHistory
|
...sharedLaunchers.backInHistory
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "_lbForwardInHistory",
|
id: "_lbForwardInHistory",
|
||||||
...sharedLaunchers.forwardInHistory
|
...sharedLaunchers.forwardInHistory
|
||||||
},
|
},
|
||||||
@ -59,12 +59,12 @@ export default function buildLaunchBarConfig() {
|
|||||||
command: "commandPalette",
|
command: "commandPalette",
|
||||||
icon: "bx bx-chevron-right-square"
|
icon: "bx bx-chevron-right-square"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "_lbBackendLog",
|
id: "_lbBackendLog",
|
||||||
title: t("hidden-subtree.backend-log-title"),
|
title: t("hidden-subtree.backend-log-title"),
|
||||||
type: "launcher",
|
type: "launcher",
|
||||||
targetNoteId: "_backendLog",
|
targetNoteId: "_backendLog",
|
||||||
icon: "bx bx-detail"
|
icon: "bx bx-detail"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "_zenMode",
|
id: "_zenMode",
|
||||||
@ -128,7 +128,7 @@ export default function buildLaunchBarConfig() {
|
|||||||
baseSize: "50",
|
baseSize: "50",
|
||||||
growthFactor: "0"
|
growthFactor: "0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "_lbBookmarks",
|
id: "_lbBookmarks",
|
||||||
title: t("hidden-subtree.bookmarks-title"),
|
title: t("hidden-subtree.bookmarks-title"),
|
||||||
type: "launcher",
|
type: "launcher",
|
||||||
@ -139,7 +139,7 @@ export default function buildLaunchBarConfig() {
|
|||||||
id: "_lbToday",
|
id: "_lbToday",
|
||||||
...sharedLaunchers.openToday
|
...sharedLaunchers.openToday
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "_lbSpacer2",
|
id: "_lbSpacer2",
|
||||||
title: t("hidden-subtree.spacer-title"),
|
title: t("hidden-subtree.spacer-title"),
|
||||||
type: "launcher",
|
type: "launcher",
|
||||||
@ -179,7 +179,11 @@ export default function buildLaunchBarConfig() {
|
|||||||
|
|
||||||
const mobileAvailableLaunchers: HiddenSubtreeItem[] = [
|
const mobileAvailableLaunchers: HiddenSubtreeItem[] = [
|
||||||
{ id: "_lbMobileNewNote", ...sharedLaunchers.newNote },
|
{ id: "_lbMobileNewNote", ...sharedLaunchers.newNote },
|
||||||
{ id: "_lbMobileToday", ...sharedLaunchers.openToday }
|
{ id: "_lbMobileToday", ...sharedLaunchers.openToday },
|
||||||
|
{
|
||||||
|
id: "_lbMobileRecentChanges",
|
||||||
|
...sharedLaunchers.recentChanges
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const mobileVisibleLaunchers: HiddenSubtreeItem[] = [
|
const mobileVisibleLaunchers: HiddenSubtreeItem[] = [
|
||||||
@ -203,8 +207,10 @@ export default function buildLaunchBarConfig() {
|
|||||||
...sharedLaunchers.calendar
|
...sharedLaunchers.calendar
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "_lbMobileRecentChanges",
|
id: "_lbMobileTabSwitcher",
|
||||||
...sharedLaunchers.recentChanges
|
title: t("hidden-subtree.tab-switcher-title"),
|
||||||
|
type: "launcher",
|
||||||
|
builtinWidget: "mobileTabSwitcher"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -214,4 +220,4 @@ export default function buildLaunchBarConfig() {
|
|||||||
mobileAvailableLaunchers,
|
mobileAvailableLaunchers,
|
||||||
mobileVisibleLaunchers
|
mobileVisibleLaunchers
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,7 +45,8 @@ export interface HiddenSubtreeItem {
|
|||||||
| "quickSearch"
|
| "quickSearch"
|
||||||
| "aiChatLauncher"
|
| "aiChatLauncher"
|
||||||
| "commandPalette"
|
| "commandPalette"
|
||||||
| "toggleZenMode";
|
| "toggleZenMode"
|
||||||
|
| "mobileTabSwitcher";
|
||||||
command?: keyof typeof Command;
|
command?: keyof typeof Command;
|
||||||
/**
|
/**
|
||||||
* If set to true, then branches will be enforced to be in the correct place.
|
* If set to true, then branches will be enforced to be in the correct place.
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user