Merge remote-tracking branch 'origin/develop' into feature/improved_promoted_attributes

; Conflicts:
;	src/public/app/layouts/desktop_layout.js
This commit is contained in:
Elian Doran 2024-11-27 21:29:15 +02:00
commit bb4164f10f
No known key found for this signature in database
103 changed files with 2611 additions and 863 deletions

View File

@ -3,6 +3,12 @@ description: Report a bug
title: "(Bug report) "
labels: "Type: Bug"
body:
- type: textarea
attributes:
label: Description
description: A clear and concise description of the bug and any additional information.
validations:
required: true
- type: input
attributes:
label: TriliumNext Version
@ -38,12 +44,6 @@ body:
placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
validations:
required: true
- type: textarea
attributes:
label: Description
description: A clear and concise description of the bug and any additional information.
validations:
required: true
- type: textarea
attributes:
label: Error logs

View File

@ -9,6 +9,12 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
GHCR_REGISTRY: ghcr.io
DOCKERHUB_REGISTRY: docker.io
IMAGE_NAME: ${{ github.repository_owner }}/notes
TEST_TAG: ${{ github.repository_owner }}/notes:test
jobs:
build_docker:
name: Build Docker image
@ -30,4 +36,66 @@ jobs:
with:
context: .
cache-from: type=gha
cache-to: type=gha,mode=max
cache-to: type=gha,mode=max
test_docker:
name: Check Docker build
runs-on: ubuntu-latest
strategy:
matrix:
include:
- dockerfile: Dockerfile.alpine
- dockerfile: Dockerfile
steps:
- name: Checkout the repository
uses: actions/checkout@v4
- name: Set IMAGE_NAME to lowercase
run: echo "IMAGE_NAME=${IMAGE_NAME,,}" >> $GITHUB_ENV
- name: Set TEST_TAG to lowercase
run: echo "TEST_TAG=${TEST_TAG,,}" >> $GITHUB_ENV
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up node & dependencies
uses: actions/setup-node@v4
with:
node-version: 20
cache: "npm"
- run: npm ci
- name: Run the TypeScript build
run: npx tsc
- name: Create server-package.json
run: cat package.json | grep -v electron > server-package.json
- name: Build and export to Docker
uses: docker/build-push-action@v6
with:
context: .
file: ${{ matrix.dockerfile }}
load: true
tags: ${{ env.TEST_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Validate container run output
run: |
CONTAINER_ID=$(docker run -d --log-driver=journald --rm --name trilium_local ${{ env.TEST_TAG }})
echo "Container ID: $CONTAINER_ID"
- name: Wait for the healthchecks to pass
uses: stringbean/docker-healthcheck-action@v1
with:
container: trilium_local
wait-time: 50
require-status: running
require-healthy: true
# Print the entire log of the container thus far, regardless if the healthcheck failed or succeeded
- name: Print entire log
if: always()
run: |
journalctl -u docker CONTAINER_NAME=trilium_local --no-pager

View File

@ -17,17 +17,45 @@
#
# --------------------------------------------------------------------------------------------------
number_of_keys() {
[ -f "$1" ] && jq 'path(..) | select(length == 2) | .[1]' "$1" | wc -l || echo "0"
}
stats() {
# Print the number of existing strings on the JSON files for each locale
s=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[0]}/en/server.json" | wc -l)
c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/en/translation.json" | wc -l)
echo "|locale |server strings |client strings |"
echo "|-------|---------------|---------------|"
echo "| en | ${s} | ${c} |"
s=$(number_of_keys "${paths[0]}/en/server.json")
c=$(number_of_keys "${paths[1]}/en/translation.json")
echo "| locale |server strings |client strings |"
echo "|--------|---------------|---------------|"
echo "| en | ${s} | ${c} |"
for locale in "${locales[@]}"; do
s=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[0]}/${locale}/server.json" | wc -l)
c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/${locale}/translation.json" | wc -l)
echo "| ${locale} | ${s} | ${c} |"
s=$(number_of_keys "${paths[0]}/${locale}/server.json")
c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
n1=$(((8 - ${#locale}) / 2))
n2=$((n1 == 1 ? n1 + 1 : n1))
echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")| ${s} | ${c} |"
done
}
update_1() {
# Update PO files from English and localized JSON files as source
# NOTE: if you want a new language you need to first create the JSON files
# on their corresponding place with `{}` as content to avoid error on `json2po`
local locales=("$@")
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
done
done
}
update_2() {
# Recover translation from PO files to localized JSON files
local locales=("$@")
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
done
done
}
@ -35,11 +63,11 @@ help() {
echo -e "\nDescription:"
echo -e "\tCreate PO files to make easier the labor of translation"
echo -e "\nUsage:"
echo -e "\t./translation.sh [--stats] [--update <OPT_LOCALE>] [--update2 <OPT_LOCALE>]"
echo -e "\t./translation.sh [--stats] [--update1 <OPT_LOCALE>] [--update2 <OPT_LOCALE>]"
echo -e "\nFlags:"
echo -e " --clear\n\tClear all po-* directories"
echo -e " --stats\n\tPrint the number of existing strings on the JSON files for each locale"
echo -e " --update <LOCALE>\n\tUpdate PO files from English and localized JSON files as source"
echo -e " --update1 <LOCALE>\n\tUpdate PO files from English and localized JSON files as source"
echo -e " --update2 <LOCALE>\n\tRecover translation from PO files to localized JSON files"
}
@ -51,7 +79,7 @@ file_path="$(
pwd -P
)"
paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/")
locales=(cn es fr ro)
locales=(cn de es fr pt_br ro tw)
if [ $# -eq 1 ]; then
if [ "$1" == "--clear" ]; then
@ -62,34 +90,18 @@ if [ $# -eq 1 ]; then
done
elif [ "$1" == "--stats" ]; then
stats
elif [ "$1" == "--update" ]; then
# Update PO files from English and localized JSON files as source
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
done
done
elif [ "$1" == "--update1" ]; then
update_1 "${locales[@]}"
elif [ "$1" == "--update2" ]; then
# Recover translation from PO files to localized JSON files
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
done
done
update_2 "${locales[@]}"
else
help
fi
elif [ $# -eq 2 ]; then
if [ "$1" == "--update" ]; then
locale="$2"
for path in "${paths[@]}"; do
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
done
if [ "$1" == "--update1" ]; then
update_1 "$2"
elif [ "$1" == "--update2" ]; then
locale="$2"
for path in "${paths[@]}"; do
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
done
update_2 "$2"
else
help
fi

Binary file not shown.

1
libraries/mermaid-elk/elk.min.js vendored Normal file

File diff suppressed because one or more lines are too long

13
libraries/mermaid-elk/package-lock.json generated Normal file
View File

@ -0,0 +1,13 @@
{
"name": "mermaid-elk",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "mermaid-elk",
"version": "1.0.0",
"license": "ISC"
}
}
}

View File

@ -0,0 +1,13 @@
{
"name": "mermaid-elk",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"build": "cross-env node --import ../../loader-register.js ../../node_modules/webpack/bin/webpack.js -c webpack.config.cjs"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"dependencies": {}
}

View File

@ -0,0 +1,19 @@
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: "../../node_modules/@mermaid-js/layout-elk/dist/mermaid-layout-elk.esm.min.mjs",
output: {
library: "MERMAID_ELK",
filename: "elk.min.js",
path: path.resolve(__dirname),
libraryTarget: "umd",
libraryExport: "default"
},
plugins: [
new webpack.optimize.LimitChunkCountPlugin({
maxChunks: 1
})
]
}

31
package-lock.json generated
View File

@ -1,18 +1,19 @@
{
"name": "trilium",
"version": "0.90.11-beta",
"version": "0.90.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "trilium",
"version": "0.90.11-beta",
"version": "0.90.12",
"license": "AGPL-3.0-only",
"dependencies": {
"@braintree/sanitize-url": "7.1.0",
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.10.0",
"@mermaid-js/layout-elk": "0.1.5",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -47,7 +48,7 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.5",
"i18next": "23.16.4",
"i18next": "23.16.8",
"i18next-fs-backend": "2.3.2",
"i18next-http-backend": "2.6.2",
"image-type": "4.1.0",
@ -3072,6 +3073,18 @@
"url": "https://github.com/malept/cross-spawn-promise?sponsor=1"
}
},
"node_modules/@mermaid-js/layout-elk": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@mermaid-js/layout-elk/-/layout-elk-0.1.5.tgz",
"integrity": "sha512-6ML4iWdVdyIkSW47KiID9runHzaomLxdMfNo9U60LJvfcQkB/FAjg0Vjc4AZEQnnBq7ibAoAknAWlT1XetwXSg==",
"dependencies": {
"d3": "^7.9.0",
"elkjs": "^0.9.3"
},
"peerDependencies": {
"mermaid": "^11.0.0"
}
},
"node_modules/@mermaid-js/parser": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz",
@ -7904,6 +7917,11 @@
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
},
"node_modules/elkjs": {
"version": "0.9.3",
"resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
"integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
},
"node_modules/emitter-listener": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
@ -10024,9 +10042,9 @@
}
},
"node_modules/i18next": {
"version": "23.16.4",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.4.tgz",
"integrity": "sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==",
"version": "23.16.8",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
"integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
"funding": [
{
"type": "individual",
@ -10041,6 +10059,7 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
}
],
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2"
}

View File

@ -2,7 +2,7 @@
"name": "trilium",
"productName": "TriliumNext Notes",
"description": "Build your personal knowledge base with TriliumNext Notes",
"version": "0.90.11-beta",
"version": "0.90.12",
"license": "AGPL-3.0-only",
"main": "./dist/electron-main.js",
"author": {
@ -54,6 +54,7 @@
"@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.10.0",
"@mermaid-js/layout-elk": "0.1.5",
"archiver": "7.0.1",
"async-mutex": "0.5.0",
"autocomplete.js": "0.38.1",
@ -88,7 +89,7 @@
"html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.5",
"i18next": "23.16.4",
"i18next": "23.16.8",
"i18next-fs-backend": "2.3.2",
"i18next-http-backend": "2.6.2",
"image-type": "4.1.0",

10
renovate.json Normal file
View File

@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"repositories": ["TriliumNext/Notes"],
"schedule": ["before 3am"],
"labels": ["dependencies", "renovate"],
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"branchConcurrentLimit": 0
}

View File

@ -88,8 +88,11 @@ export default class Component {
if (fun) {
return this.callMethod(fun, data);
}
else {
} else {
if (!this.parent) {
throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
}
return this.parent.triggerCommand(name, data);
}
}

View File

@ -586,6 +586,11 @@ export default class TabManager extends Component {
}
}
async copyTabToNewWindowCommand({ntxId}) {
const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId);
this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
}
async reopenLastTabCommand() {
let closeLastEmptyTab = null;

View File

@ -0,0 +1,3 @@
<p>隐藏树用于记录各种应用层数据,这些数据大部分时间可能对用户不可见。</p>
<p>确保你知道自己在做什么。对这个子树的错误更改可能会导致应用程序崩溃。</p>

View File

@ -0,0 +1 @@
<p>此启动器操作的键盘快捷键可以在“选项”->“快捷键”中进行配置。</p>

View File

@ -0,0 +1,3 @@
<p>“后退”和“前进”按钮允许您在导航历史中移动。</p>
<p>这些启动器仅在桌面版本中有效,在服务器版本中将被忽略,您可以使用浏览器的原生导航按钮代替。</p>

View File

@ -0,0 +1,11 @@
<p>欢迎来到启动栏配置界面。</p>
<p>您可以在此处执行以下操作:</p>
<ul>
<li>通过拖动将可用的启动器移动到可见列表中(从而将它们放入启动栏)</li>
<li>通过拖动将可见的启动器移动到可用列表中(从而将它们从启动栏中隐藏)</li>
<li>您可以通过拖动重新排列列表中的项目</li>
<li>通过右键点击“可见启动器”文件夹来创建新的启动器</li>
<li>如果您想恢复默认设置,可以在右键菜单中找到“重置”选项。</li>
</ul>

View File

@ -0,0 +1,9 @@
<p>您可以定义以下属性:</p>
<ol>
<li><code>target</code> - 激活启动器时应打开的笔记</li>
<li><code>hoistedNote</code> - 可选,在打开目标笔记之前将更改提升的笔记</li>
<li><code>keyboardShortcut</code> - 可选,按下键盘快捷键将打开该笔记</li>
</ol>
<p>启动栏显示来自启动器的标题/图标,这不一定与目标笔记的标题/图标一致。</p>

View File

@ -0,0 +1,12 @@
<p>脚本启动器可以执行通过 <code>~script</code> 关系连接的脚本(代码笔记)。</p>
<ol>
<li><code>script</code> - 与应在启动器激活时执行的脚本笔记的关系</li>
<li><code>keyboardShortcut</code> - 可选,按下键盘快捷键将激活启动器</li>
</ol>
<h4>示例脚本</h4>
<pre>
api.showMessage("当前笔记是 " + api.getActiveContextNote().title);
</pre>

View File

@ -0,0 +1,6 @@
<p>间隔器允许您在视觉上将启动器分组。您可以在提升的属性中进行配置:</p>
<ul>
<li><code>baseSize</code> - 定义以像素为单位的大小(如果有足够的空间)</li>
<li><code>growthFactor</code> - 如果您希望间隔器保持恒定的 <code>baseSize</code>,则设置为 0如果设置为正值它将增长。</li>
</ul>

View File

@ -0,0 +1,34 @@
<p>请在提升的属性中定义目标小部件笔记。该小部件将用于渲染启动栏图标。</p>
<h4>示例启动栏小部件</h4>
<pre>
const TPL = `&lt;div style="height: 53px; width: 53px;"&gt;&lt;/div&gt;`;
class ExampleLaunchbarWidget extends api.NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
}
async refreshWithNote(note) {
this.$widget.css("background-color", this.stringToColor(note.title));
}
stringToColor(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
let color = '#';
for (let i = 0; i < 3; i++) {
const value = (hash >> (i * 8)) & 0xFF;
color += ('00' + value.toString(16)).substr(-2);
}
return color;
}
}
module.exports = new ExampleLaunchbarWidget();
</pre>

View File

@ -0,0 +1 @@
<p>在这里您可以找到所有分享的笔记。</p>

View File

