mirror of
https://github.com/zadam/trilium.git
synced 2025-11-01 19:05:59 +01:00
Compare commits
17 Commits
feat/fix-j
...
feat/ui-op
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aab606deb | ||
|
|
2e11681b52 | ||
|
|
8cca6637f7 | ||
|
|
82e076378c | ||
|
|
94ddad3c49 | ||
|
|
d35dbca18b | ||
|
|
7468d6147a | ||
|
|
7c78d749de | ||
|
|
85dd99a3c4 | ||
|
|
0a9c0234e2 | ||
|
|
3e3cc8c541 | ||
|
|
d1538508e8 | ||
|
|
9b1da8c311 | ||
|
|
e4a8258acf | ||
|
|
5e88043c7b | ||
|
|
bedf9112fb | ||
|
|
03681d23c5 |
@@ -30,8 +30,6 @@ export interface Suggestion {
|
|||||||
notePathTitle?: string;
|
notePathTitle?: string;
|
||||||
notePath?: string;
|
notePath?: string;
|
||||||
highlightedNotePathTitle?: string;
|
highlightedNotePathTitle?: string;
|
||||||
attributeSnippet?: string;
|
|
||||||
highlightedAttributeSnippet?: string;
|
|
||||||
action?: string | "create-note" | "search-notes" | "external-link" | "command";
|
action?: string | "create-note" | "search-notes" | "external-link" | "command";
|
||||||
parentNoteId?: string;
|
parentNoteId?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
@@ -310,12 +308,11 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
displayKey: "notePathTitle",
|
displayKey: "notePathTitle",
|
||||||
templates: {
|
templates: {
|
||||||
suggestion: (suggestion) => {
|
suggestion: (suggestion) => {
|
||||||
// Handle different suggestion types
|
|
||||||
if (suggestion.action === "command") {
|
if (suggestion.action === "command") {
|
||||||
let html = `<div class="command-suggestion">`;
|
let html = `<div class="command-suggestion">`;
|
||||||
html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
|
html += `<span class="command-icon ${suggestion.icon || "bx bx-terminal"}"></span>`;
|
||||||
html += `<div class="command-content">`;
|
html += `<div class="command-content">`;
|
||||||
html += `<div class="command-name">${suggestion.highlightedNotePathTitle || suggestion.noteTitle || ''}</div>`;
|
html += `<div class="command-name">${suggestion.highlightedNotePathTitle}</div>`;
|
||||||
if (suggestion.commandDescription) {
|
if (suggestion.commandDescription) {
|
||||||
html += `<div class="command-description">${suggestion.commandDescription}</div>`;
|
html += `<div class="command-description">${suggestion.commandDescription}</div>`;
|
||||||
}
|
}
|
||||||
@@ -326,20 +323,7 @@ function initNoteAutocomplete($el: JQuery<HTMLElement>, options?: Options) {
|
|||||||
html += '</div>';
|
html += '</div>';
|
||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
return `<span class="${suggestion.icon ?? "bx bx-note"}"></span> ${suggestion.highlightedNotePathTitle}`;
|
||||||
// For note suggestions, match Quick Search structure
|
|
||||||
// Title row with icon
|
|
||||||
let html = `<div style="display: flex; align-items: center; gap: 6px;">`;
|
|
||||||
html += `<span class="${suggestion.icon ?? "bx bx-note"}" style="flex-shrink: 0;"></span>`;
|
|
||||||
html += `<span class="search-result-title" style="flex: 1;">${suggestion.highlightedNotePathTitle || ''}</span>`;
|
|
||||||
html += `</div>`;
|
|
||||||
|
|
||||||
// Add attribute snippet if available (inline display)
|
|
||||||
if (suggestion.highlightedAttributeSnippet && suggestion.highlightedAttributeSnippet.trim()) {
|
|
||||||
html += `<div class="search-result-attributes" style="margin-left: 20px; margin-top: 2px; color: var(--muted-text-color); font-size: 0.9em;">${suggestion.highlightedAttributeSnippet}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
// we can't cache identical searches because notes can be created / renamed, new recent notes can be added
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
/**
|
|
||||||
* Quick Search specific result renderer
|
|
||||||
*
|
|
||||||
* This module provides HTML rendering functionality specifically for the Quick Search widget.
|
|
||||||
* The Jump To dialog (note_autocomplete) intentionally has its own inline rendering logic
|
|
||||||
* with different styling and layout requirements.
|
|
||||||
*
|
|
||||||
* SECURITY NOTE: HTML Snippet Handling
|
|
||||||
* The highlighted snippet fields (highlightedAttributeSnippet) contain
|
|
||||||
* pre-sanitized HTML from the server. The server-side processing:
|
|
||||||
* 1. Escapes all HTML using the escape-html library
|
|
||||||
* 2. Adds safe HTML tags for display: <b> for search term highlighting
|
|
||||||
* 3. See apps/server/src/services/search/services/search.ts for implementation
|
|
||||||
*
|
|
||||||
* This means the HTML snippets can be safely inserted without additional escaping on the client side.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { Suggestion } from "./note_autocomplete.js";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates HTML for a Quick Search result item
|
|
||||||
*
|
|
||||||
* @param result - The search result item to render
|
|
||||||
* @returns HTML string formatted for Quick Search widget display
|
|
||||||
*/
|
|
||||||
export function createSearchResultHtml(result: Suggestion): string {
|
|
||||||
// Handle command action
|
|
||||||
if (result.action === "command") {
|
|
||||||
let html = `<div class="command-suggestion">`;
|
|
||||||
html += `<span class="command-icon ${result.icon || "bx bx-terminal"}"></span>`;
|
|
||||||
html += `<div class="command-content">`;
|
|
||||||
html += `<div class="command-name">${result.highlightedNotePathTitle || ''}</div>`;
|
|
||||||
if (result.commandDescription) {
|
|
||||||
html += `<div class="command-description">${result.commandDescription}</div>`;
|
|
||||||
}
|
|
||||||
html += `</div>`;
|
|
||||||
if (result.commandShortcut) {
|
|
||||||
html += `<kbd class="command-shortcut">${result.commandShortcut}</kbd>`;
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
return html;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Default: render as note result
|
|
||||||
// Wrap everything in a flex column container
|
|
||||||
let itemHtml = `<div style="display: flex; flex-direction: column; gap: 2px;">`;
|
|
||||||
|
|
||||||
// Title row with icon
|
|
||||||
itemHtml += `<div style="display: flex; align-items: center; gap: 6px;">`;
|
|
||||||
itemHtml += `<span class="${result.icon || 'bx bx-note'}" style="flex-shrink: 0;"></span>`;
|
|
||||||
itemHtml += `<span class="search-result-title" style="flex: 1;">${result.highlightedNotePathTitle || result.notePathTitle || ''}</span>`;
|
|
||||||
itemHtml += `</div>`;
|
|
||||||
|
|
||||||
// Add attribute snippet if available (inline display)
|
|
||||||
if (result.highlightedAttributeSnippet && result.highlightedAttributeSnippet.trim()) {
|
|
||||||
itemHtml += `<div class="search-result-attributes" style="margin-left: 20px; color: var(--muted-text-color); font-size: 0.9em;">${result.highlightedAttributeSnippet}</div>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
itemHtml += `</div>`;
|
|
||||||
|
|
||||||
return itemHtml;
|
|
||||||
}
|
|
||||||
@@ -28,6 +28,28 @@
|
|||||||
--ck-mention-list-max-height: 500px;
|
--ck-mention-list-max-height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body#trilium-app.motion-disabled *,
|
||||||
|
body#trilium-app.motion-disabled *::before,
|
||||||
|
body#trilium-app.motion-disabled *::after {
|
||||||
|
/* Disable transitions and animations */
|
||||||
|
transition: none !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#trilium-app.shadows-disabled *,
|
||||||
|
body#trilium-app.shadows-disabled *::before,
|
||||||
|
body#trilium-app.shadows-disabled *::after {
|
||||||
|
/* Disable shadows */
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body#trilium-app.backdrop-effects-disabled *,
|
||||||
|
body#trilium-app.backdrop-effects-disabled *::before,
|
||||||
|
body#trilium-app.backdrop-effects-disabled *::after {
|
||||||
|
/* Disable backdrop effects */
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
--bs-table-bg: transparent !important;
|
--bs-table-bg: transparent !important;
|
||||||
}
|
}
|
||||||
@@ -355,7 +377,7 @@ body.desktop .tabulator-popup-container {
|
|||||||
|
|
||||||
@supports (animation-fill-mode: forwards) {
|
@supports (animation-fill-mode: forwards) {
|
||||||
/* Delay the opening of submenus */
|
/* Delay the opening of submenus */
|
||||||
body.desktop .dropdown-submenu .dropdown-menu {
|
body.desktop:not(.motion-disabled) .dropdown-submenu .dropdown-menu {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
animation-fill-mode: forwards;
|
animation-fill-mode: forwards;
|
||||||
animation-delay: var(--submenu-opening-delay);
|
animation-delay: var(--submenu-opening-delay);
|
||||||
@@ -840,25 +862,8 @@ table.promoted-attributes-in-tooltip th {
|
|||||||
|
|
||||||
.aa-dropdown-menu .aa-suggestion {
|
.aa-dropdown-menu .aa-suggestion {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 12px 16px;
|
padding: 5px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
line-height: 1.4;
|
|
||||||
position: relative;
|
|
||||||
white-space: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add separator between Jump To suggestions like Quick Search */
|
|
||||||
.jump-to-note-results .aa-suggestion:not(:last-child)::after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
width: 80%;
|
|
||||||
height: 2px;
|
|
||||||
background: var(--main-border-color);
|
|
||||||
border-radius: 1px;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.aa-dropdown-menu .aa-suggestion p {
|
.aa-dropdown-menu .aa-suggestion p {
|
||||||
@@ -1803,7 +1808,7 @@ textarea {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.jump-to-note-results .aa-suggestions {
|
.jump-to-note-results .aa-suggestions {
|
||||||
padding: 0;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Command palette styling */
|
/* Command palette styling */
|
||||||
@@ -2277,43 +2282,13 @@ footer.webview-footer button {
|
|||||||
padding: 1px 10px 1px 10px;
|
padding: 1px 10px 1px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search result highlighting - applies to both Quick Search and Jump To */
|
/* Search result highlighting */
|
||||||
.search-result-title b,
|
.search-result-title b,
|
||||||
.search-result-content b,
|
.search-result-content b {
|
||||||
.search-result-attributes b,
|
|
||||||
.quick-search .search-result-title b,
|
|
||||||
.quick-search .search-result-content b,
|
|
||||||
.quick-search .search-result-attributes b {
|
|
||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
color: var(--admonition-warning-accent-color);
|
color: var(--admonition-warning-accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Quick Search specific snippet styling */
|
|
||||||
.quick-search .search-result-content {
|
|
||||||
font-size: 0.85em;
|
|
||||||
color: var(--main-text-color);
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-search .search-result-attributes {
|
|
||||||
font-size: 0.75em;
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Jump To (autocomplete) specific snippet styling */
|
|
||||||
.aa-dropdown-menu .search-result-content {
|
|
||||||
font-size: 0.82em;
|
|
||||||
color: var(--main-text-color);
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.aa-dropdown-menu .search-result-attributes {
|
|
||||||
font-size: 0.75em;
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Customized icons */
|
/* Customized icons */
|
||||||
|
|
||||||
.bx-tn-toc::before {
|
.bx-tn-toc::before {
|
||||||
|
|||||||
@@ -89,6 +89,7 @@
|
|||||||
|
|
||||||
--menu-text-color: #e3e3e3;
|
--menu-text-color: #e3e3e3;
|
||||||
--menu-background-color: #222222d9;
|
--menu-background-color: #222222d9;
|
||||||
|
--menu-background-color-no-backdrop: #1b1b1b;
|
||||||
--menu-item-icon-color: #8c8c8c;
|
--menu-item-icon-color: #8c8c8c;
|
||||||
--menu-item-disabled-opacity: 0.5;
|
--menu-item-disabled-opacity: 0.5;
|
||||||
--menu-item-keyboard-shortcut-color: #ffffff8f;
|
--menu-item-keyboard-shortcut-color: #ffffff8f;
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
|
|
||||||
--menu-text-color: #272727;
|
--menu-text-color: #272727;
|
||||||
--menu-background-color: #ffffffd9;
|
--menu-background-color: #ffffffd9;
|
||||||
|
--menu-background-color-no-backdrop: #fdfdfd;
|
||||||
--menu-item-icon-color: #727272;
|
--menu-item-icon-color: #727272;
|
||||||
--menu-item-disabled-opacity: 0.6;
|
--menu-item-disabled-opacity: 0.6;
|
||||||
--menu-item-keyboard-shortcut-color: #666666a8;
|
--menu-item-keyboard-shortcut-color: #666666a8;
|
||||||
|
|||||||
@@ -83,6 +83,12 @@
|
|||||||
--tab-note-icons: true;
|
--tab-note-icons: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.backdrop-effects-disabled {
|
||||||
|
/* Backdrop effects are disabled, replace the menu background color with the
|
||||||
|
* no-backdrop fallback color */
|
||||||
|
--menu-background-color: var(--menu-background-color-no-backdrop);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* MENUS
|
* MENUS
|
||||||
*
|
*
|
||||||
@@ -530,16 +536,17 @@ body.mobile .dropdown-menu .dropdown-item.submenu-open .dropdown-toggle::after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* List item */
|
/* List item */
|
||||||
.jump-to-note-dialog .aa-suggestions .aa-suggestion,
|
.jump-to-note-dialog .aa-suggestions div,
|
||||||
.note-detail-empty .aa-suggestions .aa-suggestion {
|
.note-detail-empty .aa-suggestions div {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
color: var(--menu-text-color);
|
color: var(--menu-text-color);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Selected list item */
|
/* Selected list item */
|
||||||
.jump-to-note-dialog .aa-suggestions .aa-suggestion.aa-cursor,
|
.jump-to-note-dialog .aa-suggestions div.aa-cursor,
|
||||||
.note-detail-empty .aa-suggestions .aa-suggestion.aa-cursor {
|
.note-detail-empty .aa-suggestions div.aa-cursor {
|
||||||
background: var(--hover-item-background-color);
|
background: var(--hover-item-background-color);
|
||||||
color: var(--hover-item-text-color);
|
color: var(--hover-item-text-color);
|
||||||
}
|
}
|
||||||
@@ -1113,6 +1113,12 @@
|
|||||||
"layout-vertical-description": "launcher bar is on the left (default)",
|
"layout-vertical-description": "launcher bar is on the left (default)",
|
||||||
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
|
"layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
|
||||||
},
|
},
|
||||||
|
"ui-performance": {
|
||||||
|
"title": "Performance",
|
||||||
|
"enable-motion": "Enable transitions and animations",
|
||||||
|
"enable-shadows": "Enable shadows",
|
||||||
|
"enable-backdrop-effects": "Enable background effects for menus, popups and panels"
|
||||||
|
},
|
||||||
"ai_llm": {
|
"ai_llm": {
|
||||||
"not_started": "Not started",
|
"not_started": "Not started",
|
||||||
"title": "AI Settings",
|
"title": "AI Settings",
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import utils from "../../services/utils.js";
|
import { EventData } from "../../components/app_context.js";
|
||||||
import type BasicWidget from "../basic_widget.js";
|
|
||||||
import FlexContainer from "./flex_container.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";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The root container is the top-most widget/container, from which the entire layout derives.
|
* The root container is the top-most widget/container, from which the entire layout derives.
|
||||||
@@ -20,6 +22,7 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
this.id("root-widget");
|
this.id("root-widget");
|
||||||
this.css("height", "100dvh");
|
this.css("height", "100dvh");
|
||||||
this.originalViewportHeight = getViewportHeight();
|
this.originalViewportHeight = getViewportHeight();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render(): JQuery<HTMLElement> {
|
render(): JQuery<HTMLElement> {
|
||||||
@@ -27,15 +30,45 @@ export default class RootContainer extends FlexContainer<BasicWidget> {
|
|||||||
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
|
window.visualViewport?.addEventListener("resize", () => this.#onMobileResize());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#setMotion(options.is("motionEnabled"));
|
||||||
|
this.#setShadows(options.is("shadowsEnabled"));
|
||||||
|
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
||||||
|
|
||||||
return super.render();
|
return super.render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||||
|
if (loadResults.isOptionReloaded("motionEnabled")) {
|
||||||
|
this.#setMotion(options.is("motionEnabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadResults.isOptionReloaded("shadowsEnabled")) {
|
||||||
|
this.#setShadows(options.is("shadowsEnabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadResults.isOptionReloaded("backdropEffectsEnabled")) {
|
||||||
|
this.#setBackdropEffects(options.is("backdropEffectsEnabled"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#onMobileResize() {
|
#onMobileResize() {
|
||||||
const currentViewportHeight = getViewportHeight();
|
const currentViewportHeight = getViewportHeight();
|
||||||
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
|
const isKeyboardOpened = (currentViewportHeight < this.originalViewportHeight);
|
||||||
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
this.$widget.toggleClass("virtual-keyboard-opened", isKeyboardOpened);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#setMotion(enabled: boolean) {
|
||||||
|
document.body.classList.toggle("motion-disabled", !enabled);
|
||||||
|
jQuery.fx.off = !enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
#setShadows(enabled: boolean) {
|
||||||
|
document.body.classList.toggle("shadows-disabled", !enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
#setBackdropEffects(enabled: boolean) {
|
||||||
|
document.body.classList.toggle("backdrop-effects-disabled", !enabled);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getViewportHeight() {
|
function getViewportHeight() {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import appContext from "../components/app_context.js";
|
|||||||
import shortcutService from "../services/shortcuts.js";
|
import shortcutService from "../services/shortcuts.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import { Dropdown, Tooltip } from "bootstrap";
|
import { Dropdown, Tooltip } from "bootstrap";
|
||||||
import { createSearchResultHtml } from "../services/quick_search_renderer.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
const TPL = /*html*/`
|
||||||
<div class="quick-search input-group input-group-sm">
|
<div class="quick-search input-group input-group-sm">
|
||||||
@@ -92,6 +91,8 @@ interface QuickSearchResponse {
|
|||||||
noteTitle: string;
|
noteTitle: string;
|
||||||
notePathTitle: string;
|
notePathTitle: string;
|
||||||
highlightedNotePathTitle: string;
|
highlightedNotePathTitle: string;
|
||||||
|
contentSnippet?: string;
|
||||||
|
highlightedContentSnippet?: string;
|
||||||
attributeSnippet?: string;
|
attributeSnippet?: string;
|
||||||
highlightedAttributeSnippet?: string;
|
highlightedAttributeSnippet?: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
@@ -235,14 +236,24 @@ export default class QuickSearchWidget extends BasicWidget {
|
|||||||
|
|
||||||
const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">');
|
const $item = $('<a class="dropdown-item" tabindex="0" href="javascript:">');
|
||||||
|
|
||||||
// Use the shared renderer for consistent display
|
// Build the display HTML with content snippet below the title
|
||||||
const itemHtml = createSearchResultHtml({
|
let itemHtml = `<div style="display: flex; flex-direction: column;">
|
||||||
icon: result.icon,
|
<div style="display: flex; align-items: flex-start; gap: 6px;">
|
||||||
notePathTitle: result.notePathTitle,
|
<span class="${result.icon}" style="flex-shrink: 0; margin-top: 1px;"></span>
|
||||||
highlightedNotePathTitle: result.highlightedNotePathTitle,
|
<span style="flex: 1;" class="search-result-title">${result.highlightedNotePathTitle}</span>
|
||||||
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
|
</div>`;
|
||||||
highlightedContentSnippet: result.highlightedContentSnippet
|
|
||||||
});
|
// Add attribute snippet (tags/attributes) below the title if available
|
||||||
|
if (result.highlightedAttributeSnippet) {
|
||||||
|
itemHtml += `<div style="font-size: 0.75em; color: var(--muted-text-color); opacity: 0.5; margin-left: 20px; margin-top: 2px; line-height: 1.2;" class="search-result-attributes">${result.highlightedAttributeSnippet}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add content snippet below the attributes if available
|
||||||
|
if (result.highlightedContentSnippet) {
|
||||||
|
itemHtml += `<div style="font-size: 0.85em; color: var(--main-text-color); opacity: 0.7; margin-left: 20px; margin-top: 4px; line-height: 1.3;" class="search-result-content">${result.highlightedContentSnippet}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
itemHtml += `</div>`;
|
||||||
|
|
||||||
$item.html(itemHtml);
|
$item.html(itemHtml);
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export default function AppearanceSettings() {
|
|||||||
<ApplicationTheme />
|
<ApplicationTheme />
|
||||||
{overrideThemeFonts === "true" && <Fonts />}
|
{overrideThemeFonts === "true" && <Fonts />}
|
||||||
{isElectron() && <ElectronIntegration /> }
|
{isElectron() && <ElectronIntegration /> }
|
||||||
|
<Performance />
|
||||||
<MaxContentWidth />
|
<MaxContentWidth />
|
||||||
<RelatedSettings items={[
|
<RelatedSettings items={[
|
||||||
{
|
{
|
||||||
@@ -245,6 +246,35 @@ function ElectronIntegration() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Performance() {
|
||||||
|
const [ motionEnabled, setMotionEnabled ] = useTriliumOptionBool("motionEnabled");
|
||||||
|
const [ shadowsEnabled, setShadowsEnabled ] = useTriliumOptionBool("shadowsEnabled");
|
||||||
|
const [ backdropEffectsEnabled, setBackdropEffectsEnabled ] = useTriliumOptionBool("backdropEffectsEnabled");
|
||||||
|
|
||||||
|
|
||||||
|
return <OptionsSection title={t("ui-performance.title")}>
|
||||||
|
<FormGroup name="motion-enabled">
|
||||||
|
<FormCheckbox
|
||||||
|
label={t("ui-performance.enable-motion")}
|
||||||
|
currentValue={motionEnabled} onChange={setMotionEnabled}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup name="shadows-enabled">
|
||||||
|
<FormCheckbox
|
||||||
|
label={t("ui-performance.enable-shadows")}
|
||||||
|
currentValue={shadowsEnabled} onChange={setShadowsEnabled}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup name="backdrop-effects-enabled">
|
||||||
|
<FormCheckbox
|
||||||
|
label={t("ui-performance.enable-backdrop-effects")}
|
||||||
|
currentValue={backdropEffectsEnabled} onChange={setBackdropEffectsEnabled}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</OptionsSection>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function MaxContentWidth() {
|
function MaxContentWidth() {
|
||||||
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
|
const [ maxContentWidth, setMaxContentWidth ] = useTriliumOption("maxContentWidth");
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
<link rel="manifest" crossorigin="use-credentials" href="manifest.webmanifest">
|
||||||
<title>Trilium Notes</title>
|
<title>Trilium Notes</title>
|
||||||
</head>
|
</head>
|
||||||
<body class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>">
|
<body id="trilium-app" class="desktop heading-style-<%= headingStyle %> layout-<%= layoutOrientation %> platform-<%= platform %> <%= isElectron ? 'electron' : '' %> <%= hasNativeTitleBar ? 'native-titlebar' : '' %> <%= hasBackgroundEffects ? 'background-effects' : '' %>">
|
||||||
<noscript><%= t("javascript-required") %></noscript>
|
<noscript><%= t("javascript-required") %></noscript>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
|||||||
"dailyBackupEnabled",
|
"dailyBackupEnabled",
|
||||||
"weeklyBackupEnabled",
|
"weeklyBackupEnabled",
|
||||||
"monthlyBackupEnabled",
|
"monthlyBackupEnabled",
|
||||||
|
"motionEnabled",
|
||||||
|
"shadowsEnabled",
|
||||||
|
"backdropEffectsEnabled",
|
||||||
"maxContentWidth",
|
"maxContentWidth",
|
||||||
"compressImages",
|
"compressImages",
|
||||||
"downloadImagesAutomatically",
|
"downloadImagesAutomatically",
|
||||||
|
|||||||
@@ -152,6 +152,10 @@ const defaultOptions: DefaultOption[] = [
|
|||||||
},
|
},
|
||||||
isSynced: false
|
isSynced: false
|
||||||
},
|
},
|
||||||
|
{ name: "motionEnabled", value: "true", isSynced: false },
|
||||||
|
{ name: "shadowsEnabled", value: "true", isSynced: false },
|
||||||
|
{ name: "backdropEffectsEnabled", value: "true", isSynced: false },
|
||||||
|
|
||||||
|
|
||||||
// Internationalization
|
// Internationalization
|
||||||
{ name: "locale", value: "en", isSynced: true },
|
{ name: "locale", value: "en", isSynced: true },
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ class SearchResult {
|
|||||||
score: number;
|
score: number;
|
||||||
notePathTitle: string;
|
notePathTitle: string;
|
||||||
highlightedNotePathTitle?: string;
|
highlightedNotePathTitle?: string;
|
||||||
|
contentSnippet?: string;
|
||||||
|
highlightedContentSnippet?: string;
|
||||||
attributeSnippet?: string;
|
attributeSnippet?: string;
|
||||||
highlightedAttributeSnippet?: string;
|
highlightedAttributeSnippet?: string;
|
||||||
private fuzzyScore: number; // Track fuzzy score separately
|
private fuzzyScore: number; // Track fuzzy score separately
|
||||||
|
|||||||
@@ -285,19 +285,15 @@ function performSearch(expression: Expression, searchContext: SearchContext, ena
|
|||||||
|
|
||||||
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
|
const noteSet = expression.execute(allNoteSet, executionContext, searchContext);
|
||||||
|
|
||||||
const searchResults = noteSet.notes
|
const searchResults = noteSet.notes.map((note) => {
|
||||||
.map((note) => {
|
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
|
||||||
const notePathArray = executionContext.noteIdToNotePath[note.noteId] || note.getBestNotePath();
|
|
||||||
|
|
||||||
if (!notePathArray) {
|
if (!notePathArray) {
|
||||||
// Log the orphaned note but don't throw - just skip it
|
throw new Error(`Can't find note path for note ${JSON.stringify(note.getPojo())}`);
|
||||||
log.info(`Skipping orphaned note without path: ${note.noteId} "${note.title}"`);
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SearchResult(notePathArray);
|
return new SearchResult(notePathArray);
|
||||||
})
|
});
|
||||||
.filter(result => result !== null) as SearchResult[];
|
|
||||||
|
|
||||||
for (const res of searchResults) {
|
for (const res of searchResults) {
|
||||||
res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens, enableFuzzyMatching);
|
res.computeScore(searchContext.fulltextQuery, searchContext.highlightedTokens, enableFuzzyMatching);
|
||||||
@@ -501,12 +497,6 @@ function extractContentSnippet(noteId: string, searchTokens: string[], maxLength
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no match found in content, always show the beginning of the note
|
|
||||||
// This ensures users always get context even when searching by tags/attributes
|
|
||||||
if (!matchFound) {
|
|
||||||
snippetStart = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract snippet
|
// Extract snippet
|
||||||
let snippet = content.substring(snippetStart, snippetStart + maxLength);
|
let snippet = content.substring(snippetStart, snippetStart + maxLength);
|
||||||
|
|
||||||
@@ -585,29 +575,11 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no matching attributes found but we have attributes, show the first few
|
|
||||||
// This provides context even when searching didn't match attributes
|
|
||||||
if (matchingAttributes.length === 0 && attributes.length > 0) {
|
|
||||||
// Filter out internal attributes if not searching for them
|
|
||||||
const visibleAttributes = attributes.filter(attr =>
|
|
||||||
!attr.name?.startsWith("internal") &&
|
|
||||||
!attr.name?.startsWith("dateCreated") &&
|
|
||||||
!attr.name?.startsWith("dateModified")
|
|
||||||
);
|
|
||||||
|
|
||||||
// Take up to 4 visible attributes
|
|
||||||
matchingAttributes = visibleAttributes.slice(0, 4).map(attr => ({
|
|
||||||
name: attr.name || "",
|
|
||||||
value: attr.value || "",
|
|
||||||
type: attr.type || ""
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matchingAttributes.length === 0) {
|
if (matchingAttributes.length === 0) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display attributes inline, separated by spaces
|
// Limit to 4 lines maximum, similar to content snippet logic
|
||||||
const lines: string[] = [];
|
const lines: string[] = [];
|
||||||
for (const attr of matchingAttributes.slice(0, 4)) {
|
for (const attr of matchingAttributes.slice(0, 4)) {
|
||||||
let line = "";
|
let line = "";
|
||||||
@@ -625,7 +597,7 @@ function extractAttributeSnippet(noteId: string, searchTokens: string[], maxLeng
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let snippet = lines.join(' '); // Join with spaces instead of newlines
|
let snippet = lines.join('\n');
|
||||||
|
|
||||||
// Apply length limit while preserving line structure
|
// Apply length limit while preserving line structure
|
||||||
if (snippet.length > maxLength) {
|
if (snippet.length > maxLength) {
|
||||||
@@ -665,8 +637,9 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
|
|||||||
|
|
||||||
const trimmed = allSearchResults.slice(0, 200);
|
const trimmed = allSearchResults.slice(0, 200);
|
||||||
|
|
||||||
// Extract attribute snippets only (content snippets removed for performance)
|
// Extract content and attribute snippets
|
||||||
for (const result of trimmed) {
|
for (const result of trimmed) {
|
||||||
|
result.contentSnippet = extractContentSnippet(result.noteId, searchContext.highlightedTokens);
|
||||||
result.attributeSnippet = extractAttributeSnippet(result.noteId, searchContext.highlightedTokens);
|
result.attributeSnippet = extractAttributeSnippet(result.noteId, searchContext.highlightedTokens);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,6 +652,8 @@ function searchNotesForAutocomplete(query: string, fastSearch: boolean = true) {
|
|||||||
noteTitle: title,
|
noteTitle: title,
|
||||||
notePathTitle: result.notePathTitle,
|
notePathTitle: result.notePathTitle,
|
||||||
highlightedNotePathTitle: result.highlightedNotePathTitle,
|
highlightedNotePathTitle: result.highlightedNotePathTitle,
|
||||||
|
contentSnippet: result.contentSnippet,
|
||||||
|
highlightedContentSnippet: result.highlightedContentSnippet,
|
||||||
attributeSnippet: result.attributeSnippet,
|
attributeSnippet: result.attributeSnippet,
|
||||||
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
|
highlightedAttributeSnippet: result.highlightedAttributeSnippet,
|
||||||
icon: icon ?? "bx bx-note"
|
icon: icon ?? "bx bx-note"
|
||||||
@@ -704,9 +679,17 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
|||||||
for (const result of searchResults) {
|
for (const result of searchResults) {
|
||||||
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, "");
|
result.highlightedNotePathTitle = result.notePathTitle.replace(/[<{}]/g, "");
|
||||||
|
|
||||||
|
// Initialize highlighted content snippet
|
||||||
|
if (result.contentSnippet) {
|
||||||
|
// Escape HTML but preserve newlines for later conversion to <br>
|
||||||
|
result.highlightedContentSnippet = escapeHtml(result.contentSnippet);
|
||||||
|
// Remove any stray < { } that might interfere with our highlighting markers
|
||||||
|
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/[<{}]/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize highlighted attribute snippet
|
// Initialize highlighted attribute snippet
|
||||||
if (result.attributeSnippet) {
|
if (result.attributeSnippet) {
|
||||||
// Escape HTML
|
// Escape HTML but preserve newlines for later conversion to <br>
|
||||||
result.highlightedAttributeSnippet = escapeHtml(result.attributeSnippet);
|
result.highlightedAttributeSnippet = escapeHtml(result.attributeSnippet);
|
||||||
// Remove any stray < { } that might interfere with our highlighting markers
|
// Remove any stray < { } that might interfere with our highlighting markers
|
||||||
result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/[<{}]/g, "");
|
result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/[<{}]/g, "");
|
||||||
@@ -738,6 +721,16 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Highlight in content snippet
|
||||||
|
if (result.highlightedContentSnippet) {
|
||||||
|
const contentRegex = new RegExp(escapeRegExp(token), "gi");
|
||||||
|
while ((match = contentRegex.exec(normalizeString(result.highlightedContentSnippet))) !== null) {
|
||||||
|
result.highlightedContentSnippet = wrapText(result.highlightedContentSnippet, match.index, token.length, "{", "}");
|
||||||
|
// 2 characters are added, so we need to adjust the index
|
||||||
|
contentRegex.lastIndex += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight in attribute snippet
|
// Highlight in attribute snippet
|
||||||
if (result.highlightedAttributeSnippet) {
|
if (result.highlightedAttributeSnippet) {
|
||||||
const attributeRegex = new RegExp(escapeRegExp(token), "gi");
|
const attributeRegex = new RegExp(escapeRegExp(token), "gi");
|
||||||
@@ -755,10 +748,18 @@ function highlightSearchResults(searchResults: SearchResult[], highlightedTokens
|
|||||||
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "<b>").replace(/}/g, "</b>");
|
result.highlightedNotePathTitle = result.highlightedNotePathTitle.replace(/{/g, "<b>").replace(/}/g, "</b>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (result.highlightedContentSnippet) {
|
||||||
|
// Replace highlighting markers with HTML tags
|
||||||
|
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>");
|
||||||
|
// Convert newlines to <br> tags for HTML display
|
||||||
|
result.highlightedContentSnippet = result.highlightedContentSnippet.replace(/\n/g, "<br>");
|
||||||
|
}
|
||||||
|
|
||||||
if (result.highlightedAttributeSnippet) {
|
if (result.highlightedAttributeSnippet) {
|
||||||
// Replace highlighting markers with HTML tags
|
// Replace highlighting markers with HTML tags
|
||||||
result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>");
|
result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/{/g, "<b>").replace(/}/g, "</b>");
|
||||||
// Keep inline display - no conversion of newlines needed
|
// Convert newlines to <br> tags for HTML display
|
||||||
|
result.highlightedAttributeSnippet = result.highlightedAttributeSnippet.replace(/\n/g, "<br>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,6 +93,9 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
|||||||
|
|
||||||
// Appearance
|
// Appearance
|
||||||
splitEditorOrientation: "horziontal" | "vertical";
|
splitEditorOrientation: "horziontal" | "vertical";
|
||||||
|
motionEnabled: boolean;
|
||||||
|
shadowsEnabled: boolean;
|
||||||
|
backdropEffectsEnabled: boolean;
|
||||||
codeNoteTheme: string;
|
codeNoteTheme: string;
|
||||||
|
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
|
|||||||
Reference in New Issue
Block a user