Compare commits

..

7 Commits

Author SHA1 Message Date
Jin
19781bb14c Merge branch 'main' into week-note 2026-02-01 14:29:45 +00:00
Jin
7fd2bc30cc test: add test for week number with different first day of week setting 2026-02-01 14:26:11 +00:00
Jin
bd7e47d8f0 refactor: clear calendar code 2026-01-19 21:15:18 +00:00
Jin
457a7a03fb test: add test for week notes utils 2026-01-19 21:01:07 +00:00
Jin
6e486c64f1 fix: move calendar to use common week note utils 2026-01-19 20:59:15 +00:00
Jin
4f3575d765 fix: move date_notes to use common week note utils 2026-01-19 20:56:52 +00:00
Jin
f5f1b27754 fix: add common week note utils 2026-01-19 20:56:12 +00:00
170 changed files with 2009 additions and 6366 deletions

View File

@@ -42,8 +42,5 @@
},
"eslint.rules.customizations": [
{ "rule": "*", "severity": "warn" }
],
"cSpell.words": [
"Trilium"
]
}
}

View File

@@ -11,7 +11,7 @@
"license": "AGPL-3.0-only",
"packageManager": "pnpm@10.28.2",
"devDependencies": {
"@redocly/cli": "2.15.1",
"@redocly/cli": "2.15.0",
"archiver": "7.0.1",
"fs-extra": "11.3.3",
"react": "19.2.4",

View File

@@ -42,8 +42,8 @@
"color": "5.0.3",
"debounce": "3.0.0",
"draggabilly": "3.0.0",
"force-graph": "1.51.1",
"globals": "17.3.0",
"force-graph": "1.51.0",
"globals": "17.2.0",
"i18next": "25.8.0",
"i18next-http-backend": "3.0.2",
"jquery": "4.0.0",
@@ -56,7 +56,7 @@
"mark.js": "8.11.1",
"marked": "17.0.1",
"mermaid": "11.12.2",
"mind-elixir": "5.7.1",
"mind-elixir": "5.6.1",
"normalize.css": "8.0.1",
"panzoom": "9.4.3",
"preact": "10.28.3",
@@ -78,7 +78,7 @@
"@types/reveal.js": "5.2.2",
"@types/tabulator-tables": "6.3.1",
"copy-webpack-plugin": "13.0.1",
"happy-dom": "20.5.0",
"happy-dom": "20.4.0",
"lightningcss": "1.31.1",
"script-loader": "0.7.2",
"vite-plugin-static-copy": "3.2.0"

View File

@@ -46,6 +46,10 @@ if (utils.isElectron()) {
electronContextMenu.setupContextMenu();
}
if (utils.isPWA()) {
initPWATopbarColor();
}
function initOnElectron() {
const electron: typeof Electron = utils.dynamicRequire("electron");
electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
@@ -130,3 +134,20 @@ function initDarkOrLightMode(style: CSSStyleDeclaration) {
const { nativeTheme } = utils.dynamicRequire("@electron/remote") as typeof ElectronRemote;
nativeTheme.themeSource = themeSource;
}
function initPWATopbarColor() {
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}

View File

@@ -1,76 +0,0 @@
#background-color-tracker {
color: var(--main-background-color) !important;
}
span.keyboard-shortcut,
kbd {
display: none;
}
.dropdown-menu {
font-size: larger;
}
.action-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.25em;
padding-inline-start: 0.5em;
padding-inline-end: 0.5em;
color: var(--main-text-color);
}
.quick-search {
margin: 0;
}
.quick-search .dropdown-menu {
max-width: 350px;
}
/* #region Tree */
.tree-wrapper {
max-height: 100%;
margin-top: 0px;
overflow-y: auto;
contain: content;
padding-inline-start: 10px;
}
.fancytree-title {
margin-inline-start: 0.6em !important;
}
.fancytree-node {
padding: 5px;
}
span.fancytree-expander {
width: 24px !important;
margin-inline-end: 5px;
}
.fancytree-loading span.fancytree-expander {
width: 24px;
height: 32px;
}
.fancytree-loading span.fancytree-expander:after {
width: 20px;
height: 20px;
margin-top: 4px;
border-width: 2px;
border-style: solid;
}
.tree-wrapper .collapse-tree-button,
.tree-wrapper .scroll-to-active-note-button,
.tree-wrapper .tree-settings-button {
position: fixed;
margin-inline-end: 16px;
display: none;
}
.tree-wrapper .unhoist-button {
font-size: 200%;
}
/* #endregion */

View File

@@ -1,41 +1,128 @@
import "./mobile_layout.css";
import type AppContext from "../components/app_context.js";
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
import CloseZenModeButton from "../widgets/close_zen_button.js";
import NoteList from "../widgets/collections/NoteList.jsx";
import ContentHeader from "../widgets/containers/content_header.js";
import FlexContainer from "../widgets/containers/flex_container.js";
import RootContainer from "../widgets/containers/root_container.js";
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import FindWidget from "../widgets/find.js";
import FloatingButtons from "../widgets/FloatingButtons.jsx";
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
import LauncherContainer from "../widgets/launch_bar/LauncherContainer.jsx";
import InlineTitle from "../widgets/layout/InlineTitle.jsx";
import NoteBadges from "../widgets/layout/NoteBadges.jsx";
import NoteTitleActions from "../widgets/layout/NoteTitleActions.jsx";
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
import SidebarContainer from "../widgets/mobile_widgets/sidebar_container.js";
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
import NoteIconWidget from "../widgets/note_icon.jsx";
import NoteTitleWidget from "../widgets/note_title.js";
import NoteTreeWidget from "../widgets/note_tree.js";
import NoteWrapperWidget from "../widgets/note_wrapper.js";
import NoteDetail from "../widgets/NoteDetail.jsx";
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
import QuickSearchWidget from "../widgets/quick_search.js";
import { useNoteContext } from "../widgets/react/hooks.jsx";
import ReadOnlyNoteInfoBar from "../widgets/ReadOnlyNoteInfoBar.jsx";
import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx";
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
import SearchDefinitionTab from "../widgets/ribbon/SearchDefinitionTab.jsx";
import SearchResult from "../widgets/search_result.jsx";
import SharedInfoWidget from "../widgets/shared_info.js";
import TabRowWidget from "../widgets/tab_row.js";
import MobileEditorToolbar from "../widgets/type_widgets/text/mobile_editor_toolbar.jsx";
import { applyModals } from "./layout_commons.js";
const MOBILE_CSS = `
<style>
span.keyboard-shortcut,
kbd {
display: none;
}
.dropdown-menu {
font-size: larger;
}
.action-button {
background: none;
border: none;
cursor: pointer;
font-size: 1.25em;
padding-inline-start: 0.5em;
padding-inline-end: 0.5em;
color: var(--main-text-color);
}
.quick-search {
margin: 0;
}
.quick-search .dropdown-menu {
max-width: 350px;
}
</style>`;
const FANCYTREE_CSS = `
<style>
.tree-wrapper {
max-height: 100%;
margin-top: 0px;
overflow-y: auto;
contain: content;
padding-inline-start: 10px;
}
.fancytree-custom-icon {
font-size: 2em;
}
.fancytree-title {
font-size: 1.5em;
margin-inline-start: 0.6em !important;
}
.fancytree-node {
padding: 5px;
}
.fancytree-node .fancytree-expander:before {
font-size: 2em !important;
}
span.fancytree-expander {
width: 24px !important;
margin-inline-end: 5px;
}
.fancytree-loading span.fancytree-expander {
width: 24px;
height: 32px;
}
.fancytree-loading span.fancytree-expander:after {
width: 20px;
height: 20px;
margin-top: 4px;
border-width: 2px;
border-style: solid;
}
.tree-wrapper .collapse-tree-button,
.tree-wrapper .scroll-to-active-note-button,
.tree-wrapper .tree-settings-button {
position: fixed;
margin-inline-end: 16px;
display: none;
}
.tree-wrapper .unhoist-button {
font-size: 200%;
}
</style>`;
export default class MobileLayout {
getRootWidget(appContext: typeof AppContext) {
const rootContainer = new RootContainer(true)
.setParent(appContext)
.class("horizontal-layout")
.cssBlock(MOBILE_CSS)
.child(new FlexContainer("column").id("mobile-sidebar-container"))
.child(
new FlexContainer("row")
@@ -49,7 +136,7 @@ export default class MobileLayout {
.css("padding-inline-start", "0")
.css("padding-inline-end", "0")
.css("contain", "content")
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget()))
.child(new FlexContainer("column").filling().id("mobile-sidebar-wrapper").child(new QuickSearchWidget()).child(new NoteTreeWidget().cssBlock(FANCYTREE_CSS)))
)
.child(
new ScreenContainer("detail", "row")
@@ -60,28 +147,30 @@ export default class MobileLayout {
new NoteWrapperWidget()
.child(
new FlexContainer("row")
.class("title-row note-split-title")
.contentSized()
.css("font-size", "larger")
.css("align-items", "center")
.child(<ToggleSidebarButton />)
.child(<NoteIconWidget />)
.child(<NoteTitleWidget />)
.child(<NoteBadges />)
.child(<MobileDetailMenu />)
)
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
.child(<PromotedAttributes />)
.child(
new ScrollingContainer()
.filling()
.contentSized()
.child(<InlineTitle />)
.child(<NoteTitleActions />)
.child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />)
.child(<SharedInfoWidget />)
)
.child(<NoteDetail />)
.child(<NoteList media="screen" />)
.child(<StandaloneRibbonAdapter component={SearchDefinitionTab} />)
.child(<SearchResult />)
.child(<FilePropertiesWrapper />)
)
.child(<MobileEditorToolbar />)
.child(new FindWidget())
)
)
)

View File

@@ -248,7 +248,7 @@ class ContextMenu {
if ("uiIcon" in item || "checked" in item) {
const icon = (item.checked ? "bx bx-check" : item.uiIcon);
if (icon) {
$icon.addClass([icon, "tn-icon"]);
$icon.addClass(icon);
} else {
$icon.append("&nbsp;");
}

View File

@@ -1,12 +1,12 @@
import type { ContextMenuCommandData,FilteredCommandNames } from "../components/app_context.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import dialogService from "../services/dialog.js";
import froca from "../services/froca.js";
import { t } from "../services/i18n.js";
import server from "../services/server.js";
import treeService from "../services/tree.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import froca from "../services/froca.js";
import contextMenu, { type MenuCommandItem, type MenuItem } from "./context_menu.js";
import dialogService from "../services/dialog.js";
import server from "../services/server.js";
import { t } from "../services/i18n.js";
import type { SelectMenuItemEventListener } from "../components/events.js";
import type NoteTreeWidget from "../widgets/note_tree.js";
import type { FilteredCommandNames, ContextMenuCommandData } from "../components/app_context.js";
type LauncherCommandNames = FilteredCommandNames<ContextMenuCommandData>;
@@ -32,8 +32,8 @@ export default class LauncherContextMenu implements SelectMenuItemEventListener<
const note = this.node.data.noteId ? await froca.getNote(this.node.data.noteId) : null;
const parentNoteId = this.node.getParent().data.noteId;
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers" || note?.noteId === "_lbMobileVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers" || note?.noteId === "_lbMobileAvailableLaunchers";
const isVisibleRoot = note?.noteId === "_lbVisibleLaunchers";
const isAvailableRoot = note?.noteId === "_lbAvailableLaunchers";
const isVisibleItem = parentNoteId === "_lbVisibleLaunchers" || parentNoteId === "_lbMobileVisibleLaunchers";
const isAvailableItem = parentNoteId === "_lbAvailableLaunchers" || parentNoteId === "_lbMobileAvailableLaunchers";
const isItem = isVisibleItem || isAvailableItem;

View File

@@ -1,9 +0,0 @@
.rendered-content.no-preview > div {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
height: 100%;
font-size: 500%;
flex-grow: 1;
}

View File

@@ -1,5 +1,3 @@
import "./content_renderer.css";
import { normalizeMimeTypeForCKEditor } from "@triliumnext/commons";
import WheelZoom from 'vanilla-js-wheel-zoom';
@@ -73,9 +71,18 @@ export async function getRenderedContent(this: {} | { ctx: string }, entity: FNo
$renderedContent.append($("<div>").append("<div>This note is protected and to access it you need to enter password.</div>").append("<br/>").append($button));
} else if (entity instanceof FNote) {
$renderedContent.addClass("no-preview");
$renderedContent
.css("display", "flex")
.css("flex-direction", "column");
$renderedContent.append(
$("<div>").append($("<span>").addClass(entity.getIcon()))
$("<div>")
.css("display", "flex")
.css("justify-content", "space-around")
.css("align-items", "center")
.css("height", "100%")
.css("font-size", "500%")
.css("flex-grow", "1")
.append($("<span>").addClass(entity.getIcon()))
);
if (entity.type === "webView" && entity.hasLabel("webViewSrc")) {
@@ -285,11 +292,10 @@ function getRenderingType(entity: FNote | FAttachment) {
}
const mime = "mime" in entity && entity.mime;
const isIconPack = entity instanceof FNote && entity.hasLabel("iconPack");
if (type === "file" && mime === "application/pdf") {
type = "pdf";
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime) && !isIconPack) {
} else if ((type === "file" || type === "viewConfig") && mime && CODE_MIME_TYPES.has(mime)) {
type = "code";
} else if (type === "file" && mime && mime.startsWith("audio/")) {
type = "audio";

View File

@@ -1,6 +1,5 @@
import { t } from "./i18n";
import options from "./options";
import { isMobile } from "./utils";
export interface ExperimentalFeature {
id: string;
@@ -22,7 +21,7 @@ let enabledFeatures: Set<ExperimentalFeatureId> | null = null;
export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId): boolean {
if (featureId === "new-layout") {
return (isMobile() || options.is("newLayout"));
return options.is("newLayout");
}
return getEnabledFeatures().has(featureId);
@@ -30,7 +29,7 @@ export function isExperimentalFeatureEnabled(featureId: ExperimentalFeatureId):
export function getEnabledExperimentalFeatureIds() {
const values = [ ...getEnabledFeatures().values() ];
if (isMobile() || options.is("newLayout")) {
if (options.is("newLayout")) {
values.push("new-layout");
}
return values;

View File

@@ -28,7 +28,7 @@
--bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px;
--tn-modal-max-height: 90svh;
--tn-modal-max-height: 90vh;
--tree-item-light-theme-max-color-lightness: 50;
--tree-item-dark-theme-min-color-lightness: 75;
@@ -111,7 +111,6 @@ body.mobile #root-widget.virtual-keyboard-opened #mobile-bottom-bar {
}
#mobile-bottom-bar {
border-top: 1px solid var(--main-border-color);
padding-bottom: var(--mobile-bottom-offset);
}
@@ -410,7 +409,6 @@ body.desktop .tabulator-popup-container,
.dropdown-menu.static {
box-shadow: unset;
backdrop-filter: unset !important;
}
.dropend .dropdown-toggle::after {
@@ -456,7 +454,7 @@ body.desktop .tabulator-popup-container,
visibility: hidden;
}
.dropdown-menu:not(#context-menu-container) .dropdown-item,
body.desktop .dropdown-menu:not(#context-menu-container) .dropdown-item,
body.desktop .dropdown-menu .dropdown-toggle,
body #context-menu-container .dropdown-item > span,
body.mobile .dropdown .dropdown-submenu > span {
@@ -464,15 +462,6 @@ body.mobile .dropdown .dropdown-submenu > span {
align-items: center;
}
body.mobile .dropdown .dropdown-submenu {
flex-wrap: wrap;
& > span {
flex-grow: 1;
}
}
.dropdown-item span.keyboard-shortcut,
.dropdown-item *:not(.keyboard-shortcut) > kbd {
flex-grow: 1;
@@ -1541,8 +1530,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
@media (max-width: 991px) {
body.mobile #launcher-pane .dropdown.global-menu > .dropdown-menu.show,
body.mobile #launcher-container .dropdown > .dropdown-menu.show,
body.mobile .dropdown-menu.mobile-bottom-menu.show {
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
--dropdown-bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
position: fixed !important;
bottom: var(--dropdown-bottom) !important;
@@ -1554,16 +1542,6 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
}
body.mobile #launcher-container .dropdown > .dropdown-menu.show {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
body.mobile .dropdown-menu.mobile-bottom-menu.show {
--dropdown-bottom: 0px;
padding-bottom: calc(max(var(--menu-padding-size), env(safe-area-inset-bottom))) !important;
}
#mobile-sidebar-container {
position: fixed;
top: 0;
@@ -1689,15 +1667,47 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
background: var(--main-background-color);
}
body.mobile {
.modal-dialog {
margin: var(--bs-modal-margin);
max-width: 80%;
}
.modal-dialog {
margin: var(--bs-modal-margin);
max-width: 80%;
}
.modal-content {
height: 100%;
}
.modal-content {
height: 100%;
}
}
@media (max-width: 991px) {
body.mobile.force-fixed-tree #mobile-sidebar-wrapper {
padding-top: 0;
position: static;
height: 40vh;
width: 100vw;
transform: none !important;
background-color: var(--left-pane-background-color) !important;
border-bottom: 0.5px solid var(--main-border-color);
}
body.mobile.force-fixed-tree #mobile-sidebar-container {
display: none !important;
}
body.mobile.force-fixed-tree #mobile-sidebar-wrapper .quick-search {
display: none;
}
body.mobile.force-fixed-tree .component > button.bx-sidebar {
visibility: hidden;
padding: 0;
width: 6px;
}
body.mobile.force-fixed-tree #mobile-rest-container {
flex-direction: column !important;
}
body.mobile.force-fixed-tree #detail-container {
flex-grow: 1;
}
}
@@ -2613,14 +2623,14 @@ iframe.print-iframe {
}
}
#root-widget.virtual-keyboard-opened .note-split:not(.active) {
#root-widget.virtual-keyboard-opened .note-split:not(:focus-within) {
max-height: 80px;
opacity: 0.4;
}
}
}
.title-row {
body.desktop .title-row {
height: 50px;
min-height: 50px;
align-items: center;

View File

@@ -134,7 +134,6 @@
--left-pane-collapsed-border-color: #0009;
--left-pane-background-color: #1f1f1f;
--left-pane-text-color: #aaaaaa;
--left-pane-icon-color: #c5c5c5;
--left-pane-item-hover-background: #ffffff0d;
--left-pane-item-selected-background: #ffffff25;
--left-pane-item-selected-color: #dfdfdf;

View File

@@ -127,7 +127,6 @@
--left-pane-collapsed-border-color: #0000000d;
--left-pane-background-color: #f2f2f2;
--left-pane-text-color: #383838;
--left-pane-icon-color: currentColor;
--left-pane-item-hover-background: rgba(0, 0, 0, 0.032);
--left-pane-item-selected-background: white;
--left-pane-item-selected-color: black;

View File

@@ -47,14 +47,9 @@
}
/* The toolbar show / hide button for the current text block */
:root .ck.ck-block-toolbar-button {
--ck-color-block-toolbar-button: var(--muted-text-color);
.ck.ck-block-toolbar-button {
--ck-color-button-on-background: transparent;
--ck-color-button-on-color: var(--ck-editor-toolbar-button-on-color);
translate: -40% 0;
min-width: 0;
padding: 0;
z-index: 1600;
--ck-color-button-on-color: currentColor;
}
:root .ck.ck-toolbar .ck-button:not(.ck-disabled):active,
@@ -522,10 +517,6 @@ button.ck.ck-button:is(.ck-button-action, .ck-button-save, .ck-button-cancel).ck
* EDITOR'S CONTENT
*/
.note-detail-editable-text-editor > .ck-placeholder {
opacity: .5;
}
/*
* Code Blocks
*/

View File

@@ -57,12 +57,12 @@
height: 18px;
}
/*
/*
* SEARCH PAGE
*/
/* Button bar */
.search-definition-widget .search-setting-table .search-actions-container {
.search-definition-widget .search-setting-table tbody:last-child div {
justify-content: flex-end;
gap: 8px;
}
@@ -143,7 +143,7 @@
/*
* OPTIONS PAGES
*/
:root {
--options-card-min-width: 500px;
--options-card-max-width: 900px;
@@ -156,10 +156,6 @@
--preferred-max-content-width: var(--options-card-max-width);
}
.note-split.options .collection-properties {
visibility: hidden;
}
/* Create a gap at the top of the option pages */
.note-detail-content-widget-content.options>*:first-child {
margin-top: var(--options-first-item-top-margin, 1em);
@@ -335,4 +331,4 @@ nav.options-section-tabs + .options-section {
.etapi-options-section div {
height: auto !important;
}
}

View File

@@ -739,12 +739,18 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
transform: translateX(-25%);
}
body.mobile .fancytree-expander::before,
body.mobile .fancytree-title,
body.mobile .fancytree-node > span {
font-size: 1rem !important;
}
@media (max-width: 991px) {
body.mobile #mobile-sidebar-container {
background-color: rgba(0, 0, 0, 0.5);
}
body.mobile #mobile-sidebar-wrapper {
body.mobile:not(.force-fixed-tree) #mobile-sidebar-wrapper {
border-top-right-radius: 12px;
border-bottom-right-radius: 12px;
border-inline-end: 1px solid var(--subtle-border-color);
@@ -763,9 +769,9 @@ body[dir=rtl] #left-pane span.fancytree-node.protected > span.fancytree-custom-i
#left-pane .fancytree-custom-icon {
margin-top: 0; /* Use this to align the icon with the tree view item's caption */
color: var(--custom-color, var(--left-pane-icon-color));
}
#left-pane span.fancytree-active .fancytree-title {
font-weight: normal;
}
@@ -1266,7 +1272,7 @@ body.layout-horizontal #rest-pane > .classic-toolbar-widget {
#center-pane .note-split {
padding-top: 2px;
background-color: var(--note-split-background-color, var(--main-background-color));
transition: border-color 150ms ease-out;
transition: border-color 250ms ease-in;
border: 2px solid transparent;
}
@@ -1316,7 +1322,7 @@ body.mobile .note-title {
margin-inline-start: 0;
}
body.desktop .title-row {
.title-row {
/* Aligns the "Create new split" button with the note menu button (the three dots button) */
padding-inline-end: 3px;
}

View File

@@ -206,7 +206,6 @@ span.fancytree-selected .fancytree-title {
}
span.fancytree-selected .fancytree-custom-icon::before {
font-family: "boxicons";
content: "\eb43";
border: 1px solid var(--main-border-color);
border-radius: 3px;

View File

@@ -29,9 +29,7 @@
"widget-render-error": {
"title": "فشل عرض عنصر واجهة مستخدم React مخصص"
},
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك.",
"open-script-note": "فتح ملاحظة برمجية",
"scripting-error": "خطأ في النص البرمجي المخصص: {{title}}"
"widget-missing-parent": "لا تحتوي الأداة المخصصة على خاصية إلزامية '{{property}}'.\n\nإذا كان من المفترض تشغيل هذا البرنامج النصي بدون عنصر واجهة مستخدم، فاستخدم '#run=frontendStartup' بدلاً من ذلك."
},
"add_link": {
"add_link": "أضافة رابط",
@@ -39,19 +37,14 @@
"search_note": "البحث عن الملاحظة بالاسم",
"link_title": "عنوان الرابط",
"button_add_link": "اضافة رابط",
"help_on_links": "مساعدة حول الارتباطات التشعبية",
"link_title_mirrors": "عنوان الرابط يعكس العنوان الحالي للملاحظة",
"link_title_arbitrary": "يمكن تغيير عنوان الرابط حسب الرغبة"
"help_on_links": "مساعدة حول الارتباطات التشعبية"
},
"branch_prefix": {
"edit_branch_prefix": "تعديل بادئة الفرع",
"prefix": "البادئة: ",
"save": "حفظ",
"help_on_tree_prefix": "مساعدة حول بادئة الشجرة",
"branch_prefix_saved": "تم حفظ بادئة الفرع.",
"edit_branch_prefix_multiple": "تعديل البادئة لـ {{count}} من تفرعات الملاحظات",
"branch_prefix_saved_multiple": "تم حفظ بادئة التفرع لـ {{count}} من التفرعات.",
"affected_branches": "الفروع المتأثرة ({{count}}):"
"branch_prefix_saved": "تم حفظ بادئة الفرع."
},
"bulk_actions": {
"bulk_actions": "اجراءات جماعية",

View File

@@ -745,7 +745,7 @@
"button_title": "导出SVG格式图片"
},
"relation_map_buttons": {
"create_child_note_title": "创建子笔记并添加到图",
"create_child_note_title": "创建新的子笔记并添加到关系图",
"reset_pan_zoom_title": "重置平移和缩放到初始坐标和放大倍率",
"zoom_in_title": "放大",
"zoom_out_title": "缩小"
@@ -759,8 +759,7 @@
"delete_this_note": "删除此笔记",
"error_cannot_get_branch_id": "无法获取 notePath '{{notePath}}' 的 branchId",
"error_unrecognized_command": "无法识别的命令 {{command}}",
"note_revisions": "笔记历史版本",
"backlinks": "反链"
"note_revisions": "笔记历史版本"
},
"note_icon": {
"change_note_icon": "更改笔记图标",
@@ -1783,8 +1782,8 @@
"desktop-application": "桌面应用程序",
"native-title-bar": "原生标题栏",
"native-title-bar-description": "对于 Windows 和 macOS关闭原生标题栏可使应用程序看起来更紧凑。在 Linux 上,保留原生标题栏可以更好地与系统集成。",
"background-effects": "启用背景效果",
"background-effects-description": "为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
"background-effects": "启用背景效果(仅适用于 Windows 11",
"background-effects-description": "Mica 效果为应用窗口添加模糊且时尚的背景,营造出深度感和现代外观。「原生标题栏」必須被禁用。",
"restart-app-button": "重启应用程序以查看更改",
"zoom-factor": "缩放系数"
},
@@ -1803,8 +1802,7 @@
"geo-map": {
"create-child-note-title": "创建一个新的子笔记并将其添加到地图中",
"create-child-note-instruction": "单击地图以在该位置创建新笔记,或按 Escape 以取消。",
"unable-to-load-map": "无法加载地图。",
"create-child-note-text": "添加标记"
"unable-to-load-map": "无法加载地图。"
},
"geo-map-context": {
"open-location": "打开位置",
@@ -2119,7 +2117,7 @@
},
"call_to_action": {
"background_effects_title": "背景效果现已推出稳定版本",
"background_effects_message": "在 Windows 和 macOS 设备上,背景效果现在已稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。",
"background_effects_message": "在 Windows 装置上,背景效果现在已完全稳定。背景效果通过模糊背后的背景,为使用者界面增添一抹色彩。此技术也用于其他应用程序,例如 Windows 资源管理器。",
"background_effects_button": "启用背景效果",
"next_theme_title": "试用新 Trilium 主题",
"next_theme_message": "当前使用旧版主题,要试用新主题吗?",
@@ -2255,12 +2253,5 @@
"pages_alt": "第{{pageNumber}}页",
"pages_loading": "加载中...",
"layers_other": "{{count}} 层"
},
"platform_indicator": {
"available_on": "在 {{platform}} 上可用"
},
"mobile_tab_switcher": {
"title_other": "{{count}} 选项卡",
"more_options": "更多选项"
}
}