@ -0,0 +1 @@
<p>此笔记作为一个子树,用于存储由用户脚本生成的数据,这些数据本应避免在隐藏子树中随意创建。</p>

View File

@ -83,6 +83,7 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
export default class DesktopLayout {
constructor(customWidgets) {
@ -92,112 +93,120 @@ export default class DesktopLayout {
getRootWidget(appContext) {
appContext.noteTreeWidget = new NoteTreeWidget();
return new RootContainer()
const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal);
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext)
.child(new FlexContainer("column")
.id("launcher-pane")
.css("width", "53px")
.child(new GlobalMenuWidget())
.child(new LauncherContainer())
.child(new LeftPaneToggleWidget())
.optChild(launcherPaneIsHorizontal, new FlexContainer('row')
.child(new TabRowWidget().class("full-width"))
.child(new TitleBarButtonsWidget())
.css('height', '40px')
.css('background-color', 'var(--launcher-pane-background-color)')
.setParent(appContext)
)
.child(new LeftPaneContainer()
.child(new QuickSearchWidget())
.child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane'))
)
.child(new FlexContainer('column')
.id('rest-pane')
.optChild(launcherPaneIsHorizontal, launcherPane)
.child(new FlexContainer('row')
.css("flex-grow", "1")
.child(new FlexContainer('row')
.child(new TabRowWidget())
.child(new TitleBarButtonsWidget())
.css('height', '40px')
.optChild(!launcherPaneIsHorizontal, launcherPane)
.child(new LeftPaneContainer()
.optChild(!launcherPaneIsHorizontal, new QuickSearchWidget())
.child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane'))
)
.child(new FlexContainer('row')
.filling()
.collapsible()
.child(new FlexContainer('column')
.child(new FlexContainer('column')
.id('rest-pane')
.css("flex-grow", "1")
.optChild(!launcherPaneIsHorizontal, new FlexContainer('row')
.child(new TabRowWidget())
.child(new TitleBarButtonsWidget())
.css('height', '40px')
)
.child(new FlexContainer('row')
.filling()
.collapsible()
.id('center-pane')
.child(new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(new FlexContainer('row').class('title-row')
.css("height", "50px")
.css("min-height", "50px")
.css('align-items', "center")
.cssBlock('.title-row > * { margin: 5px; }')
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(new SpacerWidget(0, 1))
.child(new MovePaneButton(true))
.child(new MovePaneButton(false))
.child(new ClosePaneButton())
.child(new CreatePaneButton())
)
.child(
new RibbonContainer()
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())
.ribbon(new EditedNotesWidget())
.ribbon(new BookPropertiesWidget())
.ribbon(new NotePropertiesWidget())
.ribbon(new FilePropertiesWidget())
.ribbon(new ImagePropertiesWidget())
.ribbon(new BasicPropertiesWidget())
.ribbon(new OwnedAttributeListWidget())
.ribbon(new InheritedAttributesWidget())
.ribbon(new NotePathsWidget())
.ribbon(new NoteMapRibbonWidget())
.ribbon(new SimilarNotesWidget())
.ribbon(new NoteInfoWidget())
.button(new RevisionsButton())
.button(new NoteActionsWidget())
)
.child(new SharedInfoWidget())
.child(new WatchedFileUpdateStatusWidget())
.child(new FloatingButtons()
.child(new EditButton())
.child(new ShowTocWidgetButton())
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())
.child(
new ScrollingContainer()
.filling()
.child(new PromotedAttributesWidget())
.child(new SqlTableSchemasWidget())
.child(new NoteDetailWidget())
.child(new NoteListWidget())
.child(new SearchResultWidget())
.child(new SqlResultWidget())
.child(new ScrollPaddingWidget())
)
.child(new ApiLogWidget())
.child(new FindWidget())
.child(
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC
...this.customWidgets.get('note-detail-pane')
)
.child(new FlexContainer('column')
.filling()
.collapsible()
.id('center-pane')
.child(new SplitNoteContainer(() =>
new NoteWrapperWidget()
.child(new FlexContainer('row').class('title-row')
.css("height", "50px")
.css("min-height", "50px")
.css('align-items', "center")
.cssBlock('.title-row > * { margin: 5px; }')
.child(new NoteIconWidget())
.child(new NoteTitleWidget())
.child(new SpacerWidget(0, 1))
.child(new MovePaneButton(true))
.child(new MovePaneButton(false))
.child(new ClosePaneButton())
.child(new CreatePaneButton())
)
.child(
new RibbonContainer()
// the order of the widgets matter. Some of these want to "activate" themselves
// when visible. When this happens to multiple of them, the first one "wins".
// promoted attributes should always win.
.ribbon(new ClassicEditorToolbar())
.ribbon(new ScriptExecutorWidget())
.ribbon(new SearchDefinitionWidget())
.ribbon(new EditedNotesWidget())
.ribbon(new BookPropertiesWidget())
.ribbon(new NotePropertiesWidget())
.ribbon(new FilePropertiesWidget())
.ribbon(new ImagePropertiesWidget())
.ribbon(new BasicPropertiesWidget())
.ribbon(new OwnedAttributeListWidget())
.ribbon(new InheritedAttributesWidget())
.ribbon(new NotePathsWidget())
.ribbon(new NoteMapRibbonWidget())
.ribbon(new SimilarNotesWidget())
.ribbon(new NoteInfoWidget())
.button(new RevisionsButton())
.button(new NoteActionsWidget())
)
.child(new SharedInfoWidget())
.child(new WatchedFileUpdateStatusWidget())
.child(new FloatingButtons()
.child(new EditButton())
.child(new ShowTocWidgetButton())
.child(new ShowHighlightsListWidgetButton())
.child(new CodeButtonsWidget())
.child(new RelationMapButtons())
.child(new CopyImageReferenceButton())
.child(new SvgExportButton())
.child(new BacklinksWidget())
.child(new HideFloatingButtonsButton())
)
.child(new MermaidWidget())
.child(
new ScrollingContainer()
.filling()
.child(new PromotedAttributesWidget())
.child(new SqlTableSchemasWidget())
.child(new NoteDetailWidget())
.child(new NoteListWidget())
.child(new SearchResultWidget())
.child(new SqlResultWidget())
.child(new ScrollPaddingWidget())
)
.child(new ApiLogWidget())
.child(new FindWidget())
.child(
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC
...this.customWidgets.get('note-detail-pane')
)
)
)
.child(...this.customWidgets.get('center-pane'))
)
.child(new RightPaneContainer()
.child(new TocWidget())
.child(new HighlightsListWidget())
.child(...this.customWidgets.get('right-pane'))
)
.child(...this.customWidgets.get('center-pane'))
)
.child(new RightPaneContainer()
.child(new TocWidget())
.child(new HighlightsListWidget())
.child(...this.customWidgets.get('right-pane'))
)
)
)
@ -225,4 +234,27 @@ export default class DesktopLayout {
.child(new ConfirmDialog())
.child(new PromptDialog());
}
#buildLauncherPane(isHorizontal) {
let launcherPane;
if (isHorizontal) {
launcherPane = new FlexContainer("row")
.css("height", "53px")
.class("horizontal")
.child(new LeftPaneToggleWidget(true))
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true))
} else {
launcherPane = new FlexContainer("column")
.css("width", "53px")
.class("vertical")
.child(new GlobalMenuWidget(false))
.child(new LauncherContainer(false))
.child(new LeftPaneToggleWidget(false));
}
launcherPane.id("launcher-pane");
return launcherPane;
}
}

View File

@ -24,6 +24,7 @@ import RootContainer from "../widgets/containers/root_container.js";
import SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
const MOBILE_CSS = `
<style>
@ -112,15 +113,12 @@ span.fancytree-expander {
export default class MobileLayout {
getRootWidget(appContext) {
return new RootContainer()
const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext)
.cssBlock(MOBILE_CSS)
.child(new FlexContainer("column")
.id("launcher-pane")
.css("width", "53px")
.child(new GlobalMenuWidget())
.child(new LauncherContainer())
)
.child(this.#buildLauncherPane(launcherPaneIsHorizontal))
.child(new FlexContainer("row")
.filling()
.child(new ScreenContainer("tree", 'column')
@ -140,12 +138,14 @@ export default class MobileLayout {
.child(new FlexContainer('row').contentSized()
.css('font-size', 'larger')
.css('align-items', 'center')
.child(new MobileDetailMenuWidget().contentSized())
.optChild(!launcherPaneIsHorizontal, new MobileDetailMenuWidget(false).contentSized())
.child(new NoteTitleWidget()
.contentSized()
.css("position: relative;")
.css("top: 5px;")
.optCss(launcherPaneIsHorizontal, "padding-left", "0.5em")
)
.optChild(launcherPaneIsHorizontal, new MobileDetailMenuWidget(true).contentSized())
.child(new CloseDetailButtonWidget().contentSized()))
.child(new SharedInfoWidget())
.child(new FloatingButtons()
@ -174,4 +174,25 @@ export default class MobileLayout {
.child(new ConfirmDialog())
);
}
#buildLauncherPane(isHorizontal) {
let launcherPane;
if (isHorizontal) {
launcherPane = new FlexContainer("row")
.class("horizontal")
.css("height", "53px")
.child(new LauncherContainer(true))
.child(new GlobalMenuWidget(true));
} else {
launcherPane = new FlexContainer("column")
.class("vertical")
.css("width", "53px")
.child(new GlobalMenuWidget(false))
.child(new LauncherContainer(false));
}
launcherPane.id("launcher-pane");
return launcherPane;
}
}

View File

@ -3,6 +3,7 @@ import keyboardActionService from '../services/keyboard_actions.js';
class ContextMenu {
constructor() {
this.$widget = $("#context-menu-container");
this.$widget.addClass("dropend");
this.dateContextMenuOpenedMs = 0;
$(document).on('click', () => this.hide());
@ -11,6 +12,12 @@ class ContextMenu {
async show(options) {
this.options = options;
if (this.$widget.hasClass("show")) {
// The menu is already visible. Hide the menu then open it again
// at the new location to re-trigger the opening animation.
await this.hide();
}
this.$widget.empty();
this.addItems(this.$widget, options.items);
@ -96,6 +103,10 @@ class ContextMenu {
.append(" &nbsp; ") // some space between icon and text
.append(item.title);
if (item.shortcut) {
$link.append($("<kbd>").text(item.shortcut));
}
const $item = $("<li>")
.addClass("dropdown-item")
.append($link)
@ -142,18 +153,26 @@ class ContextMenu {
}
}
hide() {
async hide() {
// this date checking comes from change in FF66 - https://github.com/zadam/trilium/issues/468
// "contextmenu" event also triggers "click" event which depending on the timing can close the just opened context menu
// we might filter out right clicks, but then it's better if even right clicks close the context menu
if (Date.now() - this.dateContextMenuOpenedMs > 300) {
// seems like if we hide the menu immediately, some clicks can get propagated to the underlying component
// see https://github.com/zadam/trilium/pull/3805 for details
setTimeout(() => this.$widget.hide(), 100);
await timeout(100);
this.$widget.removeClass("show");
this.$widget.hide()
}
}
}
function timeout(ms) {
return new Promise((accept, reject) => {
setTimeout(accept, ms);
});
}
const contextMenu = new ContextMenu();
export default contextMenu;

View File

@ -2,6 +2,7 @@ import utils from "../services/utils.js";
import options from "../services/options.js";
import zoomService from "../components/zoom.js";
import contextMenu from "./context_menu.js";
import { t } from "../services/i18n.js";
function setupContextMenu() {
const electron = utils.dynamicRequire('electron');
@ -28,7 +29,7 @@ function setupContextMenu() {
}
items.push({
title: `Add "${params.misspelledWord}" to dictionary`,
title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
uiIcon: "bx bx-plus",
handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
});
@ -39,7 +40,8 @@ function setupContextMenu() {
if (params.isEditable) {
items.push({
enabled: editFlags.canCut && hasText,
title: `Cut <kbd>${platformModifier}+X`,
title: t("electron_context_menu.cut"),
shortcut: `${platformModifier}+X`,
uiIcon: "bx bx-cut",
handler: () => webContents.cut()
});
@ -48,7 +50,8 @@ function setupContextMenu() {
if (params.isEditable || hasText) {
items.push({
enabled: editFlags.canCopy && hasText,
title: `Copy <kbd>${platformModifier}+C`,
title: t("electron_context_menu.copy"),
shortcut: `${platformModifier}+C`,
uiIcon: "bx bx-copy",
handler: () => webContents.copy()
});
@ -56,7 +59,7 @@ function setupContextMenu() {
if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') {
items.push({
title: `Copy link`,
title: t("electron_context_menu.copy-link"),
uiIcon: "bx bx-copy",
handler: () => {
electron.clipboard.write({
@ -70,7 +73,8 @@ function setupContextMenu() {
if (params.isEditable) {
items.push({
enabled: editFlags.canPaste,
title: `Paste <kbd>${platformModifier}+V`,
title: t("electron_context_menu.paste"),
shortcut: `${platformModifier}+V`,
uiIcon: "bx bx-paste",
handler: () => webContents.paste()
});
@ -79,7 +83,8 @@ function setupContextMenu() {
if (params.isEditable) {
items.push({
enabled: editFlags.canPaste,
title: `Paste as plain text <kbd>${platformModifier}+Shift+V`,
title: t("electron_context_menu.paste-as-plain-text"),
shortcut: `${platformModifier}+Shift+V`,
uiIcon: "bx bx-paste",
handler: () => webContents.pasteAndMatchStyle()
});
@ -106,9 +111,11 @@ function setupContextMenu() {
// Replace the placeholder with the real search keyword.
let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
items.push({title: "----"});
items.push({
enabled: editFlags.canPaste,
title: `Search for "${shortenedSelection}" with ${searchEngineName}`,
title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }),
uiIcon: "bx bx-search-alt",
handler: () => electron.shell.openExternal(searchUrl)
});

View File

@ -1,3 +1,4 @@
import { t } from '../services/i18n.js';
import utils from "../services/utils.js";
import contextMenu from "./context_menu.js";
import imageService from "../services/image.js";
@ -18,11 +19,15 @@ function setupContextMenu($image) {
y: e.pageY,
items: [
{
title: "Copy reference to clipboard",
title: t("image_context_menu.copy_reference_to_clipboard"),
command: "copyImageReferenceToClipboard",
uiIcon: "bx bx-empty"
uiIcon: "bx bx-directions"
},
{
title: t("image_context_menu.copy_image_to_clipboard"),
command: "copyImageToClipboard",
uiIcon: "bx bx-copy"
},
{ title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty" },
],
selectMenuItemHandler: async ({ command }) => {
if (command === 'copyImageReferenceToClipboard') {
@ -57,4 +62,4 @@ function setupContextMenu($image) {
export default {
setupContextMenu
};
};

View File

@ -37,18 +37,22 @@ export default class LauncherContextMenu {
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();
return [
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-plus" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-note" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-code-curly" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-customize" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-dots-horizontal" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: "----" } : null,
{ title: `${t("launcher_context_menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted },
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-empty", enabled: canBeReset},
{ title: "----" },
isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
{ title: `${t("launcher_context_menu.duplicate-launcher")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
enabled: isItem }
(isVisibleItem || isAvailableItem) ? { title: "----" } : null,
{ title: `${t("launcher_context_menu.duplicate-launcher")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem },
{ title: `${t("launcher_context_menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted },
{ title: "----" },
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset}
].filter(row => row !== null);
}

View File

@ -1,3 +1,4 @@
import { t } from "../services/i18n.js";
import contextMenu from "./context_menu.js";
import appContext from "../components/app_context.js";
@ -6,9 +7,9 @@ function openContextMenu(notePath, e, viewScope = {}, hoistedNoteId = null) {
x: e.pageX,
y: e.pageY,
items: [
{title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "bx bx-empty"},
{title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
{title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
{title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external"},
{title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
{title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
],
selectMenuItemHandler: ({command}) => {
if (!hoistedNoteId) {

View File

@ -49,56 +49,98 @@ export default class TreeContextMenu {
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [
{ title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
{ title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.insert-note-after")} <kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bxs-chevrons-up", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ title: "----" },
{ title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions },
{ title: `${t("tree-context-menu.insert-child-note")} <kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
{ title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptions },
{ title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
{ title: "----" },
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes },
isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
!isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
{ title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-empty",
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.advanced"), uiIcon: "bx bx-empty", enabled: true, items: [
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted && notOptions },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-empty", enabled: true }
] },
{ title: "----" },
{ title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
{ title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
{ title: "----" },
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-empty",
enabled: isNotRoot && !isHoisted },
{ title: t("tree-context-menu.advanced"), uiIcon: "bx bxs-wrench", enabled: true, items: [
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true },
{ title: "----" },
{ title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-rename", enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions },
{ title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
{ title: "----" },
{ title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
{ title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, command: "sortChildNotes", uiIcon: "bx bx-sort-down", enabled: noSelectedNotes && notSearch },
{ title: "----" },
{ title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-directions", enabled: true },
{ title: t("tree-context-menu.recent-changes-in-subtree"), command: "recentChangesInSubtree", uiIcon: "bx bx-history", enabled: noSelectedNotes && notOptions },
] },
{ title: "----" },
{ title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`, command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-empty",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`, command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
{ title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
{ title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-transfer",
enabled: isNotRoot && !isHoisted && parentNotSearch },
{ title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate",
enabled: isNotRoot && !isHoisted },
{ title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon",
enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
{ title: "----" },
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-empty",
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-empty",
{ title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export",
enabled: notSearch && noSelectedNotes && notOptions },
{ title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus",
enabled: true }
{ title: "----" },
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
enabled: notSearch && noSelectedNotes },
].filter(row => row !== null);
}

View File

@ -13,22 +13,23 @@ import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js";
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
import { t } from "./i18n.js";
const ACTION_GROUPS = [
{
title: 'Labels',
title: t("bulk_actions.labels"),
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
},
{
title: 'Relations',
title: t("bulk_actions.relations"),
actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
},
{
title: 'Notes',
title: t("bulk_actions.notes"),
actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction],
},
{
title: 'Other',
title: t("bulk_actions.other"),
actions: [ExecuteScriptBulkAction]
}
];

View File

@ -12,6 +12,7 @@ import FAttachment from "../entities/fattachment.js";
import imageContextMenuService from "../menus/image_context_menu.js";
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
import mime_types from "./mime_types.js";
import { loadElkIfNeeded } from "./mermaid.js";
let idCounter = 1;
@ -237,6 +238,7 @@ async function renderMermaid(note, $renderedContent) {
mermaid.mermaidAPI.initialize({startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: 'antiscript'});
try {
await loadElkIfNeeded(content);
const {svg} = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
$renderedContent.append($(svg));

View File

@ -58,7 +58,19 @@ const FORCE_GRAPH = {
};
const MERMAID = {
js: [ "node_modules/mermaid/dist/mermaid.min.js" ]
js: [
"node_modules/mermaid/dist/mermaid.min.js"
]
}
/**
* The ELK extension of Mermaid.js, which supports more advanced layouts.
* See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
*/
const MERMAID_ELK = {
js: [
"libraries/mermaid-elk/elk.min.js"
]
}
const EXCALIDRAW = {
@ -197,6 +209,7 @@ export default {
WHEEL_ZOOM,
FORCE_GRAPH,
MERMAID,
MERMAID_ELK,
EXCALIDRAW,
MARKJS,
I18NEXT,

View File

@ -254,8 +254,15 @@ function goToLinkExt(evt, hrefLink, $link) {
window.open(hrefLink, '_blank');
} else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
const electron = utils.dynamicRequire('electron');
electron.shell.openPath(hrefLink);
} else {
// Enable protocols supported by CKEditor 5 to be clickable.
// Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts.
// Adding `:` to these links might be safer.
const otherAllowedProtocols = ['mailto:', 'tel:', 'sms:', 'sftp:', 'smb:', 'slack:', 'zotero:'];
if (otherAllowedProtocols.some(protocol => hrefLink.toLowerCase().startsWith(protocol))){
window.open(hrefLink, '_blank');
}
}
}
}

View File

@ -0,0 +1,28 @@
import library_loader from "./library_loader.js";
let elkLoaded = false;
/**
* Determines whether the ELK extension of Mermaid.js needs to be loaded (which is a relatively large library), based on the
* front-matter of the diagram and loads the library if needed.
*
* <p>
* If the library has already been loaded or the diagram does not require it, the method will exit immediately.
*
* @param mermaidContent the plain text of the mermaid diagram, potentially including a frontmatter.
*/
export async function loadElkIfNeeded(mermaidContent) {
if (elkLoaded) {
// Exit immediately since the ELK library is already loaded.
return;
}
const parsedContent = await mermaid.parse(mermaidContent, {
suppressErrors: true
});
if (parsedContent?.config?.layout === "elk") {
elkLoaded = true;
await library_loader.requireLibrary(library_loader.MERMAID_ELK);
mermaid.registerLayoutLoaders(MERMAID_ELK);
}
}

View File

@ -45,6 +45,16 @@ async function autocompleteSource(term, cb, options = {}) {
].concat(results);
}
if (term.trim().length >= 1 && options.allowSearchNotes) {
results = results.concat([
{
action: 'search-notes',
noteTitle: term,
highlightedNotePathTitle: `Search for "${utils.escapeHtml(term)}" <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
}
]);
}
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
results = [
{
@ -138,6 +148,17 @@ function initNoteAutocomplete($el, options) {
autocompleteOptions.debug = true; // don't close on blur
}
if (options.allowSearchNotes) {
$el.on('keydown', (event) => {
if (event.ctrlKey && event.key === 'Enter') {
// Prevent Ctrl + Enter from triggering autoComplete.
event.stopImmediatePropagation();
event.preventDefault();
$el.trigger('autocomplete:selected', { action: 'search-notes', noteTitle: $el.autocomplete("val")});
}
});
}
$el.autocomplete({
...autocompleteOptions,
appendTo: document.querySelector('body'),
@ -192,6 +213,12 @@ function initNoteAutocomplete($el, options) {
suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
}
if (suggestion.action === 'search-notes') {
const searchString = suggestion.noteTitle;
appContext.triggerCommand('searchNotes', { searchString });
return;
}
$el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null);

View File

@ -6,7 +6,7 @@ function parse(value) {
if (token === 'promoted') {
defObj.isPromoted = true;
}
else if (['text', 'number', 'boolean', 'date', 'datetime', 'url'].includes(token)) {
else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
defObj.labelType = token;
}
else if (['single', 'multi'].includes(token)) {

View File

@ -125,6 +125,7 @@ const TPL = `
<option value="boolean">${t('attribute_detail.boolean')}</option>
<option value="date">${t('attribute_detail.date')}</option>
<option value="datetime">${t('attribute_detail.date_time')}</option>
<option value="time">${t('attribute_detail.time')}</option>
<option value="url">${t('attribute_detail.url')}</option>
</select>
</td>

View File

@ -40,6 +40,21 @@ class BasicWidget extends Component {
return this;
}
/**
* Conditionally adds the given components as children to this component.
*
* @param {boolean} condition whether to add the components.
* @param {...any} components the components to be added as children to this component provided the condition is truthy.
* @returns self for chaining.
*/
optChild(condition, ...components) {
if (condition) {
return this.child(...components);
} else {
return this;
}
}
id(id) {
this.attrs.id = id;
return this;
@ -50,11 +65,34 @@ class BasicWidget extends Component {
return this;
}
/**
* Sets the CSS attribute of the given name to the given value.
*
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining.
*/
css(name, value) {
this.attrs.style += `${name}: ${value};`;
return this;
}
/**
* Sets the CSS attribute of the given name to the given value, but only if the condition provided is truthy.
*
* @param {boolean} condition `true` in order to apply the CSS, `false` to ignore it.
* @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
* @param {string} value the value of the CSS attribute to set (e.g. `12px`).
* @returns self for chaining.
*/
optCss(condition, name, value) {
if (condition) {
return this.css(name, value);
}
return this;
}
contentSized() {
this.css("contain", "none");

View File

@ -4,8 +4,8 @@ import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
import froca from "../services/froca.js";
export default class BookmarkButtons extends FlexContainer {
constructor() {
super("column");
constructor(isHorizontalLayout) {
super(isHorizontalLayout ? "row" : "column");
this.contentSized();
}

View File

@ -23,7 +23,11 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.tooltip = new bootstrap.Tooltip(this.$widget, {
html: true, title: () => this.getTitle(), trigger: 'hover'
html: true,
title: () => this.getTitle(),
trigger: 'hover',
placement: this.settings.titlePlacement,
fallbackPlacements: [ this.settings.titlePlacement ]
})
if (this.settings.onContextMenu) {
@ -36,8 +40,6 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
});
}
this.$widget.attr("data-placement", this.settings.titlePlacement);
super.doRender();
}

View File

@ -20,6 +20,13 @@ const TPL = `
width: 20em;
}
.attachment-actions .dropdown-item .bx {
position: relative;
top: 3px;
font-size: 120%;
margin-right: 5px;
}
.attachment-actions .dropdown-item[disabled], .attachment-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
@ -32,16 +39,39 @@ const TPL = `
style="position: relative; top: 3px;"></button>
<div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="openAttachment" class="dropdown-item"
title="${t('attachments_actions.open_externally_title')}">${t('attachments_actions.open_externally')}</a>
<a data-trigger-command="openAttachmentCustom" class="dropdown-item"
title="${t('attachments_actions.open_custom_title')}">${t('attachments_actions.open_custom')}</a>
<a data-trigger-command="downloadAttachment" class="dropdown-item">${t('attachments_actions.download')}</a>
<a data-trigger-command="renameAttachment" class="dropdown-item">${t('attachments_actions.rename_attachment')}</a>
<a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">${t('attachments_actions.upload_new_revision')}</a>
<a data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item">${t('attachments_actions.copy_link_to_clipboard')}</a>
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">${t('attachments_actions.convert_attachment_into_note')}</a>
<a data-trigger-command="deleteAttachment" class="dropdown-item">${t('attachments_actions.delete_attachment')}</a>
<li data-trigger-command="openAttachment" class="dropdown-item"
title="${t('attachments_actions.open_externally_title')}"><span class="bx bx-file-find"></span> ${t('attachments_actions.open_externally')}</li>
<li data-trigger-command="openAttachmentCustom" class="dropdown-item"
title="${t('attachments_actions.open_custom_title')}"><span class="bx bx-customize"></span> ${t('attachments_actions.open_custom')}</li>
<li data-trigger-command="downloadAttachment" class="dropdown-item">
<span class="bx bx-download"></span> ${t('attachments_actions.download')}</li>
<li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-link">
</span> ${t('attachments_actions.copy_link_to_clipboard')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item"><span class="bx bx-upload">
</span> ${t('attachments_actions.upload_new_revision')}</li>
<li data-trigger-command="renameAttachment" class="dropdown-item">
<span class="bx bx-rename"></span> ${t('attachments_actions.rename_attachment')}</li>
<li data-trigger-command="deleteAttachment" class="dropdown-item">
<span class="bx bx-trash destructive-action-icon"></span> ${t('attachments_actions.delete_attachment')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note">
</span> ${t('attachments_actions.convert_attachment_into_note')}</li>
</div>
<input type="file" class="attachment-upload-new-revision-input" style="display: none">
@ -83,14 +113,14 @@ export default class AttachmentActionsWidget extends BasicWidget {
const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']");
$openAttachmentButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.append($('<span class="bx bx-info-circle disabled-tooltip" />')
.attr("title", t('attachments_actions.open_externally_detail_page'))
);
if (isElectron) {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.append($('<span class="bx bx-info-circle disabled-tooltip" />')
.attr("title", t('attachments_actions.open_externally_detail_page'))
);
}
@ -99,7 +129,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton
.addClass("disabled")
.append($('<span class="disabled-tooltip"> (?)</span>')
.append($('<span class="bx bx-info-circle disabled-tooltip" />')
.attr("title", t('attachments_actions.open_custom_client_only'))
);
}

View File

@ -5,7 +5,7 @@ import UpdateAvailableWidget from "./update_available.js";
import options from "../../services/options.js";
const TPL = `
<div class="dropdown global-menu dropend">
<div class="dropdown global-menu">
<style>
.global-menu {
width: 53px;
@ -100,53 +100,31 @@ const TPL = `
position: relative;
left: 0;
top: 5px;
--dropdown-shadow-opacity: 0;
--submenu-opening-delay: 0;
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action global-menu-button">
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>
<div class="global-menu-button-update-available"></div>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li class="dropdown-item" data-trigger-command="showOptions">
<span class="bx bx-cog"></span>
${t('global_menu.options')}
</li>
<li class="dropdown-item" data-trigger-command="openNewWindow">
<span class="bx bx-window-open"></span>
${t('global_menu.open_new_window')}
<kbd data-command="openNewWindow"></kbd>
</li>
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
<span class="bx bx-mobile"></span>
${t('global_menu.switch_to_mobile_version')}
<li class="dropdown-item" data-trigger-command="showShareSubtree">
<span class="bx bx-share-alt"></span>
${t('global_menu.show_shared_notes_subtree')}
</li>
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
<span class="bx bx-desktop"></span>
${t('global_menu.switch_to_desktop_version')}
</li>
<span class="zoom-container dropdown-item">
<div class="dropdown-divider"></div>
<span class="zoom-container dropdown-item dropdown-item-container">
<div>
<span class="bx bx-empty"></span>
${t('global_menu.zoom')}
@ -165,16 +143,23 @@ const TPL = `
</div>
</span>
<div class="dropdown-divider zoom-container-separator"></div>
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
<span class="bx bx-mobile"></span>
${t('global_menu.switch_to_mobile_version')}
</li>
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
<span class="bx bx-desktop"></span>
${t('global_menu.switch_to_desktop_version')}
</li>
<li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
<span class="bx bx-sidebar"></span>
${t('global_menu.configure_launchbar')}
</li>
<li class="dropdown-item" data-trigger-command="showShareSubtree">
<span class="bx bx-share-alt"></span>
${t('global_menu.show_shared_notes_subtree')}
</li>
<li class="dropdown-item dropdown-submenu">
<span class="dropdown-toggle">
<span class="bx bx-chip"></span>
@ -182,10 +167,22 @@ const TPL = `
</span>
<ul class="dropdown-menu">
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
<span class="bx bx-bug-alt"></span>
${t('global_menu.open_dev_tools')}
<kbd data-command="openDevTools"></kbd>
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
<span class="bx bx-hide"></span>
${t('global_menu.show_hidden_subtree')}
</li>
<li class="dropdown-item" data-trigger-command="showSearchHistory">
<span class="bx bx-search-alt"></span>
${t('global_menu.open_search_history')}
</li>
<div class="dropdown-divider"></div>
<li class="dropdown-item" data-trigger-command="showBackendLog">
<span class="bx bx-detail"></span>
${t('global_menu.show_backend_log')}
<kbd data-command="showBackendLog"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="showSQLConsole">
@ -198,16 +195,13 @@ const TPL = `
<span class="bx bx-data"></span>
${t('global_menu.open_sql_console_history')}
</li>
<li class="dropdown-item" data-trigger-command="showSearchHistory">
<span class="bx bx-search-alt"></span>
${t('global_menu.open_search_history')}
</li>
<li class="dropdown-item" data-trigger-command="showBackendLog">
<span class="bx bx-detail"></span>
${t('global_menu.show_backend_log')}
<kbd data-command="showBackendLog"></kbd>
<div class="dropdown-divider"></div>
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
<span class="bx bx-bug-alt"></span>
${t('global_menu.open_dev_tools')}
<kbd data-command="openDevTools"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="reloadFrontendApp"
@ -217,13 +211,16 @@ const TPL = `
<kbd data-command="reloadFrontendApp"></kbd>
</li>
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
<span class="bx bx-hide"></span>
${t('global_menu.show_hidden_subtree')}
</li>
</ul>
</li>
<li class="dropdown-item" data-trigger-command="showOptions">
<span class="bx bx-cog"></span>
${t('global_menu.options')}
</li>
<div class="dropdown-divider desktop-only"></div>
<li class="dropdown-item show-help-button" data-trigger-command="showHelp">
<span class="bx bx-help-circle"></span>
${t('global_menu.show_help')}
@ -241,6 +238,8 @@ const TPL = `
<span class="version-text"></span>
</li>
<div class="dropdown-divider logout-button-separator"></div>
<li class="dropdown-item logout-button" data-trigger-command="logout">
<span class="bx bx-log-out"></span>
${t('global_menu.logout')}
@ -250,24 +249,54 @@ const TPL = `
`;
export default class GlobalMenuWidget extends BasicWidget {
constructor() {
constructor(isHorizontalLayout) {
super();
this.updateAvailableWidget = new UpdateAvailableWidget();
this.isHorizontalLayout = isHorizontalLayout;
}
doRender() {
this.$widget = $(TPL);
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
if (!this.isHorizontalLayout) {
this.$widget.addClass("dropend");
}
this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" });
const $globalMenuButton = this.$widget.find(".global-menu-button")
if (!this.isHorizontalLayout) {
$globalMenuButton.prepend($(`\
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
<g>
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
</g>
</svg>`));
this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" });
} else {
$globalMenuButton.toggleClass("bx bx-menu");
}
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"), {
alignment: "bottom"
});
this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog"));
const isElectron = utils.isElectron();
this.$widget.find(".logout-button").toggle(!isElectron);
this.$widget.find(".logout-button-separator").toggle(!isElectron);
this.$widget.find(".open-dev-tools-button").toggle(isElectron);
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop());
this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile());
@ -283,7 +312,7 @@ export default class GlobalMenuWidget extends BasicWidget {
if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass('dropdown-toggle')) {
e.stopPropagation();
}
})
})
this.$widget.find(".global-menu-button-update-available").append(
this.updateAvailableWidget.render()
@ -293,15 +322,20 @@ export default class GlobalMenuWidget extends BasicWidget {
if (!utils.isElectron()) {
this.$widget.find(".zoom-container").hide();
this.$widget.find(".zoom-container-separator").hide();
}
this.$zoomState = this.$widget.find(".zoom-state");
this.$widget.on('show.bs.dropdown', () => {
this.updateZoomState();
this.tooltip.hide();
this.tooltip.disable();
if (this.tooltip) {
this.tooltip.hide();
this.tooltip.disable();
}
});
this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable());
if (this.tooltip) {
this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable());
}
this.$widget.find(".zoom-buttons").on("click",
// delay to wait for the actual zoom change

View File

@ -4,7 +4,7 @@ import CommandButtonWidget from "./command_button.js";
import { t } from "../../services/i18n.js";
export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor() {
constructor(isHorizontalLayout) {
super();
this.class("launcher-button");
@ -20,6 +20,10 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
this.settings.command = () => options.is('leftPaneVisible')
? "hideLeftPane"
: "showLeftPane";
if (isHorizontalLayout) {
this.settings.titlePlacement = "bottom";
}
}
refreshIcon() {

View File

@ -11,42 +11,91 @@ import { t } from "../../services/i18n.js";
const TPL = `
<div class="dropdown note-actions">
<style>
.note-actions {
width: 35px;
height: 35px;
}
.note-actions .dropdown-menu {
min-width: 15em;
}
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
.note-actions {
width: 35px;
height: 35px;
}
.note-actions .dropdown-menu {
min-width: 15em;
}
.note-actions .dropdown-item .bx {
position: relative;
top: 3px;
font-size: 120%;
margin-right: 5px;
}
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important;
background-color: transparent !important;
pointer-events: none; /* makes it unclickable */
}
</style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
class="icon-action bx bx-dots-vertical-rounded"></button>
<div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">${t('note_actions.convert_into_attachment')}</a>
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> ${t('note_actions.re_render_note')}</a>
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">${t('note_actions.search_in_note')} <kbd data-command="findInText"></kbd></a>
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> ${t('note_actions.note_source')}</a>
<a data-trigger-command="showAttachments" class="dropdown-item show-attachments-button"><kbd data-command="showAttachments"></kbd> ${t('note_actions.note_attachments')}</a>
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"
title="${t('note_actions.open_note_externally_title')}">
<kbd data-command="openNoteExternally"></kbd>
${t('note_actions.open_note_externally')}
</a>
<a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> ${t('note_actions.open_note_custom')}</a>
<a class="dropdown-item import-files-button">${t('note_actions.import_files')}</a>
<a class="dropdown-item export-note-button">${t('note_actions.export_note')}</a>
<a class="dropdown-item delete-note-button">${t('note_actions.delete_note')}</a>
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> ${t('note_actions.print_note')}</a>
<a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> ${t('note_actions.save_revision')}</a>
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
<span class="bx bx-paperclip"></span> ${t('note_actions.convert_into_attachment')}
</li>
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
<span class="bx bx-extension"></span> ${t('note_actions.re_render_note')}<kbd data-command="renderActiveNote"></kbd>
</li>
<li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
<span class='bx bx-search'></span> ${t('note_actions.search_in_note')}<kbd data-command="findInText"></kbd>
</li>
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
<span class="bx bx-printer"></span> ${t('note_actions.print_note')}<kbd data-command="printActiveNote"></kbd></li>
<div class="dropdown-divider"></div>
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t('note_actions.import_files')}</li>
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t('note_actions.export_note')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" title="${t('note_actions.open_note_externally_title')}">
<span class="bx bx-file-find"></span> ${t('note_actions.open_note_externally')}<kbd data-command="openNoteExternally"></kbd>
</li>
<li data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button">
<span class="bx bx-customize"></span> ${t('note_actions.open_note_custom')}<kbd data-command="openNoteCustom"></kbd>
</li>
<li data-trigger-command="showNoteSource" class="dropdown-item show-source-button">
<span class="bx bx-code"></span> ${t('note_actions.note_source')}<kbd data-command="showNoteSource"></kbd>
</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button">
<span class="bx bx-save"></span> ${t('note_actions.save_revision')}<kbd data-command="forceSaveRevision"></kbd>
</li>
<li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t('note_actions.delete_note')}</li>
<div class="dropdown-divider"></div>
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
<span class="bx bx-paperclip"></span> ${t('note_actions.note_attachments')}<kbd data-command="showAttachments"></kbd>
</li>
</div>
</div>`;

View File

@ -2,13 +2,7 @@ import BasicWidget from "../basic_widget.js";
const TPL = `
<div class="dropdown right-dropdown-widget dropend">
<style>
.right-dropdown-widget {
height: 53px;
}
</style>
<button type="button" data-bs-toggle="dropdown" data-placement="right"
<button type="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false"
class="bx right-dropdown-button launcher-button"></button>
@ -25,6 +19,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.iconClass = iconClass;
this.title = title;
this.dropdownTpl = dropdownTpl;
this.settings = {
titlePlacement: "right"
};
}
doRender() {
@ -33,7 +31,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
this.tooltip = new bootstrap.Tooltip(this.$tooltip);
this.tooltip = new bootstrap.Tooltip(this.$tooltip, {
placement: this.settings.titlePlacement,
fallbackPlacements: [ this.settings.titlePlacement ]
});
this.$widget.find(".right-dropdown-button")
.addClass(this.iconClass)

View File

@ -10,12 +10,14 @@ import CommandButtonWidget from "../buttons/command_button.js";
import utils from "../../services/utils.js";
import TodayLauncher from "../buttons/launcher/today_launcher.js";
import HistoryNavigationButton from "../buttons/history_navigation.js";
import QuickSearchLauncherWidget from "../quick_search_launcher.js";
export default class LauncherWidget extends BasicWidget {
constructor() {
constructor(isHorizontalLayout) {
super();
this.innerWidget = null;
this.isHorizontalLayout = isHorizontalLayout;
}
isEnabled() {
@ -63,6 +65,9 @@ export default class LauncherWidget extends BasicWidget {
}
this.child(this.innerWidget);
if (this.isHorizontalLayout && this.innerWidget.settings) {
this.innerWidget.settings.titlePlacement = "bottom";
}
return true;
}
@ -86,29 +91,31 @@ export default class LauncherWidget extends BasicWidget {
initBuiltinWidget(note) {
const builtinWidget = note.getLabelValue("builtinWidget");
if (builtinWidget === 'calendar') {
return new CalendarWidget(note.title, note.getIcon());
} else if (builtinWidget === 'spacer') {
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor);
} else if (builtinWidget === 'bookmarks') {
return new BookmarkButtons();
} else if (builtinWidget === 'protectedSession') {
return new ProtectedSessionStatusWidget();
} else if (builtinWidget === 'syncStatus') {
return new SyncStatusWidget();
} else if (builtinWidget === 'backInHistoryButton') {
return new HistoryNavigationButton(note, "backInNoteHistory");
} else if (builtinWidget === 'forwardInHistoryButton') {
return new HistoryNavigationButton(note, "forwardInNoteHistory");
} else if (builtinWidget === 'todayInJournal') {
return new TodayLauncher(note);
} else {
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
switch (builtinWidget) {
case "calendar":
return new CalendarWidget(note.title, note.getIcon());
case "spacer":
// || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor);
case "bookmarks":
return new BookmarkButtons(this.isHorizontalLayout);
case "protectedSession":
return new ProtectedSessionStatusWidget();
case "syncStatus":
return new SyncStatusWidget();
case "backInHistoryButton":
return new HistoryNavigationButton(note, "backInNoteHistory");
case "forwardInHistoryButton":
return new HistoryNavigationButton(note, "forwardInNoteHistory");
case "todayInJournal":
return new TodayLauncher(note);
case "quickSearch":
return new QuickSearchLauncherWidget(this.isHorizontalLayout);
default:
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
}
}
}

View File

@ -4,12 +4,13 @@ import appContext from "../../components/app_context.js";
import LauncherWidget from "./launcher.js";
export default class LauncherContainer extends FlexContainer {
constructor() {
super('column');
constructor(isHorizontalLayout) {
super(isHorizontalLayout ? "row" : "column");
this.id('launcher-container');
this.css('height', '100%');
this.css(isHorizontalLayout ? "width" : 'height', '100%');
this.filling();
this.isHorizontalLayout = isHorizontalLayout;
this.load();
}
@ -29,7 +30,7 @@ export default class LauncherContainer extends FlexContainer {
for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
try {
const launcherWidget = new LauncherWidget();
const launcherWidget = new LauncherWidget(this.isHorizontalLayout);
const success = await launcherWidget.initLauncher(launcherNote);
if (success) {

View File

@ -351,6 +351,11 @@ export default class RibbonContainer extends NoteContextAwareWidget {
}
}
noteTypeMimeChangedEvent() {
// We are ignoring the event which triggers a refresh since it is usually already done by a different
// event and causing a race condition in which the items appear twice.
}
/**
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
*

View File

@ -1,8 +1,8 @@
import FlexContainer from "./flex_container.js";
export default class RootContainer extends FlexContainer {
constructor() {
super('row');
constructor(isHorizontalLayout) {
super(isHorizontalLayout ? "column" : "row");
this.id('root-widget');
this.css('height', '100%');

View File

@ -58,6 +58,7 @@ export default class JumpToNoteDialog extends BasicWidget {
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
allowCreatingNotes: true,
hideGoToSelectedNoteButton: true,
allowSearchNotes: true,
container: this.$results
})
// clear any event listener added in previous invocation of this function

View File

@ -54,7 +54,7 @@ const TPL = `
data-bs-toggle="dropdown" data-bs-display="static">
</button>
<div class="revision-list dropdown-menu" style="position: static; height: 100%; overflow: auto;"></div>
<div class="revision-list dropdown-menu static" style="position: static; height: 100%; overflow: auto;"></div>
</div>
<div class="revision-content-wrapper">

View File

@ -5,6 +5,7 @@
import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import attributeService from "../services/attributes.js";
import FindInText from "./find_in_text.js";
import FindInCode from "./find_in_code.js";
import FindInHtml from "./find_in_html.js";
@ -16,27 +17,26 @@ const waitForEnter = (findWidgetDelayMillis < 0);
// the focusout handler is called with relatedTarget equal to the label instead
// of undefined. It's -1 instead of > 0, so they don't tabstop
const TPL = `
<div style="contain: none;">
<div class='find-replace-widget' style="contain: none; border-top: 1px solid var(--main-border-color);">
<style>
.find-widget-box {
padding: 10px;
border-top: 1px solid var(--main-border-color);
.find-widget-box, .replace-widget-box {
padding: 2px 10px 2px 10px;
align-items: center;
}
.find-widget-box > * {
.find-widget-box > *, .replace-widget-box > *{
margin-right: 15px;
}
.find-widget-box {
.find-widget-box, .replace-widget-box {
display: flex;
}
.find-widget-found-wrapper {
font-weight: bold;
}
.find-widget-search-term-input-group {
.find-widget-search-term-input-group, .replace-widget-replacetext-input {
max-width: 300px;
}
@ -47,19 +47,23 @@ const TPL = `
<div class="find-widget-box">
<div class="input-group find-widget-search-term-input-group">
<input type="text" class="form-control find-widget-search-term-input">
<input type="text" class="form-control find-widget-search-term-input" placeholder="${t('find.find_placeholder')}">
<button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button>
<button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">
<label tabIndex="-1" class="form-check-label">${t('find.case_sensitive')}</label>
<label tabIndex="-1" class="form-check-label">
<input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">
${t('find.case_sensitive')}
</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
<label tabIndex="-1" class="form-check-label">${t('find.match_words')}</label>
<label tabIndex="-1" class="form-check-label">
<input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
${t('find.match_words')}
</label>
</div>
<div class="find-widget-found-wrapper">
@ -72,6 +76,12 @@ const TPL = `
<div class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div>
</div>
<div class="replace-widget-box" style='display: none'>
<input type="text" class="form-control replace-widget-replacetext-input" placeholder="${t('find.replace_placeholder')}">
<button class="btn btn-sm replace-widget-replaceall-button" type="button">${t('find.replace_all')}</button>
<button class="btn btn-sm replace-widget-replace-button" type="button">${t('find.replace')}</button>
</div>
</div>`;
export default class FindWidget extends NoteContextAwareWidget {
@ -93,8 +103,7 @@ export default class FindWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.$findBox = this.$widget.find('.find-widget-box');
this.$findBox.hide();
this.$widget.hide();
this.$input = this.$widget.find('.find-widget-search-term-input');
this.$currentFound = this.$widget.find('.find-widget-current-found');
this.$totalFound = this.$widget.find('.find-widget-total-found');
@ -109,6 +118,13 @@ export default class FindWidget extends NoteContextAwareWidget {
this.$closeButton = this.$widget.find(".find-widget-close-button");
this.$closeButton.on("click", () => this.closeSearch());
this.$replaceWidgetBox = this.$widget.find(".replace-widget-box");
this.$replaceTextInput = this.$widget.find(".replace-widget-replacetext-input");
this.$replaceAllButton = this.$widget.find(".replace-widget-replaceall-button");
this.$replaceAllButton.on("click", () => this.replaceAll());
this.$replaceButton = this.$widget.find(".replace-widget-replace-button");
this.$replaceButton.on("click", () => this.replace());
this.$input.keydown(async e => {
if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) {
// If ctrl+f is pressed when the findbox is shown, select the
@ -121,7 +137,7 @@ export default class FindWidget extends NoteContextAwareWidget {
}
});
this.$findBox.keydown(async e => {
this.$widget.keydown(async e => {
if (e.key === 'Escape') {
await this.closeSearch();
}
@ -142,13 +158,25 @@ export default class FindWidget extends NoteContextAwareWidget {
}
this.handler = await this.getHandler();
const isReadOnly = await this.noteContext.isReadOnly();
const selectedText = window.getSelection().toString() || "";
this.$findBox.show();
let selectedText = '';
if (this.note.type === 'code' && !isReadOnly){
const codeEditor = await this.noteContext.getCodeEditor();
selectedText = codeEditor.getSelection();
}else{
selectedText = window.getSelection().toString() || "";
}
this.$widget.show();
this.$input.focus();
if (['text', 'code'].includes(this.note.type) && !isReadOnly) {
this.$replaceWidgetBox.show();
}else{
this.$replaceWidgetBox.hide();
}
const isAlreadyVisible = this.$findBox.is(":visible");
const isAlreadyVisible = this.$widget.is(":visible");
if (isAlreadyVisible) {
if (selectedText) {
@ -254,8 +282,8 @@ export default class FindWidget extends NoteContextAwareWidget {
}
async closeSearch() {
if (this.$findBox.is(":visible")) {
this.$findBox.hide();
if (this.$widget.is(":visible")) {
this.$widget.hide();
// Restore any state, if there's a current occurrence clear markers
// and scroll to and select the last occurrence
@ -268,13 +296,27 @@ export default class FindWidget extends NoteContextAwareWidget {
}
}
async replace() {
const replaceText = this.$replaceTextInput.val();
await this.handler.replace(replaceText);
}
async replaceAll() {
const replaceText = this.$replaceTextInput.val();
await this.handler.replaceAll(replaceText);
}
isEnabled() {
return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type);
}
async entitiesReloadedEvent({loadResults}) {
async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) {
this.$totalFound.text("?")
} else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
&& (attr.name.toLowerCase().includes('readonly'))
&& attributeService.isAffecting(attr, this.note))) {
this.closeSearch();
}
}
}

View File

@ -170,4 +170,55 @@ export default class FindInCode {
codeEditor.focus();
}
async replace(replaceText) {
// this.findResult may be undefined and null
if (!this.findResult || this.findResult.length===0){
return;
}
let currentFound = -1;
this.findResult.forEach((marker, index) => {
const pos = marker.find();
if (pos) {
if (marker.className === FIND_RESULT_SELECTED_CSS_CLASSNAME) {
currentFound = index;
return;
}
}
});
if (currentFound >= 0) {
let marker = this.findResult[currentFound];
let pos = marker.find();
const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc;
doc.replaceRange(replaceText, pos.from, pos.to);
marker.clear();
let nextFound;
if (currentFound === this.findResult.length - 1) {
nextFound = 0;
} else {
nextFound = currentFound;
}
this.findResult.splice(currentFound, 1);
if (this.findResult.length > 0) {
this.findNext(0, nextFound, nextFound);
}
}
}
async replaceAll(replaceText) {
if (!this.findResult || this.findResult.length===0){
return;
}
const codeEditor = await this.getCodeEditor();
const doc = codeEditor.doc;
codeEditor.operation(() => {
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
let marker = this.findResult[currentFound];
let pos = marker.find();
doc.replaceRange(replaceText, pos.from, pos.to);
marker.clear();
}
});
this.findResult = [];
}
}

View File

@ -21,6 +21,7 @@ export default class FindInText {
const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing');
findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.stop();
this.editingState = findAndReplaceEditing.state;
if (searchTerm !== "") {
// Parameters are callback/text, options.matchCase=false, options.wholeWords=false
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44
@ -29,7 +30,7 @@ export default class FindInText {
// let re = new RegExp(searchTerm, 'gi');
// let m = text.match(re);
// totalFound = m ? m.length : 0;
const options = { "matchCase" : matchCase, "wholeWords" : wholeWord };
const options = { "matchCase": matchCase, "wholeWords": wholeWord };
findResult = textEditor.execute('find', searchTerm, options);
totalFound = findResult.results.length;
// Find the result beyond the cursor
@ -102,4 +103,18 @@ export default class FindInText {
textEditor.focus();
}
async replace(replaceText) {
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
const textEditor = await this.getTextEditor();
textEditor.execute('replace', replaceText, this.editingState.highlightedResult);
}
}
async replaceAll(replaceText) {
if (this.editingState !== undefined && this.editingState.results.length > 0) {
const textEditor = await this.getTextEditor();
textEditor.execute('replaceAll', replaceText, this.editingState.results);
}
}
}

View File

@ -5171,7 +5171,7 @@ const icons = [
"type_of_icon": "REGULAR"
},
{
"name": '_share',
"name": "share",
"slug": "share-regular",
"category_id": 101,
"type_of_icon": "REGULAR"
@ -6826,7 +6826,7 @@ const icons = [
"type_of_icon": "SOLID"
},
{
"name": '_share',
"name": "share",
"slug": "share-solid",
"category_id": 101,
"type_of_icon": "SOLID"

View File

@ -3,6 +3,7 @@ import libraryLoader from "../services/library_loader.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js";
import utils from "../services/utils.js";
import { loadElkIfNeeded } from "../services/mermaid.js";
const TPL = `<div class="mermaid-widget">
<style>
@ -57,10 +58,10 @@ export default class MermaidWidget extends NoteContextAwareWidget {
this.$errorContainer.hide();
await libraryLoader.requireLibrary(libraryLoader.MERMAID);
const documentStyle = window.getComputedStyle(document.documentElement);
const mermaidTheme = documentStyle.getPropertyValue('--mermaid-theme');
const mermaidTheme = documentStyle.getPropertyValue('--mermaid-theme');
mermaid.mermaidAPI.initialize({
startOnLoad: false,
theme: mermaidTheme.trim(),
@ -111,6 +112,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
zoomOnClick: false
});
} catch (e) {
console.warn(e);
this.$errorMessage.text(e.message);
this.$errorContainer.show();
}
@ -122,6 +124,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
const blob = await this.note.getBlob();
const content = blob.content || "";
await loadElkIfNeeded(content);
const {svg} = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
return svg;
}

View File

@ -6,12 +6,20 @@ import branchService from "../../services/branches.js";
import treeService from "../../services/tree.js";
import { t } from "../../services/i18n.js";
const TPL = `<button type="button" class="action-button bx bx-menu" style="padding-top: 10px;"></button>`;
const TPL = `<button type="button" class="action-button bx" style="padding-top: 10px;"></button>`;
class MobileDetailMenuWidget extends BasicWidget {
constructor(isHorizontalLayout) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
doRender() {
this.$widget = $(TPL);
this.$widget.addClass(this.isHorizontalLayout ? "bx-dots-vertical-rounded" : "bx-menu");
this.$widget.on("click", async e => {
const note = appContext.tabManager.getActiveContextNote();

View File

@ -0,0 +1,33 @@
import utils from "../services/utils.js";
import QuickSearchWidget from "./quick_search.js";
/**
* Similar to the {@link QuickSearchWidget} but meant to be included inside the launcher bar.
*
* <p>
* Adds specific tweaks such as:
*
* - Hiding the widget on mobile.
*/
export default class QuickSearchLauncherWidget extends QuickSearchWidget {
constructor(isHorizontalLayout) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
isEnabled() {
if (!this.isHorizontalLayout) {
// The quick search widget is added somewhere else on the vertical layout.
return false;
}
if (utils.isMobile()) {
// The widget takes too much spaces to be included in the mobile layout.
return false;
}
return super.isEnabled();
}
}

View File

@ -60,7 +60,7 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
show: await this.#shouldDisplay(),
activate: true,
title: t("classic_editor_toolbar.title"),
icon: "bx bx-edit-alt"
icon: "bx bx-text"
};
}

View File

@ -233,6 +233,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
else if (definition.labelType === 'datetime') {
$input.prop('type', 'datetime-local')
}
else if (definition.labelType === 'time') {
$input.prop('type', 'time')
}
else if (definition.labelType === 'url') {
$input.prop("placeholder", t("promoted_attributes.url_placeholder"));

View File

@ -55,6 +55,10 @@ const TAB_ROW_TPL = `
background: var(--main-background-color);
overflow: hidden;
}
.tab-row-widget.full-width {
background: var(--launcher-pane-background-color);
}
.tab-row-widget * {
box-sizing: inherit;
@ -263,8 +267,15 @@ export default class TabRowWidget extends BasicWidget {
{title: t('tab_row.close_other_tabs'), command: "closeOtherTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.length !== 1},
{title: t('tab_row.close_right_tabs'), command: "closeRightTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.at(-1).ntxId !== ntxId},
{title: t('tab_row.close_all_tabs'), command: "closeAllTabs", uiIcon: "bx bx-empty"},
{ title: "----" },
{title: t('tab_row.move_tab_to_new_window'), command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"}
{title: "----"},
{title: t('tab_row.reopen_last_tab'), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0},
{title: "----"},
{title: t('tab_row.move_tab_to_new_window'), command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"},
{title: t('tab_row.copy_tab_to_new_window'), command: "copyTabToNewWindow", uiIcon: "bx bx-empty"}
],
selectMenuItemHandler: ({command}) => {
this.triggerCommand(command, {ntxId});

View File

@ -1,45 +1,53 @@
import NoteContextAwareWidget from "../../note_context_aware_widget.js";
import server from "../../../services/server.js";
import { t } from "../../../services/i18n.js";
import AbstractCodeTypeWidget from "../abstract_code_type_widget.js";
const TPL = `<div style="height: 100%; display: flex; flex-direction: column;">
<style>
.backend-log-textarea {
.backend-log-editor {
flex-grow: 1;
width: 100%;
border: none;
resize: none;
}
</style>
<textarea class="backend-log-textarea" readonly="readonly"></textarea>
<pre class="backend-log-editor"></pre>
<div style="display: flex; justify-content: space-around; margin-top: 10px;">
<button class="refresh-backend-log-button btn btn-primary">${t("backend_log.refresh")}</button>
</div>
</div>`;
export default class BackendLogWidget extends NoteContextAwareWidget {
export default class BackendLogWidget extends AbstractCodeTypeWidget {
doRender() {
super.doRender();
this.$widget = $(TPL);
this.$backendLogTextArea = this.$widget.find(".backend-log-textarea");
this.$editor = this.$widget.find(".backend-log-editor");
this.$refreshBackendLog = this.$widget.find(".refresh-backend-log-button");
this.$refreshBackendLog.on('click', () => this.load());
}
scrollToBottom() {
this.$backendLogTextArea.scrollTop(this.$backendLogTextArea[0].scrollHeight);
}
async refresh() {
await this.load();
}
getExtraOpts() {
return {
lineWrapping: false,
readOnly: true
};
}
async load() {
const backendLog = await server.get('backend-log');
const content = await server.get('backend-log');
await this.initialized;
this.$backendLogTextArea.text(backendLog);
this.scrollToBottom();
this._update({
mime: "text/plain"
}, content);
this.show();
this.scrollToEnd();
}
}

View File

@ -31,7 +31,14 @@ export default class DocTypeWidget extends TypeWidget {
const docName = note.getLabelValue('docName');
if (docName) {
this.$content.load(`${window.glob.appPath}/doc_notes/${docName}.html`);
// find doc based on language
const lng = i18next.language;
this.$content.load(`${window.glob.appPath}/doc_notes/${lng}/${docName}.html`, (response, status) => {
// fallback to english doc if no translation available
if (status === 'error') {
this.$content.load(`${window.glob.appPath}/doc_notes/en/${docName}.html`);
}
});
} else {
this.$content.empty();
}

View File

@ -70,6 +70,7 @@ export default class EmptyTypeWidget extends TypeWidget {
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
hideGoToSelectedNoteButton: true,
allowCreatingNotes: true,
allowSearchNotes: true,
container: this.$results
})
.on('autocomplete:noteselected', function(event, suggestion, dataset) {

View File

@ -2,6 +2,8 @@ import OptionsWidget from "../options_widget.js";
import utils from "../../../../services/utils.js";
import { t } from "../../../../services/i18n.js";
const MIN_VALUE = 640;
const TPL = `
<div class="options-section">
<h4>${t("max_content_width.title")}</h4>
@ -11,7 +13,7 @@ const TPL = `
<div class="form-group row">
<div class="col-6">
<label>${t("max_content_width.max_width_label")}</label>
<input type="number" min="200" step="10" class="max-content-width form-control options-number-input">
<input type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
</div>
</div>
@ -34,6 +36,6 @@ export default class MaxContentWidthOptions extends OptionsWidget {
}
async optionsLoaded(options) {
this.$maxContentWidth.val(options.maxContentWidth);
this.$maxContentWidth.val(Math.max(MIN_VALUE, options.maxContentWidth));
}
}

View File

@ -5,6 +5,26 @@ import { t } from "../../../../services/i18n.js";
const TPL = `
<div class="options-section">
<h4>${t("theme.layout")}</h4>
<div class="form-group row">
<div>
<label>
<input type="radio" name="layout-orientation" value="vertical" />
<strong>${t("theme.layout-vertical-title")}</strong>
- ${t("theme.layout-vertical-description")}
</label>
</div>
<div>
<label>
<input type="radio" name="layout-orientation" value="horizontal" />
<strong>${t("theme.layout-horizontal-title")}</strong>
- ${t("theme.layout-horizontal-description")}
</label>
</div>
</div>
<h4>${t("theme.title")}</h4>
<div class="form-group row">
@ -19,7 +39,7 @@ const TPL = `
${t("theme.override_theme_fonts_label")}
</label>
</div>
</div>
</div>
</div>`;
export default class ThemeOptions extends OptionsWidget {
@ -27,6 +47,11 @@ export default class ThemeOptions extends OptionsWidget {
this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select");
this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
this.$layoutOrientation = this.$widget.find(`input[name="layout-orientation"]`).on("change", async () => {
const newLayoutOrientation = this.$widget.find(`input[name="layout-orientation"]:checked`).val();
await this.updateOption("layoutOrientation", newLayoutOrientation);
utils.reloadFrontendApp("layout orientation change");
});
this.$themeSelect.on('change', async () => {
const newTheme = this.$themeSelect.val();
@ -57,5 +82,8 @@ export default class ThemeOptions extends OptionsWidget {
this.$themeSelect.val(options.theme);
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
this.$widget.find(`input[name="layout-orientation"][value="${options.layoutOrientation}"]`)
.prop("checked", "true");
}
}

View File

@ -42,7 +42,21 @@ const TPL = `
<div class="options-section">
<h4>${t('backup.existing_backups')}</h4>
<ul class="existing-backup-list"></ul>
<table class="table table-stripped">
<colgroup>
<col width="33%" />
<col />
</colgroup>
<thead>
<tr>
<th>${t("backup.date-and-time")}</th>
<th>${t("backup.path")}</th>
</tr>
</thead>
<tbody class="existing-backup-list-items">
</tbody>
</table>
</div>
`;
@ -73,7 +87,7 @@ export default class BackupOptions extends OptionsWidget {
this.$monthlyBackupEnabled.on('change', () =>
this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled));
this.$existingBackupList = this.$widget.find(".existing-backup-list");
this.$existingBackupList = this.$widget.find(".existing-backup-list-items");
}
optionsLoaded(options) {
@ -85,11 +99,34 @@ export default class BackupOptions extends OptionsWidget {
this.$existingBackupList.empty();
if (!backupFiles.length) {
backupFiles = [{filePath: t('backup.no_backup_yet'), mtime: ''}];
this.$existingBackupList.append($(`
<tr>
<td class="empty-table-placeholder" colspan="2">${t('backup.no_backup_yet')}</td>
</tr>
`));
return;
}
// Sort the backup files by modification date & time in a desceding order
backupFiles.sort((a, b) => {
if (a.mtime < b.mtime) return 1;
if (a.mtime > b.mtime) return -1;
return 0;
});
const dateTimeFormatter = new Intl.DateTimeFormat(navigator.language, {
dateStyle: "medium",
timeStyle: "medium"
});
for (const {filePath, mtime} of backupFiles) {
this.$existingBackupList.append($("<li>").text(`${filePath} ${mtime ? ` - ${mtime}` : ''}`));
this.$existingBackupList.append($(`
<tr>
<td>${(mtime) ? dateTimeFormatter.format(new Date(mtime)) : "-"}</td>
<td>${filePath}</td>
</tr>
`));
}
});
}

View File

@ -95,9 +95,9 @@ export default class EtapiOptions extends OptionsWidget {
.append($("<td>").text(token.name))
.append($("<td>").text(token.utcDateCreated))
.append($("<td>").append(
$('<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>')
$(`<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>`)
.on("click", () => this.renameToken(token.etapiTokenId, token.name)),
$('<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>')
$(`<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>`)
.on("click", () => this.deleteToken(token.etapiTokenId, token.name))
))
);

View File

@ -6,25 +6,37 @@ const TPL = `
<div class="options-section">
<h4>${t("editing.editor_type.label")}</h4>
<select class="editor-type-select form-select">
<option value="ckeditor-balloon">${t("editing.editor_type.floating")}</option>
<option value="ckeditor-classic">${t("editing.editor_type.fixed")}</option>
</select>
<div>
<label>
<input type="radio" name="editor-type" value="ckeditor-balloon" />
<strong>${t("editing.editor_type.floating.title")}</strong>
- ${t("editing.editor_type.floating.description")}
</label>
</div>
<div>
<label>
<input type="radio" name="editor-type" value="ckeditor-classic" />
<strong>${t("editing.editor_type.fixed.title")}</strong>
- ${t("editing.editor_type.fixed.description")}
</label>
</div>
</div>`;
export default class EditorOptions extends OptionsWidget {
doRender() {
this.$widget = $(TPL);
this.$body = $("body");
this.$editorType = this.$widget.find(".editor-type-select");
this.$editorType.on('change', async () => {
const newEditorType = this.$editorType.val();
this.$widget.find(`input[name="editor-type"]`).on('change', async () => {
const newEditorType = this.$widget.find(`input[name="editor-type"]:checked`).val();
await this.updateOption('textNoteEditorType', newEditorType);
utils.reloadFrontendApp("editor type change");
});
}
async optionsLoaded(options) {
this.$editorType.val(options.textNoteEditorType);
this.$widget.find(`input[name="editor-type"][value="${options.textNoteEditorType}"]`)
.prop("checked", "true");
}
}

View File

@ -51,7 +51,7 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
await this.initialized;
resolve(this.$content);
resolve(this.$editor);
}
format(html) {

View File

@ -19,6 +19,10 @@
--bs-table-bg: transparent !important;
}
:root {
--submenu-opening-delay: 300ms;
}
html {
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
height: 100%;
@ -36,6 +40,10 @@ body {
font-size: var(--main-font-size);
}
body.mobile .desktop-only {
display: none !important;
}
a {
text-decoration: none;
}
@ -229,7 +237,15 @@ div.ui-tooltip {
}
.dropdown-divider {
background-color: var(--menu-text-color);
border-color: var(--dropdown-border-color);
}
@keyframes dropdown-menu-opening {
from {
opacity: 0;
} to {
opacity: 1;
}
}
.dropdown-menu {
@ -237,6 +253,26 @@ div.ui-tooltip {
color: var(--menu-text-color) !important;
background-color: var(--menu-background-color) !important;
font-size: inherit;
box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
animation: dropdown-menu-opening 100ms ease-in;
}
@supports(animation-fill-mode: forwards) {
/* Delay the opening of submenus */
.dropdown-submenu .dropdown-menu {
opacity: 0;
animation-fill-mode: forwards;
animation-delay: var(--submenu-opening-delay);
}
}
.dropdown-menu.static {
box-shadow: unset;
}
.dropend .dropdown-toggle::after {
margin-left: .5em;
color: var(--muted-text-color);
}
.dropdown-menu .disabled {
@ -246,17 +282,29 @@ div.ui-tooltip {
.dropdown-menu .disabled .disabled-tooltip {
pointer-events: all;
color: var(--menu-text-color);
margin-left: 8px;
font-size: .5em;
color: var(--disabled-tooltip-icon-color);
cursor: help;
opacity: .75;
}
.dropdown-menu a:hover:not(.disabled), .dropdown-item:hover:not(.disabled) {
.dropdown-menu .disabled .disabled-tooltip:hover {
opacity: 1;
}
.dropdown-menu a:hover:not(.disabled), .dropdown-item:hover:not(.disabled, .dropdown-item-container) {
color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important;
border-color: var(--hover-item-border-color) !important;
cursor: pointer;
}
.dropdown-item-container, .dropdown-item-container:hover, .dropdown-item-container:active {
background: transparent;
cursor: default;
}
.dropdown-menu a:not(.selected) .check {
visibility: hidden;
}
@ -288,6 +336,10 @@ div.ui-tooltip {
outline: none;
}
.dropdown-item .destructive-action-icon {
color: var(--dropdown-item-icon-destructive-color);
}
.CodeMirror {
height: 100%;
background: inherit;
@ -974,6 +1026,18 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
overflow: auto;
}
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
left: calc(-100% + 10px);
}
#launcher-pane.horizontal .right-dropdown-widget {
width: 53px;
}
#launcher-pane.vertical .right-dropdown-widget {
height: 53px;
}
/* rotate caret on hover */
.dropdown-menu > li > a:hover:after {
text-decoration: underline;
@ -1075,9 +1139,21 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
cursor: pointer;
border: none;
color: var(--launcher-pane-text-color);
background-color: var(--launcher-pane-background-color);
height: 53px;
background-color: var(--launcher-pane-background-color);
}
#launcher-pane.vertical .launcher-button {
width: 100%;
height: 53px;
}
#launcher-pane.horizontal .launcher-button {
width: 53px;
height: 100%;
}
#launcher-pane.horizontal .quick-search {
width: 350px;
}
#launcher-pane .icon-action:hover {
@ -1238,3 +1314,8 @@ textarea {
padding: 1rem;
}
.empty-table-placeholder {
text-align: center;
color: var(--muted-text-color);
}

View File

@ -17,6 +17,10 @@
--main-text-color: #ccc;
--main-border-color: #aaa;
--dropdown-border-color: #555;
--dropdown-shadow-opacity: .4;
--dropdown-item-icon-destructive-color: #de6e5b;
--disabled-tooltip-icon-color: #7fd2ef;
--accented-background-color: #555;
--more-accented-background-color: #777;

View File

@ -21,6 +21,9 @@ html {
--main-text-color: black;
--main-border-color: #ccc;
--dropdown-border-color: #ccc;
--dropdown-shadow-opacity: .2;
--dropdown-item-icon-destructive-color: #ec5138;
--disabled-tooltip-icon-color: #004382;
--accented-background-color: #f5f5f5;
--more-accented-background-color: #ddd;

View File

@ -138,13 +138,13 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
span.fancytree-node.multiple-parents.shared .fancytree-title::after {
font-family: 'boxicons' !important;
font-size: smaller;
content: " \ec27 \ec03";
content: " \eb3d \ec03";
}
span.fancytree-node.multiple-parents .fancytree-title::after {
font-family: 'boxicons' !important;
font-size: smaller;
content: " \ec27"; /* lookup code for "star" in boxicons.css */
content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
}
span.fancytree-node.shared .fancytree-title::after {

View File

@ -15,7 +15,13 @@
"message": "发生了严重错误,导致客户端应用程序无法启动:\n\n{{message}}\n\n这很可能是由于脚本以意外的方式失败引起的。请尝试以安全模式启动应用程序并解决问题。"
},
"widget-error": {
"title": "小部件初始化失败"
"title": "小部件初始化失败",
"message-custom": "来自 ID 为 \"{{id}}\"、标题为 \"{{title}}\" 的笔记的自定义小部件因以下原因无法初始化:\n\n{{message}}",
"message-unknown": "未知小部件因以下原因无法初始化:\n\n{{message}}"
},
"bundle-error": {
"title": "加载自定义脚本失败",
"message": "来自 ID 为 \"{{id}}\"、标题为 \"{{title}}\" 的笔记的脚本因以下原因无法执行:\n\n{{message}}"
}
},
"add_link": {
@ -45,7 +51,11 @@
"chosen_actions": "选择的操作",
"execute_bulk_actions": "执行批量操作",
"bulk_actions_executed": "批量操作已成功执行。",
"none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。"
"none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
"labels": "标签",
"relations": "关联关系",
"notes": "笔记",
"other": "其它"
},
"clone_to": {
"clone_notes_to": "克隆笔记到...",
@ -164,7 +174,8 @@
"textImportedAsText": "如果元数据不明确将HTML、Markdown和TXT导入为文本笔记",
"codeImportedAsCode": "如果元数据不明确,将识别的代码文件(例如<code>.json</code>)导入为代码笔记",
"replaceUnderscoresWithSpaces": "在导入的笔记名称中将下划线替换为空格",
"import": "导入"
"import": "导入",
"failed": "导入失败: {{message}}."
},
"include_note": {
"dialog_title": "包含笔记",
@ -304,6 +315,7 @@
"boolean": "布尔值",
"date": "日期",
"date_time": "日期和时间",
"time": "时间",
"url": "网址",
"precision_title": "值设置界面中浮点数后的位数。",
"precision": "精度",
@ -630,7 +642,10 @@
"export_note": "导出笔记",
"delete_note": "删除笔记",
"print_note": "打印笔记",
"save_revision": "保存笔记历史"
"save_revision": "保存笔记历史",
"convert_into_attachment_failed": "笔记 '{{title}}' 转换失败。",
"convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
"convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?"
},
"onclick_button": {
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
@ -885,7 +900,8 @@
"label_rock_or_pop": "只需一个标签存在即可",
"label_year_comparison": "数字比较(也包括>>=<)。",
"label_date_created": "上个月创建的笔记",
"error": "搜索错误:{{error}}"
"error": "搜索错误:{{error}}",
"search_prefix": "搜索:"
},
"attachment_detail": {
"open_help_page": "打开附件帮助页面",
@ -919,7 +935,15 @@
},
"protected_session": {
"enter_password_instruction": "显示受保护的笔记需要输入您的密码:",
"start_session_button": "开始受保护的会话"
"start_session_button": "开始受保护的会话",
"started": "受保护的会话已启动。",
"wrong_password": "密码错误。",
"protecting-finished-successfully": "保护操作已成功完成。",
"unprotecting-finished-successfully": "解除保护操作已成功完成。",
"protecting-in-progress": "保护进行中:{{count}}",
"unprotecting-in-progress-count": "解除保护进行中:{{count}}",
"protecting-title": "保护状态",
"unprotecting-title": "解除保护状态"
},
"relation_map": {
"open_in_new_tab": "在新标签页中打开",
@ -990,7 +1014,9 @@
"fill_entity_changes_button": "填充实体变更记录",
"full_sync_triggered": "全量同步已触发",
"filling_entity_changes": "正在填充实体变更行...",
"sync_rows_filled_successfully": "同步行填充成功"
"sync_rows_filled_successfully": "同步行填充成功",
"finished-successfully": "同步已完成。",
"failed": "同步失败:{{message}}"
},
"vacuum_database": {
"title": "数据库清理",
@ -1036,8 +1062,13 @@
"theme_label": "主题",
"override_theme_fonts_label": "覆盖主题字体",
"light_theme": "浅色",
"dark_theme": "深色"
},
"dark_theme": "深色",
"layout": "布局",
"layout-vertical-title": "垂直",
"layout-horizontal-title": "水平",
"layout-vertical-description": "启动栏位于左侧(默认)",
"layout-horizontal-description": "启动栏位于标签栏下方,标签栏现在是全宽的。"
},
"zoom_factor": {
"title": "缩放系数(仅桌面客户端有效)",
"description": "缩放也可以通过 CTRL+- 和 CTRL+= 快捷键进行控制。"
@ -1162,6 +1193,8 @@
"backup_now": "立即备份",
"backup_database_now": "立即备份数据库",
"existing_backups": "现有备份",
"date-and-time": "日期和时间",
"path": "路径",
"database_backed_up_to": "数据库已备份到",
"no_backup_yet": "尚无备份"
},
@ -1309,7 +1342,9 @@
"duplicate-subtree": "复制子树",
"export": "导出",
"import-into-note": "导入到笔记",
"apply-bulk-actions": "应用批量操作"
"apply-bulk-actions": "应用批量操作",
"converted-to-attachments": "{{count}} 个笔记已被转换为附件。",
"convert-to-attachment-confirm": "确定要将选中的笔记转换为其父笔记的附件吗?"
},
"shared_info": {
"shared_publicly": "此笔记已公开分享在",
@ -1332,7 +1367,8 @@
"image": "图片",
"launcher": "启动器",
"doc": "文档",
"widget": "小部件"
"widget": "小部件",
"confirm-change": "当笔记内容不为空时,不建议更改笔记类型。您仍然要继续吗?"
},
"protect_note": {
"toggle-on": "保护笔记",
@ -1355,7 +1391,11 @@
"open-help-page": "打开帮助页面",
"find": {
"case_sensitive": "区分大小写",
"match_words": "匹配单词"
"match_words": "匹配单词",
"find_placeholder": "在文本中查找...",
"replace_placeholder": "替换为...",
"replace": "替换",
"replace_all": "全部替换"
},
"highlights_list_2": {
"title": "高亮列表",
@ -1378,7 +1418,9 @@
"hide-archived-notes": "隐藏已归档笔记",
"automatically-collapse-notes": "自动折叠笔记",
"automatically-collapse-notes-title": "笔记在一段时间内未使用将被折叠,以减少树形结构的杂乱。",
"save-changes": "保存并应用更改"
"save-changes": "保存并应用更改",
"auto-collapsing-notes-after-inactivity": "在不活动后自动折叠笔记...",
"saved-search-note-refreshed": "已保存的搜索笔记已刷新。"
},
"title_bar_buttons": {
"window-on-top": "保持此窗口置顶"
@ -1407,8 +1449,11 @@
"add_new_tab": "添加新标签页",
"close": "关闭",
"close_other_tabs": "关闭其他标签页",
"close_right_tabs": "关闭右侧标签页",
"close_all_tabs": "关闭所有标签页",
"reopen_last_tab": "重新打开最后一个关闭的标签页",
"move_tab_to_new_window": "将此标签页移动到新窗口",
"copy_tab_to_new_window": "将此标签页复制到新窗口",
"new_tab": "新标签页"
},
"toc": {
@ -1422,5 +1467,101 @@
},
"app_context": {
"please_wait_for_save": "请等待几秒钟以完成保存,然后您可以尝试再操作一次。"
},
"note_create": {
"duplicated": "笔记 \"{{title}}\" 已被复制。"
},
"image": {
"copied-to-clipboard": "图片的引用已复制到剪贴板,可以粘贴到任何文本笔记中。",
"cannot-copy": "无法将图片引用复制到剪贴板。"
},
"clipboard": {
"cut": "笔记已剪切到剪贴板。",
"copied": "笔记已复制到剪贴板。"
},
"entrypoints": {
"note-revision-created": "笔记修订已创建。",
"note-executed": "笔记已执行。",
"sql-error": "执行 SQL 查询时发生错误:{{message}}"
},
"branches": {
"cannot-move-notes-here": "无法将笔记移动到这里。",
"delete-status": "删除状态",
"delete-notes-in-progress": "正在删除笔记:{{count}}",
"delete-finished-successfully": "删除成功完成。",
"undeleting-notes-in-progress": "正在恢复删除的笔记:{{count}}",
"undeleting-notes-finished-successfully": "恢复删除的笔记已成功完成。"
},
"frontend_script_api": {
"async_warning": "您正在将一个异步函数传递给 `api.runOnBackend()`,这可能无法按预期工作。\\n要么使该函数同步通过移除 `async` 关键字),要么使用 `api.runAsyncOnBackendWithManualTransactionHandling()`。",
"sync_warning": "您正在将一个同步函数传递给 `api.runAsyncOnBackendWithManualTransactionHandling()`\\n而您可能应该使用 `api.runOnBackend()`。"
},
"ws": {
"sync-check-failed": "同步检查失败!",
"consistency-checks-failed": "一致性检查失败!请查看日志了解详细信息。",
"encountered-error": "遇到错误 \"{{message}}\",请查看控制台。"
},
"hoisted_note": {
"confirm_unhoisting": "请求的笔记 '{{requestedNote}}' 位于提升的笔记 '{{hoistedNote}}' 的子树之外,您必须取消提升才能访问该笔记。是否继续取消提升?"
},
"launcher_context_menu": {
"reset_launcher_confirm": "您确定要重置 \"{{title}}\" 吗?此笔记(及其子项)中的所有数据/设置将丢失,且启动器将恢复到其原始位置。",
"add-note-launcher": "添加笔记启动器",
"add-script-launcher": "添加脚本启动器",
"add-custom-widget": "添加自定义小部件",
"add-spacer": "添加间隔",
"delete": "删除",
"reset": "重置",
"move-to-visible-launchers": "移动到可见启动器",
"move-to-available-launchers": "移动到可用启动器",
"duplicate-launcher": "复制启动器"
},
"editable-text": {
"auto-detect-language": "自动检测"
},
"highlighting": {
"title": "文本笔记的代码语法高亮",
"description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
"color-scheme": "颜色方案"
},
"code_block": {
"word_wrapping": "自动换行"
},
"classic_editor_toolbar": {
"title": "格式化"
},
"editor": {
"title": "编辑器"
},
"editing": {
"editor_type": {
"label": "格式化工具栏",
"floating": {
"title": "浮动",
"description": "编辑工具出现在光标附近;"
},
"fixed": {
"title": "固定",
"description": "编辑工具出现在 \"格式化\" 功能区标签中。"
}
}
},
"electron_context_menu": {
"add-term-to-dictionary": "将 \"{{term}}\" 添加到字典",
"cut": "剪切",
"copy": "复制",
"copy-link": "复制链接",
"paste": "粘贴",
"paste-as-plain-text": "以纯文本粘贴",
"search_online": "用 {{searchEngine}} 搜索 \"{{term}}\""
},
"image_context_menu": {
"copy_reference_to_clipboard": "复制引用到剪贴板",
"copy_image_to_clipboard": "复制图片到剪贴板"
},
"link_context_menu": {
"open_note_in_new_tab": "在新标签页中打开笔记",
"open_note_in_new_split": "在新分屏中打开笔记",
"open_note_in_new_window": "在新窗口中打开笔记"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,11 @@
"chosen_actions": "Chosen actions",
"execute_bulk_actions": "Execute bulk actions",
"bulk_actions_executed": "Bulk actions have been executed successfully.",
"none_yet": "None yet... add an action by clicking one of the available ones above."
"none_yet": "None yet... add an action by clicking one of the available ones above.",
"labels": "Labels",
"relations": "Relations",
"notes": "Notes",
"other": "Other"
},
"clone_to": {
"clone_notes_to": "Clone notes to...",
@ -238,23 +242,23 @@
"confirm_undelete": "Do you want to undelete this note and its sub-notes?"
},
"revisions": {
"note_revisions": "Note revisions",
"note_revisions": "Note Revisions",
"delete_all_revisions": "Delete all revisions of this note",
"delete_all_button": "Delete all revisions",
"help_title": "Help on Note revisions",
"help_title": "Help on Note Revisions",
"revision_last_edited": "This revision was last edited on {{date}}",
"confirm_delete_all": "Do you want to delete all revisions of this note? This action will erase revision title and content, but still preserve revision metadata.",
"confirm_delete_all": "Do you want to delete all revisions of this note? This action will erase the revision title and content, but still preserve the revision metadata.",
"no_revisions": "No revisions for this note yet...",
"restore_button": "Restore this revision",
"confirm_restore": "Do you want to restore this revision? This will overwrite current title and content of the note with this revision.",
"confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.",
"delete_button": "Delete this revision",
"confirm_delete": "Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.",
"revisions_deleted": "Note revisions has been deleted.",
"confirm_delete": "Do you want to delete this revision? This action will delete the revision title and content, but still preserve the revision metadata.",
"revisions_deleted": "Note revisions have been deleted.",
"revision_restored": "Note revision has been restored.",
"revision_deleted": "Note revision has been deleted.",
"snapshot_interval": "Note Revisions Snapshot Interval: {{seconds}}s.",
"maximum_revisions": "Maximum revisions for current note: {{number}}.",
"settings": "Settings for Note revisions",
"snapshot_interval": "Note Revision Snapshot Interval: {{seconds}}s.",
"maximum_revisions": "Note Revision Snapshot Limit: {{number}}.",
"settings": "Note Revision Settings",
"download_button": "Download",
"mime": "MIME: ",
"file_size": "File size:",
@ -311,6 +315,7 @@
"boolean": "Boolean",
"date": "Date",
"date_time": "Date & Time",
"time": "Time",
"url": "URL",
"precision_title": "What number of digits after floating point should be available in the value setting interface.",
"precision": "Precision",
@ -1057,7 +1062,12 @@
"theme_label": "Theme",
"override_theme_fonts_label": "Override theme fonts",
"light_theme": "Light",
"dark_theme": "Dark"
"dark_theme": "Dark",
"layout": "Layout",
"layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal",
"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."
},
"zoom_factor": {
"title": "Zoom Factor (desktop build only)",
@ -1108,12 +1118,12 @@
"deleted_notes_erased": "Deleted notes have been erased."
},
"revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Note Revisions Snapshot Interval",
"note_revisions_snapshot_description": "Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time in seconds after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
"snapshot_time_interval_label": "Note revision snapshot time interval (in seconds):"
},
"revisions_snapshot_limit": {
"note_revisions_snapshot_limit_title": "Note Revision Snapshots Limit",
"note_revisions_snapshot_limit_title": "Note Revision Snapshot Limit",
"note_revisions_snapshot_limit_description": "The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions. You can set the maximum revisions for a single note through the #versioningLimit label.",
"snapshot_number_limit_label": "Note revision snapshot number limit:",
"erase_excess_revision_snapshots": "Erase excess revision snapshots now",
@ -1183,6 +1193,8 @@
"backup_now": "Backup now",
"backup_database_now": "Backup database now",
"existing_backups": "Existing backups",
"date-and-time": "Date & time",
"path": "Path",
"database_backed_up_to": "Database has been backed up to",
"no_backup_yet": "no backup yet"
},
@ -1378,8 +1390,12 @@
},
"open-help-page": "Open help page",
"find": {
"case_sensitive": "case sensitive",
"match_words": "match words"
"case_sensitive": "Case sensitive",
"match_words": "Match words",
"find_placeholder": "Find in text...",
"replace_placeholder": "Replace with...",
"replace": "Replace",
"replace_all": "Replace all"
},
"highlights_list_2": {
"title": "Highlights List",
@ -1435,7 +1451,9 @@
"close_other_tabs": "Close other tabs",
"close_right_tabs": "Close tabs to the right",
"close_all_tabs": "Close all tabs",
"reopen_last_tab": "Reopen last closed tab",
"move_tab_to_new_window": "Move this tab to a new window",
"copy_tab_to_new_window": "Copy this tab to a new window",
"new_tab": "New tab"
},
"toc": {
@ -1518,8 +1536,32 @@
"editing": {
"editor_type": {
"label": "Formatting toolbar",
"floating": "Floating (editing tools appear near the cursor)",
"fixed": "Fixed (editing tools appear in the \"Formatting\" ribbon tab)"
"floating": {
"title": "Floating",
"description": "editing tools appear near the cursor;"
},
"fixed": {
"title": "Fixed",
"description": "editing tools appear in the \"Formatting\" ribbon tab."
}
}
},
"electron_context_menu": {
"add-term-to-dictionary": "Add \"{{term}}\" to dictionary",
"cut": "Cut",
"copy": "Copy",
"copy-link": "Copy link",
"paste": "Paste",
"paste-as-plain-text": "Paste as plain text",
"search_online": "Search for \"{{term}}\" with {{searchEngine}}"
},
"image_context_menu": {
"copy_reference_to_clipboard": "Copy reference to clipboard",
"copy_image_to_clipboard": "Copy image to clipboard"
},
"link_context_menu": {
"open_note_in_new_tab": "Open note in a new tab",
"open_note_in_new_split": "Open note in a new split",
"open_note_in_new_window": "Open note in a new window"
}
}

View File

@ -51,7 +51,11 @@
"chosen_actions": "Acciones elegidas",
"execute_bulk_actions": "Ejecutar acciones en bloque",
"bulk_actions_executed": "Las acciones en bloque se han ejecutado con éxito.",
"none_yet": "Ninguna todavía... agrega una acción haciendo clic en una de las disponibles arriba."
"none_yet": "Ninguna todavía... agregue una acción haciendo clic en una de las disponibles arriba.",
"labels": "Etiquetas",
"relations": "Relaciones",
"notes": "Notas",
"other": "Otro"
},
"clone_to": {
"clone_notes_to": "Clonar notas a...",
@ -311,6 +315,7 @@
"boolean": "Booleano",
"date": "Fecha",
"date_time": "Fecha y hora",
"time": "Hora",
"url": "URL",
"precision_title": "Cantidad de dígitos después del punto flotante que deben estar disponibles en la interfaz de configuración del valor.",
"precision": "Precisión",
@ -1057,7 +1062,12 @@
"theme_label": "Tema",
"override_theme_fonts_label": "Sobreescribir fuentes de tema",
"light_theme": "Claro",
"dark_theme": "Oscuro"
"dark_theme": "Oscuro",
"layout": "Disposición",
"layout-vertical-title": "Vertical",
"layout-horizontal-title": "Horizontal",
"layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)",
"layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo."
},
"zoom_factor": {
"title": "Factor de zoom (solo versión de escritorio)",
@ -1183,6 +1193,8 @@
"backup_now": "Realizar copia de seguridad ahora",
"backup_database_now": "Realizar copia de seguridad de la base de datos ahora",
"existing_backups": "Copias de seguridad existentes",
"date-and-time": "Fecha y hora",
"path": "Ruta",
"database_backed_up_to": "Se ha realizado una copia de seguridad de la base de datos en",
"no_backup_yet": "no hay copia de seguridad todavía"
},
@ -1378,8 +1390,12 @@
},
"open-help-page": "Abrir página de ayuda",
"find": {
"case_sensitive": "distingue entre mayúsculas y minúsculas",
"match_words": "coincidir palabras"
"case_sensitive": "Distingue entre mayúsculas y minúsculas",
"match_words": "Coincidir palabras",
"find_placeholder": "Encontrar en texto...",
"replace_placeholder": "Reemplazar con...",
"replace": "Reemplazar",
"replace_all": "Reemplazar todo"
},
"highlights_list_2": {
"title": "Lista de destacados",
@ -1435,7 +1451,9 @@
"close_other_tabs": "Cerrar otras pestañas",
"close_right_tabs": "Cerrar pestañas a la derecha",
"close_all_tabs": "Cerras todas las pestañas",
"reopen_last_tab": "Reabrir última pestaña cerrada",
"move_tab_to_new_window": "Mover esta pestaña a una nueva ventana",
"copy_tab_to_new_window": "Copiar esta pestaña a una ventana nueva",
"new_tab": "Nueva pestaña"
},
"toc": {
@ -1518,8 +1536,23 @@
"editing": {
"editor_type": {
"label": "Barra de herramientas de formato",
"floating": "Flotante (las herramientas de edición aparecen cerca del cursor)",
"fixed": "Fijo (las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")"
"floating": {
"title": "Flotante",
"description": "las herramientas de edición aparecen cerca del cursor;"
},
"fixed": {
"title": "Fijo",
"description": "las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")."
}
}
},
"electron_context_menu": {
"add-term-to-dictionary": "Agregar \"{{term}}\" al diccionario.",
"cut": "Cortar",
"copy": "Copiar",
"copy-link": "Copiar enlace",
"paste": "Pegar",
"paste-as-plain-text": "Pegar como texto plano",
"search_online": "Buscar \"{{term}}\" con {{searchEngine}}"
}
}

View File

@ -311,6 +311,7 @@
"boolean": "Booléen",
"date": "Date",
"date_time": "Date et heure",
"time": "Heure",
"url": "URL",
"precision_title": "Nombre de chiffres après la virgule devant être disponible dans l'interface définissant la valeur.",
"precision": "Précision",

View File

@ -0,0 +1 @@
{}

View File

@ -127,6 +127,7 @@
"custom_resource_provider": "a se vedea <a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">Custom request handler</a>",
"date": "Dată",
"date_time": "Dată și timp",
"time": "Timp",
"delete": "Șterge",
"digits": "număr de zecimale",
"disable_inclusion": "script-urile cu această etichetă nu vor fi incluse în execuția scriptului părinte.",
@ -254,6 +255,8 @@
"enable_monthly_backup": "Activează copia de siguranță lunară",
"enable_weekly_backup": "Activează copia de siguranță săptămânală",
"existing_backups": "Copii de siguranță existente",
"date-and-time": "Data și ora",
"path": "Calea fișierului",
"no_backup_yet": "nu există încă nicio copie de siguranță"
},
"basic_properties": {
@ -297,7 +300,11 @@
"close": "Închide",
"execute_bulk_actions": "Execută acțiunile în masă",
"include_descendants": "Include descendenții notiței selectate",
"none_yet": "Nicio acțiune... adaugați una printr-un click pe cele disponibile mai jos."
"none_yet": "Nicio acțiune... adăugați una printr-un click pe cele disponibile mai jos.",
"labels": "Etichete",
"notes": "Notițe",
"other": "Altele",
"relations": "Relații"
},
"calendar": {
"april": "Aprilie",
@ -1349,7 +1356,11 @@
"open-help-page": "Deschide pagina de informații",
"find": {
"match_words": "doar cuvinte întregi",
"case_sensitive": "ține cont de majuscule"
"case_sensitive": "ține cont de majuscule",
"replace_all": "Înlocuiește totul",
"replace_placeholder": "Înlocuiește cu...",
"replace": "Înlocuiește",
"find_placeholder": "Căutați în text..."
},
"highlights_list_2": {
"options": "Setări",
@ -1439,7 +1450,9 @@
"close_tab": "Închide tab",
"move_tab_to_new_window": "Mută acest tab în altă fereastră",
"new_tab": "Tab nou",
"close_right_tabs": "Închide taburile din dreapta"
"close_right_tabs": "Închide taburile din dreapta",
"copy_tab_to_new_window": "Copiază tab-ul într-o fereastră nouă",
"reopen_last_tab": "Redeschide ultimul tab închis"
},
"toc": {
"options": "Setări",
@ -1514,12 +1527,27 @@
},
"editing": {
"editor_type": {
"fixed": "Editor cu bară fixă (uneltele de editare vor apărea în tab-ul „Formatare” din panglică)",
"floating": "Editor cu bară flotantă (uneltele de editare vor apărea lângă cursor)",
"label": "Bară de formatare"
"label": "Bară de formatare",
"floating": {
"title": "Editor cu bară flotantă",
"description": "uneltele de editare vor apărea lângă cursor;"
},
"fixed": {
"title": "Editor cu bară fixă",
"description": "uneltele de editare vor apărea în tab-ul „Formatare” din panglică."
}
}
},
"editor": {
"title": "Editor"
},
"electron_context_menu": {
"add-term-to-dictionary": "Adaugă „{{term}}” în dicționar",
"copy": "Copiază",
"copy-link": "Copiază legătura",
"cut": "Decupează",
"paste": "Lipește",
"paste-as-plain-text": "Lipește doar textul",
"search_online": "Caută „{{term}}” cu {{searchEngine}}"
}
}

View File

@ -0,0 +1 @@
{}

View File

@ -66,7 +66,8 @@ const ALLOWED_OPTIONS = new Set([
'editedNotesOpenInRibbon',
'locale',
'firstDayOfWeek',
'textNoteEditorType'
'textNoteEditorType',
'layoutOrientation'
]);
function getOptions() {

View File

@ -42,7 +42,7 @@ function index(req: Request, res: Response) {
isDev: env.isDev(),
isMainWindow: !req.query.extraWindow,
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
maxContentWidth: parseInt(options.maxContentWidth),
maxContentWidth: Math.max(640, parseInt(options.maxContentWidth)),
triliumVersion: packageJson.version,
assetPath: assetPath,
appPath: appPath

View File

@ -34,8 +34,17 @@ interface Item {
baseSize?: string;
growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar";
command?: "jumpToNote" | "searchNotes" | "createNoteIntoInbox" | "showRecentChanges";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar" | "quickSearch";
command?: keyof typeof Command;
}
// TODO: Move this into a commons project once the client/server architecture is well split.
enum Command {
jumpToNote,
searchNotes,
createNoteIntoInbox,
showRecentChanges,
showOptions
}
/*
@ -94,7 +103,8 @@ const HIDDEN_SUBTREE_DEFINITION: Item = {
type: 'contentWidget',
icon: 'bx-terminal',
attributes: [
{ type: 'label', name: 'keepCurrentHoisting' }
{ type: 'label', name: 'keepCurrentHoisting' },
{ type: 'label', name: 'fullContentWidth' }
]
},
{
@ -231,8 +241,10 @@ const HIDDEN_SUBTREE_DEFINITION: Item = {
{ id: '_lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
{ id: '_lbToday', title: "Open Today's Journal Note", type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' },
{ id: '_lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
{ id: '_lbQuickSearch', title: "Quick Search", type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
{ id: '_lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
{ id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' }
{ id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' },
{ id: '_lbSettings', title: 'Settings', type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' }
]
}
]

View File

@ -134,7 +134,9 @@ const defaultOptions: DefaultOption[] = [
{ name: "codeBlockWordWrap", value: "false", isSynced: true },
// Text note configuration
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true }
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
{ name: "layoutOrientation", value: "vertical", isSynced: false }
];
/**

View File

@ -8,7 +8,7 @@ function parse(value: string): DefinitionObject {
if (token === 'promoted') {
defObj.isPromoted = true;
}
else if (['text', 'number', 'boolean', 'date', 'datetime', 'url'].includes(token)) {
else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
defObj.labelType = token;
}
else if (['single', 'multi'].includes(token)) {

View File

@ -27,43 +27,52 @@ class SearchResult {
this.score = 0;
const note = becca.notes[this.noteId];
const normalizedQuery = fulltextQuery.toLowerCase();
const normalizedTitle = note.title.toLowerCase();
// Note ID exact match, much higher score
if (note.noteId.toLowerCase() === fulltextQuery) {
this.score += 100;
this.score += 1000;
}
if (note.title.toLowerCase() === fulltextQuery) {
this.score += 100; // high reward for exact match #3470
// Title matching scores, make sure to always win
if (normalizedTitle === normalizedQuery) {
this.score += 2000; // Increased from 1000 to ensure exact matches always win
}
else if (normalizedTitle.startsWith(normalizedQuery)) {
this.score += 500; // Increased to give more weight to prefix matches
}
else if (normalizedTitle.includes(` ${normalizedQuery} `) ||
normalizedTitle.startsWith(`${normalizedQuery} `) ||
normalizedTitle.endsWith(` ${normalizedQuery}`)) {
this.score += 300; // Increased to better distinguish word matches
}
// notes with matches on its own note title as opposed to ancestors or descendants
this.addScoreForStrings(tokens, note.title, 1.5);
// matches in attributes don't get extra points and thus are implicitly valued less than note path matches
this.addScoreForStrings(tokens, this.notePathTitle, 1);
// Add scores for partial matches with adjusted weights
this.addScoreForStrings(tokens, note.title, 2.0); // Increased to give more weight to title matches
this.addScoreForStrings(tokens, this.notePathTitle, 0.3); // Reduced to further de-emphasize path matches
if (note.isInHiddenSubtree()) {
this.score = this.score / 2;
this.score = this.score / 3; // Increased penalty for hidden notes
}
}
addScoreForStrings(tokens: string[], str: string, factor: number) {
const chunks = str.toLowerCase().split(" ");
this.score = 0;
let tokenScore = 0;
for (const chunk of chunks) {
for (const token of tokens) {
if (chunk === token) {
this.score += 4 * token.length * factor;
tokenScore += 4 * token.length * factor;
} else if (chunk.startsWith(token)) {
this.score += 2 * token.length * factor;
tokenScore += 2 * token.length * factor;
} else if (chunk.includes(token)) {
this.score += token.length * factor;
tokenScore += token.length * factor;
}
}
}
this.score += tokenScore;
}
}

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