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

View File

@ -9,6 +9,12 @@ concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true 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: jobs:
build_docker: build_docker:
name: Build Docker image name: Build Docker image
@ -30,4 +36,66 @@ jobs:
with: with:
context: . context: .
cache-from: type=gha 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() { stats() {
# Print the number of existing strings on the JSON files for each locale # 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) s=$(number_of_keys "${paths[0]}/en/server.json")
c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/en/translation.json" | wc -l) c=$(number_of_keys "${paths[1]}/en/translation.json")
echo "|locale |server strings |client strings |" echo "| locale |server strings |client strings |"
echo "|-------|---------------|---------------|" echo "|--------|---------------|---------------|"
echo "| en | ${s} | ${c} |" echo "| en | ${s} | ${c} |"
for locale in "${locales[@]}"; do for locale in "${locales[@]}"; do
s=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[0]}/${locale}/server.json" | wc -l) s=$(number_of_keys "${paths[0]}/${locale}/server.json")
c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/${locale}/translation.json" | wc -l) c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
echo "| ${locale} | ${s} | ${c} |" 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 done
} }
@ -35,11 +63,11 @@ help() {
echo -e "\nDescription:" echo -e "\nDescription:"
echo -e "\tCreate PO files to make easier the labor of translation" echo -e "\tCreate PO files to make easier the labor of translation"
echo -e "\nUsage:" 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 "\nFlags:"
echo -e " --clear\n\tClear all po-* directories" 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 " --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" echo -e " --update2 <LOCALE>\n\tRecover translation from PO files to localized JSON files"
} }
@ -51,7 +79,7 @@ file_path="$(
pwd -P pwd -P
)" )"
paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/") 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 [ $# -eq 1 ]; then
if [ "$1" == "--clear" ]; then if [ "$1" == "--clear" ]; then
@ -62,34 +90,18 @@ if [ $# -eq 1 ]; then
done done
elif [ "$1" == "--stats" ]; then elif [ "$1" == "--stats" ]; then
stats stats
elif [ "$1" == "--update" ]; then elif [ "$1" == "--update1" ]; then
# Update PO files from English and localized JSON files as source update_1 "${locales[@]}"
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
done
done
elif [ "$1" == "--update2" ]; then elif [ "$1" == "--update2" ]; then
# Recover translation from PO files to localized JSON files update_2 "${locales[@]}"
for path in "${paths[@]}"; do
for locale in "${locales[@]}"; do
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
done
done
else else
help help
fi fi
elif [ $# -eq 2 ]; then elif [ $# -eq 2 ]; then
if [ "$1" == "--update" ]; then if [ "$1" == "--update1" ]; then
locale="$2" update_1 "$2"
for path in "${paths[@]}"; do
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
done
elif [ "$1" == "--update2" ]; then elif [ "$1" == "--update2" ]; then
locale="$2" update_2 "$2"
for path in "${paths[@]}"; do
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
done
else else
help help
fi 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", "name": "trilium",
"version": "0.90.11-beta", "version": "0.90.12",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "trilium", "name": "trilium",
"version": "0.90.11-beta", "version": "0.90.12",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",
"dependencies": { "dependencies": {
"@braintree/sanitize-url": "7.1.0", "@braintree/sanitize-url": "7.1.0",
"@electron/remote": "2.1.2", "@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.6", "@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.10.0", "@highlightjs/cdn-assets": "11.10.0",
"@mermaid-js/layout-elk": "0.1.5",
"archiver": "7.0.1", "archiver": "7.0.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"autocomplete.js": "0.38.1", "autocomplete.js": "0.38.1",
@ -47,7 +48,7 @@
"html2plaintext": "2.1.4", "html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2", "http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.5", "https-proxy-agent": "7.0.5",
"i18next": "23.16.4", "i18next": "23.16.8",
"i18next-fs-backend": "2.3.2", "i18next-fs-backend": "2.3.2",
"i18next-http-backend": "2.6.2", "i18next-http-backend": "2.6.2",
"image-type": "4.1.0", "image-type": "4.1.0",
@ -3072,6 +3073,18 @@
"url": "https://github.com/malept/cross-spawn-promise?sponsor=1" "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": { "node_modules/@mermaid-js/parser": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz", "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", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" "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": { "node_modules/emitter-listener": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
@ -10024,9 +10042,9 @@
} }
}, },
"node_modules/i18next": { "node_modules/i18next": {
"version": "23.16.4", "version": "23.16.8",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.4.tgz", "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
"integrity": "sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==", "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
"funding": [ "funding": [
{ {
"type": "individual", "type": "individual",
@ -10041,6 +10059,7 @@
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"@babel/runtime": "^7.23.2" "@babel/runtime": "^7.23.2"
} }

View File

@ -2,7 +2,7 @@
"name": "trilium", "name": "trilium",
"productName": "TriliumNext Notes", "productName": "TriliumNext Notes",
"description": "Build your personal knowledge base with 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", "license": "AGPL-3.0-only",
"main": "./dist/electron-main.js", "main": "./dist/electron-main.js",
"author": { "author": {
@ -54,6 +54,7 @@
"@electron/remote": "2.1.2", "@electron/remote": "2.1.2",
"@excalidraw/excalidraw": "0.17.6", "@excalidraw/excalidraw": "0.17.6",
"@highlightjs/cdn-assets": "11.10.0", "@highlightjs/cdn-assets": "11.10.0",
"@mermaid-js/layout-elk": "0.1.5",
"archiver": "7.0.1", "archiver": "7.0.1",
"async-mutex": "0.5.0", "async-mutex": "0.5.0",
"autocomplete.js": "0.38.1", "autocomplete.js": "0.38.1",
@ -88,7 +89,7 @@
"html2plaintext": "2.1.4", "html2plaintext": "2.1.4",
"http-proxy-agent": "7.0.2", "http-proxy-agent": "7.0.2",
"https-proxy-agent": "7.0.5", "https-proxy-agent": "7.0.5",
"i18next": "23.16.4", "i18next": "23.16.8",
"i18next-fs-backend": "2.3.2", "i18next-fs-backend": "2.3.2",
"i18next-http-backend": "2.6.2", "i18next-http-backend": "2.6.2",
"image-type": "4.1.0", "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) { if (fun) {
return this.callMethod(fun, data); 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); 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() { async reopenLastTabCommand() {
let closeLastEmptyTab = null; 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 CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
import ScrollPaddingWidget from "../widgets/scroll_padding.js"; import ScrollPaddingWidget from "../widgets/scroll_padding.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
export default class DesktopLayout { export default class DesktopLayout {
constructor(customWidgets) { constructor(customWidgets) {
@ -92,112 +93,120 @@ export default class DesktopLayout {
getRootWidget(appContext) { getRootWidget(appContext) {
appContext.noteTreeWidget = new NoteTreeWidget(); appContext.noteTreeWidget = new NoteTreeWidget();
return new RootContainer() const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal);
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext) .setParent(appContext)
.child(new FlexContainer("column") .optChild(launcherPaneIsHorizontal, new FlexContainer('row')
.id("launcher-pane") .child(new TabRowWidget().class("full-width"))
.css("width", "53px") .child(new TitleBarButtonsWidget())
.child(new GlobalMenuWidget()) .css('height', '40px')
.child(new LauncherContainer()) .css('background-color', 'var(--launcher-pane-background-color)')
.child(new LeftPaneToggleWidget()) .setParent(appContext)
) )
.child(new LeftPaneContainer() .optChild(launcherPaneIsHorizontal, launcherPane)
.child(new QuickSearchWidget()) .child(new FlexContainer('row')
.child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane'))
)
.child(new FlexContainer('column')
.id('rest-pane')
.css("flex-grow", "1") .css("flex-grow", "1")
.child(new FlexContainer('row') .optChild(!launcherPaneIsHorizontal, launcherPane)
.child(new TabRowWidget()) .child(new LeftPaneContainer()
.child(new TitleBarButtonsWidget()) .optChild(!launcherPaneIsHorizontal, new QuickSearchWidget())
.css('height', '40px') .child(appContext.noteTreeWidget)
.child(...this.customWidgets.get('left-pane'))
) )
.child(new FlexContainer('row') .child(new FlexContainer('column')
.filling() .id('rest-pane')
.collapsible() .css("flex-grow", "1")
.child(new FlexContainer('column') .optChild(!launcherPaneIsHorizontal, new FlexContainer('row')
.child(new TabRowWidget())
.child(new TitleBarButtonsWidget())
.css('height', '40px')
)
.child(new FlexContainer('row')
.filling() .filling()
.collapsible() .collapsible()
.id('center-pane') .child(new FlexContainer('column')
.child(new SplitNoteContainer(() => .filling()
new NoteWrapperWidget() .collapsible()
.child(new FlexContainer('row').class('title-row') .id('center-pane')
.css("height", "50px") .child(new SplitNoteContainer(() =>
.css("min-height", "50px") new NoteWrapperWidget()
.css('align-items', "center") .child(new FlexContainer('row').class('title-row')
.cssBlock('.title-row > * { margin: 5px; }') .css("height", "50px")
.child(new NoteIconWidget()) .css("min-height", "50px")
.child(new NoteTitleWidget()) .css('align-items', "center")
.child(new SpacerWidget(0, 1)) .cssBlock('.title-row > * { margin: 5px; }')
.child(new MovePaneButton(true)) .child(new NoteIconWidget())
.child(new MovePaneButton(false)) .child(new NoteTitleWidget())
.child(new ClosePaneButton()) .child(new SpacerWidget(0, 1))
.child(new CreatePaneButton()) .child(new MovePaneButton(true))
) .child(new MovePaneButton(false))
.child( .child(new ClosePaneButton())
new RibbonContainer() .child(new CreatePaneButton())
// 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". .child(
// promoted attributes should always win. new RibbonContainer()
.ribbon(new ClassicEditorToolbar()) // the order of the widgets matter. Some of these want to "activate" themselves
.ribbon(new ScriptExecutorWidget()) // when visible. When this happens to multiple of them, the first one "wins".
.ribbon(new SearchDefinitionWidget()) // promoted attributes should always win.
.ribbon(new EditedNotesWidget()) .ribbon(new ClassicEditorToolbar())
.ribbon(new BookPropertiesWidget()) .ribbon(new ScriptExecutorWidget())
.ribbon(new NotePropertiesWidget()) .ribbon(new SearchDefinitionWidget())
.ribbon(new FilePropertiesWidget()) .ribbon(new EditedNotesWidget())
.ribbon(new ImagePropertiesWidget()) .ribbon(new BookPropertiesWidget())
.ribbon(new BasicPropertiesWidget()) .ribbon(new NotePropertiesWidget())
.ribbon(new OwnedAttributeListWidget()) .ribbon(new FilePropertiesWidget())
.ribbon(new InheritedAttributesWidget()) .ribbon(new ImagePropertiesWidget())
.ribbon(new NotePathsWidget()) .ribbon(new BasicPropertiesWidget())
.ribbon(new NoteMapRibbonWidget()) .ribbon(new OwnedAttributeListWidget())
.ribbon(new SimilarNotesWidget()) .ribbon(new InheritedAttributesWidget())
.ribbon(new NoteInfoWidget()) .ribbon(new NotePathsWidget())
.button(new RevisionsButton()) .ribbon(new NoteMapRibbonWidget())
.button(new NoteActionsWidget()) .ribbon(new SimilarNotesWidget())
) .ribbon(new NoteInfoWidget())
.child(new SharedInfoWidget()) .button(new RevisionsButton())
.child(new WatchedFileUpdateStatusWidget()) .button(new NoteActionsWidget())
.child(new FloatingButtons() )
.child(new EditButton()) .child(new SharedInfoWidget())
.child(new ShowTocWidgetButton()) .child(new WatchedFileUpdateStatusWidget())
.child(new ShowHighlightsListWidgetButton()) .child(new FloatingButtons()
.child(new CodeButtonsWidget()) .child(new EditButton())
.child(new RelationMapButtons()) .child(new ShowTocWidgetButton())
.child(new CopyImageReferenceButton()) .child(new ShowHighlightsListWidgetButton())
.child(new SvgExportButton()) .child(new CodeButtonsWidget())
.child(new BacklinksWidget()) .child(new RelationMapButtons())
.child(new HideFloatingButtonsButton()) .child(new CopyImageReferenceButton())
) .child(new SvgExportButton())
.child(new MermaidWidget()) .child(new BacklinksWidget())
.child( .child(new HideFloatingButtonsButton())
new ScrollingContainer() )
.filling() .child(new MermaidWidget())
.child(new PromotedAttributesWidget()) .child(
.child(new SqlTableSchemasWidget()) new ScrollingContainer()
.child(new NoteDetailWidget()) .filling()
.child(new NoteListWidget()) .child(new PromotedAttributesWidget())
.child(new SearchResultWidget()) .child(new SqlTableSchemasWidget())
.child(new SqlResultWidget()) .child(new NoteDetailWidget())
.child(new ScrollPaddingWidget()) .child(new NoteListWidget())
) .child(new SearchResultWidget())
.child(new ApiLogWidget()) .child(new SqlResultWidget())
.child(new FindWidget()) .child(new ScrollPaddingWidget())
.child( )
...this.customWidgets.get('node-detail-pane'), // typo, let's keep it for a while as BC .child(new ApiLogWidget())
...this.customWidgets.get('note-detail-pane') .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 ConfirmDialog())
.child(new PromptDialog()); .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 SharedInfoWidget from "../widgets/shared_info.js";
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js"; import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js"; import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
import options from "../services/options.js";
const MOBILE_CSS = ` const MOBILE_CSS = `
<style> <style>
@ -112,15 +113,12 @@ span.fancytree-expander {
export default class MobileLayout { export default class MobileLayout {
getRootWidget(appContext) { getRootWidget(appContext) {
return new RootContainer() const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
return new RootContainer(launcherPaneIsHorizontal)
.setParent(appContext) .setParent(appContext)
.cssBlock(MOBILE_CSS) .cssBlock(MOBILE_CSS)
.child(new FlexContainer("column") .child(this.#buildLauncherPane(launcherPaneIsHorizontal))
.id("launcher-pane")
.css("width", "53px")
.child(new GlobalMenuWidget())
.child(new LauncherContainer())
)
.child(new FlexContainer("row") .child(new FlexContainer("row")
.filling() .filling()
.child(new ScreenContainer("tree", 'column') .child(new ScreenContainer("tree", 'column')
@ -140,12 +138,14 @@ export default class MobileLayout {
.child(new FlexContainer('row').contentSized() .child(new FlexContainer('row').contentSized()
.css('font-size', 'larger') .css('font-size', 'larger')
.css('align-items', 'center') .css('align-items', 'center')
.child(new MobileDetailMenuWidget().contentSized()) .optChild(!launcherPaneIsHorizontal, new MobileDetailMenuWidget(false).contentSized())
.child(new NoteTitleWidget() .child(new NoteTitleWidget()
.contentSized() .contentSized()
.css("position: relative;") .css("position: relative;")
.css("top: 5px;") .css("top: 5px;")
.optCss(launcherPaneIsHorizontal, "padding-left", "0.5em")
) )
.optChild(launcherPaneIsHorizontal, new MobileDetailMenuWidget(true).contentSized())
.child(new CloseDetailButtonWidget().contentSized())) .child(new CloseDetailButtonWidget().contentSized()))
.child(new SharedInfoWidget()) .child(new SharedInfoWidget())
.child(new FloatingButtons() .child(new FloatingButtons()
@ -174,4 +174,25 @@ export default class MobileLayout {
.child(new ConfirmDialog()) .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 { class ContextMenu {
constructor() { constructor() {
this.$widget = $("#context-menu-container"); this.$widget = $("#context-menu-container");
this.$widget.addClass("dropend");
this.dateContextMenuOpenedMs = 0; this.dateContextMenuOpenedMs = 0;
$(document).on('click', () => this.hide()); $(document).on('click', () => this.hide());
@ -11,6 +12,12 @@ class ContextMenu {
async show(options) { async show(options) {
this.options = 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.$widget.empty();
this.addItems(this.$widget, options.items); this.addItems(this.$widget, options.items);
@ -96,6 +103,10 @@ class ContextMenu {
.append(" &nbsp; ") // some space between icon and text .append(" &nbsp; ") // some space between icon and text
.append(item.title); .append(item.title);
if (item.shortcut) {
$link.append($("<kbd>").text(item.shortcut));
}
const $item = $("<li>") const $item = $("<li>")
.addClass("dropdown-item") .addClass("dropdown-item")
.append($link) .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 // 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 // "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 // 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) { if (Date.now() - this.dateContextMenuOpenedMs > 300) {
// seems like if we hide the menu immediately, some clicks can get propagated to the underlying component // 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 // 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(); const contextMenu = new ContextMenu();
export default contextMenu; export default contextMenu;

View File

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

View File

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

View File

@ -37,18 +37,22 @@ export default class LauncherContextMenu {
const canBeReset = !canBeDeleted && note.isLaunchBarConfig(); const canBeReset = !canBeDeleted && note.isLaunchBarConfig();
return [ 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-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-note" } : 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-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-plus" } : 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-plus" } : null, (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-dots-horizontal" } : null,
(isVisibleRoot || isAvailableRoot) ? { title: "----" } : 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, 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, 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", (isVisibleItem || isAvailableItem) ? { title: "----" } : null,
enabled: isItem }
{ 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); ].filter(row => row !== null);
} }

View File

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

View File

@ -49,56 +49,98 @@ export default class TreeContextMenu {
const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch; const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
return [ 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.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, items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions }, 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, items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
enabled: notSearch && noSelectedNotes && notOptions }, 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: "----" },
{ title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes }, { 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: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
{ title: "----" }, { 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", { title: t("tree-context-menu.advanced"), uiIcon: "bx bxs-wrench", enabled: true, items: [
enabled: isNotRoot && !isHoisted }, { 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", { title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`, command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
enabled: isNotRoot && !isHoisted && parentNotSearch }, 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", { title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`, command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes }, enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
{ title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste", { title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes }, 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: "----" },
{ 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 }, 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 }, 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); ].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 AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js"; import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js"; import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
import { t } from "./i18n.js";
const ACTION_GROUPS = [ const ACTION_GROUPS = [
{ {
title: 'Labels', title: t("bulk_actions.labels"),
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction] actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
}, },
{ {
title: 'Relations', title: t("bulk_actions.relations"),
actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction] actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
}, },
{ {
title: 'Notes', title: t("bulk_actions.notes"),
actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction], actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction],
}, },
{ {
title: 'Other', title: t("bulk_actions.other"),
actions: [ExecuteScriptBulkAction] actions: [ExecuteScriptBulkAction]
} }
]; ];

View File

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

View File

@ -58,7 +58,19 @@ const FORCE_GRAPH = {
}; };
const MERMAID = { 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 = { const EXCALIDRAW = {
@ -197,6 +209,7 @@ export default {
WHEEL_ZOOM, WHEEL_ZOOM,
FORCE_GRAPH, FORCE_GRAPH,
MERMAID, MERMAID,
MERMAID_ELK,
EXCALIDRAW, EXCALIDRAW,
MARKJS, MARKJS,
I18NEXT, I18NEXT,

View File

@ -254,8 +254,15 @@ function goToLinkExt(evt, hrefLink, $link) {
window.open(hrefLink, '_blank'); window.open(hrefLink, '_blank');
} else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) { } else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
const electron = utils.dynamicRequire('electron'); const electron = utils.dynamicRequire('electron');
electron.shell.openPath(hrefLink); 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); ].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) { if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
results = [ results = [
{ {
@ -138,6 +148,17 @@ function initNoteAutocomplete($el, options) {
autocompleteOptions.debug = true; // don't close on blur 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({ $el.autocomplete({
...autocompleteOptions, ...autocompleteOptions,
appendTo: document.querySelector('body'), appendTo: document.querySelector('body'),
@ -192,6 +213,12 @@ function initNoteAutocomplete($el, options) {
suggestion.notePath = note.getBestNotePathString(hoistedNoteId); suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
} }
if (suggestion.action === 'search-notes') {
const searchString = suggestion.noteTitle;
appContext.triggerCommand('searchNotes', { searchString });
return;
}
$el.setSelectedNotePath(suggestion.notePath); $el.setSelectedNotePath(suggestion.notePath);
$el.setSelectedExternalLink(null); $el.setSelectedExternalLink(null);

View File

@ -6,7 +6,7 @@ function parse(value) {
if (token === 'promoted') { if (token === 'promoted') {
defObj.isPromoted = true; 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; defObj.labelType = token;
} }
else if (['single', 'multi'].includes(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="boolean">${t('attribute_detail.boolean')}</option>
<option value="date">${t('attribute_detail.date')}</option> <option value="date">${t('attribute_detail.date')}</option>
<option value="datetime">${t('attribute_detail.date_time')}</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> <option value="url">${t('attribute_detail.url')}</option>
</select> </select>
</td> </td>

View File

@ -40,6 +40,21 @@ class BasicWidget extends Component {
return this; 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) { id(id) {
this.attrs.id = id; this.attrs.id = id;
return this; return this;
@ -50,11 +65,34 @@ class BasicWidget extends Component {
return this; 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) { css(name, value) {
this.attrs.style += `${name}: ${value};`; this.attrs.style += `${name}: ${value};`;
return this; 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() { contentSized() {
this.css("contain", "none"); this.css("contain", "none");

View File

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

View File

@ -23,7 +23,11 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.tooltip = new bootstrap.Tooltip(this.$widget, { 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) { if (this.settings.onContextMenu) {
@ -36,8 +40,6 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
}); });
} }
this.$widget.attr("data-placement", this.settings.titlePlacement);
super.doRender(); super.doRender();
} }

View File

@ -20,6 +20,13 @@ const TPL = `
width: 20em; 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 { .attachment-actions .dropdown-item[disabled], .attachment-actions .dropdown-item[disabled]:hover {
color: var(--muted-text-color) !important; color: var(--muted-text-color) !important;
background-color: transparent !important; background-color: transparent !important;
@ -32,16 +39,39 @@ const TPL = `
style="position: relative; top: 3px;"></button> style="position: relative; top: 3px;"></button>
<div class="dropdown-menu dropdown-menu-right"> <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> <li data-trigger-command="openAttachment" class="dropdown-item"
<a data-trigger-command="openAttachmentCustom" class="dropdown-item" title="${t('attachments_actions.open_externally_title')}"><span class="bx bx-file-find"></span> ${t('attachments_actions.open_externally')}</li>
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> <li data-trigger-command="openAttachmentCustom" class="dropdown-item"
<a data-trigger-command="renameAttachment" class="dropdown-item">${t('attachments_actions.rename_attachment')}</a> title="${t('attachments_actions.open_custom_title')}"><span class="bx bx-customize"></span> ${t('attachments_actions.open_custom')}</li>
<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> <li data-trigger-command="downloadAttachment" class="dropdown-item">
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">${t('attachments_actions.convert_attachment_into_note')}</a> <span class="bx bx-download"></span> ${t('attachments_actions.download')}</li>
<a data-trigger-command="deleteAttachment" class="dropdown-item">${t('attachments_actions.delete_attachment')}</a>
<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> </div>
<input type="file" class="attachment-upload-new-revision-input" style="display: none"> <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']"); const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']");
$openAttachmentButton $openAttachmentButton
.addClass("disabled") .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')) .attr("title", t('attachments_actions.open_externally_detail_page'))
); );
if (isElectron) { if (isElectron) {
const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']"); const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton $openAttachmentCustomButton
.addClass("disabled") .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')) .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']"); const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
$openAttachmentCustomButton $openAttachmentCustomButton
.addClass("disabled") .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')) .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"; import options from "../../services/options.js";
const TPL = ` const TPL = `
<div class="dropdown global-menu dropend"> <div class="dropdown global-menu">
<style> <style>
.global-menu { .global-menu {
width: 53px; width: 53px;
@ -100,53 +100,31 @@ const TPL = `
position: relative; position: relative;
left: 0; left: 0;
top: 5px; top: 5px;
--dropdown-shadow-opacity: 0;
--submenu-opening-delay: 0;
} }
</style> </style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" <button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
aria-expanded="false" class="icon-action global-menu-button"> 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> <div class="global-menu-button-update-available"></div>
</button> </button>
<ul class="dropdown-menu dropdown-menu-right"> <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"> <li class="dropdown-item" data-trigger-command="openNewWindow">
<span class="bx bx-window-open"></span> <span class="bx bx-window-open"></span>
${t('global_menu.open_new_window')} ${t('global_menu.open_new_window')}
<kbd data-command="openNewWindow"></kbd> <kbd data-command="openNewWindow"></kbd>
</li> </li>
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion"> <li class="dropdown-item" data-trigger-command="showShareSubtree">
<span class="bx bx-mobile"></span> <span class="bx bx-share-alt"></span>
${t('global_menu.switch_to_mobile_version')} ${t('global_menu.show_shared_notes_subtree')}
</li> </li>
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion"> <div class="dropdown-divider"></div>
<span class="bx bx-desktop"></span>
${t('global_menu.switch_to_desktop_version')} <span class="zoom-container dropdown-item dropdown-item-container">
</li>
<span class="zoom-container dropdown-item">
<div> <div>
<span class="bx bx-empty"></span> <span class="bx bx-empty"></span>
${t('global_menu.zoom')} ${t('global_menu.zoom')}
@ -165,16 +143,23 @@ const TPL = `
</div> </div>
</span> </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"> <li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
<span class="bx bx-sidebar"></span> <span class="bx bx-sidebar"></span>
${t('global_menu.configure_launchbar')} ${t('global_menu.configure_launchbar')}
</li> </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"> <li class="dropdown-item dropdown-submenu">
<span class="dropdown-toggle"> <span class="dropdown-toggle">
<span class="bx bx-chip"></span> <span class="bx bx-chip"></span>
@ -182,10 +167,22 @@ const TPL = `
</span> </span>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools"> <li class="dropdown-item" data-trigger-command="showHiddenSubtree">
<span class="bx bx-bug-alt"></span> <span class="bx bx-hide"></span>
${t('global_menu.open_dev_tools')} ${t('global_menu.show_hidden_subtree')}
<kbd data-command="openDevTools"></kbd> </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>
<li class="dropdown-item" data-trigger-command="showSQLConsole"> <li class="dropdown-item" data-trigger-command="showSQLConsole">
@ -198,16 +195,13 @@ const TPL = `
<span class="bx bx-data"></span> <span class="bx bx-data"></span>
${t('global_menu.open_sql_console_history')} ${t('global_menu.open_sql_console_history')}
</li> </li>
<li class="dropdown-item" data-trigger-command="showSearchHistory"> <div class="dropdown-divider"></div>
<span class="bx bx-search-alt"></span>
${t('global_menu.open_search_history')} <li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
</li> <span class="bx bx-bug-alt"></span>
${t('global_menu.open_dev_tools')}
<li class="dropdown-item" data-trigger-command="showBackendLog"> <kbd data-command="openDevTools"></kbd>
<span class="bx bx-detail"></span>
${t('global_menu.show_backend_log')}
<kbd data-command="showBackendLog"></kbd>
</li> </li>
<li class="dropdown-item" data-trigger-command="reloadFrontendApp" <li class="dropdown-item" data-trigger-command="reloadFrontendApp"
@ -217,13 +211,16 @@ const TPL = `
<kbd data-command="reloadFrontendApp"></kbd> <kbd data-command="reloadFrontendApp"></kbd>
</li> </li>
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
<span class="bx bx-hide"></span>
${t('global_menu.show_hidden_subtree')}
</li>
</ul> </ul>
</li> </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"> <li class="dropdown-item show-help-button" data-trigger-command="showHelp">
<span class="bx bx-help-circle"></span> <span class="bx bx-help-circle"></span>
${t('global_menu.show_help')} ${t('global_menu.show_help')}
@ -241,6 +238,8 @@ const TPL = `
<span class="version-text"></span> <span class="version-text"></span>
</li> </li>
<div class="dropdown-divider logout-button-separator"></div>
<li class="dropdown-item logout-button" data-trigger-command="logout"> <li class="dropdown-item logout-button" data-trigger-command="logout">
<span class="bx bx-log-out"></span> <span class="bx bx-log-out"></span>
${t('global_menu.logout')} ${t('global_menu.logout')}
@ -250,24 +249,54 @@ const TPL = `
`; `;
export default class GlobalMenuWidget extends BasicWidget { export default class GlobalMenuWidget extends BasicWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.updateAvailableWidget = new UpdateAvailableWidget(); this.updateAvailableWidget = new UpdateAvailableWidget();
this.isHorizontalLayout = isHorizontalLayout;
} }
doRender() { doRender() {
this.$widget = $(TPL); 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")); this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog"));
const isElectron = utils.isElectron(); const isElectron = utils.isElectron();
this.$widget.find(".logout-button").toggle(!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(".open-dev-tools-button").toggle(isElectron);
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop()); this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop());
this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile()); this.$widget.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')) { if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass('dropdown-toggle')) {
e.stopPropagation(); e.stopPropagation();
} }
}) })
this.$widget.find(".global-menu-button-update-available").append( this.$widget.find(".global-menu-button-update-available").append(
this.updateAvailableWidget.render() this.updateAvailableWidget.render()
@ -293,15 +322,20 @@ export default class GlobalMenuWidget extends BasicWidget {
if (!utils.isElectron()) { if (!utils.isElectron()) {
this.$widget.find(".zoom-container").hide(); this.$widget.find(".zoom-container").hide();
this.$widget.find(".zoom-container-separator").hide();
} }
this.$zoomState = this.$widget.find(".zoom-state"); this.$zoomState = this.$widget.find(".zoom-state");
this.$widget.on('show.bs.dropdown', () => { this.$widget.on('show.bs.dropdown', () => {
this.updateZoomState(); this.updateZoomState();
this.tooltip.hide(); if (this.tooltip) {
this.tooltip.disable(); 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", this.$widget.find(".zoom-buttons").on("click",
// delay to wait for the actual zoom change // 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"; import { t } from "../../services/i18n.js";
export default class LeftPaneToggleWidget extends CommandButtonWidget { export default class LeftPaneToggleWidget extends CommandButtonWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.class("launcher-button"); this.class("launcher-button");
@ -20,6 +20,10 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
this.settings.command = () => options.is('leftPaneVisible') this.settings.command = () => options.is('leftPaneVisible')
? "hideLeftPane" ? "hideLeftPane"
: "showLeftPane"; : "showLeftPane";
if (isHorizontalLayout) {
this.settings.titlePlacement = "bottom";
}
} }
refreshIcon() { refreshIcon() {

View File

@ -11,42 +11,91 @@ import { t } from "../../services/i18n.js";
const TPL = ` const TPL = `
<div class="dropdown note-actions"> <div class="dropdown note-actions">
<style> <style>
.note-actions { .note-actions {
width: 35px; width: 35px;
height: 35px; height: 35px;
} }
.note-actions .dropdown-menu { .note-actions .dropdown-menu {
min-width: 15em; min-width: 15em;
} }
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover { .note-actions .dropdown-item .bx {
color: var(--muted-text-color) !important; position: relative;
background-color: transparent !important; top: 3px;
pointer-events: none; /* makes it unclickable */ 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> </style>
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button> class="icon-action bx bx-dots-vertical-rounded"></button>
<div class="dropdown-menu dropdown-menu-right"> <div class="dropdown-menu dropdown-menu-right">
<a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">${t('note_actions.convert_into_attachment')}</a> <li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> ${t('note_actions.re_render_note')}</a> <span class="bx bx-paperclip"></span> ${t('note_actions.convert_into_attachment')}
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">${t('note_actions.search_in_note')} <kbd data-command="findInText"></kbd></a> </li>
<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> <li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" <span class="bx bx-extension"></span> ${t('note_actions.re_render_note')}<kbd data-command="renderActiveNote"></kbd>
title="${t('note_actions.open_note_externally_title')}"> </li>
<kbd data-command="openNoteExternally"></kbd>
${t('note_actions.open_note_externally')} <li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
</a> <span class='bx bx-search'></span> ${t('note_actions.search_in_note')}<kbd data-command="findInText"></kbd>
<a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> ${t('note_actions.open_note_custom')}</a> </li>
<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> <li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
<a class="dropdown-item delete-note-button">${t('note_actions.delete_note')}</a> <span class="bx bx-printer"></span> ${t('note_actions.print_note')}<kbd data-command="printActiveNote"></kbd></li>
<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>
<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>
</div>`; </div>`;

View File

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

View File

@ -10,12 +10,14 @@ import CommandButtonWidget from "../buttons/command_button.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import TodayLauncher from "../buttons/launcher/today_launcher.js"; import TodayLauncher from "../buttons/launcher/today_launcher.js";
import HistoryNavigationButton from "../buttons/history_navigation.js"; import HistoryNavigationButton from "../buttons/history_navigation.js";
import QuickSearchLauncherWidget from "../quick_search_launcher.js";
export default class LauncherWidget extends BasicWidget { export default class LauncherWidget extends BasicWidget {
constructor() { constructor(isHorizontalLayout) {
super(); super();
this.innerWidget = null; this.innerWidget = null;
this.isHorizontalLayout = isHorizontalLayout;
} }
isEnabled() { isEnabled() {
@ -63,6 +65,9 @@ export default class LauncherWidget extends BasicWidget {
} }
this.child(this.innerWidget); this.child(this.innerWidget);
if (this.isHorizontalLayout && this.innerWidget.settings) {
this.innerWidget.settings.titlePlacement = "bottom";
}
return true; return true;
} }
@ -86,29 +91,31 @@ export default class LauncherWidget extends BasicWidget {
initBuiltinWidget(note) { initBuiltinWidget(note) {
const builtinWidget = note.getLabelValue("builtinWidget"); const builtinWidget = note.getLabelValue("builtinWidget");
switch (builtinWidget) {
if (builtinWidget === 'calendar') { case "calendar":
return new CalendarWidget(note.title, note.getIcon()); return new CalendarWidget(note.title, note.getIcon());
} else if (builtinWidget === 'spacer') { case "spacer":
// || has to be inside since 0 is a valid value // || has to be inside since 0 is a valid value
const baseSize = parseInt(note.getLabelValue("baseSize") || "40"); const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100"); const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
return new SpacerWidget(baseSize, growthFactor); return new SpacerWidget(baseSize, growthFactor);
} else if (builtinWidget === 'bookmarks') { case "bookmarks":
return new BookmarkButtons(); return new BookmarkButtons(this.isHorizontalLayout);
} else if (builtinWidget === 'protectedSession') { case "protectedSession":
return new ProtectedSessionStatusWidget(); return new ProtectedSessionStatusWidget();
} else if (builtinWidget === 'syncStatus') { case "syncStatus":
return new SyncStatusWidget(); return new SyncStatusWidget();
} else if (builtinWidget === 'backInHistoryButton') { case "backInHistoryButton":
return new HistoryNavigationButton(note, "backInNoteHistory"); return new HistoryNavigationButton(note, "backInNoteHistory");
} else if (builtinWidget === 'forwardInHistoryButton') { case "forwardInHistoryButton":
return new HistoryNavigationButton(note, "forwardInNoteHistory"); return new HistoryNavigationButton(note, "forwardInNoteHistory");
} else if (builtinWidget === 'todayInJournal') { case "todayInJournal":
return new TodayLauncher(note); return new TodayLauncher(note);
} else { case "quickSearch":
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`); 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"; import LauncherWidget from "./launcher.js";
export default class LauncherContainer extends FlexContainer { export default class LauncherContainer extends FlexContainer {
constructor() { constructor(isHorizontalLayout) {
super('column'); super(isHorizontalLayout ? "row" : "column");
this.id('launcher-container'); this.id('launcher-container');
this.css('height', '100%'); this.css(isHorizontalLayout ? "width" : 'height', '100%');
this.filling(); this.filling();
this.isHorizontalLayout = isHorizontalLayout;
this.load(); this.load();
} }
@ -29,7 +30,7 @@ export default class LauncherContainer extends FlexContainer {
for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) { for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
try { try {
const launcherWidget = new LauncherWidget(); const launcherWidget = new LauncherWidget(this.isHorizontalLayout);
const success = await launcherWidget.initLauncher(launcherNote); const success = await launcherWidget.initLauncher(launcherNote);
if (success) { 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. * 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"; import FlexContainer from "./flex_container.js";
export default class RootContainer extends FlexContainer { export default class RootContainer extends FlexContainer {
constructor() { constructor(isHorizontalLayout) {
super('row'); super(isHorizontalLayout ? "column" : "row");
this.id('root-widget'); this.id('root-widget');
this.css('height', '100%'); this.css('height', '100%');

View File

@ -58,6 +58,7 @@ export default class JumpToNoteDialog extends BasicWidget {
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, { noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
allowCreatingNotes: true, allowCreatingNotes: true,
hideGoToSelectedNoteButton: true, hideGoToSelectedNoteButton: true,
allowSearchNotes: true,
container: this.$results container: this.$results
}) })
// clear any event listener added in previous invocation of this function // 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"> data-bs-toggle="dropdown" data-bs-display="static">
</button> </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>
<div class="revision-content-wrapper"> <div class="revision-content-wrapper">

View File

@ -5,6 +5,7 @@
import { t } from "../services/i18n.js"; import { t } from "../services/i18n.js";
import NoteContextAwareWidget from "./note_context_aware_widget.js"; import NoteContextAwareWidget from "./note_context_aware_widget.js";
import attributeService from "../services/attributes.js";
import FindInText from "./find_in_text.js"; import FindInText from "./find_in_text.js";
import FindInCode from "./find_in_code.js"; import FindInCode from "./find_in_code.js";
import FindInHtml from "./find_in_html.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 // 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 // of undefined. It's -1 instead of > 0, so they don't tabstop
const TPL = ` const TPL = `
<div style="contain: none;"> <div class='find-replace-widget' style="contain: none; border-top: 1px solid var(--main-border-color);">
<style> <style>
.find-widget-box { .find-widget-box, .replace-widget-box {
padding: 10px; padding: 2px 10px 2px 10px;
border-top: 1px solid var(--main-border-color);
align-items: center; align-items: center;
} }
.find-widget-box > * { .find-widget-box > *, .replace-widget-box > *{
margin-right: 15px; margin-right: 15px;
} }
.find-widget-box { .find-widget-box, .replace-widget-box {
display: flex; display: flex;
} }
.find-widget-found-wrapper { .find-widget-found-wrapper {
font-weight: bold; font-weight: bold;
} }
.find-widget-search-term-input-group { .find-widget-search-term-input-group, .replace-widget-replacetext-input {
max-width: 300px; max-width: 300px;
} }
@ -47,19 +47,23 @@ const TPL = `
<div class="find-widget-box"> <div class="find-widget-box">
<div class="input-group find-widget-search-term-input-group"> <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-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> <button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button>
</div> </div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox"> <label tabIndex="-1" class="form-check-label">
<label tabIndex="-1" class="form-check-label">${t('find.case_sensitive')}</label> <input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">
${t('find.case_sensitive')}
</label>
</div> </div>
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input find-widget-match-words-checkbox"> <label tabIndex="-1" class="form-check-label">
<label tabIndex="-1" class="form-check-label">${t('find.match_words')}</label> <input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
${t('find.match_words')}
</label>
</div> </div>
<div class="find-widget-found-wrapper"> <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 class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div>
</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>`; </div>`;
export default class FindWidget extends NoteContextAwareWidget { export default class FindWidget extends NoteContextAwareWidget {
@ -93,8 +103,7 @@ export default class FindWidget extends NoteContextAwareWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$findBox = this.$widget.find('.find-widget-box'); this.$widget.hide();
this.$findBox.hide();
this.$input = this.$widget.find('.find-widget-search-term-input'); this.$input = this.$widget.find('.find-widget-search-term-input');
this.$currentFound = this.$widget.find('.find-widget-current-found'); this.$currentFound = this.$widget.find('.find-widget-current-found');
this.$totalFound = this.$widget.find('.find-widget-total-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 = this.$widget.find(".find-widget-close-button");
this.$closeButton.on("click", () => this.closeSearch()); 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 => { this.$input.keydown(async e => {
if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) { if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) {
// If ctrl+f is pressed when the findbox is shown, select the // 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') { if (e.key === 'Escape') {
await this.closeSearch(); await this.closeSearch();
} }
@ -142,13 +158,25 @@ export default class FindWidget extends NoteContextAwareWidget {
} }
this.handler = await this.getHandler(); this.handler = await this.getHandler();
const isReadOnly = await this.noteContext.isReadOnly();
const selectedText = window.getSelection().toString() || ""; let selectedText = '';
if (this.note.type === 'code' && !isReadOnly){
this.$findBox.show(); const codeEditor = await this.noteContext.getCodeEditor();
selectedText = codeEditor.getSelection();
}else{
selectedText = window.getSelection().toString() || "";
}
this.$widget.show();
this.$input.focus(); 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 (isAlreadyVisible) {
if (selectedText) { if (selectedText) {
@ -254,8 +282,8 @@ export default class FindWidget extends NoteContextAwareWidget {
} }
async closeSearch() { async closeSearch() {
if (this.$findBox.is(":visible")) { if (this.$widget.is(":visible")) {
this.$findBox.hide(); this.$widget.hide();
// Restore any state, if there's a current occurrence clear markers // Restore any state, if there's a current occurrence clear markers
// and scroll to and select the last occurrence // 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() { isEnabled() {
return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type); return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type);
} }
async entitiesReloadedEvent({loadResults}) { async entitiesReloadedEvent({ loadResults }) {
if (loadResults.isNoteContentReloaded(this.noteId)) { if (loadResults.isNoteContentReloaded(this.noteId)) {
this.$totalFound.text("?") 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(); 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'); const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing');
findAndReplaceEditing.state.clear(model); findAndReplaceEditing.state.clear(model);
findAndReplaceEditing.stop(); findAndReplaceEditing.stop();
this.editingState = findAndReplaceEditing.state;
if (searchTerm !== "") { if (searchTerm !== "") {
// Parameters are callback/text, options.matchCase=false, options.wholeWords=false // 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 // 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 re = new RegExp(searchTerm, 'gi');
// let m = text.match(re); // let m = text.match(re);
// totalFound = m ? m.length : 0; // totalFound = m ? m.length : 0;
const options = { "matchCase" : matchCase, "wholeWords" : wholeWord }; const options = { "matchCase": matchCase, "wholeWords": wholeWord };
findResult = textEditor.execute('find', searchTerm, options); findResult = textEditor.execute('find', searchTerm, options);
totalFound = findResult.results.length; totalFound = findResult.results.length;
// Find the result beyond the cursor // Find the result beyond the cursor
@ -102,4 +103,18 @@ export default class FindInText {
textEditor.focus(); 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" "type_of_icon": "REGULAR"
}, },
{ {
"name": '_share', "name": "share",
"slug": "share-regular", "slug": "share-regular",
"category_id": 101, "category_id": 101,
"type_of_icon": "REGULAR" "type_of_icon": "REGULAR"
@ -6826,7 +6826,7 @@ const icons = [
"type_of_icon": "SOLID" "type_of_icon": "SOLID"
}, },
{ {
"name": '_share', "name": "share",
"slug": "share-solid", "slug": "share-solid",
"category_id": 101, "category_id": 101,
"type_of_icon": "SOLID" "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 NoteContextAwareWidget from "./note_context_aware_widget.js";
import server from "../services/server.js"; import server from "../services/server.js";
import utils from "../services/utils.js"; import utils from "../services/utils.js";
import { loadElkIfNeeded } from "../services/mermaid.js";
const TPL = `<div class="mermaid-widget"> const TPL = `<div class="mermaid-widget">
<style> <style>
@ -57,10 +58,10 @@ export default class MermaidWidget extends NoteContextAwareWidget {
this.$errorContainer.hide(); this.$errorContainer.hide();
await libraryLoader.requireLibrary(libraryLoader.MERMAID); await libraryLoader.requireLibrary(libraryLoader.MERMAID);
const documentStyle = window.getComputedStyle(document.documentElement); const documentStyle = window.getComputedStyle(document.documentElement);
const mermaidTheme = documentStyle.getPropertyValue('--mermaid-theme'); const mermaidTheme = documentStyle.getPropertyValue('--mermaid-theme');
mermaid.mermaidAPI.initialize({ mermaid.mermaidAPI.initialize({
startOnLoad: false, startOnLoad: false,
theme: mermaidTheme.trim(), theme: mermaidTheme.trim(),
@ -111,6 +112,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
zoomOnClick: false zoomOnClick: false
}); });
} catch (e) { } catch (e) {
console.warn(e);
this.$errorMessage.text(e.message); this.$errorMessage.text(e.message);
this.$errorContainer.show(); this.$errorContainer.show();
} }
@ -122,6 +124,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
const blob = await this.note.getBlob(); const blob = await this.note.getBlob();
const content = blob.content || ""; const content = blob.content || "";
await loadElkIfNeeded(content);
const {svg} = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content); const {svg} = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
return svg; return svg;
} }

View File

@ -6,12 +6,20 @@ import branchService from "../../services/branches.js";
import treeService from "../../services/tree.js"; import treeService from "../../services/tree.js";
import { t } from "../../services/i18n.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 { class MobileDetailMenuWidget extends BasicWidget {
constructor(isHorizontalLayout) {
super();
this.isHorizontalLayout = isHorizontalLayout;
}
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$widget.addClass(this.isHorizontalLayout ? "bx-dots-vertical-rounded" : "bx-menu");
this.$widget.on("click", async e => { this.$widget.on("click", async e => {
const note = appContext.tabManager.getActiveContextNote(); 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(), show: await this.#shouldDisplay(),
activate: true, activate: true,
title: t("classic_editor_toolbar.title"), 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') { else if (definition.labelType === 'datetime') {
$input.prop('type', 'datetime-local') $input.prop('type', 'datetime-local')
} }
else if (definition.labelType === 'time') {
$input.prop('type', 'time')
}
else if (definition.labelType === 'url') { else if (definition.labelType === 'url') {
$input.prop("placeholder", t("promoted_attributes.url_placeholder")); $input.prop("placeholder", t("promoted_attributes.url_placeholder"));

View File

@ -55,6 +55,10 @@ const TAB_ROW_TPL = `
background: var(--main-background-color); background: var(--main-background-color);
overflow: hidden; overflow: hidden;
} }
.tab-row-widget.full-width {
background: var(--launcher-pane-background-color);
}
.tab-row-widget * { .tab-row-widget * {
box-sizing: inherit; 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_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_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: 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}) => { selectMenuItemHandler: ({command}) => {
this.triggerCommand(command, {ntxId}); 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 server from "../../../services/server.js";
import { t } from "../../../services/i18n.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;"> const TPL = `<div style="height: 100%; display: flex; flex-direction: column;">
<style> <style>
.backend-log-textarea { .backend-log-editor {
flex-grow: 1; flex-grow: 1;
width: 100%; width: 100%;
border: none; border: none;
resize: none;
} }
</style> </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;"> <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> <button class="refresh-backend-log-button btn btn-primary">${t("backend_log.refresh")}</button>
</div> </div>
</div>`; </div>`;
export default class BackendLogWidget extends NoteContextAwareWidget { export default class BackendLogWidget extends AbstractCodeTypeWidget {
doRender() { doRender() {
super.doRender();
this.$widget = $(TPL); 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 = this.$widget.find(".refresh-backend-log-button");
this.$refreshBackendLog.on('click', () => this.load()); this.$refreshBackendLog.on('click', () => this.load());
} }
scrollToBottom() {
this.$backendLogTextArea.scrollTop(this.$backendLogTextArea[0].scrollHeight);
}
async refresh() { async refresh() {
await this.load(); await this.load();
} }
getExtraOpts() {
return {
lineWrapping: false,
readOnly: true
};
}
async load() { async load() {
const backendLog = await server.get('backend-log'); const content = await server.get('backend-log');
await this.initialized;
this.$backendLogTextArea.text(backendLog); this._update({
mime: "text/plain"
this.scrollToBottom(); }, content);
this.show();
this.scrollToEnd();
} }
} }

View File

@ -31,7 +31,14 @@ export default class DocTypeWidget extends TypeWidget {
const docName = note.getLabelValue('docName'); const docName = note.getLabelValue('docName');
if (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 { } else {
this.$content.empty(); this.$content.empty();
} }

View File

@ -70,6 +70,7 @@ export default class EmptyTypeWidget extends TypeWidget {
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, { noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
hideGoToSelectedNoteButton: true, hideGoToSelectedNoteButton: true,
allowCreatingNotes: true, allowCreatingNotes: true,
allowSearchNotes: true,
container: this.$results container: this.$results
}) })
.on('autocomplete:noteselected', function(event, suggestion, dataset) { .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 utils from "../../../../services/utils.js";
import { t } from "../../../../services/i18n.js"; import { t } from "../../../../services/i18n.js";
const MIN_VALUE = 640;
const TPL = ` const TPL = `
<div class="options-section"> <div class="options-section">
<h4>${t("max_content_width.title")}</h4> <h4>${t("max_content_width.title")}</h4>
@ -11,7 +13,7 @@ const TPL = `
<div class="form-group row"> <div class="form-group row">
<div class="col-6"> <div class="col-6">
<label>${t("max_content_width.max_width_label")}</label> <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>
</div> </div>
@ -34,6 +36,6 @@ export default class MaxContentWidthOptions extends OptionsWidget {
} }
async optionsLoaded(options) { 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 = ` const TPL = `
<div class="options-section"> <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> <h4>${t("theme.title")}</h4>
<div class="form-group row"> <div class="form-group row">
@ -19,7 +39,7 @@ const TPL = `
${t("theme.override_theme_fonts_label")} ${t("theme.override_theme_fonts_label")}
</label> </label>
</div> </div>
</div> </div>
</div>`; </div>`;
export default class ThemeOptions extends OptionsWidget { export default class ThemeOptions extends OptionsWidget {
@ -27,6 +47,11 @@ export default class ThemeOptions extends OptionsWidget {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$themeSelect = this.$widget.find(".theme-select"); this.$themeSelect = this.$widget.find(".theme-select");
this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts"); 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 () => { this.$themeSelect.on('change', async () => {
const newTheme = this.$themeSelect.val(); const newTheme = this.$themeSelect.val();
@ -57,5 +82,8 @@ export default class ThemeOptions extends OptionsWidget {
this.$themeSelect.val(options.theme); this.$themeSelect.val(options.theme);
this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts); 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"> <div class="options-section">
<h4>${t('backup.existing_backups')}</h4> <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> </div>
`; `;
@ -73,7 +87,7 @@ export default class BackupOptions extends OptionsWidget {
this.$monthlyBackupEnabled.on('change', () => this.$monthlyBackupEnabled.on('change', () =>
this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled)); this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled));
this.$existingBackupList = this.$widget.find(".existing-backup-list"); this.$existingBackupList = this.$widget.find(".existing-backup-list-items");
} }
optionsLoaded(options) { optionsLoaded(options) {
@ -85,11 +99,34 @@ export default class BackupOptions extends OptionsWidget {
this.$existingBackupList.empty(); this.$existingBackupList.empty();
if (!backupFiles.length) { 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) { 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.name))
.append($("<td>").text(token.utcDateCreated)) .append($("<td>").text(token.utcDateCreated))
.append($("<td>").append( .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)), .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)) .on("click", () => this.deleteToken(token.etapiTokenId, token.name))
)) ))
); );

View File

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

View File

@ -19,6 +19,10 @@
--bs-table-bg: transparent !important; --bs-table-bg: transparent !important;
} }
:root {
--submenu-opening-delay: 300ms;
}
html { html {
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */ /* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
height: 100%; height: 100%;
@ -36,6 +40,10 @@ body {
font-size: var(--main-font-size); font-size: var(--main-font-size);
} }
body.mobile .desktop-only {
display: none !important;
}
a { a {
text-decoration: none; text-decoration: none;
} }
@ -229,7 +237,15 @@ div.ui-tooltip {
} }
.dropdown-divider { .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 { .dropdown-menu {
@ -237,6 +253,26 @@ div.ui-tooltip {
color: var(--menu-text-color) !important; color: var(--menu-text-color) !important;
background-color: var(--menu-background-color) !important; background-color: var(--menu-background-color) !important;
font-size: inherit; 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 { .dropdown-menu .disabled {
@ -246,17 +282,29 @@ div.ui-tooltip {
.dropdown-menu .disabled .disabled-tooltip { .dropdown-menu .disabled .disabled-tooltip {
pointer-events: all; pointer-events: all;
color: var(--menu-text-color); margin-left: 8px;
font-size: .5em;
color: var(--disabled-tooltip-icon-color);
cursor: help; 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; color: var(--hover-item-text-color) !important;
background-color: var(--hover-item-background-color) !important; background-color: var(--hover-item-background-color) !important;
border-color: var(--hover-item-border-color) !important; border-color: var(--hover-item-border-color) !important;
cursor: pointer; cursor: pointer;
} }
.dropdown-item-container, .dropdown-item-container:hover, .dropdown-item-container:active {
background: transparent;
cursor: default;
}
.dropdown-menu a:not(.selected) .check { .dropdown-menu a:not(.selected) .check {
visibility: hidden; visibility: hidden;
} }
@ -288,6 +336,10 @@ div.ui-tooltip {
outline: none; outline: none;
} }
.dropdown-item .destructive-action-icon {
color: var(--dropdown-item-icon-destructive-color);
}
.CodeMirror { .CodeMirror {
height: 100%; height: 100%;
background: inherit; background: inherit;
@ -974,6 +1026,18 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
overflow: auto; 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 */ /* rotate caret on hover */
.dropdown-menu > li > a:hover:after { .dropdown-menu > li > a:hover:after {
text-decoration: underline; text-decoration: underline;
@ -1075,9 +1139,21 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
cursor: pointer; cursor: pointer;
border: none; border: none;
color: var(--launcher-pane-text-color); color: var(--launcher-pane-text-color);
background-color: var(--launcher-pane-background-color); background-color: var(--launcher-pane-background-color);
height: 53px; }
#launcher-pane.vertical .launcher-button {
width: 100%; 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 { #launcher-pane .icon-action:hover {
@ -1238,3 +1314,8 @@ textarea {
padding: 1rem; padding: 1rem;
} }
.empty-table-placeholder {
text-align: center;
color: var(--muted-text-color);
}

View File

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

View File

@ -21,6 +21,9 @@ html {
--main-text-color: black; --main-text-color: black;
--main-border-color: #ccc; --main-border-color: #ccc;
--dropdown-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; --accented-background-color: #f5f5f5;
--more-accented-background-color: #ddd; --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 { span.fancytree-node.multiple-parents.shared .fancytree-title::after {
font-family: 'boxicons' !important; font-family: 'boxicons' !important;
font-size: smaller; font-size: smaller;
content: " \ec27 \ec03"; content: " \eb3d \ec03";
} }
span.fancytree-node.multiple-parents .fancytree-title::after { span.fancytree-node.multiple-parents .fancytree-title::after {
font-family: 'boxicons' !important; font-family: 'boxicons' !important;
font-size: smaller; 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 { span.fancytree-node.shared .fancytree-title::after {

View File

@ -15,7 +15,13 @@
"message": "发生了严重错误,导致客户端应用程序无法启动:\n\n{{message}}\n\n这很可能是由于脚本以意外的方式失败引起的。请尝试以安全模式启动应用程序并解决问题。" "message": "发生了严重错误,导致客户端应用程序无法启动:\n\n{{message}}\n\n这很可能是由于脚本以意外的方式失败引起的。请尝试以安全模式启动应用程序并解决问题。"
}, },
"widget-error": { "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": { "add_link": {
@ -45,7 +51,11 @@
"chosen_actions": "选择的操作", "chosen_actions": "选择的操作",
"execute_bulk_actions": "执行批量操作", "execute_bulk_actions": "执行批量操作",
"bulk_actions_executed": "批量操作已成功执行。", "bulk_actions_executed": "批量操作已成功执行。",
"none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。" "none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
"labels": "标签",
"relations": "关联关系",
"notes": "笔记",
"other": "其它"
}, },
"clone_to": { "clone_to": {
"clone_notes_to": "克隆笔记到...", "clone_notes_to": "克隆笔记到...",
@ -164,7 +174,8 @@
"textImportedAsText": "如果元数据不明确将HTML、Markdown和TXT导入为文本笔记", "textImportedAsText": "如果元数据不明确将HTML、Markdown和TXT导入为文本笔记",
"codeImportedAsCode": "如果元数据不明确,将识别的代码文件(例如<code>.json</code>)导入为代码笔记", "codeImportedAsCode": "如果元数据不明确,将识别的代码文件(例如<code>.json</code>)导入为代码笔记",
"replaceUnderscoresWithSpaces": "在导入的笔记名称中将下划线替换为空格", "replaceUnderscoresWithSpaces": "在导入的笔记名称中将下划线替换为空格",
"import": "导入" "import": "导入",
"failed": "导入失败: {{message}}."
}, },
"include_note": { "include_note": {
"dialog_title": "包含笔记", "dialog_title": "包含笔记",
@ -304,6 +315,7 @@
"boolean": "布尔值", "boolean": "布尔值",
"date": "日期", "date": "日期",
"date_time": "日期和时间", "date_time": "日期和时间",
"time": "时间",
"url": "网址", "url": "网址",
"precision_title": "值设置界面中浮点数后的位数。", "precision_title": "值设置界面中浮点数后的位数。",
"precision": "精度", "precision": "精度",
@ -630,7 +642,10 @@
"export_note": "导出笔记", "export_note": "导出笔记",
"delete_note": "删除笔记", "delete_note": "删除笔记",
"print_note": "打印笔记", "print_note": "打印笔记",
"save_revision": "保存笔记历史" "save_revision": "保存笔记历史",
"convert_into_attachment_failed": "笔记 '{{title}}' 转换失败。",
"convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
"convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?"
}, },
"onclick_button": { "onclick_button": {
"no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序" "no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
@ -885,7 +900,8 @@
"label_rock_or_pop": "只需一个标签存在即可", "label_rock_or_pop": "只需一个标签存在即可",
"label_year_comparison": "数字比较(也包括>>=<)。", "label_year_comparison": "数字比较(也包括>>=<)。",
"label_date_created": "上个月创建的笔记", "label_date_created": "上个月创建的笔记",
"error": "搜索错误:{{error}}" "error": "搜索错误:{{error}}",
"search_prefix": "搜索:"
}, },
"attachment_detail": { "attachment_detail": {
"open_help_page": "打开附件帮助页面", "open_help_page": "打开附件帮助页面",
@ -919,7 +935,15 @@
}, },
"protected_session": { "protected_session": {
"enter_password_instruction": "显示受保护的笔记需要输入您的密码:", "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": { "relation_map": {
"open_in_new_tab": "在新标签页中打开", "open_in_new_tab": "在新标签页中打开",
@ -990,7 +1014,9 @@
"fill_entity_changes_button": "填充实体变更记录", "fill_entity_changes_button": "填充实体变更记录",
"full_sync_triggered": "全量同步已触发", "full_sync_triggered": "全量同步已触发",
"filling_entity_changes": "正在填充实体变更行...", "filling_entity_changes": "正在填充实体变更行...",
"sync_rows_filled_successfully": "同步行填充成功" "sync_rows_filled_successfully": "同步行填充成功",
"finished-successfully": "同步已完成。",
"failed": "同步失败:{{message}}"
}, },
"vacuum_database": { "vacuum_database": {
"title": "数据库清理", "title": "数据库清理",
@ -1036,8 +1062,13 @@
"theme_label": "主题", "theme_label": "主题",
"override_theme_fonts_label": "覆盖主题字体", "override_theme_fonts_label": "覆盖主题字体",
"light_theme": "浅色", "light_theme": "浅色",
"dark_theme": "深色" "dark_theme": "深色",
}, "layout": "布局",
"layout-vertical-title": "垂直",
"layout-horizontal-title": "水平",
"layout-vertical-description": "启动栏位于左侧(默认)",
"layout-horizontal-description": "启动栏位于标签栏下方,标签栏现在是全宽的。"
},
"zoom_factor": { "zoom_factor": {
"title": "缩放系数(仅桌面客户端有效)", "title": "缩放系数(仅桌面客户端有效)",
"description": "缩放也可以通过 CTRL+- 和 CTRL+= 快捷键进行控制。" "description": "缩放也可以通过 CTRL+- 和 CTRL+= 快捷键进行控制。"
@ -1162,6 +1193,8 @@
"backup_now": "立即备份", "backup_now": "立即备份",
"backup_database_now": "立即备份数据库", "backup_database_now": "立即备份数据库",
"existing_backups": "现有备份", "existing_backups": "现有备份",
"date-and-time": "日期和时间",
"path": "路径",
"database_backed_up_to": "数据库已备份到", "database_backed_up_to": "数据库已备份到",
"no_backup_yet": "尚无备份" "no_backup_yet": "尚无备份"
}, },
@ -1309,7 +1342,9 @@
"duplicate-subtree": "复制子树", "duplicate-subtree": "复制子树",
"export": "导出", "export": "导出",
"import-into-note": "导入到笔记", "import-into-note": "导入到笔记",
"apply-bulk-actions": "应用批量操作" "apply-bulk-actions": "应用批量操作",
"converted-to-attachments": "{{count}} 个笔记已被转换为附件。",
"convert-to-attachment-confirm": "确定要将选中的笔记转换为其父笔记的附件吗?"
}, },
"shared_info": { "shared_info": {
"shared_publicly": "此笔记已公开分享在", "shared_publicly": "此笔记已公开分享在",
@ -1332,7 +1367,8 @@
"image": "图片", "image": "图片",
"launcher": "启动器", "launcher": "启动器",
"doc": "文档", "doc": "文档",
"widget": "小部件" "widget": "小部件",
"confirm-change": "当笔记内容不为空时,不建议更改笔记类型。您仍然要继续吗?"
}, },
"protect_note": { "protect_note": {
"toggle-on": "保护笔记", "toggle-on": "保护笔记",
@ -1355,7 +1391,11 @@
"open-help-page": "打开帮助页面", "open-help-page": "打开帮助页面",
"find": { "find": {
"case_sensitive": "区分大小写", "case_sensitive": "区分大小写",
"match_words": "匹配单词" "match_words": "匹配单词",
"find_placeholder": "在文本中查找...",
"replace_placeholder": "替换为...",
"replace": "替换",
"replace_all": "全部替换"
}, },
"highlights_list_2": { "highlights_list_2": {
"title": "高亮列表", "title": "高亮列表",
@ -1378,7 +1418,9 @@
"hide-archived-notes": "隐藏已归档笔记", "hide-archived-notes": "隐藏已归档笔记",
"automatically-collapse-notes": "自动折叠笔记", "automatically-collapse-notes": "自动折叠笔记",
"automatically-collapse-notes-title": "笔记在一段时间内未使用将被折叠,以减少树形结构的杂乱。", "automatically-collapse-notes-title": "笔记在一段时间内未使用将被折叠,以减少树形结构的杂乱。",
"save-changes": "保存并应用更改" "save-changes": "保存并应用更改",
"auto-collapsing-notes-after-inactivity": "在不活动后自动折叠笔记...",
"saved-search-note-refreshed": "已保存的搜索笔记已刷新。"
}, },
"title_bar_buttons": { "title_bar_buttons": {
"window-on-top": "保持此窗口置顶" "window-on-top": "保持此窗口置顶"
@ -1407,8 +1449,11 @@
"add_new_tab": "添加新标签页", "add_new_tab": "添加新标签页",
"close": "关闭", "close": "关闭",
"close_other_tabs": "关闭其他标签页", "close_other_tabs": "关闭其他标签页",
"close_right_tabs": "关闭右侧标签页",
"close_all_tabs": "关闭所有标签页", "close_all_tabs": "关闭所有标签页",
"reopen_last_tab": "重新打开最后一个关闭的标签页",
"move_tab_to_new_window": "将此标签页移动到新窗口", "move_tab_to_new_window": "将此标签页移动到新窗口",
"copy_tab_to_new_window": "将此标签页复制到新窗口",
"new_tab": "新标签页" "new_tab": "新标签页"
}, },
"toc": { "toc": {
@ -1422,5 +1467,101 @@
}, },
"app_context": { "app_context": {
"please_wait_for_save": "请等待几秒钟以完成保存,然后您可以尝试再操作一次。" "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", "chosen_actions": "Chosen actions",
"execute_bulk_actions": "Execute bulk actions", "execute_bulk_actions": "Execute bulk actions",
"bulk_actions_executed": "Bulk actions have been executed successfully.", "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_to": {
"clone_notes_to": "Clone notes to...", "clone_notes_to": "Clone notes to...",
@ -238,23 +242,23 @@
"confirm_undelete": "Do you want to undelete this note and its sub-notes?" "confirm_undelete": "Do you want to undelete this note and its sub-notes?"
}, },
"revisions": { "revisions": {
"note_revisions": "Note revisions", "note_revisions": "Note Revisions",
"delete_all_revisions": "Delete all revisions of this note", "delete_all_revisions": "Delete all revisions of this note",
"delete_all_button": "Delete all revisions", "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}}", "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...", "no_revisions": "No revisions for this note yet...",
"restore_button": "Restore this revision", "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", "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.", "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 has been deleted.", "revisions_deleted": "Note revisions have been deleted.",
"revision_restored": "Note revision has been restored.", "revision_restored": "Note revision has been restored.",
"revision_deleted": "Note revision has been deleted.", "revision_deleted": "Note revision has been deleted.",
"snapshot_interval": "Note Revisions Snapshot Interval: {{seconds}}s.", "snapshot_interval": "Note Revision Snapshot Interval: {{seconds}}s.",
"maximum_revisions": "Maximum revisions for current note: {{number}}.", "maximum_revisions": "Note Revision Snapshot Limit: {{number}}.",
"settings": "Settings for Note revisions", "settings": "Note Revision Settings",
"download_button": "Download", "download_button": "Download",
"mime": "MIME: ", "mime": "MIME: ",
"file_size": "File size:", "file_size": "File size:",
@ -311,6 +315,7 @@
"boolean": "Boolean", "boolean": "Boolean",
"date": "Date", "date": "Date",
"date_time": "Date & Time", "date_time": "Date & Time",
"time": "Time",
"url": "URL", "url": "URL",
"precision_title": "What number of digits after floating point should be available in the value setting interface.", "precision_title": "What number of digits after floating point should be available in the value setting interface.",
"precision": "Precision", "precision": "Precision",
@ -1057,7 +1062,12 @@
"theme_label": "Theme", "theme_label": "Theme",
"override_theme_fonts_label": "Override theme fonts", "override_theme_fonts_label": "Override theme fonts",
"light_theme": "Light", "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": { "zoom_factor": {
"title": "Zoom Factor (desktop build only)", "title": "Zoom Factor (desktop build only)",
@ -1108,12 +1118,12 @@
"deleted_notes_erased": "Deleted notes have been erased." "deleted_notes_erased": "Deleted notes have been erased."
}, },
"revisions_snapshot_interval": { "revisions_snapshot_interval": {
"note_revisions_snapshot_interval_title": "Note Revisions Snapshot Interval", "note_revisions_snapshot_interval_title": "Note Revision 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_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):" "snapshot_time_interval_label": "Note revision snapshot time interval (in seconds):"
}, },
"revisions_snapshot_limit": { "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.", "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:", "snapshot_number_limit_label": "Note revision snapshot number limit:",
"erase_excess_revision_snapshots": "Erase excess revision snapshots now", "erase_excess_revision_snapshots": "Erase excess revision snapshots now",
@ -1183,6 +1193,8 @@
"backup_now": "Backup now", "backup_now": "Backup now",
"backup_database_now": "Backup database now", "backup_database_now": "Backup database now",
"existing_backups": "Existing backups", "existing_backups": "Existing backups",
"date-and-time": "Date & time",
"path": "Path",
"database_backed_up_to": "Database has been backed up to", "database_backed_up_to": "Database has been backed up to",
"no_backup_yet": "no backup yet" "no_backup_yet": "no backup yet"
}, },
@ -1378,8 +1390,12 @@
}, },
"open-help-page": "Open help page", "open-help-page": "Open help page",
"find": { "find": {
"case_sensitive": "case sensitive", "case_sensitive": "Case sensitive",
"match_words": "match words" "match_words": "Match words",
"find_placeholder": "Find in text...",
"replace_placeholder": "Replace with...",
"replace": "Replace",
"replace_all": "Replace all"
}, },
"highlights_list_2": { "highlights_list_2": {
"title": "Highlights List", "title": "Highlights List",
@ -1435,7 +1451,9 @@
"close_other_tabs": "Close other tabs", "close_other_tabs": "Close other tabs",
"close_right_tabs": "Close tabs to the right", "close_right_tabs": "Close tabs to the right",
"close_all_tabs": "Close all tabs", "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", "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" "new_tab": "New tab"
}, },
"toc": { "toc": {
@ -1518,8 +1536,32 @@
"editing": { "editing": {
"editor_type": { "editor_type": {
"label": "Formatting toolbar", "label": "Formatting toolbar",
"floating": "Floating (editing tools appear near the cursor)", "floating": {
"fixed": "Fixed (editing tools appear in the \"Formatting\" ribbon tab)" "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", "chosen_actions": "Acciones elegidas",
"execute_bulk_actions": "Ejecutar acciones en bloque", "execute_bulk_actions": "Ejecutar acciones en bloque",
"bulk_actions_executed": "Las acciones en bloque se han ejecutado con éxito.", "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_to": {
"clone_notes_to": "Clonar notas a...", "clone_notes_to": "Clonar notas a...",
@ -311,6 +315,7 @@
"boolean": "Booleano", "boolean": "Booleano",
"date": "Fecha", "date": "Fecha",
"date_time": "Fecha y hora", "date_time": "Fecha y hora",
"time": "Hora",
"url": "URL", "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_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", "precision": "Precisión",
@ -1057,7 +1062,12 @@
"theme_label": "Tema", "theme_label": "Tema",
"override_theme_fonts_label": "Sobreescribir fuentes de tema", "override_theme_fonts_label": "Sobreescribir fuentes de tema",
"light_theme": "Claro", "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": { "zoom_factor": {
"title": "Factor de zoom (solo versión de escritorio)", "title": "Factor de zoom (solo versión de escritorio)",
@ -1183,6 +1193,8 @@
"backup_now": "Realizar copia de seguridad ahora", "backup_now": "Realizar copia de seguridad ahora",
"backup_database_now": "Realizar copia de seguridad de la base de datos ahora", "backup_database_now": "Realizar copia de seguridad de la base de datos ahora",
"existing_backups": "Copias de seguridad existentes", "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", "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" "no_backup_yet": "no hay copia de seguridad todavía"
}, },
@ -1378,8 +1390,12 @@
}, },
"open-help-page": "Abrir página de ayuda", "open-help-page": "Abrir página de ayuda",
"find": { "find": {
"case_sensitive": "distingue entre mayúsculas y minúsculas", "case_sensitive": "Distingue entre mayúsculas y minúsculas",
"match_words": "coincidir palabras" "match_words": "Coincidir palabras",
"find_placeholder": "Encontrar en texto...",
"replace_placeholder": "Reemplazar con...",
"replace": "Reemplazar",
"replace_all": "Reemplazar todo"
}, },
"highlights_list_2": { "highlights_list_2": {
"title": "Lista de destacados", "title": "Lista de destacados",
@ -1435,7 +1451,9 @@
"close_other_tabs": "Cerrar otras pestañas", "close_other_tabs": "Cerrar otras pestañas",
"close_right_tabs": "Cerrar pestañas a la derecha", "close_right_tabs": "Cerrar pestañas a la derecha",
"close_all_tabs": "Cerras todas las pestañas", "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", "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" "new_tab": "Nueva pestaña"
}, },
"toc": { "toc": {
@ -1518,8 +1536,23 @@
"editing": { "editing": {
"editor_type": { "editor_type": {
"label": "Barra de herramientas de formato", "label": "Barra de herramientas de formato",
"floating": "Flotante (las herramientas de edición aparecen cerca del cursor)", "floating": {
"fixed": "Fijo (las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")" "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", "boolean": "Booléen",
"date": "Date", "date": "Date",
"date_time": "Date et heure", "date_time": "Date et heure",
"time": "Heure",
"url": "URL", "url": "URL",
"precision_title": "Nombre de chiffres après la virgule devant être disponible dans l'interface définissant la valeur.", "precision_title": "Nombre de chiffres après la virgule devant être disponible dans l'interface définissant la valeur.",
"precision": "Précision", "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>", "custom_resource_provider": "a se vedea <a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">Custom request handler</a>",
"date": "Dată", "date": "Dată",
"date_time": "Dată și timp", "date_time": "Dată și timp",
"time": "Timp",
"delete": "Șterge", "delete": "Șterge",
"digits": "număr de zecimale", "digits": "număr de zecimale",
"disable_inclusion": "script-urile cu această etichetă nu vor fi incluse în execuția scriptului părinte.", "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_monthly_backup": "Activează copia de siguranță lunară",
"enable_weekly_backup": "Activează copia de siguranță săptămânală", "enable_weekly_backup": "Activează copia de siguranță săptămânală",
"existing_backups": "Copii de siguranță existente", "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ță" "no_backup_yet": "nu există încă nicio copie de siguranță"
}, },
"basic_properties": { "basic_properties": {
@ -297,7 +300,11 @@
"close": "Închide", "close": "Închide",
"execute_bulk_actions": "Execută acțiunile în masă", "execute_bulk_actions": "Execută acțiunile în masă",
"include_descendants": "Include descendenții notiței selectate", "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": { "calendar": {
"april": "Aprilie", "april": "Aprilie",
@ -1349,7 +1356,11 @@
"open-help-page": "Deschide pagina de informații", "open-help-page": "Deschide pagina de informații",
"find": { "find": {
"match_words": "doar cuvinte întregi", "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": { "highlights_list_2": {
"options": "Setări", "options": "Setări",
@ -1439,7 +1450,9 @@
"close_tab": "Închide tab", "close_tab": "Închide tab",
"move_tab_to_new_window": "Mută acest tab în altă fereastră", "move_tab_to_new_window": "Mută acest tab în altă fereastră",
"new_tab": "Tab nou", "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": { "toc": {
"options": "Setări", "options": "Setări",
@ -1514,12 +1527,27 @@
}, },
"editing": { "editing": {
"editor_type": { "editor_type": {
"fixed": "Editor cu bară fixă (uneltele de editare vor apărea în tab-ul „Formatare” din panglică)", "label": "Bară de formatare",
"floating": "Editor cu bară flotantă (uneltele de editare vor apărea lângă cursor)", "floating": {
"label": "Bară de formatare" "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": { "editor": {
"title": "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', 'editedNotesOpenInRibbon',
'locale', 'locale',
'firstDayOfWeek', 'firstDayOfWeek',
'textNoteEditorType' 'textNoteEditorType',
'layoutOrientation'
]); ]);
function getOptions() { function getOptions() {

View File

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

View File

@ -34,8 +34,17 @@ interface Item {
baseSize?: string; baseSize?: string;
growthFactor?: string; growthFactor?: string;
targetNoteId?: "_backendLog" | "_globalNoteMap"; targetNoteId?: "_backendLog" | "_globalNoteMap";
builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar"; builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar" | "quickSearch";
command?: "jumpToNote" | "searchNotes" | "createNoteIntoInbox" | "showRecentChanges"; 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', type: 'contentWidget',
icon: 'bx-terminal', icon: 'bx-terminal',
attributes: [ 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: '_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: '_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: '_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: '_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 }, { name: "codeBlockWordWrap", value: "false", isSynced: true },
// Text note configuration // 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') { if (token === 'promoted') {
defObj.isPromoted = true; 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; defObj.labelType = token;
} }
else if (['single', 'multi'].includes(token)) { else if (['single', 'multi'].includes(token)) {

View File

@ -27,43 +27,52 @@ class SearchResult {
this.score = 0; this.score = 0;
const note = becca.notes[this.noteId]; 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) { if (note.noteId.toLowerCase() === fulltextQuery) {
this.score += 100; this.score += 1000;
} }
if (note.title.toLowerCase() === fulltextQuery) { // Title matching scores, make sure to always win
this.score += 100; // high reward for exact match #3470 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 // Add scores for partial matches with adjusted weights
this.addScoreForStrings(tokens, note.title, 1.5); 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
// matches in attributes don't get extra points and thus are implicitly valued less than note path matches
this.addScoreForStrings(tokens, this.notePathTitle, 1);
if (note.isInHiddenSubtree()) { 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) { addScoreForStrings(tokens: string[], str: string, factor: number) {
const chunks = str.toLowerCase().split(" "); const chunks = str.toLowerCase().split(" ");
this.score = 0; let tokenScore = 0;
for (const chunk of chunks) { for (const chunk of chunks) {
for (const token of tokens) { for (const token of tokens) {
if (chunk === token) { if (chunk === token) {
this.score += 4 * token.length * factor; tokenScore += 4 * token.length * factor;
} else if (chunk.startsWith(token)) { } else if (chunk.startsWith(token)) {
this.score += 2 * token.length * factor; tokenScore += 2 * token.length * factor;
} else if (chunk.includes(token)) { } 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