View File

@@ -742,7 +742,7 @@
"button_title": "Diagramm als SVG exportieren"
},
"relation_map_buttons": {
"create_child_note_title": "Erstelle eine untergeordnete Notiz und füge sie dieser Karte hinzu",
"create_child_note_title": "Erstelle eine neue untergeordnete Notiz und füge sie dieser Beziehungskarte hinzu",
"reset_pan_zoom_title": "Schwenken und Zoomen auf die ursprünglichen Koordinaten und Vergrößerung zurücksetzen",
"zoom_in_title": "Hineinzoom",
"zoom_out_title": "Herauszoomen"
@@ -757,8 +757,7 @@
"delete_this_note": "Diese Notiz löschen",
"error_cannot_get_branch_id": "BranchId für notePath „{{notePath}}“ kann nicht abgerufen werden",
"error_unrecognized_command": "Unbekannter Befehl {{command}}",
"note_revisions": "Notiz Revisionen",
"backlinks": "Rücklinks"
"note_revisions": "Notiz Revisionen"
},
"note_icon": {
"change_note_icon": "Notiz-Icon ändern",
@@ -1772,8 +1771,7 @@
"geo-map": {
"create-child-note-title": "Neue Unternotiz anlegen und zur Karte hinzufügen",
"create-child-note-instruction": "Auf die Karte klicken, um eine neue Notiz an der Stelle zu erstellen oder Escape drücken um abzubrechen.",
"unable-to-load-map": "Karte konnte nicht geladen werden.",
"create-child-note-text": "Marker hinzufügen"
"unable-to-load-map": "Karte konnte nicht geladen werden."
},
"geo-map-context": {
"open-location": "Ort öffnen",
@@ -2272,10 +2270,5 @@
},
"platform_indicator": {
"available_on": "Verfügbar auf {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} Tab",
"title_other": "{{count}} Tabs",
"more_options": "Weitere Optionen"
}
}

View File

@@ -662,8 +662,7 @@
"show-cheatsheet": "Show Cheatsheet",
"toggle-zen-mode": "Zen Mode",
"new-version-available": "New Update Available",
"download-update": "Get Version {{latestVersion}}",
"search_notes": "Search notes"
"download-update": "Get Version {{latestVersion}}"
},
"zen_mode": {
"button_exit": "Exit Zen Mode"
@@ -746,7 +745,7 @@
"button_title": "Export diagram as SVG"
},
"relation_map_buttons": {
"create_child_note_title": "Create child note and add it to map",
"create_child_note_title": "Create new child note and add it into this relation map",
"reset_pan_zoom_title": "Reset pan & zoom to initial coordinates and magnification",
"zoom_in_title": "Zoom In",
"zoom_out_title": "Zoom Out"
@@ -761,9 +760,7 @@
"delete_this_note": "Delete this note",
"note_revisions": "Note revisions",
"error_cannot_get_branch_id": "Cannot get branchId for notePath '{{notePath}}'",
"error_unrecognized_command": "Unrecognized command {{command}}",
"backlinks": "Backlinks",
"content_language_switcher": "Content language: {{language}}"
"error_unrecognized_command": "Unrecognized command {{command}}"
},
"note_icon": {
"change_note_icon": "Change note icon",
@@ -908,7 +905,6 @@
"debug": "debug",
"debug_description": "Debug will print extra debugging information into the console to aid in debugging complex queries",
"action": "action",
"option": "option",
"search_button": "Search",
"search_execute": "Search & Execute actions",
"save_to_note": "Save to note",
@@ -2280,8 +2276,5 @@
"title_one": "{{count}} tab",
"title_other": "{{count}} tabs",
"more_options": "More options"
},
"bookmark_buttons": {
"bookmarks": "Bookmarks"
}
}

View File

@@ -745,7 +745,7 @@
"button_title": "Exportar diagrama como SVG"
},
"relation_map_buttons": {
"create_child_note_title": "Crear una subnota y agregarla al mapa",
"create_child_note_title": "Crear una nueva subnota y agregarla a este mapa de relaciones",
"reset_pan_zoom_title": "Restablecer la panorámica y el zoom a las coordenadas y ampliación iniciales",
"zoom_in_title": "Acercar",
"zoom_out_title": "Alejar"
@@ -754,15 +754,14 @@
"relation": "relación",
"backlink_one": "{{count}} Vínculo de retroceso",
"backlink_many": "{{count}} Vínculos de retroceso",
"backlink_other": "{{count}} Vínculos de retroceso"
"backlink_other": "{{count}} vínculos de retroceso"
},
"mobile_detail_menu": {
"insert_child_note": "Insertar subnota",
"delete_this_note": "Eliminar esta nota",
"error_cannot_get_branch_id": "No se puede obtener el branchID del notePath '{{notePath}}'",
"error_unrecognized_command": "Comando no reconocido {{command}}",
"note_revisions": "Revisiones de notas",
"backlinks": "Vínculos de retroceso"
"note_revisions": "Revisiones de notas"
},
"note_icon": {
"change_note_icon": "Cambiar icono de nota",
@@ -2286,11 +2285,5 @@
},
"platform_indicator": {
"available_on": "Disponible en {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} pestaña",
"title_many": "{{count}} pestañas",
"title_other": "{{count}} pestañas",
"more_options": "Más opciones"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -407,7 +407,7 @@
"relation_map_buttons": {
"zoom_out_title": "ズームアウト",
"zoom_in_title": "ズームイン",
"create_child_note_title": "子ノートを作成し、マップに追加",
"create_child_note_title": "新しい子ノートを作成し、関連マップに追加",
"reset_pan_zoom_title": "パンとズームを初期座標と倍率にリセット"
},
"tree-context-menu": {
@@ -1648,8 +1648,7 @@
"error_unrecognized_command": "認識されないコマンド {{command}}",
"insert_child_note": "子ノートを挿入",
"error_cannot_get_branch_id": "ノートパス 「{{notePath}} のbranchIdを取得できません",
"note_revisions": "ノートの変更履歴",
"backlinks": "バックリンク"
"note_revisions": "ノートの変更履歴"
},
"inherited_attribute_list": {
"title": "継承属性",
@@ -2009,8 +2008,7 @@
"geo-map": {
"create-child-note-title": "新しい子ノートを作成し、マップに追加する",
"create-child-note-instruction": "地図をクリックしてその場所に新しいートを作成するか、Esc キーを押して閉じます。",
"unable-to-load-map": "マップを読み込めません。",
"create-child-note-text": "マーカーを追加"
"unable-to-load-map": "マップを読み込めません。"
},
"geo-map-context": {
"open-location": "現在位置を表示",
@@ -2258,9 +2256,5 @@
},
"platform_indicator": {
"available_on": "{{platform}} で利用可能"
},
"mobile_tab_switcher": {
"title_other": "{{count}} タブ",
"more_options": "その他のオプション"
}
}

View File

@@ -1761,8 +1761,8 @@
"show-recent-notes": "Afișează notițele recente"
},
"electron_integration": {
"background-effects": "Activează efectele de fundal",
"background-effects-description": "Adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
"background-effects": "Activează efectele de fundal (doar pentru Windows 11)",
"background-effects-description": "Efectul Mica adaugă un fundal estompat și elegant ferestrelor aplicațiilor, creând profunzime și un aspect modern. Opțiunea „Bară de titlu nativă” trebuie să fie dezactivată.",
"desktop-application": "Aplicația desktop",
"native-title-bar": "Bară de titlu nativă",
"native-title-bar-description": "Pentru Windows și macOS, dezactivarea bării de titlu native face aplicația să pară mai compactă. Pe Linux, păstrarea bării integrează mai bine aplicația cu restul sistemului de operare.",
@@ -1781,8 +1781,7 @@
"geo-map": {
"create-child-note-title": "Crează o notiță nouă și adaug-o pe hartă",
"unable-to-load-map": "Nu s-a putut încărca harta.",
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula.",
"create-child-note-text": "Adaugă marcaj"
"create-child-note-instruction": "Click pe hartă pentru a crea o nouă notiță la acea poziție sau apăsați Escape pentru a anula."
},
"duration": {
"days": "zile",
@@ -2128,7 +2127,7 @@
},
"call_to_action": {
"background_effects_title": "Efectele de fundal sunt acum stabile",
"background_effects_message": "Pe dispozitive cu Windows și macOS, efectele de fundal sunt stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei.",
"background_effects_message": "Pe dispozitive cu Windows, efectele de fundal sunt complet stabile. Acestea adaugă un strop de culoare interfeței grafice prin estomparea fundalului din spatele ferestrei. Această tehnică este folosită și în alte aplicații precum Windows Explorer.",
"background_effects_button": "Activează efectele de fundal",
"next_theme_title": "Încercați noua temă Trilium",
"next_theme_message": "Utilizați tema clasică, doriți să încercați noua temă?",
@@ -2282,14 +2281,5 @@
"pages_other": "{{count}} de pagini",
"pages_alt": "Pagina {{pageNumber}}",
"pages_loading": "Încărcare..."
},
"platform_indicator": {
"available_on": "Disponibil pe {{platform}}"
},
"mobile_tab_switcher": {
"title_one": "{{count}} tab",
"title_few": "{{count}} taburi",
"title_other": "{{count}} de taburi",
"more_options": "Mai multe opțiuni"
}
}

View File

@@ -757,8 +757,7 @@
"delete_this_note": "刪除此筆記",
"error_cannot_get_branch_id": "無法獲取 notePath '{{notePath}}' 的 branchId",
"error_unrecognized_command": "無法識別的命令 {{command}}",
"note_revisions": "筆記歷史版本",
"backlinks": "反向連結"
"note_revisions": "筆記歷史版本"
},
"note_icon": {
"change_note_icon": "更改筆記圖標",
@@ -1940,8 +1939,8 @@
"desktop-application": "桌面版應用程式",
"native-title-bar": "原生標題列",
"native-title-bar-description": "對於 Windows 和 macOS關閉原生標題列會讓應用程式看起來更緊湊。在 Linux 上,開啟原生標題列可以與系統的其他部分整合得更好。",
"background-effects": "啟用背景效果",
"background-effects-description": "為程式視窗新增模糊且時尚的背景,營造立體感和現代化外觀。「原生標題列」必須被禁用。",
"background-effects": "啟用背景效果(僅適用於 Windows 11",
"background-effects-description": "Mica 效果為程式視窗新增模糊且時尚的背景,營造出深度感和現代化外觀。「原生標題列」必須被禁用。",
"restart-app-button": "重新啟動應用程式以查看更改",
"zoom-factor": "縮放係數"
},
@@ -1960,8 +1959,7 @@
"geo-map": {
"create-child-note-title": "建立一個新的子筆記並將其加至地圖中",
"create-child-note-instruction": "點擊地圖以在該位置建立新筆記,或按 Escape 以取消。",
"unable-to-load-map": "無法載入地圖。",
"create-child-note-text": "新增地圖標記"
"unable-to-load-map": "無法載入地圖。"
},
"geo-map-context": {
"open-location": "打開位置",
@@ -2124,7 +2122,7 @@
},
"call_to_action": {
"background_effects_title": "背景特效已推出穩定版本",
"background_effects_message": "在 Windows 和macOS裝置上,背景特效現在已穩定。背景特效透過模糊背後的背景,為使用者介面增添一抹色彩。",
"background_effects_message": "在 Windows 裝置上,背景特效現在已完全穩定。背景特效透過模糊背後的背景,為使用者介面增添一抹色彩。此技術也用於其他應用程式,例如 Windows 檔案總管。",
"background_effects_button": "啟用背景特效",
"next_theme_title": "試用新 Trilium 主題",
"next_theme_message": "您正在使用舊版主題,要試用新主題嗎?",
@@ -2269,12 +2267,5 @@
"pages_other": "",
"pages_alt": "第 {{pageNumber}} 頁",
"pages_loading": "正在載入…"
},
"mobile_tab_switcher": {
"more_options": "更多選項",
"title_one": "{{count}} 個分頁"
},
"platform_indicator": {
"available_on": "可於 {{platform}} 使用"
}
}

View File

@@ -1,83 +0,0 @@
.tn-backlinks-widget .backlinks-items {
list-style-type: none;
margin: 0;
padding: 0;
position: static;
width: unset;
> li {
--border-radius: 8px;
max-width: 600px;
padding: 10px 20px;
background: var(--card-background-color);
& + li {
margin-top: 2px;
}
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* Card header */
& > span:first-child {
display: block;
> span {
display: flex;
flex-wrap: wrap;
align-items: center;
/* Note path */
> small {
flex: 100%;
order: -1;
font-size: .65rem;
.note-path {
padding: 0;
}
}
/* Note icon */
> .tn-icon {
color: var(--menu-item-icon-color);
}
/* Note title */
> a {
margin-inline-start: 4px;
color: currentColor;
font-weight: 500;
}
}
}
/* Card content - excerpt */
.backlink-excerpt {
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
display: block;
margin: 8px 0;
border-radius: 4px;
background: var(--quick-search-result-content-background);
padding: 8px;
font-size: .75rem;
a {
background: transparent;
color: var(--quick-search-result-highlight-color);
text-decoration: underline;
}
p {
margin: 0;
}
}
}
}

View File

@@ -1,5 +1,3 @@
import "./Backlinks.css";
import { BacklinkCountResponse, BacklinksResponse, SaveSqlConsoleResponse } from "@triliumnext/commons";
import { VNode } from "preact";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
@@ -62,6 +60,14 @@ export const DESKTOP_FLOATING_BUTTONS: FloatingButtonsList = [
Backlinks
];
export const MOBILE_FLOATING_BUTTONS: FloatingButtonsList = [
RefreshBackendLogButton,
EditButton,
RelationMapButtons,
ExportImageButtons,
Backlinks
];
/**
* Floating buttons that should be hidden in popup editor (Quick edit).
*/

View File

@@ -3,30 +3,6 @@
font-family: var(--detail-font-family);
font-size: var(--detail-font-size);
contain: none;
&.fixed-tree {
display: flex;
flex-direction: column;
height: 100%;
.fixed-note-tree-container {
height: 60%;
border-bottom: 1px solid var(--main-border-color);
overflow: auto;
.tree-wrapper {
padding: 0;
}
.tree {
padding: 0;
}
ul {
margin: 0;
}
}
}
}
body.prefers-centered-content .note-detail {
@@ -36,4 +12,4 @@ body.prefers-centered-content .note-detail {
.note-detail > * {
contain: none;
}
}

View File

@@ -1,6 +1,5 @@
import "./NoteDetail.css";
import clsx from "clsx";
import { isValidElement, VNode } from "preact";
import { useEffect, useRef, useState } from "preact/hooks";
@@ -13,9 +12,8 @@ import { t } from "../services/i18n";
import protected_session_holder from "../services/protected_session_holder";
import toast from "../services/toast.js";
import { dynamicRequire, isElectron, isMobile } from "../services/utils";
import NoteTreeWidget from "./note_tree";
import { ExtendedNoteType, TYPE_MAPPINGS, TypeWidget } from "./note_types";
import { useLegacyWidget, useNoteContext, useTriliumEvent } from "./react/hooks";
import { useNoteContext, useTriliumEvent } from "./react/hooks";
import { NoteListWithLinks } from "./react/NoteList";
import { TypeWidgetProps } from "./type_widgets/type_widget";
@@ -38,7 +36,6 @@ export default function NoteDetail() {
const [ noteTypesToRender, setNoteTypesToRender ] = useState<{ [ key in ExtendedNoteType ]?: (props: TypeWidgetProps) => VNode }>({});
const [ activeNoteType, setActiveNoteType ] = useState<ExtendedNoteType>();
const widgetRequestId = useRef(0);
const hasFixedTree = note && noteContext?.hoistedNoteId === "_lbMobileRoot" && isMobile() && note.noteId.startsWith("_lbMobile");
const props: TypeWidgetProps = {
note: note!,
@@ -122,6 +119,13 @@ export default function NoteDetail() {
}
});
// Fixed tree for launch bar config on mobile.
useEffect(() => {
if (!isMobile) return;
const hasFixedTree = noteContext?.hoistedNoteId === "_lbMobileRoot";
document.body.classList.toggle("force-fixed-tree", hasFixedTree);
}, [ note ]);
// Handle toast notifications.
useEffect(() => {
if (!isElectron()) return;
@@ -211,13 +215,8 @@ export default function NoteDetail() {
return (
<div
ref={containerRef}
class={clsx("component note-detail", {
"full-height": isFullHeight,
"fixed-tree": hasFixedTree
})}
class={`component note-detail ${isFullHeight ? "full-height" : ""}`}
>
{hasFixedTree && <FixedTree noteContext={noteContext} />}
{Object.entries(noteTypesToRender).map(([ itemType, Element ]) => {
return <NoteDetailWrapper
Element={Element}
@@ -232,11 +231,6 @@ export default function NoteDetail() {
);
}
function FixedTree({ noteContext }: { noteContext: NoteContext }) {
const [ treeEl ] = useLegacyWidget(() => new NoteTreeWidget(), { noteContext });
return <div class="fixed-note-tree-container">{treeEl}</div>;
}
/**
* Wraps a single note type widget, in order to keep it in the DOM even after the user has switched away to another note type. This allows faster loading of the same note type again. The properties are cached, so that they are updated only
* while the widget is visible, to avoid rendering in the background. When not visible, the DOM element is simply hidden.

View File

@@ -5,7 +5,6 @@ import clsx from "clsx";
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
import { Dispatch, StateUpdater, useEffect, useRef, useState } from "preact/hooks";
import NoteContext from "../components/note_context";
import FAttribute from "../entities/fattribute";
import FNote from "../entities/fnote";
import { Attribute } from "../services/attribute_parser";
@@ -41,8 +40,8 @@ type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | J
type OnChangeListener = (e: OnChangeEventData) => Promise<void>;
export default function PromotedAttributes() {
const { note, componentId, noteContext } = useNoteContext();
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
const { note, componentId } = useNoteContext();
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
return <PromotedAttributesContent note={note} componentId={componentId} cells={cells} setCells={setCells} />;
}
@@ -75,12 +74,12 @@ export function PromotedAttributesContent({ note, componentId, cells, setCells }
*
* The cells are returned as a state since they can also be altered internally if needed, for example to add a new empty cell.
*/
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string, noteContext: NoteContext | undefined): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
export function usePromotedAttributeData(note: FNote | null | undefined, componentId: string): [ Cell[] | undefined, Dispatch<StateUpdater<Cell[] | undefined>> ] {
const [ viewType ] = useNoteLabel(note, "viewType");
const [ cells, setCells ] = useState<Cell[]>();
function refresh() {
if (!note || viewType === "table" || noteContext?.viewScope?.viewMode !== "default") {
if (!note || viewType === "table") {
setCells([]);
return;
}
@@ -125,7 +124,7 @@ export function usePromotedAttributeData(note: FNote | null | undefined, compone
setCells(cells);
}
useEffect(refresh, [ note, viewType, noteContext ]);
useEffect(refresh, [ note, viewType ]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows(componentId).find((attr) => attributes.isAffecting(attr, note))) {
refresh();

View File

@@ -29,6 +29,7 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
const isVerticalLayout = !isHorizontalLayout;
const parentComponent = useContext(ParentComponent);
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
const isMobileLocal = isMobile();
const logoRef = useRef<SVGSVGElement>(null);
useStaticTooltip(logoRef);
@@ -43,12 +44,9 @@ export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout:
</div>}
</>}
noDropdownListStyle
mobileBackdrop
onShown={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover") : undefined}
onHidden={isMobileLocal ? () => document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover") : undefined}
>
{isMobile() && <>
<MenuItem command="searchNotes" icon="bx bx-search" text={t("global_menu.search_notes")} />
<FormDropdownDivider />
</>}
<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")} />
@@ -109,7 +107,8 @@ function BrowserOnlyOptions() {
function DevelopmentOptions({ dropStart }: { dropStart: boolean }) {
return <>
<FormListHeader text="Development Options" />
<FormDropdownDivider />
<FormListItem disabled>Development Options</FormListItem>
<FormDropdownSubmenu icon="bx bx-test-tube" title="Experimental features" dropStart={dropStart}>
{experimentalFeatures.map((feature) => (
<ExperimentalFeatureToggle key={feature.id} experimentalFeature={feature as ExperimentalFeature} />

View File

@@ -10,6 +10,12 @@
--card-padding: 0.6em;
}
body.mobile .board-view {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.board-view-container {
height: 100%;
display: flex;
@@ -20,12 +26,6 @@
overflow-x: auto;
}
body.mobile .board-view-container {
scroll-snap-type: x mandatory;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.board-view-container .board-column {
width: 250px;
flex-shrink: 0;

View File

@@ -79,7 +79,6 @@ export const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, (() => Promise<{ de
es: () => import("@fullcalendar/core/locales/es"),
fr: () => import("@fullcalendar/core/locales/fr"),
it: () => import("@fullcalendar/core/locales/it"),
ga: null,
cn: () => import("@fullcalendar/core/locales/zh-cn"),
tw: () => import("@fullcalendar/core/locales/zh-tw"),
ro: () => import("@fullcalendar/core/locales/ro"),

View File

@@ -1,12 +1,11 @@
import { LOCALES } from "@triliumnext/commons";
import { EventData } from "../../components/app_context.js";
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
import options from "../../services/options.js";
import utils, { isMobile } from "../../services/utils.js";
import { LOCALES } from "@triliumnext/commons";
import { readCssVar } from "../../utils/css-var.js";
import type BasicWidget from "../basic_widget.js";
import FlexContainer from "./flex_container.js";
import options from "../../services/options.js";
import type BasicWidget from "../basic_widget.js";
import utils from "../../services/utils.js";
import { getEnabledExperimentalFeatureIds } from "../../services/experimental_features.js";
/**
* The root container is the top-most widget/container, from which the entire layout derives.
@@ -18,12 +17,14 @@ import FlexContainer from "./flex_container.js";
* - `#root-container.vertical-layout`, if the current layout is horizontal.
*/
export default class RootContainer extends FlexContainer<BasicWidget> {
private originalViewportHeight: number;
constructor(isHorizontalLayout: boolean) {
super(isHorizontalLayout ? "column" : "row");
this.id("root-widget");
this.css("height", "100dvh");
this.originalViewportHeight = getViewportHeight();
}
render(): JQuery<HTMLElement> {
@@ -38,7 +39,6 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
this.#setThemeCapabilities();
this.#setLocaleAndDirection(options.get("locale"));
this.#setExperimentalFeatures();
this.#initPWATopbarColor();
return super.render();
}
@@ -64,12 +64,8 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
}
#onMobileResize() {
const viewportHeight = window.visualViewport?.height ?? window.innerHeight;
const windowHeight = window.innerHeight;
// If viewport is significantly smaller, keyboard is likely open
const isKeyboardOpened = windowHeight - viewportHeight > 150;
const currentViewportHeight = getViewportHeight();
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
}
@@ -92,7 +88,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
}
#setBackdropEffects() {
const enabled = options.is("backdropEffectsEnabled") && !isMobile();
const enabled = options.is("backdropEffectsEnabled");
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
}
@@ -100,7 +96,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
// Supports background effects
const useBgfx = readCssVar(document.documentElement, "allow-background-effects")
.asBoolean(false);
.asBoolean(false);
document.body.classList.toggle("theme-supports-background-effects", useBgfx);
}
@@ -116,23 +112,8 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
document.body.lang = locale;
document.body.dir = correspondingLocale?.rtl ? "rtl" : "ltr";
}
#initPWATopbarColor() {
if (!utils.isPWA()) return;
const tracker = $("#background-color-tracker");
if (tracker.length) {
const applyThemeColor = () => {
let meta = $("meta[name='theme-color']");
if (!meta.length) {
meta = $(`<meta name="theme-color">`).appendTo($("head"));
}
meta.attr("content", tracker.css("color"));
};
tracker.on("transitionend", applyThemeColor);
applyThemeColor();
}
}
}
function getViewportHeight() {
return window.visualViewport?.height ?? window.innerHeight;
}

View File

@@ -1,22 +1,14 @@
.scrolling-container {
--content-margin-inline: 24px;
overflow: auto;
scroll-behavior: smooth;
position: relative;
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor,
> .inline-title,
> .note-detail > .note-detail-editable-text,
> .note-list-widget:not(.full-height) .note-list-wrapper {
margin-inline: var(--content-margin-inline);
padding-inline: 24px;
}
> .inline-title {
padding-inline: var(--content-margin-inline);
}
> .note-detail > .note-detail-editable-text > .note-detail-editable-text-editor {
overflow: unset;
}
}
.note-split.type-code:not(.mime-text-x-sqlite) {

View File

@@ -91,9 +91,8 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
height: 100%;
}
.modal.popup-editor-dialog .note-detail-editable-text-editor {
margin: 0 28px;
overflow: visible; /* Allow selection rectangle to go outside of the editor area */
.modal.popup-editor-dialog .note-detail-editable-text {
padding: 0 1em;
}
.modal.popup-editor-dialog .note-detail-file {

View File

@@ -5,15 +5,13 @@ import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import appContext from "../../components/app_context";
import NoteContext from "../../components/note_context";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
import froca from "../../services/froca";
import { t } from "../../services/i18n";
import tree from "../../services/tree";
import utils from "../../services/utils";
import NoteList from "../collections/NoteList";
import FloatingButtons from "../FloatingButtons";
import { DESKTOP_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
import NoteBadges from "../layout/NoteBadges";
import { DESKTOP_FLOATING_BUTTONS, MOBILE_FLOATING_BUTTONS, POPUP_HIDDEN_FLOATING_BUTTONS } from "../FloatingButtonsDefinitions";
import NoteIcon from "../note_icon";
import NoteTitleWidget from "../note_title";
import NoteDetail from "../NoteDetail";
@@ -25,6 +23,8 @@ import ReadOnlyNoteInfoBar from "../ReadOnlyNoteInfoBar";
import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter";
import FormattingToolbar from "../ribbon/FormattingToolbar";
import MobileEditorToolbar from "../type_widgets/text/mobile_editor_toolbar";
import NoteBadges from "../layout/NoteBadges";
import { isExperimentalFeatureEnabled } from "../../services/experimental_features";
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
@@ -34,7 +34,7 @@ export default function PopupEditor() {
const [ noteContext, setNoteContext ] = useState(new NoteContext("_popup-editor"));
const isMobile = utils.isMobile();
const items = useMemo(() => {
const baseItems = isMobile ? [] : DESKTOP_FLOATING_BUTTONS;
const baseItems = isMobile ? MOBILE_FLOATING_BUTTONS : DESKTOP_FLOATING_BUTTONS;
return baseItems.filter(item => !POPUP_HIDDEN_FLOATING_BUTTONS.includes(item));
}, [ isMobile ]);

View File

@@ -1,16 +1,11 @@
import "./BookmarkButtons.css";
import { CSSProperties } from "preact";
import { useContext, useMemo } from "preact/hooks";
import type FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { FormDropdownSubmenu, FormListItem } from "../react/FormList";
import { useChildNotes, useNote, useNoteIcon, useNoteLabelBoolean } from "../react/hooks";
import NoteLink from "../react/NoteLink";
import ResponsiveContainer from "../react/ResponsiveContainer";
import { CustomNoteLauncher, launchCustomNoteLauncher } from "./GenericButtons";
import { LaunchBarContext, LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
import { CSSProperties } from "preact";
import type FNote from "../../entities/fnote";
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
import "./BookmarkButtons.css";
import NoteLink from "../react/NoteLink";
import { CustomNoteLauncher } from "./GenericButtons";
const PARENT_NOTE_ID = "_lbBookmarks";
@@ -24,64 +19,17 @@ export default function BookmarkButtons() {
const childNotes = useChildNotes(PARENT_NOTE_ID);
return (
<ResponsiveContainer
desktop={
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</div>
}
mobile={
<LaunchBarDropdownButton
icon="bx bx-bookmark"
title={t("bookmark_buttons.bookmarks")}
>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</LaunchBarDropdownButton>
}
/>
);
<div style={style}>
{childNotes?.map(childNote => <SingleBookmark key={childNote.noteId} note={childNote} />)}
</div>
)
}
function SingleBookmark({ note }: { note: FNote }) {
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
return <ResponsiveContainer
desktop={
bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
}
mobile={<MobileBookmarkItem noteId={note.noteId} bookmarkFolder={bookmarkFolder} />}
/>;
}
function MobileBookmarkItem({ noteId, bookmarkFolder }: { noteId: string, bookmarkFolder: boolean }) {
const note = useNote(noteId);
const noteIcon = useNoteIcon(note);
if (!note) return null;
return (
!bookmarkFolder
? <FormListItem icon={noteIcon} onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: note, getTargetNoteId: () => note.noteId })}>{note.title}</FormListItem>
: <MobileBookmarkFolder note={note} />
);
}
function MobileBookmarkFolder({ note }: { note: FNote }) {
const childNotes = useChildNotes(note.noteId);
return (
<FormDropdownSubmenu icon="bx bx-folder" title={note.title}>
{childNotes.map(childNote => (
<FormListItem
key={childNote.noteId}
icon={childNote.getIcon()}
onClick={(e) => launchCustomNoteLauncher(e, { launcherNote: childNote, getTargetNoteId: () => childNote.noteId })}
>
{childNote.title}
</FormListItem>
))}
</FormDropdownSubmenu>
);
return bookmarkFolder
? <BookmarkFolder note={note} />
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
}
function BookmarkFolder({ note }: { note: FNote }) {
@@ -107,5 +55,5 @@ function BookmarkFolder({ note }: { note: FNote }) {
</ul>
</div>
</LaunchBarDropdownButton>
);
)
}

View File

@@ -3,7 +3,7 @@ import clsx from "clsx";
import server from "../../services/server";
import { TargetedMouseEvent, VNode } from "preact";
import { useEffect, useState } from "preact/hooks";
import { Dayjs } from "@triliumnext/commons";
import { Dayjs, getWeekInfo, WeekSettings } from "@triliumnext/commons";
import { t } from "../../services/i18n";
interface DateNotesForMonth {
@@ -22,6 +22,7 @@ const DAYS_OF_WEEK = [
interface DateRangeInfo {
weekNumbers: number[];
weekYears: number[];
dates: Dayjs[];
}
@@ -36,19 +37,27 @@ export interface CalendarArgs {
export default function Calendar(args: CalendarArgs) {
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek");
const [ firstWeekOfYear ] = useTriliumOptionInt("firstWeekOfYear");
const [ minDaysInFirstWeek ] = useTriliumOptionInt("minDaysInFirstWeek");
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
const weekSettings = {
firstDayOfWeek: firstDayOfWeekISO,
firstWeekOfYear: firstWeekOfYear ?? 0,
minDaysInFirstWeek: minDaysInFirstWeek ?? 4
};
const date = args.date;
const firstDay = date.startOf('month');
const firstDayISO = firstDay.isoWeekday();
const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
const monthInfo = getMonthInformation(date, firstDayISO, weekSettings);
return (
<>
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
<div className="calendar-body" data-calendar-area="month">
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} {...args} />}
<CurrentMonthDays firstDayOfWeekISO={firstDayOfWeekISO} {...args} />
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays info={monthInfo.prevMonth} weekSettings={weekSettings} {...args} />}
<CurrentMonthDays weekSettings={weekSettings} {...args} />
<NextMonthDays dates={monthInfo.nextMonth.dates} {...args} />
</div>
</>
@@ -67,7 +76,7 @@ function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }
)
}
function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { date: Dayjs, info: DateRangeInfo } & CalendarArgs) {
function PreviousMonthDays({ date, info: { dates, weekNumbers, weekYears }, weekSettings, ...args }: { date: Dayjs, info: DateRangeInfo, weekSettings: WeekSettings } & CalendarArgs) {
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
@@ -77,27 +86,28 @@ function PreviousMonthDays({ date, info: { dates, weekNumbers }, ...args }: { da
return (
<>
<CalendarWeek date={date} weekNumber={weekNumbers[0]} {...args} />
<CalendarWeek date={date} weekNumber={weekNumbers[0]} weekYear={weekYears[0]} {...args} />
{dates.map(date => <CalendarDay key={date.toISOString()} date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" {...args} />)}
</>
)
}
function CurrentMonthDays({ date, firstDayOfWeekISO, ...args }: { date: Dayjs, firstDayOfWeekISO: number } & CalendarArgs) {
function CurrentMonthDays({ date, weekSettings, ...args }: { date: Dayjs, weekSettings: WeekSettings } & CalendarArgs) {
let dateCursor = date;
const currentMonth = date.month();
const items: VNode[] = [];
const curMonthString = date.format('YYYY-MM');
const [ dateNotesForCurMonth, setDateNotesForCurMonth ] = useState<DateNotesForMonth>();
const { firstDayOfWeek, firstWeekOfYear, minDaysInFirstWeek } = weekSettings;
useEffect(() => {
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${curMonthString}`).then(setDateNotesForCurMonth);
}, [ date ]);
while (dateCursor.month() === currentMonth) {
const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
items.push(<CalendarWeek key={`${dateCursor.year()}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} {...args}/>)
const { weekYear, weekNumber } = getWeekInfo(dateCursor, weekSettings);
if (dateCursor.isoWeekday() === firstDayOfWeek) {
items.push(<CalendarWeek key={`${weekYear}-W${weekNumber}`} date={dateCursor} weekNumber={weekNumber} weekYear={weekYear} {...args}/>)
}
items.push(<CalendarDay key={dateCursor.toISOString()} date={dateCursor} dateNotesForMonth={dateNotesForCurMonth} {...args} />)
@@ -141,14 +151,8 @@ function CalendarDay({ date, dateNotesForMonth, className, activeDate, todaysDat
);
}
function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumber: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
const localDate = date.local();
// Handle case where week is in between years.
let year = localDate.year();
if (localDate.month() === 11 && weekNumber === 1) year++;
const weekString = `${year}-W${String(weekNumber).padStart(2, '0')}`;
function CalendarWeek({ date, weekNumber, weekYear, weekNotes, onWeekClicked }: { weekNumber: number, weekYear: number, weekNotes: string[] } & Pick<CalendarArgs, "date" | "onWeekClicked">) {
const weekString = `${weekYear}-W${String(weekNumber).padStart(2, '0')}`;
if (onWeekClicked) {
return (
@@ -169,33 +173,33 @@ function CalendarWeek({ date, weekNumber, weekNotes, onWeekClicked }: { weekNumb
>{weekNumber}</span>);
}
export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
export function getMonthInformation(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings) {
return {
prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
prevMonth: getPrevMonthDays(date, firstDayISO, weekSettings),
nextMonth: getNextMonthDays(date, weekSettings.firstDayOfWeek)
}
}
function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo {
function getPrevMonthDays(date: Dayjs, firstDayISO: number, weekSettings: WeekSettings): DateRangeInfo {
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
const daysToAdd = (firstDayISO - weekSettings.firstDayOfWeek + 7) % 7;
const dates: Dayjs[] = [];
const firstDay = date.startOf('month');
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
const { weekYear, weekNumber } = getWeekInfo(firstDay, weekSettings);
// Get dates from previous month
for (let i = daysToAdd - 1; i >= 0; i--) {
dates.push(prevMonthLastDay.subtract(i, 'day'));
}
return { weekNumbers: [ weekNumber ], dates };
return { weekNumbers: [ weekNumber ], weekYears: [ weekYear ], dates };
}
function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo {
function getNextMonthDays(date: Dayjs, firstDayOfWeek: number): DateRangeInfo {
const lastDayOfMonth = date.endOf('month');
const lastDayISO = lastDayOfMonth.isoWeekday();
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
const lastDayOfUserWeek = ((firstDayOfWeek + 6 - 1) % 7) + 1;
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
const dates: Dayjs[] = [];
@@ -206,16 +210,5 @@ function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo
dates.push(nextMonthFirstDay.add(i, 'day'));
}
}
return { weekNumbers: [], dates };
}
export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
return weekStart.isoWeek();
}
function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
const currentISO = date.isoWeekday();
const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
return date.clone().subtract(diff, "day").startOf("day");
return { weekNumbers: [], weekYears: [], dates };
}

View File

@@ -7,18 +7,32 @@ import { isCtrlKey } from "../../services/utils";
import { useGlobalShortcut, useNoteLabel } from "../react/hooks";
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
export function CustomNoteLauncher(props: {
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
launcherNote: FNote;
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
getHoistedNoteId?: (launcherNote: FNote) => string | null;
keyboardShortcut?: string;
}) {
const { launcherNote, getTargetNoteId } = props;
const { icon, title } = useLauncherIconAndTitle(launcherNote);
const launch = useCallback(async (evt: MouseEvent | KeyboardEvent) => {
await launchCustomNoteLauncher(evt, props);
}, [ props ]);
if (evt.which === 3) {
return;
}
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = !!evt.shiftKey;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
}
}, [ launcherNote, getTargetNoteId, getHoistedNoteId ]);
// Keyboard shortcut.
const [ shortcut ] = useNoteLabel(launcherNote, "keyboardShortcut");
@@ -40,24 +54,3 @@ export function CustomNoteLauncher(props: {
/>
);
}
export async function launchCustomNoteLauncher(evt: MouseEvent | KeyboardEvent, { launcherNote, getTargetNoteId, getHoistedNoteId }: {
launcherNote: FNote;
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>;
getHoistedNoteId?: (launcherNote: FNote) => string | null;
}) {
if (evt.which === 3) return;
const targetNoteId = await getTargetNoteId(launcherNote);
if (!targetNoteId) return;
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
const ctrlKey = isCtrlKey(evt);
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
const activate = !!evt.shiftKey;
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
} else {
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteIdWithDefault);
}
}

View File

@@ -49,7 +49,6 @@ export function LaunchBarDropdownButton({ children, icon, dropdownOptions, ...pr
placement: isHorizontalLayout ? "bottom" : "right"
}
}}
mobileBackdrop
{...props}
>{children}</Dropdown>
);

View File

@@ -50,9 +50,6 @@ body.experimental-feature-new-layout {
}
}
}
body.desktop .title-actions {
> .collapsible,
> .note-type-switcher {
padding-inline-start: calc(24px - var(--title-actions-padding-start));
@@ -60,4 +57,3 @@ body.experimental-feature-new-layout {
}
}
}

View File

@@ -1,21 +1,25 @@
import "./NoteTitleActions.css";
import clsx from "clsx";
import { useEffect, useState } from "preact/hooks";
import NoteContext from "../../components/note_context";
import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import CollectionProperties from "../note_bars/CollectionProperties";
import { checkFullHeight, getExtendedWidgetType } from "../NoteDetail";
import { PromotedAttributesContent, usePromotedAttributeData } from "../PromotedAttributes";
import SimpleBadge from "../react/Badge";
import Collapsible, { ExternallyControlledCollapsible } from "../react/Collapsible";
import { useNoteContext, useNoteLabel, useNoteProperty, useTriliumEvent, useTriliumOptionBool } from "../react/hooks";
import { NewNoteLink } from "../react/NoteLink";
import NoteLink, { NewNoteLink } from "../react/NoteLink";
import { useEditedNotes } from "../ribbon/EditedNotesTab";
import SearchDefinitionTab from "../ribbon/SearchDefinitionTab";
import NoteTypeSwitcher from "./NoteTypeSwitcher";
export default function NoteTitleActions() {
const { note, ntxId, componentId, noteContext, viewScope } = useNoteContext();
const { note, ntxId, componentId, noteContext } = useNoteContext();
const isHiddenNote = note && note.noteId !== "_search" && note.noteId.startsWith("_");
const noteType = useNoteProperty(note, "type");
return (
@@ -23,7 +27,7 @@ export default function NoteTitleActions() {
<PromotedAttributes note={note} componentId={componentId} noteContext={noteContext} />
{noteType === "search" && <SearchProperties note={note} ntxId={ntxId} />}
<EditedNotes />
{(!viewScope?.viewMode || viewScope.viewMode === "default") && <NoteTypeSwitcher />}
<NoteTypeSwitcher />
</div>
);
}
@@ -44,7 +48,7 @@ function PromotedAttributes({ note, componentId, noteContext }: {
componentId: string,
noteContext: NoteContext | undefined
}) {
const [ cells, setCells ] = usePromotedAttributeData(note, componentId, noteContext);
const [ cells, setCells ] = usePromotedAttributeData(note, componentId);
const [ expanded, setExpanded ] = useState(false);
useEffect(() => {

View File

@@ -58,12 +58,101 @@
.dropdown-note-info {
padding: 1em !important;
ul {
--row-block-margin: .2em;
list-style-type: none;
padding: 0;
margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: var(--row-block-margin) 0;
opacity: .5;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
.dropdown-note-paths {
.note-paths-widget {
padding: 0.5em;
}
.note-path-intro {
color: var(--muted-text-color);
}
.note-path-list {
margin: 12px 0;
padding: 0;
list-style: none;
/* Note path card */
li {
--border-radius: 6px;
position: relative;
background: var(--card-background-color);
padding: 8px 20px 8px 25px;
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
& + li {
margin-top: 2px;
}
/* Current path arrow */
&.path-current::before {
position: absolute;
display: flex;
justify-content: flex-end;
align-items: center;
content: "\ee8f";
top: 0;
left: 0;
width: 20px;
bottom: 0;
font-family: "boxicons";
font-size: .75em;
color: var(--menu-item-icon-color);
}
}
/* Note path segment */
a {
margin-inline: 2px;
padding-inline: 2px;
color: currentColor;
font-weight: normal;
text-decoration: none;
/* The last segment of the note path */
&.basename {
color: var(--muted-text-color);
}
}
}
}
.backlinks-widget > .dropdown-menu {
@@ -71,6 +160,84 @@
max-height: 60vh;
overflow-y: scroll;
/* Backlink card */
li {
--border-radius: 8px;
max-width: 600px;
padding: 10px 20px;
background: var(--card-background-color);
& + li {
margin-top: 2px;
}
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
/* Card header */
& > span:first-child {
display: block;
> span {
display: flex;
flex-wrap: wrap;
align-items: center;
/* Note path */
> small {
flex: 100%;
order: -1;
font-size: .65rem;
.note-path {
padding: 0;
}
}
/* Note icon */
> .tn-icon {
color: var(--menu-item-icon-color);
}
/* Note title */
> a {
margin-inline-start: 4px;
color: currentColor;
font-weight: 500;
}
}
}
/* Card content - excerpt */
& > span:nth-child(2) > div {
all: unset; /* TODO: Remove after disposing the old style from FloatingButtons.css */
display: block;
margin: 8px 0;
border-radius: 4px;
background: var(--quick-search-result-content-background);
padding: 8px;
font-size: .75rem;
a {
background: transparent;
color: var(--quick-search-result-highlight-color);
text-decoration: underline;
}
p {
margin: 0;
}
}
}
}
}
@@ -117,50 +284,16 @@
}
div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
}
button.select-button:not(:focus-visible) {
outline: none;
}
}
body.experimental-feature-new-layout .note-info-content {
ul {
--row-block-margin: .2em;
list-style-type: none;
padding: 0;
margin: 0;
margin-top: calc(0px - var(--row-block-margin));
margin-bottom: 12px;
display: table;
li {
display: table-row;
> strong {
display: table-cell;
padding: var(--row-block-margin) 0;
opacity: .5;
}
> span {
display: table-cell;
user-select: text;
padding-left: 2em;
}
}
}
}
body.experimental-feature-new-layout div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
}
body.experimental-feature-new-layout.mobile div.similar-notes-widget div.similar-notes-wrapper {
max-height: unset;
padding: 0;
}
.bottom-panel {
margin: 0 !important;
padding: 0;

View File

@@ -56,7 +56,7 @@ export default function StatusBar() {
similarNotesShown: activePane === "similar-notes",
setSimilarNotesShown: (shown) => setActivePane(shown && "similar-notes")
};
const isHiddenNote = note?.isHiddenCompletely();
const isHiddenNote = note?.isInHiddenSubtree();
return (
<div className="status-bar">
@@ -212,8 +212,8 @@ export function getLocaleName(locale: Locale | null | undefined) {
//#region Note info & Similar
interface NoteInfoContext extends StatusBarContext {
similarNotesShown?: boolean;
setSimilarNotesShown?: (value: boolean) => void;
similarNotesShown: boolean;
setSimilarNotesShown: (value: boolean) => void;
}
export function NoteInfoBadge(context: NoteInfoContext) {
@@ -225,7 +225,7 @@ export function NoteInfoBadge(context: NoteInfoContext) {
// Keyboard shortcut.
useTriliumEvent("toggleRibbonTabNoteInfo", () => enabled && dropdownRef.current?.show());
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown && setSimilarNotesShown(!similarNotesShown));
useTriliumEvent("toggleRibbonTabSimilarNotes", () => setSimilarNotesShown(!similarNotesShown));
return (enabled &&
<StatusBarDropdown
@@ -242,8 +242,8 @@ export function NoteInfoBadge(context: NoteInfoContext) {
);
}
export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: Pick<NoteInfoContext, "note" | "setSimilarNotesShown"> & {
dropdownRef?: RefObject<BootstrapDropdown>;
function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdownRef }: NoteInfoContext & {
dropdownRef: RefObject<BootstrapDropdown>;
noteType: NoteType;
}) {
const { metadata, ...sizeProps } = useNoteMetadata(note);
@@ -251,7 +251,7 @@ export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdown
const noteTypeMapping = useMemo(() => NOTE_TYPES.find(t => t.type === noteType), [ noteType ]);
return (
<div className="note-info-content">
<>
<ul>
{originalFileName && <NoteInfoValue text={t("file_properties.original_file_name")} value={originalFileName} />}
<NoteInfoValue text={t("note_info_widget.created")} value={formatDateTime(metadata?.dateCreated)} />
@@ -262,14 +262,14 @@ export function NoteInfoContent({ note, setSimilarNotesShown, noteType, dropdown
<NoteInfoValue text={t("note_info_widget.note_size")} title={t("note_info_widget.note_size_info")} value={<NoteSizeWidget {...sizeProps} />} />
</ul>
{setSimilarNotesShown && <LinkButton
<LinkButton
text={t("note_info_widget.show_similar_notes")}
onClick={() => {
dropdownRef?.current?.hide();
dropdownRef.current?.hide();
setSimilarNotesShown(true);
}}
/>}
</div>
/>
</>
);
}
@@ -300,7 +300,7 @@ function BacklinksBadge({ note, viewScope }: StatusBarContext) {
const count = useBacklinkCount(note, viewScope?.viewMode === "default");
return (note && count > 0 &&
<StatusBarDropdown
className="backlinks-badge backlinks-widget tn-backlinks-widget"
className="backlinks-badge backlinks-widget"
icon="bx bx-link"
text={t("status_bar.backlinks", { count })}
title={t("status_bar.backlinks_title", { count })}

View File

@@ -89,25 +89,12 @@
&.type-text {
padding: 10px;
--ck-content-todo-list-checkmark-size: 8px;
p { margin-bottom: 0.2em;}
hr { margin-block: 0.1em; height: 1px; }
h2 { font-size: 1.20em; }
h3 { font-size: 1.15em; }
h4 { font-size: 1.10em; }
h5 { font-size: 1.05em}
h6 { font-size: 1em; }
ul, ol { margin: 0 }
}
&.type-book,
&.type-contentWidget,
&.type-search,
&.type-empty,
&.type-relationMap,
&.type-launcher,
&.tab-preview-placeholder {
&.type-empty {
display: flex;
align-items: center;
justify-content: center;
@@ -118,6 +105,13 @@
.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 {

View File

@@ -1,7 +1,6 @@
import "./TabSwitcher.css";
import clsx from "clsx";
import { ComponentChild } from "preact";
import { createPortal, Fragment } from "preact/compat";
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
@@ -12,7 +11,6 @@ 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 type { ViewMode, ViewScope } from "../../services/link";
import { NoteContent } from "../collections/legacy/ListOrGridView";
import { LaunchBarActionButton } from "../launch_bar/launch_bar_widgets";
import { ICON_MAPPINGS } from "../note_bars/CollectionProperties";
@@ -22,13 +20,6 @@ import Icon from "../react/Icon";
import LinkButton from "../react/LinkButton";
import Modal from "../react/Modal";
const VIEW_MODE_ICON_MAPPINGS: Record<Exclude<ViewMode, "default">, string> = {
source: "bx bx-code",
"contextual-help": "bx bx-help-circle",
"note-map": "bx bxs-network-chart",
attachments: "bx bx-paperclip",
};
export default function TabSwitcher() {
const [ shown, setShown ] = useState(false);
const mainNoteContexts = useMainNoteContexts();
@@ -147,6 +138,7 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
activeNtxId: string | null | undefined;
}) {
const { note } = noteContext;
const iconClass = useNoteIcon(note);
const colorClass = note?.getColorClass() || '';
const workspaceTabBackgroundColorHue = getWorkspaceTabBackgroundColorHue(noteContext);
const subContexts = noteContext.getSubContexts();
@@ -166,65 +158,46 @@ function Tab({ noteContext, containerRef, selectTab, activeNtxId }: {
>
{subContexts.map(subContext => (
<Fragment key={subContext.ntxId}>
<TabHeader noteContext={subContext} colorClass={colorClass} />
<TabPreviewContent note={subContext.note} viewScope={subContext.viewScope} />
<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 TabHeader({ noteContext, colorClass }: { noteContext: NoteContext, colorClass: string }) {
const iconClass = useNoteIcon(noteContext.note);
const [ navigationTitle, setNavigationTitle ] = useState<string | null>(null);
function TabPreviewContent({ note }: {
note: FNote | null
}) {
if (!note) {
return <PreviewPlaceholder icon="bx bx-plus" />;
}
// Manage the title for read-only notes
useEffect(() => {
noteContext?.getNavigationTitle().then(setNavigationTitle);
}, [noteContext]);
if (note.type === "book") {
return <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
}
return (
<header className={colorClass}>
{noteContext.note && <Icon icon={iconClass} />}
<span className="title">{navigationTitle ?? noteContext.note?.title ?? t("tab_row.new_tab")}</span>
{noteContext.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(noteContext.ntxId);
}}
/>}
</header>
);
}
function TabPreviewContent({ note, viewScope }: {
note: FNote | null,
viewScope: ViewScope | undefined
}) {
let el: ComponentChild;
let isPlaceholder = true;
if (!note) {
el = <PreviewPlaceholder icon="bx bx-plus" />;
} else if (note.type === "book") {
el = <PreviewPlaceholder icon={ICON_MAPPINGS[note.getLabelValue("viewType") ?? ""] ?? "bx bx-book"} />;
} else if (viewScope?.viewMode && viewScope.viewMode !== "default") {
el = <PreviewPlaceholder icon={VIEW_MODE_ICON_MAPPINGS[viewScope?.viewMode ?? ""] ?? "bx bx-empty"} />;
} else {
el = <NoteContent
<NoteContent
note={note}
highlightedTokens={undefined}
trim
includeArchivedNotes={false}
/>;
isPlaceholder = false;
}
return (
<div className={clsx("tab-preview", `type-${note?.type ?? "empty"}`, { "tab-preview-placeholder": isPlaceholder })}>{el}</div>
/>
);
}

View File

@@ -1,3 +0,0 @@
.code-note-switcher-modal .dropdown-menu {
background: none !important;
}

View File

@@ -1,240 +1,84 @@
import "./mobile_detail_menu.css";
import { useContext } from "preact/hooks";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import { createPortal, useRef, useState } from "preact/compat";
import FNote, { NotePathRecord } from "../../entities/fnote";
import appContext, { CommandMappings } from "../../components/app_context";
import contextMenu, { MenuItem } from "../../menus/context_menu";
import branches from "../../services/branches";
import { t } from "../../services/i18n";
import { getHelpUrlForNote } from "../../services/in_app_help";
import note_create from "../../services/note_create";
import server from "../../services/server";
import { BacklinksList, useBacklinkCount } from "../FloatingButtonsDefinitions";
import { getLocaleName, NoteInfoContent } from "../layout/StatusBar";
import tree from "../../services/tree";
import { openInAppHelpFromUrl } from "../../services/utils";
import BasicWidget from "../basic_widget";
import ActionButton from "../react/ActionButton";
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
import { useNoteContext, useNoteProperty } from "../react/hooks";
import Modal from "../react/Modal";
import { NoteTypeCodeNoteList, useLanguageSwitcher, useMimeTypes } from "../ribbon/BasicPropertiesTab";
import { NoteContextMenu } from "../ribbon/NoteActions";
import NoteActionsCustom from "../ribbon/NoteActionsCustom";
import { NotePathsWidget, useSortedNotePaths } from "../ribbon/NotePathsTab";
import SimilarNotesTab from "../ribbon/SimilarNotesTab";
import { useProcessedLocales } from "../type_widgets/options/components/LocaleSelector";
import { ParentComponent } from "../react/react_utils";
export default function MobileDetailMenu() {
const dropdownRef = useRef<BootstrapDropdown | null>(null);
const { note, noteContext, parentComponent, ntxId, viewScope, hoistedNoteId } = useNoteContext();
const subContexts = noteContext?.getMainContext().getSubContexts() ?? [];
const isMainContext = noteContext?.isMainContext();
const [ backlinksModalShown, setBacklinksModalShown ] = useState(false);
const [ notePathsModalShown, setNotePathsModalShown ] = useState(false);
const [ noteInfoModalShown, setNoteInfoModalShown ] = useState(false);
const [ similarNotesModalShown, setSimilarNotesModalShown ] = useState(false);
const [ codeNoteSwitcherModalShown, setCodeNoteSwitcherModalShown ] = useState(false);
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
const backlinksCount = useBacklinkCount(note, viewScope?.viewMode === "default");
function closePane() {
// Wait first for the context menu to be dismissed, otherwise the backdrop stays on.
requestAnimationFrame(() => {
parentComponent.triggerCommand("closeThisNoteSplit", { ntxId });
});
}
const parentComponent = useContext(ParentComponent);
return (
<div style={{ contain: "none" }}>
{note ? (
<NoteContextMenu
dropdownRef={dropdownRef}
note={note} noteContext={noteContext}
itemsAtStart={<>
<div className="form-list-row">
<div className="form-list-col">
<FormListItem
icon="bx bx-link"
onClick={() => setBacklinksModalShown(true)}
disabled={backlinksCount === 0}
>{t("status_bar.backlinks", { count: backlinksCount })}</FormListItem>
</div>
<div className="form-list-col">
<FormListItem
icon="bx bx-directions"
onClick={() => setNotePathsModalShown(true)}
disabled={(sortedNotePaths?.length ?? 0) <= 1}
>{t("status_bar.note_paths", { count: sortedNotePaths?.length })}</FormListItem>
</div>
</div>
<FormDropdownDivider />
<ActionButton
icon="bx bx-dots-vertical-rounded"
text=""
onClick={(e) => {
const ntxId = (parentComponent as BasicWidget | null)?.getClosestNtxId();
if (!ntxId) return;
{noteContext && ntxId && <NoteActionsCustom note={note} noteContext={noteContext} ntxId={ntxId} />}
<FormListItem
onClick={() => noteContext?.notePath && note_create.createNote(noteContext.notePath)}
icon="bx bx-plus"
>{t("mobile_detail_menu.insert_child_note")}</FormListItem>
{subContexts.length < 2 && <>
<FormDropdownDivider />
<FormListItem
onClick={(e) => {
// We have to manually manage the hide because otherwise the old note context gets activated.
e.stopPropagation();
dropdownRef.current?.hide();
parentComponent.triggerCommand("openNewNoteSplit", { ntxId });
}}
icon="bx bx-dock-right"
>{t("create_pane_button.create_new_split")}</FormListItem>
</>}
{!isMainContext && <>
<FormDropdownDivider />
<FormListItem
icon="bx bx-x"
onClick={closePane}
>{t("close_pane_button.close_this_pane")}</FormListItem>
</>}
<FormDropdownDivider />
</>}
itemsNearNoteSettings={<>
{note.type === "text" && <ContentLanguageSelector note={note} />}
{note.type === "code" && <FormListItem icon={"bx bx-code"} onClick={() => setCodeNoteSwitcherModalShown(true)}>{t("status_bar.code_note_switcher")}</FormListItem>}
<FormListItem icon="bx bx-info-circle" onClick={() => setNoteInfoModalShown(true)}>{t("note_info_widget.title")}</FormListItem>
<FormListItem icon="bx bx-bar-chart" onClick={() => setSimilarNotesModalShown(true)}>{t("similar_notes.title")}</FormListItem>
<FormDropdownDivider />
</>}
/>
) : (
<ActionButton
icon="bx bx-x"
onClick={closePane}
text={t("close_pane_button.close_this_pane")}
/>
)}
const noteContext = appContext.tabManager.getNoteContextById(ntxId);
const subContexts = noteContext.getMainContext().getSubContexts();
const isMainContext = noteContext?.isMainContext();
const note = noteContext.note;
const helpUrl = getHelpUrlForNote(note);
{createPortal((
<>
<BacklinksModal note={note} modalShown={backlinksModalShown} setModalShown={setBacklinksModalShown} />
<NotePathsModal note={note} modalShown={notePathsModalShown} notePath={noteContext?.notePath} sortedNotePaths={sortedNotePaths} setModalShown={setNotePathsModalShown} />
<NoteInfoModal note={note} modalShown={noteInfoModalShown} setModalShown={setNoteInfoModalShown} />
<SimilarNotesModal note={note} modalShown={similarNotesModalShown} setModalShown={setSimilarNotesModalShown} />
<CodeNoteSwitcherModal note={note} modalShown={codeNoteSwitcherModalShown} setModalShown={setCodeNoteSwitcherModalShown} />
</>
), document.body)}
</div>
);
}
function ContentLanguageSelector({ note }: { note: FNote | null | undefined }) {
const { locales, DEFAULT_LOCALE, currentNoteLanguage, setCurrentNoteLanguage } = useLanguageSwitcher(note);
const { activeLocale, processedLocales } = useProcessedLocales(locales, DEFAULT_LOCALE, currentNoteLanguage ?? DEFAULT_LOCALE.id);
return (
<FormDropdownSubmenu
icon="bx bx-globe"
title={t("mobile_detail_menu.content_language_switcher", { language: getLocaleName(activeLocale ?? DEFAULT_LOCALE) })}
>
{processedLocales.map((locale, index) =>
(typeof locale === "object") ? (
<FormListItem
key={locale.id}
rtl={locale.rtl}
checked={locale.id === currentNoteLanguage}
onClick={() => setCurrentNoteLanguage(locale.id)}
>{locale.name}</FormListItem>
) : (
<FormDropdownDivider key={`divider-${index}`} />
)
)}
</FormDropdownSubmenu>
);
}
interface WithModal {
modalShown: boolean;
setModalShown: (shown: boolean) => void;
}
function BacklinksModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="backlinks-modal tn-backlinks-widget"
size="md"
title={t("mobile_detail_menu.backlinks")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<ul className="backlinks-items">
{note && <BacklinksList note={note} />}
</ul>
</Modal>
);
}
function NotePathsModal({ note, modalShown, notePath, sortedNotePaths, setModalShown }: { note: FNote | null | undefined, sortedNotePaths: NotePathRecord[] | undefined, notePath: string | null | undefined } & WithModal) {
return (
<Modal
className="note-paths-modal"
size="md"
title={t("note_paths.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
{note && (
<NotePathsWidget
sortedNotePaths={sortedNotePaths}
currentNotePath={notePath}
/>
)}
</Modal>
);
}
function NoteInfoModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="note-info-modal"
size="md"
title={t("note_info_widget.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
{note && <NoteInfoContent note={note} noteType={note.type} />}
</Modal>
);
}
function SimilarNotesModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
return (
<Modal
className="similar-notes-modal"
size="md"
title={t("similar_notes.title")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<SimilarNotesTab note={note} />
</Modal>
);
}
function CodeNoteSwitcherModal({ note, modalShown, setModalShown }: { note: FNote | null | undefined } & WithModal) {
const currentNoteMime = useNoteProperty(note, "mime");
const mimeTypes = useMimeTypes();
return (
<Modal
className="code-note-switcher-modal"
size="md"
title={t("status_bar.code_note_switcher")}
show={modalShown}
onHidden={() => setModalShown(false)}
>
<div className="dropdown-menu static show">
{note && <NoteTypeCodeNoteList
currentMimeType={currentNoteMime}
mimeTypes={mimeTypes}
changeNoteType={(type, mime) => {
server.put(`notes/${note.noteId}/type`, { type, mime });
setModalShown(false);
}}
/>}
</div>
</Modal>
const items: (MenuItem<keyof CommandMappings>)[] = [
{ 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" },
{ kind: "separator" },
{ title: t("mobile_detail_menu.note_revisions"), command: "showRevisions", uiIcon: "bx bx-history" },
{ kind: "separator" },
helpUrl && {
title: t("help-button.title"),
uiIcon: "bx bx-help-circle",
handler: () => openInAppHelpFromUrl(helpUrl)
},
{ kind: "separator" },
subContexts.length < 2 && { title: t("create_pane_button.create_new_split"), command: "openNewNoteSplit", uiIcon: "bx bx-dock-right" },
!isMainContext && { title: t("close_pane_button.close_this_pane"), command: "closeThisNoteSplit", uiIcon: "bx bx-x" }
].filter(i => !!i) as MenuItem<keyof CommandMappings>[];
const lastItem = items.at(-1);
if (lastItem && "kind" in lastItem && lastItem.kind === "separator") {
items.pop();
}
contextMenu.show<keyof CommandMappings>({
x: e.pageX,
y: e.pageY,
items,
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, { ntxId });
}
},
forcePositionOnMobile: true
});
}}
/>
);
}

View File

@@ -117,35 +117,3 @@ body.experimental-feature-new-layout {
}
}
}
body.mobile .modal.icon-switcher {
.modal-dialog {
left: 0;
right: 0;
margin: unset;
transform: unset;
max-width: 100%;
height: 100%;
}
.modal-body {
padding: 0;
display: flex;
flex-direction: column;
> .filter-row {
padding: 0.25em var(--bs-modal-padding) 0.5em var(--bs-modal-padding);
border-bottom: 1px solid var(--main-border-color);
}
}
.icon-list {
margin: auto;
flex-grow: 1;
height: 100%;
span {
padding: 12px;
}
}
}

View File

@@ -4,8 +4,7 @@ import { IconRegistry } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import clsx from "clsx";
import { t } from "i18next";
import { CSSProperties } from "preact";
import { createPortal } from "preact/compat";
import { CSSProperties, RefObject } from "preact";
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
import type React from "react";
import { CellComponentProps, Grid } from "react-window";
@@ -13,15 +12,11 @@ import { CellComponentProps, Grid } from "react-window";
import FNote from "../entities/fnote";
import attributes from "../services/attributes";
import server from "../services/server";
import { isDesktop, isMobile } from "../services/utils";
import ActionButton from "./react/ActionButton";
import Dropdown from "./react/Dropdown";
import { FormDropdownDivider, FormListItem } from "./react/FormList";
import FormTextBox from "./react/FormTextBox";
import { useNoteContext, useNoteLabel, useStaticTooltip, useWindowSize } from "./react/hooks";
import Modal from "./react/Modal";
const ICON_SIZE = isMobile() ? 56 : 48;
import { useNoteContext, useNoteLabel, useStaticTooltip } from "./react/hooks";
interface IconToCountCache {
iconClassToCountMap: Record<string, number>;
@@ -42,10 +37,6 @@ export default function NoteIcon() {
setIcon(note?.getIcon());
}, [ note, iconClass, workspaceIconClass ]);
if (isMobile()) {
return <MobileNoteIconSwitcher note={note} icon={icon} />;
}
return (
<Dropdown
className="note-icon-widget"
@@ -57,47 +48,16 @@ export default function NoteIcon() {
hideToggleArrow
disabled={viewScope?.viewMode !== "default"}
>
{ note && <NoteIconList note={note} onHide={() => dropdownRef?.current?.hide()} columnCount={12} /> }
{ note && <NoteIconList note={note} dropdownRef={dropdownRef} /> }
</Dropdown>
);
}
function MobileNoteIconSwitcher({ note, icon }: {
note: FNote | null | undefined;
icon: string | null | undefined;
}) {
const [ modalShown, setModalShown ] = useState(false);
const { windowWidth } = useWindowSize();
return (note &&
<div className="note-icon-widget">
<ActionButton
className="note-icon"
icon={icon ?? "bx bx-empty"}
text={t("note_icon.change_note_icon")}
onClick={() => setModalShown(true)}
/>
{createPortal((
<Modal
title={t("note_icon.change_note_icon")}
size="xl"
show={modalShown} onHidden={() => setModalShown(false)}
className="icon-switcher note-icon-widget"
scrollable
>
<NoteIconList note={note} onHide={() => setModalShown(false)} columnCount={Math.max(1, Math.floor(windowWidth / ICON_SIZE))} />
</Modal>
), document.body)}
</div>
);
}
function NoteIconList({ note, onHide, columnCount }: {
note: FNote;
onHide: () => void;
columnCount: number;
function NoteIconList({ note, dropdownRef }: {
note: FNote,
dropdownRef: RefObject<BootstrapDropdown>;
}) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const iconListRef = useRef<HTMLDivElement>(null);
const [ search, setSearch ] = useState<string>();
const [ filterByPrefix, setFilterByPrefix ] = useState<string | null>(null);
@@ -113,22 +73,53 @@ function NoteIconList({ note, onHide, columnCount }: {
return (
<>
<FilterRow
note={note}
filterByPrefix={filterByPrefix}
search={search}
setSearch={setSearch}
setFilterByPrefix={setFilterByPrefix}
filteredIcons={filteredIcons}
onHide={onHide}
/>
<div class="filter-row">
<span>{t("note_icon.search")}</span>
<FormTextBox
inputRef={searchBoxRef}
type="text"
name="icon-search"
placeholder={ filterByPrefix
? t("note_icon.search_placeholder_filtered", {
number: filteredIcons.length ?? 0,
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
})
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
currentValue={search} onChange={setSearch}
autoFocus
/>
{getIconLabels(note).length > 0 && (
<div style={{ textAlign: "center" }}>
<ActionButton
icon="bx bx-reset"
text={t("note_icon.reset-default")}
onClick={() => {
if (!note) return;
for (const label of getIconLabels(note)) {
attributes.removeAttributeById(note.noteId, label.attributeId);
}
dropdownRef?.current?.hide();
}}
/>
</div>
)}
{glob.iconRegistry.sources.length > 0 && <Dropdown
buttonClassName="bx bx-filter-alt"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
title={t("note_icon.filter")}
>
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
</Dropdown>}
</div>
<div
class="icon-list"
ref={iconListRef}
style={{
width: (columnCount * ICON_SIZE + 10),
}}
onClick={(e) => {
// Make sure we are not clicking on something else than a button.
const clickedTarget = e.target as HTMLElement;
@@ -139,19 +130,18 @@ function NoteIconList({ note, onHide, columnCount }: {
const attributeToSet = note.hasOwnedLabel("workspace") ? "workspaceIconClass" : "iconClass";
attributes.setLabel(note.noteId, attributeToSet, iconClass);
}
onHide();
dropdownRef?.current?.hide();
}}
>
{filteredIcons.length ? (
<Grid
columnCount={columnCount}
columnWidth={ICON_SIZE}
rowCount={Math.ceil(filteredIcons.length / columnCount)}
rowHeight={ICON_SIZE}
columnCount={12}
columnWidth={48}
rowCount={Math.ceil(filteredIcons.length / 12)}
rowHeight={48}
cellComponent={IconItemCell}
cellProps={{
filteredIcons,
columnCount
filteredIcons
}}
/>
) : (
@@ -162,95 +152,10 @@ function NoteIconList({ note, onHide, columnCount }: {
);
}
function FilterRow({ note, filterByPrefix, search, setSearch, setFilterByPrefix, filteredIcons, onHide }: {
note: FNote;
filterByPrefix: string | null;
search: string | undefined;
setSearch: (value: string | undefined) => void;
setFilterByPrefix: (value: string | null) => void;
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons }: CellComponentProps<{
filteredIcons: IconWithName[];
onHide: () => void;
}) {
const searchBoxRef = useRef<HTMLInputElement>(null);
const hasCustomIcon = getIconLabels(note).length > 0;
function resetToDefaultIcon() {
if (!note) return;
for (const label of getIconLabels(note)) {
attributes.removeAttributeById(note.noteId, label.attributeId);
}
onHide();
}
return (
<div class="filter-row">
<span>{t("note_icon.search")}</span>
<FormTextBox
inputRef={searchBoxRef}
type="text"
name="icon-search"
placeholder={ filterByPrefix
? t("note_icon.search_placeholder_filtered", {
number: filteredIcons.length ?? 0,
name: glob.iconRegistry.sources.find(s => s.prefix === filterByPrefix)?.name ?? ""
})
: t("note_icon.search_placeholder", { number: filteredIcons.length ?? 0, count: glob.iconRegistry.sources.length })}
currentValue={search} onChange={setSearch}
autoFocus
/>
{isDesktop()
? <>
{hasCustomIcon && (
<div style={{ textAlign: "center" }}>
<ActionButton
icon="bx bx-reset"
text={t("note_icon.reset-default")}
onClick={resetToDefaultIcon}
/>
</div>
)}
{<Dropdown
buttonClassName="bx bx-filter-alt"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
title={t("note_icon.filter")}
>
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
</Dropdown>}
</> : (
<Dropdown
buttonClassName="bx bx-dots-vertical-rounded"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
dropdownContainerClassName="mobile-bottom-menu"
>
{hasCustomIcon && <>
<FormListItem
icon="bx bx-reset"
onClick={resetToDefaultIcon}
disabled={!hasCustomIcon}
>{t("note_icon.reset-default")}</FormListItem>
<FormDropdownDivider />
</>}
<IconFilterContent filterByPrefix={filterByPrefix} setFilterByPrefix={setFilterByPrefix} />
</Dropdown>
)}
</div>
);
}
function IconItemCell({ rowIndex, columnIndex, style, filteredIcons, columnCount }: CellComponentProps<{
filteredIcons: IconWithName[];
columnCount: number;
}>) {
const iconIndex = rowIndex * columnCount + columnIndex;
const iconIndex = rowIndex * 12 + columnIndex;
const iconData = filteredIcons[iconIndex] as IconWithName | undefined;
if (!iconData) return <></> as React.ReactElement;
@@ -279,7 +184,7 @@ function IconFilterContent({ filterByPrefix, setFilterByPrefix }: {
checked={filterByPrefix === "bx"}
onClick={() => setFilterByPrefix("bx")}
>{t("note_icon.filter-default")}</FormListItem>
{glob.iconRegistry.sources.length > 1 && <FormDropdownDivider />}
<FormDropdownDivider />
{glob.iconRegistry.sources.map(({ prefix, name, icon }) => (
prefix !== "bx" && <FormListItem

View File

@@ -109,29 +109,4 @@ body.experimental-feature-new-layout {
--input-focus-color: initial;
}
}
&.mobile .title-row {
.icon-action:not(.note-icon) {
--icon-button-size: 45px;
--icon-button-icon-ratio: 0.5;
flex-shrink: 0;
}
.note-actions {
width: auto;
}
.note-badges {
margin-inline: 0.5em;
flex-shrink: 0;
}
.note-icon-widget {
margin-inline: 0.5em;
.note-icon {
--icon-button-size: 24px;
}
}
}
}

View File

@@ -1,4 +1,4 @@
.tree-wrapper {
#left-pane .tree-wrapper {
.note-indicator-icon.subtree-hidden-badge {
font-family: inherit !important;
margin-inline: 0.5em;

View File

@@ -17,34 +17,21 @@
.collapsible-body {
height: 0;
overflow: hidden;
&.fully-expanded {
overflow: visible;
}
}
.collapsible-inner-body {
padding-top: 0.5em;
opacity: 0;
}
&.expanded {
.collapsible-title .arrow {
transform: rotate(90deg);
}
.collapsible-inner-body {
opacity: 1;
}
}
&.with-transition {
.collapsible-body {
transition: height 250ms ease-in;
}
.collapsible-inner-body {
transition: opacity 250ms ease-in;
}
}
}

View File

@@ -27,7 +27,6 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
const { height } = useElementSize(innerRef) ?? {};
const contentId = useUniqueName();
const [ transitionEnabled, setTransitionEnabled ] = useState(false);
const [ fullyExpanded, setFullyExpanded ] = useState(false);
useEffect(() => {
const timeout = setTimeout(() => {
@@ -36,21 +35,6 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
return () => clearTimeout(timeout);
}, []);
useEffect(() => {
if (expanded) {
if (transitionEnabled) {
const timeout = setTimeout(() => {
setFullyExpanded(true);
}, 250);
return () => clearTimeout(timeout);
} else {
setFullyExpanded(true);
}
} else {
setFullyExpanded(false);
}
}, [expanded, transitionEnabled])
return (
<div className={clsx("collapsible", className, {
expanded,
@@ -69,7 +53,7 @@ export function ExternallyControlledCollapsible({ title, children, className, ex
<div
id={contentId}
ref={bodyRef}
className={clsx("collapsible-body", {"fully-expanded": fullyExpanded})}
className="collapsible-body"
style={{ height: expanded ? height : "0" }}
aria-hidden={!expanded}
>

View File

@@ -3,7 +3,6 @@ import { ComponentChildren, HTMLAttributes } from "preact";
import { CSSProperties, HTMLProps } from "preact/compat";
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
import { isMobile } from "../../services/utils";
import { useTooltip, useUniqueName } from "./hooks";
type DataAttributes = {
@@ -33,10 +32,9 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
dropdownRef?: MutableRef<BootstrapDropdown | null>;
titlePosition?: "top" | "right" | "bottom" | "left";
titleOptions?: Partial<Tooltip.Options>;
mobileBackdrop?: boolean;
}
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions, mobileBackdrop }: DropdownProps) {
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, dropdownContainerRef: externalContainerRef, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps, dropdownRef, titlePosition, titleOptions }: DropdownProps) {
const containerRef = useRef<HTMLDivElement | null>(null);
const triggerRef = useRef<HTMLButtonElement | null>(null);
const dropdownContainerRef = useRef<HTMLUListElement | null>(null);
@@ -76,18 +74,12 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
setShown(true);
externalOnShown?.();
hideTooltip();
if (mobileBackdrop && isMobile()) {
document.getElementById("context-menu-cover")?.classList.add("show", "global-menu-cover");
}
}, [ hideTooltip, mobileBackdrop ]);
}, [ hideTooltip ]);
const onHidden = useCallback(() => {
setShown(false);
externalOnHidden?.();
if (mobileBackdrop && isMobile()) {
document.getElementById("context-menu-cover")?.classList.remove("show", "global-menu-cover");
}
}, [ mobileBackdrop ]);
}, []);
useEffect(() => {
if (!containerRef.current) return;

View File

@@ -3,9 +3,8 @@ import { useEffect, useRef } from "preact/hooks";
import ActionButton, { ActionButtonProps } from "./ActionButton";
import Button, { ButtonProps } from "./Button";
import { FormListItem, FormListItemOpts } from "./FormList";
export interface FormFileUploadProps {
interface FormFileUploadProps {
name?: string;
onChange: (files: FileList | null) => void;
multiple?: boolean;
@@ -76,25 +75,3 @@ export function FormFileUploadActionButton({ onChange, ...buttonProps }: Omit<Ac
</>
);
}
/**
* Similar to {@link FormFileUploadButton}, but uses an {@link FormListItem} instead of a normal {@link Button}.
* @param param the change listener for the file upload and the properties for the button.
*/
export function FormFileUploadFormListItem({ onChange, children, ...buttonProps }: Omit<FormListItemOpts, "onClick"> & Pick<FormFileUploadProps, "onChange">) {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<FormListItem
{...buttonProps}
onClick={() => inputRef.current?.click()}
>{children}</FormListItem>
<FormFileUpload
inputRef={inputRef}
hidden
onChange={onChange}
/>
</>
);
}

View File

@@ -27,13 +27,3 @@
}
}
}
.form-list-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
.form-list-col {
flex-grow: 1;
}
}

View File

@@ -78,7 +78,7 @@ export interface FormListBadge {
text: string;
}
export interface FormListItemOpts {
interface FormListItemOpts {
children: ComponentChildren;
icon?: string;
value?: string;

View File

@@ -1,12 +0,0 @@
import { ComponentChildren } from "preact";
import { isMobile } from "../../services/utils";
interface ResponsiveContainerProps {
mobile?: ComponentChildren;
desktop?: ComponentChildren;
}
export default function ResponsiveContainer({ mobile, desktop }: ResponsiveContainerProps) {
return (isMobile() ? mobile : desktop);
}

View File

@@ -1,4 +1,5 @@
import { MimeType, NoteType, ToggleInParentResponse } from "@triliumnext/commons";
import { ComponentChildren } from "preact";
import { createPortal } from "preact/compat";
import { Dispatch, StateUpdater, useCallback, useEffect, useMemo, useState } from "preact/hooks";
@@ -116,18 +117,19 @@ export function NoteTypeDropdownContent({ currentNoteType, currentNoteMime, note
onClick={() => changeNoteType(type, mime)}
>{title}</FormListItem>
);
} else {
return (
<>
<FormDropdownDivider />
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
);
}
return (
<>
<FormDropdownDivider />
<FormListItem
checked={checked}
disabled
>
<strong>{title}</strong>
</FormListItem>
</>
);
})}
{!noCodeNotes && <NoteTypeCodeNoteList mimeTypes={mimeTypes} changeNoteType={changeNoteType} setModalShown={setModalShown} />}
@@ -139,7 +141,7 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp
currentMimeType?: string;
mimeTypes: MimeType[];
changeNoteType(type: NoteType, mime: string): void;
setModalShown?(shown: boolean): void;
setModalShown(shown: boolean): void;
}) {
return (
<>
@@ -153,10 +155,8 @@ export function NoteTypeCodeNoteList({ currentMimeType, mimeTypes, changeNoteTyp
</FormListItem>
))}
{setModalShown && <>
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</>}
<FormDropdownDivider />
<FormListItem icon="bx bx-cog" onClick={() => setModalShown(true)}>{t("basic_properties.configure_code_notes")}</FormListItem>
</>
);
}
@@ -195,7 +195,7 @@ function ProtectedNoteSwitch({ note }: { note?: FNote | null }) {
onChange={(shouldProtect) => note && protected_session.protectNote(note.noteId, shouldProtect, false)}
/>
</div>
);
)
}
function EditabilitySelect({ note }: { note?: FNote | null }) {
@@ -417,9 +417,9 @@ function findTypeTitle(type?: NoteType, mime?: string | null) {
const found = mimeTypes.find((mt) => mt.mime === mime);
return found ? found.title : mime;
} else {
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
return noteType ? noteType.title : type;
}
const noteType = NOTE_TYPES.find((nt) => nt.type === type);
return noteType ? noteType.title : type;
}

View File

@@ -1,6 +1,6 @@
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
import { Dropdown as BootstrapDropdown } from "bootstrap";
import { ComponentChildren, RefObject } from "preact";
import { RefObject } from "preact";
import { useContext, useEffect, useRef } from "preact/hooks";
import appContext, { CommandNames } from "../../components/app_context";
@@ -22,7 +22,7 @@ import MovePaneButton from "../buttons/move_pane_button";
import ActionButton from "../react/ActionButton";
import Dropdown from "../react/Dropdown";
import { FormDropdownDivider, FormDropdownSubmenu, FormListHeader, FormListItem, FormListToggleableItem } from "../react/FormList";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useSyncedRef, useTriliumEvent, useTriliumOption } from "../react/hooks";
import { useIsNoteReadOnly, useNoteContext, useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { NoteTypeDropdownContent, useNoteBookmarkState, useShareState } from "./BasicPropertiesTab";
import NoteActionsCustom from "./NoteActionsCustom";
@@ -63,14 +63,8 @@ function RevisionsButton({ note }: { note: FNote }) {
type ItemToFocus = "basic-properties";
export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNoteSettings, dropdownRef: externalDropdownRef }: {
note: FNote,
noteContext?: NoteContext,
itemsAtStart?: ComponentChildren;
itemsNearNoteSettings?: ComponentChildren;
dropdownRef?: RefObject<BootstrapDropdown>;
}) {
const dropdownRef = useSyncedRef<BootstrapDropdown>(externalDropdownRef, null);
function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: NoteContext }) {
const dropdownRef = useRef<BootstrapDropdown>(null);
const parentComponent = useContext(ParentComponent);
const noteType = useNoteProperty(note, "type") ?? "";
const [viewType] = useNoteLabel(note, "viewType");
@@ -105,15 +99,12 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
dropdownRef={dropdownRef}
buttonClassName={ isNewLayout ? "bx bx-dots-horizontal-rounded" : "bx bx-dots-vertical-rounded" }
className="note-actions"
dropdownContainerClassName="mobile-bottom-menu"
hideToggleArrow
noSelectButtonStyle
noDropdownListStyle
iconAction
onHidden={() => itemToFocusRef.current = null }
mobileBackdrop
>
{itemsAtStart}
{isReadOnly && <>
<CommandItem icon="bx bx-pencil" text={t("read-only-info.edit-note")}
@@ -132,8 +123,6 @@ export function NoteContextMenu({ note, noteContext, itemsAtStart, itemsNearNote
<FormDropdownDivider />
</>}
{itemsNearNoteSettings}
<CommandItem icon="bx bx-import" text={t("note_actions.import_files")}
disabled={isInOptionsOrHelp || note.type === "search"}
command={() => parentComponent?.triggerCommand("showImportDialog", { noteId: note.noteId })} />
@@ -288,7 +277,7 @@ function DevelopmentActions({ note, noteContext }: { note: FNote, noteContext?:
);
}
export function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
function CommandItem({ icon, text, title, command, disabled }: { icon: string, text: string, title?: string, command: CommandNames | (() => void), disabled?: boolean, destructive?: boolean }) {
return <FormListItem
icon={icon}
title={title}

View File

@@ -1,3 +0,0 @@
body.mobile .note-actions-custom:not(:empty) {
margin-bottom: calc(var(--bs-dropdown-divider-margin-y) * 2);
}

View File

@@ -1,5 +1,3 @@
import "./NoteActionsCustom.css";
import { NoteType } from "@triliumnext/commons";
import { useContext, useEffect, useRef, useState } from "preact/hooks";
@@ -9,12 +7,11 @@ import FNote from "../../entities/fnote";
import { t } from "../../services/i18n";
import { getHelpUrlForNote } from "../../services/in_app_help";
import { downloadFileNote, openNoteExternally } from "../../services/open";
import { isMobile, openInAppHelpFromUrl } from "../../services/utils";
import { openInAppHelpFromUrl } from "../../services/utils";
import { ViewTypeOptions } from "../collections/interface";
import { buildSaveSqlToNoteHandler } from "../FloatingButtonsDefinitions";
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
import { FormFileUploadActionButton, FormFileUploadFormListItem, FormFileUploadProps } from "../react/FormFileUpload";
import { FormListItem } from "../react/FormList";
import ActionButton from "../react/ActionButton";
import { FormFileUploadActionButton } from "../react/FormFileUpload";
import { useNoteLabel, useNoteLabelBoolean, useNoteProperty, useTriliumEvent, useTriliumEvents, useTriliumOption } from "../react/hooks";
import { ParentComponent } from "../react/react_utils";
import { buildUploadNewFileRevisionListener } from "./FilePropertiesTab";
@@ -35,8 +32,6 @@ interface NoteActionsCustomInnerProps extends NoteActionsCustomProps {
viewType: ViewTypeOptions | null | undefined;
}
const cachedIsMobile = isMobile();
/**
* Part of {@link NoteActions} on the new layout, but are rendered with a slight spacing
* from the rest of the note items and the buttons differ based on the note type.
@@ -120,7 +115,7 @@ function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps
onChange: (files: FileList | null) => void;
}) {
return (
<NoteActionWithFileUpload
<FormFileUploadActionButton
icon="bx bx-folder-open"
text={t("image_properties.upload_new_revision")}
disabled={!note.isContentAvailable()}
@@ -130,8 +125,8 @@ function UploadNewRevisionButton({ note, onChange }: NoteActionsCustomInnerProps
}
function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
return (!cachedIsMobile &&
<NoteAction
return (
<ActionButton
icon="bx bx-link-external"
text={t("file_properties.open")}
disabled={note.isProtected}
@@ -142,7 +137,7 @@ function OpenExternallyButton({ note, noteMime }: NoteActionsCustomInnerProps) {
function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomInnerProps) {
return (
<NoteAction
<ActionButton
icon="bx bx-download"
text={t("file_properties.download")}
disabled={!note.isContentAvailable()}
@@ -154,7 +149,7 @@ function DownloadFileButton({ note, parentComponent, ntxId }: NoteActionsCustomI
//#region Floating buttons
function CopyReferenceToClipboardButton({ ntxId, noteType, parentComponent }: NoteActionsCustomInnerProps) {
return (["mermaid", "canvas", "mindMap", "image"].includes(noteType) &&
<NoteAction
<ActionButton
text={t("image_properties.copy_reference_to_clipboard")}
icon="bx bx-copy"
onClick={() => parentComponent?.triggerEvent("copyImageReferenceToClipboard", { ntxId })}
@@ -166,7 +161,7 @@ function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, not
const isEnabled = (note.noteId === "_backendLog" || noteType === "render") && isDefaultViewMode;
return (isEnabled &&
<NoteAction
<ActionButton
text={t("backend_log.refresh")}
icon="bx bx-refresh"
onClick={() => parentComponent.triggerEvent("refreshData", { ntxId: noteContext.ntxId })}
@@ -175,11 +170,11 @@ function RefreshButton({ note, noteType, isDefaultViewMode, parentComponent, not
}
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: NoteActionsCustomInnerProps) {
const isShown = note.type === "mermaid" && !cachedIsMobile && note.isContentAvailable() && isDefaultViewMode;
const isShown = note.type === "mermaid" && note.isContentAvailable() && isDefaultViewMode;
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
return isShown && <NoteAction
return isShown && <ActionButton
text={upcomingOrientation === "vertical" ? t("switch_layout_button.title_vertical") : t("switch_layout_button.title_horizontal")}
icon={upcomingOrientation === "vertical" ? "bx bxs-dock-bottom" : "bx bxs-dock-left"}
onClick={() => setSplitEditorOrientation(upcomingOrientation)}
@@ -193,7 +188,7 @@ export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCus
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || isSavedSqlite)
&& note.isContentAvailable() && isDefaultViewMode;
return isEnabled && <NoteAction
return isEnabled && <ActionButton
text={isReadOnly ? t("toggle_read_only_button.unlock-editing") : t("toggle_read_only_button.lock-editing")}
icon={isReadOnly ? "bx bx-lock-open-alt" : "bx bx-lock-alt"}
onClick={() => setReadOnly(!isReadOnly)}
@@ -202,7 +197,7 @@ export function ToggleReadOnlyButton({ note, isDefaultViewMode }: NoteActionsCus
function RunActiveNoteButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript") || noteMime === "text/x-sqlite;schema=trilium";
return isEnabled && <NoteAction
return isEnabled && <ActionButton
icon="bx bx-play"
text={t("code_buttons.execute_button_title")}
triggerCommand="runActiveNote"
@@ -223,7 +218,7 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
}
});
return isEnabled && <NoteAction
return isEnabled && <ActionButton
icon="bx bx-save"
text={t("code_buttons.save_to_note_button_title")}
onClick={buildSaveSqlToNoteHandler(note)}
@@ -232,7 +227,7 @@ function SaveToNoteButton({ note, noteMime }: NoteActionsCustomInnerProps) {
function OpenTriliumApiDocsButton({ noteMime }: NoteActionsCustomInnerProps) {
const isEnabled = noteMime.startsWith("application/javascript;env=");
return isEnabled && <NoteAction
return isEnabled && <ActionButton
icon="bx bx-help-circle"
text={t("code_buttons.trilium_api_docs_button_title")}
onClick={() => openInAppHelpFromUrl(noteMime.endsWith("frontend") ? "Q2z6av6JZVWm" : "MEtfsqa5VwNi")}
@@ -244,7 +239,7 @@ function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
const isEnabled = !!helpUrl;
return isEnabled && (
<NoteAction
<ActionButton
icon="bx bx-help-circle"
text={t("help-button.title")}
onClick={() => helpUrl && openInAppHelpFromUrl(helpUrl)}
@@ -254,7 +249,7 @@ function InAppHelpButton({ note }: NoteActionsCustomInnerProps) {
function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteActionsCustomInnerProps) {
if (noteType === "relationMap") {
return <NoteAction
return <ActionButton
icon="bx bx-folder-plus"
text={t("relation_map_buttons.create_child_note_title")}
onClick={() => parentComponent.triggerEvent("relationMapCreateChildNote", { ntxId })}
@@ -263,19 +258,3 @@ function AddChildButton({ parentComponent, noteType, ntxId, isReadOnly }: NoteAc
}
}
//#endregion
function NoteAction({ text, ...props }: Pick<ActionButtonProps, "text" | "icon" | "disabled" | "triggerCommand"> & {
onClick?: ((e: MouseEvent) => void) | undefined;
}) {
return (cachedIsMobile
? <FormListItem {...props}>{text}</FormListItem>
: <ActionButton text={text} {...props} />
);
}
function NoteActionWithFileUpload({ text, ...props }: Pick<ActionButtonProps, "text" | "icon" | "disabled" | "triggerCommand"> & Pick<FormFileUploadProps, "onChange">) {
return (cachedIsMobile
? <FormFileUploadFormListItem {...props}>{text}</FormFileUploadFormListItem>
: <FormFileUploadActionButton text={text} {...props} />
);
}

View File

@@ -1,63 +0,0 @@
body.experimental-feature-new-layout .note-paths-widget {
.note-path-intro {
color: var(--muted-text-color);
}
.note-path-list {
margin: 12px 0;
padding: 0;
list-style: none;
/* Note path card */
li {
--border-radius: 6px;
position: relative;
background: var(--card-background-color);
padding: 8px 20px 8px 25px;
&:first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
&:last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
& + li {
margin-top: 2px;
}
/* Current path arrow */
&.path-current::before {
position: absolute;
display: flex;
justify-content: flex-end;
align-items: center;
content: "\ee8f";
top: 0;
left: 0;
width: 20px;
bottom: 0;
font-family: "boxicons";
font-size: .75em;
color: var(--menu-item-icon-color);
}
}
/* Note path segment */
a {
margin-inline: 2px;
padding-inline: 2px;
color: currentColor;
font-weight: normal;
text-decoration: none;
/* The last segment of the note path */
&.basename {
color: var(--muted-text-color);
}
}
}
}

View File

@@ -1,16 +1,15 @@
import "./NotePathsTab.css";
import clsx from "clsx";
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
import { useEffect, useMemo, useState } from "preact/hooks";
import FNote, { NotePathRecord } from "../../entities/fnote";
import { t } from "../../services/i18n";
import { NOTE_PATH_TITLE_SEPARATOR } from "../../services/tree";
import { useTriliumEvent } from "../react/hooks";
import LinkButton from "../react/LinkButton";
import NoteLink from "../react/NoteLink";
import { joinElements, ParentComponent } from "../react/react_utils";
import { joinElements } from "../react/react_utils";
import { TabContext } from "./ribbon-interface";
import LinkButton from "../react/LinkButton";
import clsx from "clsx";
export default function NotePathsTab({ note, hoistedNoteId, notePath }: TabContext) {
const sortedNotePaths = useSortedNotePaths(note, hoistedNoteId);
@@ -21,7 +20,6 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
sortedNotePaths: NotePathRecord[] | undefined;
currentNotePath?: string | null | undefined;
}) {
const parentComponent = useContext(ParentComponent);
return (
<div class="note-paths-widget">
<>
@@ -41,7 +39,7 @@ export function NotePathsWidget({ sortedNotePaths, currentNotePath }: {
<LinkButton
text={t("note_paths.clone_button")}
onClick={() => parentComponent?.triggerCommand("cloneNoteIdsTo")}
triggerCommand="cloneNoteIdsTo"
/>
</>
</div>
@@ -114,9 +112,9 @@ function NotePath({ currentNotePath, notePathRecord }: { currentNotePath?: strin
<li class={classes}>
{joinElements(fullNotePaths.map((notePath, index, arr) => (
<NoteLink key={notePath}
className={clsx({"basename": (index === arr.length - 1)})}
notePath={notePath}
noPreview />
className={clsx({"basename": (index === arr.length - 1)})}
notePath={notePath}
noPreview />
)), NOTE_PATH_TITLE_SEPARATOR)}
{icons.map(({ icon, title }) => (

View File

@@ -2,7 +2,6 @@ import "./SearchDefinitionTab.css";
import { SaveSearchNoteResponse } from "@triliumnext/commons";
import { useContext, useEffect, useState } from "preact/hooks";
import { Fragment } from "preact/jsx-runtime";
import appContext from "../../components/app_context";
import FNote from "../../entities/fnote";
@@ -16,13 +15,12 @@ import tree from "../../services/tree";
import { getErrorMessage } from "../../services/utils";
import ws from "../../services/ws";
import RenameNoteBulkAction from "../bulk_actions/note/rename_note";
import Button, { SplitButton } from "../react/Button";
import Button from "../react/Button";
import Dropdown from "../react/Dropdown";
import { FormListHeader, FormListItem } from "../react/FormList";
import { useTriliumEvent } from "../react/hooks";
import Icon from "../react/Icon";
import { ParentComponent } from "../react/react_utils";
import ResponsiveContainer from "../react/ResponsiveContainer";
import { TabContext } from "./ribbon-interface";
import { SEARCH_OPTIONS, SearchOption } from "./SearchDefinitionOptions";
@@ -86,32 +84,15 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
<tr>
<td className="title-column">{t("search_definition.add_search_option")}</td>
<td colSpan={2} className="add-search-option">
<ResponsiveContainer
desktop={searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<Button
key={`${attributeType}-${attributeName}`}
size="small" icon={icon} text={label} title={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
mobile={
<Dropdown
buttonClassName="action-add-toggle btn btn-sm"
text={<><Icon icon="bx bx-plus" />{" "}{t("search_definition.option")}</>}
dropdownContainerClassName="mobile-bottom-menu" mobileBackdrop
noSelectButtonStyle
>
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<FormListItem
key={`${attributeType}-${attributeName}`}
icon={icon}
description={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
>{label}</FormListItem>
))}
</Dropdown>
}
/>
{searchOptions?.availableOptions.map(({ icon, label, tooltip, attributeName, attributeType, defaultValue }) => (
<Button
size="small"
icon={icon}
text={label}
title={tooltip}
onClick={() => attributes.setAttribute(note, attributeType, attributeName, defaultValue ?? "")}
/>
))}
<AddBulkActionButton note={note} />
</td>
@@ -132,7 +113,48 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
})}
</tbody>
<BulkActionsList note={note} />
<SearchButtonBar note={note} refreshResults={refreshResults} />
<tbody className="search-actions">
<tr>
<td colSpan={3}>
<div className="search-actions-container">
<Button
icon="bx bx-search"
text={t("search_definition.search_button")}
keyboardShortcut="Enter"
onClick={refreshResults}
/>
<Button
icon="bx bxs-zap"
text={t("search_definition.search_execute")}
onClick={async () => {
await server.post(`search-and-execute-note/${note.noteId}`);
refreshResults();
toast.showMessage(t("search_definition.actions_executed"), 3000);
}}
/>
{note.isHiddenCompletely() && <Button
icon="bx bx-save"
text={t("search_definition.save_to_note")}
onClick={async () => {
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
if (!notePath) {
return;
}
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
}}
/>}
</div>
</td>
</tr>
</tbody>
</table>
)}
</div>
@@ -140,56 +162,6 @@ export default function SearchDefinitionTab({ note, ntxId, hidden }: Pick<TabCon
);
}
function SearchButtonBar({ note, refreshResults }: {
note: FNote;
refreshResults(): void;
}) {
async function searchAndExecuteActions() {
await server.post(`search-and-execute-note/${note.noteId}`);
refreshResults();
toast.showMessage(t("search_definition.actions_executed"), 3000);
}
async function saveSearchNote() {
const { notePath } = await server.post<SaveSearchNoteResponse>("special-notes/save-search-note", { searchNoteId: note.noteId });
if (!notePath) return;
await ws.waitForMaxKnownEntityChangeId();
await appContext.tabManager.getActiveContext()?.setNote(notePath);
// Note the {{- notePathTitle}} in json file is not typo, it's unescaping
// See https://www.i18next.com/translation-function/interpolation#unescape
toast.showMessage(t("search_definition.search_note_saved", { notePathTitle: await tree.getNotePathTitle(notePath) }));
}
return (
<tbody className="search-actions">
<tr>
<td colSpan={3}>
<ResponsiveContainer
desktop={
<div className="search-actions-container">
<Button icon="bx bx-search" text={t("search_definition.search_button")} keyboardShortcut="Enter" onClick={refreshResults} />
<Button icon="bx bxs-zap" text={t("search_definition.search_execute")} onClick={searchAndExecuteActions} />
{note.isHiddenCompletely() && <Button icon="bx bx-save" text={t("search_definition.save_to_note")} onClick={saveSearchNote} />}
</div>
}
mobile={
<SplitButton
text={t("search_definition.search_button")} icon="bx bx-search"
onClick={refreshResults}
>
<FormListItem icon="bx bxs-zap" onClick={searchAndExecuteActions}>{t("search_definition.search_execute")}</FormListItem>
{note.isHiddenCompletely() && <FormListItem icon="bx bx-save" onClick={saveSearchNote}>{t("search_definition.save_to_note")}</FormListItem>}
</SplitButton>
}
/>
</td>
</tr>
</tbody>
);
}
function BulkActionsList({ note }: { note: FNote }) {
const [ bulkActions, setBulkActions ] = useState<RenameNoteBulkAction[]>();
@@ -222,18 +194,15 @@ function AddBulkActionButton({ note }: { note: FNote }) {
buttonClassName="action-add-toggle btn btn-sm"
text={<><Icon icon="bx bxs-zap" />{" "}{t("search_definition.action")}</>}
noSelectButtonStyle
dropdownContainerClassName="mobile-bottom-menu" mobileBackdrop
>
{ACTION_GROUPS.map(({ actions, title }, index) => (
<Fragment key={index}>
{ACTION_GROUPS.map(({ actions, title }) => (
<>
<FormListHeader text={title} />
<div>
{actions.map(({ actionName, actionTitle }) => (
<FormListItem key={actionName} onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
))}
</div>
</Fragment>
{actions.map(({ actionName, actionTitle }) => (
<FormListItem onClick={() => bulk_action.addAction(note.noteId, actionName)}>{actionTitle}</FormListItem>
))}
</>
))}
</Dropdown>
);

View File

@@ -10,12 +10,6 @@
display: flex;
justify-content: space-between;
align-items: baseline;
@media (max-width: 991px) {
margin-block: 1em;
flex-direction: column;
gap: 0.5em;
}
}
/* #endregion */
@@ -48,12 +42,6 @@
display: flex;
align-items: center;
gap: 1em;
@media (max-width: 991px) {
gap: 0.5em;
flex-wrap: wrap;
.attachment-title { flex-grow: 1; }
}
}
.attachment-details {
@@ -139,6 +127,10 @@
/* #region Attachment actions */
.attachment-actions .dropdown-menu {
width: 20em;
}
.attachment-actions .dropdown-item .tn-icon {
position: relative;
top: 3px;

View File

@@ -239,8 +239,6 @@ function AttachmentActions({ attachment, copyAttachmentLinkToClipboard }: { atta
text={<Icon icon="bx bx-dots-vertical-rounded" />}
buttonClassName="icon-action-always-border"
iconAction
dropdownContainerClassName="mobile-bottom-menu"
mobileBackdrop
>
<FormListItem
icon="bx bx-file-find"

View File

@@ -34,7 +34,6 @@ const LOCALE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Options["locale"] | null>
"en-GB": "en",
es: "es",
fr: "fr",
ga: null,
it: "it",
ja: "ja",
pt: "pt",

View File

@@ -2,7 +2,6 @@ import { LOCALES } from "@triliumnext/commons";
import { readdirSync } from "fs";
import { join } from "path";
import { describe, expect, it } from "vitest";
import { LANGUAGE_MAPPINGS } from "./i18n.js";
const localeDir = join(__dirname, "../../../../../../node_modules/@excalidraw/excalidraw/dist/prod/locales");
@@ -22,9 +21,9 @@ describe("Canvas i18n", () => {
for (const locale of LOCALES) {
if (locale.contentOnly || locale.devOnly) continue;
const languageCode = LANGUAGE_MAPPINGS[locale.id];
if (languageCode && !supportedLanguageCodes.has(languageCode)) {
if (!supportedLanguageCodes.has(languageCode)) {
console.log("Supported locales:", Array.from(supportedLanguageCodes.values()).join(", "));
expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`);
expect.fail(`Unable to find locale for ${locale.id} -> ${languageCode}.`)
}
}
});

View File

@@ -10,7 +10,6 @@ export const LANGUAGE_MAPPINGS: Record<DISPLAYABLE_LOCALE_IDS, Language["code"]
en_rtl: "en",
es: "es-ES",
fr: "fr-FR",
ga: null,
it: "it-IT",
ja: "ja-JP",
pt: "pt-PT",

View File

@@ -388,10 +388,10 @@ function Performance() {
currentValue={shadowsEnabled} onChange={setShadowsEnabled}
/>
{!isMobile() && <FormCheckbox
<FormCheckbox
label={t("ui-performance.enable-backdrop-effects")}
currentValue={backdropEffectsEnabled} onChange={setBackdropEffectsEnabled}
/>}
/>
{isElectron() && <SmoothScrollEnabledOption />}

View File

@@ -29,7 +29,7 @@ describe("CK config", () => {
if (expectedLocale === "cn") expectedLocale = "zh";
if (expectedLocale === "tw") expectedLocale = "zh-tw";
if (locale.id !== "en" && locale.id !== "ga") {
if (locale.id !== "en") {
expect((config.language as any).ui).toMatch(new RegExp(`^${expectedLocale}`));
expect(config.translations, locale.id).toBeDefined();
expect(config.translations, locale.id).toHaveLength(2);

View File

@@ -1,10 +1,8 @@
import "./mobile_editor_toolbar.css";
import { CKTextEditor, ClassicEditor } from "@triliumnext/ckeditor5";
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
import { useNoteContext, useTriliumEvent } from "../../react/hooks";
import "./mobile_editor_toolbar.css";
import { isIOS } from "../../../services/utils";
import { useIsNoteReadOnly, useNoteContext, useNoteProperty, useTriliumEvent } from "../../react/hooks";
import { CKTextEditor, ClassicEditor } from "@triliumnext/ckeditor5";
interface MobileEditorToolbarProps {
inPopupEditor?: boolean;
@@ -19,13 +17,17 @@ interface MobileEditorToolbarProps {
export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolbarProps) {
const containerRef = useRef<HTMLDivElement>(null);
const { note, noteContext, ntxId } = useNoteContext();
const noteType = useNoteProperty(note, "type");
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
const shouldDisplay = noteType === "text" && isReadOnly === false;
const [ shouldDisplay, setShouldDisplay ] = useState(false);
const [ dropdownActive, setDropdownActive ] = useState(false);
usePositioningOniOS(!inPopupEditor, containerRef);
useEffect(() => {
noteContext?.isReadOnly().then(isReadOnly => {
setShouldDisplay(note?.type === "text" && !isReadOnly);
});
}, [ note ]);
// Attach the toolbar from the CKEditor.
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
if (eventNtxId !== ntxId || !containerRef.current) return;
@@ -60,15 +62,15 @@ export default function MobileEditorToolbar({ inPopupEditor }: MobileEditorToolb
return (
<div className={`classic-toolbar-outer-container ${!shouldDisplay ? "hidden-ext" : "visible"} ${isIOS() ? "ios" : ""}`}>
<div ref={containerRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`} />
<div ref={containerRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`}></div>
</div>
);
)
}
function usePositioningOniOS(enabled: boolean, wrapperRef: MutableRef<HTMLDivElement | null>) {
const adjustPosition = useCallback(() => {
if (!wrapperRef.current) return;
const bottom = window.innerHeight - (window.visualViewport?.height || 0);
let bottom = window.innerHeight - (window.visualViewport?.height || 0);
wrapperRef.current.style.bottom = `${bottom}px`;
}, []);

1
apps/icon-pack-builder/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
*.zip

View File

@@ -1,5 +1,5 @@
import { createWriteStream, mkdirSync, writeFileSync } from "node:fs";
import { join, resolve } from "node:path";
import { createWriteStream, mkdirSync } from "node:fs";
import { join } from "node:path";
import cls from "@triliumnext/server/src/services/cls.js";
@@ -13,8 +13,7 @@ process.env.TRILIUM_RESOURCE_DIR = "../server/src";
process.env.NODE_ENV = "development";
async function main() {
const outputDir = join(__dirname, "../../website/public/resources/icon-packs");
const outputMetaDir = join(__dirname, "../../website/src/resources/icon-packs");
const outputDir = join(__dirname, "output");
mkdirSync(outputDir, { recursive: true });
const i18n = await import("@triliumnext/server/src/services/i18n.js");
@@ -50,8 +49,7 @@ async function main() {
});
// Export to zip.
const zipFileName = `${iconPack.name}.zip`;
const zipFilePath = join(outputDir, zipFileName);
const zipFilePath = join(outputDir, `${iconPack.name}.zip`);
const fileOutputStream = createWriteStream(zipFilePath);
const { exportToZip } = (await import("@triliumnext/server/src/services/export/zip.js")).default;
const taskContext = new (await import("@triliumnext/server/src/services/task_context.js")).default(
@@ -60,15 +58,7 @@ async function main() {
await exportToZip(taskContext, branch, "html", fileOutputStream, false, { skipExtraFiles: true });
await new Promise<void>((resolve) => { fileOutputStream.on("finish", resolve); });
// Save meta.
const metaFilePath = join(outputMetaDir, `${iconPack.name}.json`);
writeFileSync(metaFilePath, JSON.stringify({
name: iconPack.name,
file: zipFileName,
...iconPack.meta
}, null, 2));
console.log(`Built icon pack ${iconPack.name}.`);
console.log(`Built icon pack: ${iconPack.name} (${zipFilePath})`);
}
const builtIconPacks = [
@@ -79,8 +69,6 @@ async function main() {
phosphor("fill")
];
await Promise.all(builtIconPacks.map(buildIconPack));
console.log(`\n✅ Built icon packs are available at ${resolve(outputDir)}.`);
}
cls.init(() => {

View File

@@ -9,10 +9,5 @@ export interface IconPackData {
name: string;
mime: string;
content: Buffer;
},
meta: {
version: string;
website: string;
description: string;
}
}

View File

@@ -29,7 +29,7 @@ export default function buildIcons(pack: "basic" | "brands"): IconPackData {
return {
name: pack === "basic" ? "Boxicons 3 (Basic)" : "Boxicons 3 (Brands)",
prefix: pack === "basic" ? "bx3" : "bxl3",
icon: pack === "basic" ? "bx3 bx-cube" : "bxl3 bx-boxicons",
icon: pack === "basic" ? "bx3 bx-cube" : "bxl3 bxl-boxicons",
fontFile: {
name: `${fileName}.woff2`,
mime: "font/woff2",
@@ -37,13 +37,6 @@ export default function buildIcons(pack: "basic" | "brands"): IconPackData {
},
manifest: {
icons
},
meta: {
version: "3.0.0",
website: "https://boxicons.com/",
description: pack === "basic"
? "The Basic set of icons from Boxicons v3. This is an upgrade from Trilium's built-in icon pack (Boxicons v2)."
: "The brand set of icons from Boxicons v3."
}
};
}

View File

@@ -7,7 +7,6 @@ import { extractClassNamesFromCss, getModulePath } from "../utils";
export default function buildIcons(): IconPackData {
const baseDir = getModulePath("@mdi/font");
const packageJson = JSON.parse(readFileSync(join(baseDir, "package.json"), "utf-8"));
const cssFilePath = join(baseDir, "css", "materialdesignicons.min.css");
const cssFileContent = readFileSync(cssFilePath, "utf-8");
@@ -22,11 +21,6 @@ export default function buildIcons(): IconPackData {
name: "materialdesignicons-webfont.woff2",
mime: "font/woff2",
content: readFileSync(join(baseDir, "fonts", "materialdesignicons-webfont.woff2"))
},
meta: {
version: packageJson.version,
website: "https://pictogrammers.com/library/mdi/",
description: "The community Material Design Icons pack (@mdi/font). Not to be confused with Google's own Material Design Icons."
}
};
}

View File

@@ -5,9 +5,7 @@ import { IconPackData } from "../provider";
import { getModulePath } from "../utils";
export default function buildIcons(packName: "regular" | "fill"): IconPackData {
const moduleDir = getModulePath("@phosphor-icons/web");
const baseDir = join(moduleDir, "src", packName);
const packageJson = JSON.parse(readFileSync(join(moduleDir, "package.json"), "utf-8"));
const baseDir = join(getModulePath("@phosphor-icons/web"), "src", packName);
const iconIndex = JSON.parse(readFileSync(join(baseDir, "selection.json"), "utf-8"));
const icons: IconPackData["manifest"]["icons"] = {};
@@ -43,13 +41,6 @@ export default function buildIcons(packName: "regular" | "fill"): IconPackData {
name: fontFile!,
mime: "font/woff2",
content: readFileSync(join(baseDir, fontFile!))
},
meta: {
version: packageJson.version,
website: "https://phosphoricons.com/",
description: packName === "regular"
? "The regular weight version of Phosphor Icons."
: "The filled version of Phosphor Icons."
}
};
}

View File

@@ -362,31 +362,6 @@ paths:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/{noteId}/attachments:
parameters:
- name: noteId
in: path
required: true
schema:
$ref: "#/components/schemas/EntityId"
get:
description: Returns all attachments for a note identified by its ID
operationId: getNoteAttachments
responses:
"200":
description: list of attachments
content:
application/json; charset=utf-8:
schema:
type: array
items:
$ref: "#/components/schemas/Attachment"
default:
description: unexpected error
content:
application/json; charset=utf-8:
schema:
$ref: "#/components/schemas/Error"
/notes/{noteId}/undelete:
parameters:
- name: noteId

View File

@@ -1,82 +0,0 @@
import { Application } from "express";
import { beforeAll, describe, expect, it } from "vitest";
import supertest from "supertest";
import { createNote, login } from "./utils.js";
import config from "../../src/services/config.js";
let app: Application;
let token: string;
const USER = "etapi";
let createdNoteId: string;
let createdAttachmentId: string;
describe("etapi/get-note-attachments", () => {
beforeAll(async () => {
config.General.noAuthentication = false;
const buildApp = (await (import("../../src/app.js"))).default;
app = await buildApp();
token = await login(app);
createdNoteId = await createNote(app, token);
// Create an attachment for the note
const response = await supertest(app)
.post(`/etapi/attachments`)
.auth(USER, token, { "type": "basic" })
.send({
"ownerId": createdNoteId,
"role": "file",
"mime": "text/plain",
"title": "test-attachment.txt",
"content": "test content",
"position": 10
});
createdAttachmentId = response.body.attachmentId;
expect(createdAttachmentId).toBeTruthy();
});
it("gets attachments for a note", async () => {
const response = await supertest(app)
.get(`/etapi/notes/${createdNoteId}/attachments`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBeGreaterThan(0);
const attachment = response.body[0];
expect(attachment).toHaveProperty("attachmentId", createdAttachmentId);
expect(attachment).toHaveProperty("ownerId", createdNoteId);
expect(attachment).toHaveProperty("role", "file");
expect(attachment).toHaveProperty("mime", "text/plain");
expect(attachment).toHaveProperty("title", "test-attachment.txt");
expect(attachment).toHaveProperty("position", 10);
expect(attachment).toHaveProperty("blobId");
expect(attachment).toHaveProperty("dateModified");
expect(attachment).toHaveProperty("utcDateModified");
expect(attachment).toHaveProperty("contentLength");
});
it("returns empty array for note with no attachments", async () => {
// Create a new note without any attachments
const newNoteId = await createNote(app, token, "Note without attachments");
const response = await supertest(app)
.get(`/etapi/notes/${newNoteId}/attachments`)
.auth(USER, token, { "type": "basic" })
.expect(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body.length).toBe(0);
});
it("returns 404 for non-existent note", async () => {
const response = await supertest(app)
.get("/etapi/notes/nonexistentnote/attachments")
.auth(USER, token, { "type": "basic" })
.expect(404);
expect(response.body.code).toStrictEqual("NOTE_NOT_FOUND");
});
});

View File

@@ -354,13 +354,6 @@
<td>Hides the&nbsp;<a class="reference-link" href="#root/_help_0ESUbbAxVnoK">Note List</a>&nbsp;for
that particular note.</td>
</tr>
<tr>
<td><code>subtreeHidden</code>
</td>
<td>Hides all child notes of this note from the tree, displaying a badge with
the count of hidden children. Children remain accessible via search or
direct links.</td>
</tr>
<tr>
<td><code>printLandscape</code>
</td>
@@ -379,11 +372,6 @@
<a
class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>.</td>
</tr>
<tr>
<td><code>map:*</code>
</td>
<td>Defines specific options for the&nbsp;<a class="reference-link" href="#root/_help_81SGnPGMk7Xc">Geo Map</a>.</td>
</tr>
<tr>
<td><code>calendar:*</code>
</td>

View File

@@ -26,35 +26,10 @@
<h2>Automatically download and install the latest nightly</h2>
<p>This is pretty useful if you are a beta tester that wants to periodically
update their version:</p>
<h2>On Ubuntu (Bash)</h2><pre><code class="language-text-x-sh">#!/usr/bin/env bash
<p>On Ubuntu:</p><pre><code class="language-text-x-trilium-auto">#!/usr/bin/env bash
name=TriliumNotes-linux-x64-nightly.deb
rm -f $name*
wget https://github.com/TriliumNext/Trilium/releases/download/nightly/$name
sudo apt-get install ./$name
rm $name</code></pre>
<h2>On Windows (PowerShell)</h2><pre><code class="language-application-x-powershell">if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
$arch = "arm64";
} else {
$arch = "x64";
}
$exeUrl = "https://github.com/TriliumNext/Trilium/releases/download/nightly/TriliumNotes-main-windows-$($arch).exe";
Write-Host "Downloading $($exeUrl)"
# Generate a unique path in the temp dir
$guid = [guid]::NewGuid().ToString()
$destination = Join-Path -Path $env:TEMP -ChildPath "$guid.exe"
try {
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri $exeUrl -OutFile $destination
$process = Start-Process -FilePath $destination
} catch {
Write-Error "An error occurred: $_"
} finally {
# Clean up
if (Test-Path $destination) {
Remove-Item -Path $destination -Force
}
}</code></pre>
rm $name</code></pre>

View File

@@ -1,18 +1,12 @@
<p>Frequently used notes can be bookmarked, which will make them appear in
the&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;for
easy access.</p>
<h2>Configuring the launch bar</h2>
<p>If bookmarks don't appear in the launch bar, then most likely the bookmark
section has been hidden. Go to the&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;configuration
from the&nbsp;<a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and
ensure <em>Bookmarks</em> is in the <em>Visible Launchers</em> section.</p>
<p>To easily access selected notes, you can bookmark them. See demo:</p>
<p>
<img src="Bookmarks_bookmarks.gif">
</p>
<h2>Bookmark folder</h2>
<p>Space in the left panel is limited, and you might want to bookmark many
items. One possible solution is to bookmark a folder, so it shows its children:</p>
<p>To do this, bookmark a folder and assign it the <code spellcheck="false">#bookmarkFolder</code> label.</p>
<h2>Mobile</h2>
<p>On mobile, bookmarks are only displayed starting with v0.102.0. Because
of the more constrained screen size, the bookmarks are grouped under a
single icon instead of displaying them as separate icons.</p>
<p>When pressed, a menu will appear listing all the bookmarks. Bookmark folders
are also supported and will appear as sub-menus.</p>
<p>
<img src="Bookmarks_bookmark-folder.png">
</p>
<p>To do this, you need to add a <code spellcheck="false">#bookmarkFolder</code> label
to the note.</p>

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 KiB

View File

@@ -1,85 +1,73 @@
<figure class="image image-style-align-right image_resized" style="width:45.14%;">
<img style="aspect-ratio:854/649;" src="Icon Packs_image.png"
width="854" height="649">
</figure>
<p>By default, Trilium comes with a set of icons called Boxicons v2. Since
v0.102.0, custom icon packs allow a wider selection of icons for notes.</p>
<h2>Importing an existing icon pack</h2>
<p>Icon packs are specific to Trilium, so they must either be created from
scratch (see below) or imported from a ZIP file from a third-party developer.</p>
<h2>Sample icon packs</h2>
<p>The Trilium team maintains a few icon packs that are not shipped with
Trilium. These icon packs can be found on the official website on the
<a
href="https://triliumnotes.org/resources">Resources page</a>.</p>
<h2>Importing an existing icon pack</h2>
<aside class="admonition note">
<aside
class="admonition note">
<p><strong>Icon packs are third-party content</strong>
</p>
<p>Apart from the <a href="https://triliumnotes.org/resources">sample icon packs</a>,
the Trilium maintainers are not responsible for keeping icon packs up to
date. If you have an issue with a specific icon pack, then the issue must
be reported to the third-party developer responsible for it, not the Trilium
team.</p>
</aside>
<p>To import an icon pack:</p>
<ol>
<li>Ideally, create a dedicated spot in your note tree where to place the
icon packs.</li>
<li>Right click the note where to put it and select <em>Import into note</em>.</li>
<li
>Uncheck <em>Safe import</em>.</li>
<p>The Trilium maintainers are not responsible for keeping these icon packs
up to date. If you have an issue with a specific icon pack, then the issue
must be reported to the third-party developer responsible for it, not the
Trilium team.</p>
</aside>
<p>To import an icon pack:</p>
<ol>
<li>Ideally, create a dedicated spot in your note tree where to place the
icon packs.</li>
<li>Right click the note where to put it and select <em>Import into note</em>.</li>
<li>Uncheck <em>Safe import</em>.</li>
<li>Select <em>Import</em>.</li>
<li><a href="#root/_help_s8alTXmpFR61">Refresh the application</a>.</li>
</ol>
<aside class="admonition warning">
<p>Since <em>Safe import</em> is disabled, make sure you trust the source as
it could contain dangerous third-party scripts. One good way to check if
the icon pack is safe is to manually extract the .zip and inspect the file
contents. Icon packs should only contain a font file and a JSON file. Other
files (especially scripts) are to be considered harmful.</p>
</aside>
<h2>Creating an icon pack</h2>
<p>Creating an icon pack requires some scripting knowledge outside Trilium
in order to generate the list of icons. For information, see&nbsp;<a class="reference-link"
href="#root/_help_g1mlRoU8CsqC">Creating an icon pack</a>.</p>
<h2>Using an icon from an icon pack</h2>
<p>After <a href="#root/_help_s8alTXmpFR61">refreshing the application</a>, the
icon pack should be enabled by default. To test this, simply select an
existing note or create a new one and try to change the note icon.</p>
<p>There should be a <em>Filter</em> button to the right of the search bar
in the icon list. Clicking it allows filtering by icon pack and the newly
imported icon pack should be displayed there.</p>
<aside class="admonition note">
<p>If the icon pack is missing from that list, then most likely there's something
wrong with it.</p>
<ul>
<li>Try checking the&nbsp;<a class="reference-link" href="#root/_help_bnyigUA2UK7s">Backend (server) logs</a>&nbsp;for
clues and make sure that the icon pack has the <code spellcheck="false">#iconPack</code>
<a
href="#root/_help_HI6GBBIduIgv">label</a>with a value assigned to it (a prefix).</li>
<li>Icon packs that are <a href="#root/_help_bwg0e8ewQMak">protected</a> are ignored.</li>
</ul>
</aside>
<h2>Integration with the share and export functionality</h2>
<p>Custom icon packs are also supported by the&nbsp;<a class="reference-link"
href="#root/_help_R9pX4DGra2Vt">Sharing</a>&nbsp;feature, where they will be
shown in the note tree. However, in order for an icon pack to be visible
to the share function, the icon pack note must also be shared.</p>
<p>If you are using a custom share theme, make sure it supports the
<code
spellcheck="false">iconPackCss</code>, otherwise icons will not show up. Check the original
share template source code for reference.</p>
<p>Custom icon packs will also be preserved when&nbsp;<a class="reference-link"
href="#root/_help_ycBFjKrrwE9p">Exporting static HTML for web publishing</a>.
In this case, there's no requirement to make the icon pack shared.</p>
<h2>What happens if I remove an icon pack</h2>
<p>If an icon pack is removed or disabled (by removing or altering its
<code
spellcheck="false">#iconPack</code>label), all the notes that use this icon pack will show
in the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;with
no icon. This won't cause any issues apart from looking strange.</p>
<p>The solution is to replace the icons with some else, try using&nbsp;
<a
class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>&nbsp;which supports bulk actions, to identify the notes with
the now deleted icon pack (by looking for the prefix) and changing or removing
their <code spellcheck="false">iconClass</code>.</p>
</ol>
<aside class="admonition warning">
<p>Since <em>Safe import</em> is disabled, make sure you trust the source as
it could contain dangerous third-party scripts. One good way to check if
the icon pack is safe is to manually extract the .zip and inspect the file
contents. Icon packs should only contain a font file and a JSON file. Other
files (especially scripts) are to be considered harmful.</p>
</aside>
<h2>Creating an icon pack</h2>
<p>Creating an icon pack requires some scripting knowledge outside Trilium
in order to generate the list of icons. For information, see&nbsp;<a class="reference-link"
href="#root/_help_g1mlRoU8CsqC">Creating an icon pack</a>.</p>
<h2>Using an icon from an icon pack</h2>
<p>After <a href="#root/_help_s8alTXmpFR61">refreshing the application</a>, the
icon pack should be enabled by default. To test this, simply select an
existing note or create a new one and try to change the note icon.</p>
<p>There should be a <em>Filter</em> button to the right of the search bar
in the icon list. Clicking it allows filtering by icon pack and the newly
imported icon pack should be displayed there.</p>
<aside class="admonition note">
<p>If the icon pack is missing from that list, then most likely there's something
wrong with it.</p>
<ul>
<li>Try checking the&nbsp;<a class="reference-link" href="#root/_help_bnyigUA2UK7s">Backend (server) logs</a>&nbsp;for
clues and make sure that the icon pack has the <code spellcheck="false">#iconPack</code>
<a
href="#root/_help_HI6GBBIduIgv">label</a>with a value assigned to it (a prefix).</li>
<li>Icon packs that are <a href="#root/_help_bwg0e8ewQMak">protected</a> are ignored.</li>
</ul>
</aside>
<h2>Integration with the share and export functionality</h2>
<p>Custom icon packs are also supported by the&nbsp;<a class="reference-link"
href="#root/_help_R9pX4DGra2Vt">Sharing</a>&nbsp;feature, where they will be
shown in the note tree. However, in order for an icon pack to be visible
to the share function, the icon pack note must also be shared.</p>
<p>If you are using a custom share theme, make sure it supports the
<code
spellcheck="false">iconPackCss</code>, otherwise icons will not show up. Check the original
share template source code for reference.</p>
<p>Custom icon packs will also be preserved when&nbsp;<a class="reference-link"
href="#root/_help_ycBFjKrrwE9p">Exporting static HTML for web publishing</a>.
In this case, there's no requirement to make the icon pack shared.</p>
<h2>What happens if I remove an icon pack</h2>
<p>If an icon pack is removed or disabled (by removing or altering its
<code
spellcheck="false">#iconPack</code>label), all the notes that use this icon pack will show
in the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;with
no icon. This won't cause any issues apart from looking strange.</p>
<p>The solution is to replace the icons with some else, try using&nbsp;
<a
class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>&nbsp;which supports bulk actions, to identify the notes with
the now deleted icon pack (by looking for the prefix) and changing or removing
their <code spellcheck="false">iconClass</code>.</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View File

@@ -1,11 +1,8 @@
<h2>Position of the Launch bar</h2>
<p>On desktop, depending on the layout selected, the launcher bar will either
be on the left side of the screen with buttons displayed vertically or
at the top of the screen. See&nbsp;<a class="reference-link" href="#root/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a>&nbsp;for
<p>Depending on the layout selected, the launcher bar will either be on the
left side of the screen with buttons displayed vertically or at the top
of the screen. See&nbsp;<a href="#root/_help_x0JgW8UqGXvq">Vertical and horizontal layout</a>&nbsp;for
more information.</p>
<p>On mobile, the launch bar will always be at the bottom.</p>
<p>If there are too many items in the launch bar to fit the screen, it will
become scrollable.</p>
<h2>Terminology</h2>
<ul>
<li><strong>Launcher</strong>: a button that can be (or is) displayed on the
@@ -15,16 +12,16 @@
<li><strong>Visible Launcher</strong>: a launcher that is currently displayed
on the launch bar.</li>
</ul>
<h2>Configuring the desktop Launch bar</h2>
<h2>Configuring the Launch bar</h2>
<p>There are two ways to configure the launch bar:</p>
<ul>
<li>Right click in the empty space between launchers on the launch bar and
select <em>Configure Launchbar.</em>
</li>
<li>Click on the&nbsp;<a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and
<li>Click on the&nbsp;<a href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and
select <em>Configure Launchbar</em>.</li>
</ul>
<p>This will open a new tab with the&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;listing
<p>This will open a new tab with the&nbsp;<a href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;listing
the launchers.</p>
<p>
<img src="Launch Bar_image.png">
@@ -32,18 +29,6 @@
<p>Expanding <em>Available Launchers</em> section will show the list of launchers
that are not displayed on the launch bar. The <em>Visible Launchers</em> will
show the ones that are currently displayed.</p>
<h2>Configuring the mobile launch bar</h2>
<p>The launch bar on mobile uses a different configuration from the desktop
one. The reasoning is that not all desktop icons are available on mobile,
and fewer icons fit on a mobile screen.</p>
<p>To configure the launch bar on mobile, go to&nbsp;<a class="reference-link"
href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;and select <em>Configure Launchbar</em>.</p>
<p>The configure the mobile launch bar while on the desktop (especially useful
to configure more complicated launchers such as scripts or custom widgets),
go to&nbsp;<a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;
Advanced → Show Hidden Subtree and look for the <em>Mobile Launch Bar</em> section.
While in the hidden subtree, it's also possible to drag launchers between
the <em>Mobile Launch Bar</em> and (Desktop) <em>Launch Bar</em> sections.</p>
<h3>Adding/removing and reordering launchers</h3>
<p>To display a new launcher in the launch bar, first look for it in the <em>Available Launchers</em> section.
Then right click it and select <em>Move to visible launchers</em>. It is
@@ -51,13 +36,13 @@
<p>Similarly, to remove it from the launch bar, simply look for it in <em>Visible Launchers</em> then
right click it and select <em>Move to available launchers</em> or use drag-and-drop.</p>
<p>Drag-and-drop the items in the&nbsp;tree&nbsp;in order to change their
order. See&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;for
more interaction options, including using keyboard shortcuts.</p>
order. See&nbsp;<a href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;for more
interaction options, including using keyboard shortcuts.</p>
<h2>Customizing the launcher</h2>
<ul>
<li>The icon of a launcher can be changed just like a normal note. See&nbsp;
<a
class="reference-link" href="#root/_help_p9kXRFAkwN4o">Note Icons</a>&nbsp;for more information.</li>
href="#root/_help_p9kXRFAkwN4o">Note Icons</a>&nbsp;for more information.</li>
<li>The title of the launcher can also be changed.</li>
</ul>
<h3>Resetting</h3>
@@ -76,7 +61,7 @@
<li>Set the <code spellcheck="false">target</code> promoted attribute to the
note to navigate to.</li>
<li>Optionally, set <code spellcheck="false">hoistedNote</code> to hoist a particular
note. See&nbsp;<a class="reference-link" href="#root/_help_OR8WJ7Iz9K4U">Note Hoisting</a>&nbsp;for
note. See&nbsp;<a href="#root/_help_OR8WJ7Iz9K4U">Note Hoisting</a>&nbsp;for
more information.</li>
<li>Optionally, set a <code spellcheck="false">keyboardShortcut</code> to trigger
the launcher.</li>
@@ -86,7 +71,7 @@
<p><strong>Script Launcher</strong>
<br>An advanced launcher which will run a script upon pressing. See&nbsp;
<a
class="reference-link" href="#root/_help_CdNpE2pqjmI6">Scripting</a>&nbsp;for more information.</p>
href="#root/_help_CdNpE2pqjmI6">Scripts</a>&nbsp;for more information.</p>
<ol>
<li>Set <code spellcheck="false">script</code> to point to the desired script
to run.</li>
@@ -99,7 +84,7 @@
</p>
<p>Allows defining a custom widget to be rendered inside the launcher. See&nbsp;
<a
class="reference-link" href="#root/_help_SynTBQiBsdYJ">Widget Basics</a>&nbsp;for more information.</p>
href="#root/_help_SynTBQiBsdYJ">Widget Basics</a>&nbsp;for more information.</p>
</li>
<li>
<p><strong>Spacers</strong>
@@ -107,5 +92,4 @@
visual distinction.</p>
</li>
</ol>
<p>Launchers are configured via predefined&nbsp;<a class="reference-link"
href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>
<p>Launchers are configured via predefined&nbsp;<a href="#root/_help_OFXdgB2nNk1F">Promoted Attributes</a>.</p>

View File

@@ -36,29 +36,7 @@
<li><kbd>Ctrl</kbd>+<kbd>Tab</kbd> and <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>Tab</kbd> to
go to the next or previous tab.</li>
<li><kbd>Ctrl</kbd>+<kbd>1</kbd>, <kbd>Ctrl</kbd>+<kbd>2</kbd>, up to <kbd>Ctrl</kbd>+<kbd>9</kbd> to
activate the first, second and up to ninth tab.</li>
activate the first, second and up til ninth tab.</li>
<li>There is also a shortcut to go to the last tab, but it is not assigned
a key by default.</li>
</ul>
<h2>Mobile</h2>
<figure class="image image-style-align-right image_resized" style="width:34.12%;">
<img style="aspect-ratio:1242/2688;" src="Tabs_IMG_1767.PNG"
width="1242" height="2688">
</figure>
<p>Tabs are also supported on the&nbsp;<a class="reference-link" href="#root/_help_RDslemsQ6gCp">Mobile Frontend</a>.</p>
<p>Since v0.102.0, the tabs are displayed by pressing the dedicated tab switcher
button in the&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>.
In this view the tabs are laid out on a grid with a preview of the note
content.</p>
<p>The context menu button at the top-right of the popup allows creating
a new tab, reopening the last closed tab and closing all the tabs.</p>
<p><a class="reference-link" href="#root/_help_luNhaphA37EO">Split View</a>s are
also indicated in the tab switcher, with two titles displayed in a tab.</p>
<aside
class="admonition note">
<p>Versions prior to v0.102.0 also supported tabs, but they were displayed
directly above the&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>.
The decision to use a more mobile-like tab switcher was taken because the
original tab bar could not support many tabs at once and the new design
better aligns with how mobile applications handle tabs.</p>
</aside>
</ul>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

View File

@@ -1,135 +1,35 @@
<figure class="image image_resized image-style-align-right" style="width:33.52%;">
<img style="aspect-ratio:1242/2688;" src="Mobile Frontend_IMG_1765.PNG"
width="1242" height="2688">
</figure>
<p>Trilium has a mobile web frontend which is optimized for touch based devices
- smartphones and tablets. It is activated automatically during login process
based on browser detection.</p>
<p>Mobile frontend is limited in features compared to the full desktop version.
<p>Trilium (<a href="#root/_help_WOcw2SLH6tbX">server edition</a>) has a mobile
web frontend which is optimized for touch based devices - smartphones and
tablets. It is activated automatically during login process based on browser
detection.</p>
<p>Mobile frontend is limited in features compared to full desktop frontend.
See below for more details on this.</p>
<h2>Layout basics</h2>
<p>Unlike the desktop version, the mobile version has a slightly different
UI meant to better fit the constrained screens of a mobile phone.</p>
<p>Here is a non-exhaustive list of differences between the desktop version
and the mobile one:</p>
<ul>
<li>
<p>The&nbsp;<a class="reference-link" href="#root/_help_oPVyFC7WL2Lp">Note Tree</a>&nbsp;is
displayed as a sidebar. To display the sidebar, press the button in the
top-left of the screen.</p>
<ul>
<li>There is also a swipe gesture that can be done from the left of the screen,
but the browser's navigation gesture interferes with it most of the time
(depending on the platform).</li>
<li>Press and hold a note to display the&nbsp;<a class="reference-link" href="#root/_help_YtSN43OrfzaA">Note tree contextual menu</a>.</li>
</ul>
</li>
<li>
<p>The&nbsp;<a class="reference-link" href="#root/_help_Ms1nauBra7gq">Quick search</a>&nbsp;bar
is also displayed at the top of the note tree.</p>
</li>
<li>
<p>The full&nbsp;<a class="reference-link" href="#root/_help_eIg8jdvaoNNd">Search</a>&nbsp;function
can be triggered either from either the&nbsp;<a class="reference-link"
href="#root/_help_x3i7MxGccDuM">Global menu</a>&nbsp;or from the&nbsp;<a class="reference-link"
href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>, if configured.</p>
</li>
<li>
<p>The&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;is
displayed at the bottom of the screen.</p>
<ul>
<li>The launch bar uses a different configuration for icons than the desktop
version. See the dedicated page for more information on how to configure
it.</li>
</ul>
</li>
<li>
<p>Most of the note-related actions are grouped in the horizontal dots icon
on the top-right of the note.</p>
</li>
<li>
<p>The&nbsp;<a class="reference-link" href="#root/_help_3seOhtN8uLIY">Tabs</a>&nbsp;are
grouped under a tab switcher in the&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>,
where the tabs are displayed in a full-screen grid with preview for easy
switching, as well as additional options such as reopening closed tabs.</p>
</li>
<li>
<p>Since v0.100.0,&nbsp;<a class="reference-link" href="#root/_help_luNhaphA37EO">Split View</a>&nbsp;can
also be used in mobile view, but with a maximum of two panes at once. The
splits are displayed vertically instead of horizontally.</p>
</li>
<li>
<p>Starting with v0.102.0, the&nbsp;<a class="reference-link" href="#root/_help_IjZS7iK5EXtb">New Layout</a>&nbsp;is
enforced on mobile. This brings features such as the note badges, note
type switcher or collection properties which would otherwise not be available.</p>
</li>
</ul>
<h2>Installing as a PWA</h2>
<p>The mobile view can be set up as a PWA. While this does not offer any
offline capabilities, it will display the application in full-screen and
makes it easy to access via your mobile phone's home screen.</p>
<h3>On iOS with Safari</h3>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li>Login.</li>
<li>Press the […] button in the bottom-right of the screen and select Share.</li>
<li>Scroll down to reveal the full list of items and choose “Add to Home Screen”.</li>
<li>Press “Add” and the web app will be available.</li>
</ol>
<h3>On Android with Google Chrome</h3>
<aside class="admonition important">
<p>Google Chrome requires the server to be served over HTTPS in order to
display in full-screen. If using HTTP, the app will appear like a normal
web page (similar to a bookmark).</p>
</aside>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li>Login.</li>
<li>Press the three vertical dots icon in the top-right of the screen and
select <em>Add to Home screen.</em>
</li>
<li>Select the <em>Install</em> option.</li>
<li>Select an appropriate name.</li>
<li>The web app will appear as an application, not on the home screen.</li>
</ol>
<h3>On Android with Brave</h3>
<aside class="admonition important">
<p>Brave requires the server to be served over HTTPS in order to display
in full-screen. If using HTTP, the app will appear like a normal web page
(similar to a bookmark).</p>
</aside>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li>Login.</li>
<li>Press the three vertical dots icon in the bottom-right of the screen and
select <em>Add to Home screen</em>.</li>
<li>Press the <em>Install</em> option.</li>
<li>The web app will appear as an application, not on the home screen.</li>
</ol>
<h3>On Samsung Browser</h3>
<ol>
<li>Open your default web browser and access your Trilium instance.</li>
<li>Login.</li>
<li>Press the hamburger menu in the bottom-right of the screen.</li>
<li>Select <em>Add to</em>, followed by <em>Home screen</em>.</li>
<li>Press <em>Add</em> and the web app will appear on the home page.</li>
</ol>
<p>Note that this is not an Android/iOS app, this is just mobile friendly
web page served on the <a href="#root/_help_WOcw2SLH6tbX">server edition</a>.</p>
<h2>Testing via the desktop application</h2>
<p>If you are running Trilium without a dedicated <a href="#root/_help_WOcw2SLH6tbX">server installation</a>,
you can still test the mobile application using the desktop application.
For more information, see&nbsp;<a class="reference-link" href="#root/_help_nRqcgfTb97uV">Using the desktop application as a server</a>.
To access it go to <code spellcheck="false">http://&lt;ip&gt;:37840/login?mobile</code> .</p>
<h2>Limitations</h2>
<p>Mobile frontend provides only some of the features of the full desktop
frontend:</p>
<ul>
<li>it is possible to browse the whole note tree, read and edit all types
of notes, but you can create only text notes</li>
<li>reading and editing <a href="#root/_help_bwg0e8ewQMak">protected notes</a> is
possible, but creating them is not supported</li>
<li>editing options is not supported</li>
<li>cloning notes is not supported</li>
<li>uploading file attachments is not supported</li>
</ul>
<h2>Forcing mobile/desktop frontend</h2>
<p>Trilium decides automatically whether to use mobile or desktop front-end.
<p>Trilium decides automatically whether to use mobile or desktop frontend.
If this is not appropriate, you can use <code spellcheck="false">?mobile</code> or
<code
spellcheck="false">?desktop</code>query param on <strong>login</strong> page (Note: you might
need to log out).</p>
<p>Alternatively, simply select <em>Switch to Mobile/Desktop Version</em> in
the&nbsp;<a class="reference-link" href="#root/_help_x3i7MxGccDuM">Global menu</a>.</p>
<h2>Scripting</h2>
<p>You can alter the behavior with&nbsp;<a class="reference-link" href="#root/_help_CdNpE2pqjmI6">Scripting</a>,
just like for normal frontend. For script notes to be executed, they need
to have labeled <code spellcheck="false">#run=mobileStartup</code>.</p>
<p>Custom&nbsp;<a class="reference-link" href="#root/_help_xYmIYSP6wE3F">Launch Bar</a>&nbsp;widgets
are also supported.</p>
<p>You can alter the behavior with <a href="#root/_help_CdNpE2pqjmI6">scripts</a> just
like for normal frontend. For script notes to be executed, they need to
have labeled <code spellcheck="false">#run=mobileStartup</code>.</p>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

View File

@@ -24,7 +24,8 @@
<li>click on an image or link and save it through context menu</li>
<li>save whole page from the popup or context menu</li>
<li>save screenshot (with crop tool) from either popup or context menu</li>
<li>create short text note from popup</li>
<li
>create short text note from popup</li>
</ul>
<h2>Location of clippings</h2>
<p>Trilium will save these clippings as a new child note under a "clipper
@@ -39,8 +40,10 @@
<p>Keyboard shortcuts are available for most functions:</p>
<ul>
<li>Save selected text: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> (Mac: <kbd></kbd>+<kbd></kbd>+<kbd>S</kbd>)</li>
<li>Save whole page: <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> (Mac: <kbd></kbd>+<kbd></kbd>+<kbd>S</kbd>)</li>
<li>Save screenshot: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd> (Mac: <kbd></kbd>+<kbd></kbd>+<kbd>E</kbd>)</li>
<li
>Save whole page: <kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>S</kbd> (Mac: <kbd></kbd>+<kbd></kbd>+<kbd>S</kbd>)</li>
<li
>Save screenshot: <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>E</kbd> (Mac: <kbd></kbd>+<kbd></kbd>+<kbd>E</kbd>)</li>
</ul>
<p>To set custom shortcuts, follow the directions for your browser.</p>
<ul>
@@ -69,19 +72,20 @@
<li><a href="https://github.com/TriliumNext/Trilium/releases">GitHub Releases</a> by
looking for releases starting with <em>Web Clipper.</em>
</li>
<li>Artifacts in GitHub Actions, by looking for the <a href="https://github.com/TriliumNext/Trilium/actions/workflows/web-clipper.yml"><em>Deploy web clipper extension</em> workflow</a>.
<li>Artifacts in GitHub Actions, by looking for the <a href="https://github.com/TriliumNext/Trilium/actions/workflows/web-clipper.yml"><em>Deploy web clipper extension </em>workflow</a>.
Once a workflow run is selected, the ZIP files are available in the <em>Artifacts</em> section,
under the name <code spellcheck="false">web-clipper-extension</code>.</li>
</ul>
<h3>For Chrome</h3>
<ol>
<li>Download <code spellcheck="false">trilium-web-clipper-[x.y.z]-chrome.zip</code>.</li>
<li>Extract the archive.</li>
<li>In Chrome, navigate to <code spellcheck="false">chrome://extensions/</code>
</li>
<li>Toggle <em>Developer Mode</em> in top-right of the page.</li>
<li>Press the <em>Load unpacked</em> button near the header.</li>
<li>Point to the extracted directory from step (2).</li>
<li
>Extract the archive.</li>
<li>In Chrome, navigate to <code spellcheck="false">chrome://extensions/</code>
</li>
<li>Toggle <em>Developer Mode</em> in top-right of the page.</li>
<li>Press the <em>Load unpacked</em> button near the header.</li>
<li>Point to the extracted directory from step (2).</li>
</ol>
<h3>For Firefox</h3>
<aside class="admonition warning">
@@ -96,10 +100,12 @@
<li>Navigate to <code spellcheck="false">about:addons</code>.</li>
<li>Select <em>Extensions</em> in the left-side navigation.</li>
<li>Press the <em>Gear</em> icon on the right of the <em>Manage Your Extensions</em> title.</li>
<li>Select <em>Install Add-on From File…</em>
</li>
<li>Point it to <code spellcheck="false">trilium-web-clipper-[x.y.z]-firefox.zip</code>.</li>
<li>Press the <em>Add</em> button to confirm.</li>
<li
>Select <em>Install Add-on From File…</em>
</li>
<li>Point it to <code spellcheck="false">trilium-web-clipper-[x.y.z]-firefox.zip</code>.</li>
<li
>Press the <em>Add</em> button to confirm.</li>
</ol>
<h2>Credits</h2>
<p>Some parts of the code are based on the <a href="https://github.com/laurent22/joplin/tree/master/Clipper">Joplin Notes browser extension</a>.</p>

View File

@@ -60,7 +60,7 @@
</p>
<h2>Step 5. Making changes</h2>
<p>Simply go back to the note and change according to needs. To apply the
changes to the current window, press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>R</kbd> to
changes to the current window, press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>R </kbd> to
refresh.</p>
<p>It's a good idea to keep two windows, one for editing and the other one
for previewing the changes.</p>

View File

@@ -1,5 +1,5 @@
<aside class="admonition note">
<p>This page describes how to create custom icon packs. For a general description
<p>e This page describes how to create custom icon packs. For a general description
of how to use already existing icon packs, see&nbsp;<a class="reference-link"
href="#root/_help_gOKqSJgXLcIj">Icon Packs</a>.</p>
</aside>
@@ -74,9 +74,9 @@
"bx-ball": {
"glyph": "\ue9c2",
"terms": [ "ball" ]
},
},
"bxs-party": {
"glyph": "\uec92",
"glyph": "\uec92"
"terms": [ "party" ]
}
}

Some files were not shown because too many files have changed in this diff Show More