mirror of
				https://github.com/zadam/trilium.git
				synced 2025-11-04 04:16:17 +01:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into feature/improved_promoted_attributes
; Conflicts: ; src/public/app/layouts/desktop_layout.js
This commit is contained in:
		
							
								
								
									
										12
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							@@ -3,6 +3,12 @@ description: Report a bug
 | 
			
		||||
title: "(Bug report) "
 | 
			
		||||
labels: "Type: Bug"
 | 
			
		||||
body:
 | 
			
		||||
- type: textarea
 | 
			
		||||
  attributes:
 | 
			
		||||
    label: Description
 | 
			
		||||
    description: A clear and concise description of the bug and any additional information.
 | 
			
		||||
  validations:
 | 
			
		||||
    required: true
 | 
			
		||||
- type: input
 | 
			
		||||
  attributes:
 | 
			
		||||
    label: TriliumNext Version
 | 
			
		||||
@@ -38,12 +44,6 @@ body:
 | 
			
		||||
    placeholder: "e.g. Windows 10 version 1909, macOS Catalina 10.15.7, or Ubuntu 20.04"
 | 
			
		||||
  validations:
 | 
			
		||||
    required: true
 | 
			
		||||
- type: textarea
 | 
			
		||||
  attributes:
 | 
			
		||||
    label: Description
 | 
			
		||||
    description: A clear and concise description of the bug and any additional information.
 | 
			
		||||
  validations:
 | 
			
		||||
    required: true
 | 
			
		||||
- type: textarea
 | 
			
		||||
  attributes:
 | 
			
		||||
    label: Error logs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										68
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										68
									
								
								.github/workflows/dev.yml
									
									
									
									
										vendored
									
									
								
							@@ -9,6 +9,12 @@ concurrency:
 | 
			
		||||
  group: ${{ github.workflow }}-${{ github.ref }}
 | 
			
		||||
  cancel-in-progress: true
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  GHCR_REGISTRY: ghcr.io
 | 
			
		||||
  DOCKERHUB_REGISTRY: docker.io
 | 
			
		||||
  IMAGE_NAME: ${{ github.repository_owner }}/notes
 | 
			
		||||
  TEST_TAG: ${{ github.repository_owner }}/notes:test
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build_docker:
 | 
			
		||||
    name: Build Docker image
 | 
			
		||||
@@ -31,3 +37,65 @@ jobs:
 | 
			
		||||
          context: .
 | 
			
		||||
          cache-from: type=gha
 | 
			
		||||
          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
 | 
			
		||||
 
 | 
			
		||||
@@ -17,17 +17,45 @@
 | 
			
		||||
#
 | 
			
		||||
# --------------------------------------------------------------------------------------------------
 | 
			
		||||
 | 
			
		||||
number_of_keys() {
 | 
			
		||||
	[ -f "$1" ] && jq 'path(..) | select(length == 2) | .[1]' "$1" | wc -l || echo "0"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
stats() {
 | 
			
		||||
	# Print the number of existing strings on the JSON files for each locale
 | 
			
		||||
	s=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[0]}/en/server.json" | wc -l)
 | 
			
		||||
	c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/en/translation.json" | wc -l)
 | 
			
		||||
	echo "|locale |server strings |client strings |"
 | 
			
		||||
	echo "|-------|---------------|---------------|"
 | 
			
		||||
	s=$(number_of_keys "${paths[0]}/en/server.json")
 | 
			
		||||
	c=$(number_of_keys "${paths[1]}/en/translation.json")
 | 
			
		||||
	echo "| locale |server strings |client strings |"
 | 
			
		||||
	echo "|--------|---------------|---------------|"
 | 
			
		||||
	echo "|   en   |      ${s}      |     ${c}      |"
 | 
			
		||||
	for locale in "${locales[@]}"; do
 | 
			
		||||
		s=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[0]}/${locale}/server.json" | wc -l)
 | 
			
		||||
		c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/${locale}/translation.json" | wc -l)
 | 
			
		||||
		echo "|  ${locale}   |      ${s}      |     ${c}      |"
 | 
			
		||||
		s=$(number_of_keys "${paths[0]}/${locale}/server.json")
 | 
			
		||||
		c=$(number_of_keys "${paths[1]}/${locale}/translation.json")
 | 
			
		||||
		n1=$(((8 - ${#locale}) / 2))
 | 
			
		||||
		n2=$((n1 == 1 ? n1 + 1 : n1))
 | 
			
		||||
		echo "|$(printf "%${n1}s")${locale}$(printf "%${n2}s")|      ${s}      |     ${c}      |"
 | 
			
		||||
	done
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
update_1() {
 | 
			
		||||
	# Update PO files from English and localized JSON files as source
 | 
			
		||||
	# NOTE: if you want a new language you need to first create the JSON files
 | 
			
		||||
	# on their corresponding place with `{}` as content to avoid error on `json2po`
 | 
			
		||||
	local locales=("$@")
 | 
			
		||||
	for path in "${paths[@]}"; do
 | 
			
		||||
		for locale in "${locales[@]}"; do
 | 
			
		||||
			json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
 | 
			
		||||
		done
 | 
			
		||||
	done
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
update_2() {
 | 
			
		||||
	# Recover translation from PO files to localized JSON files
 | 
			
		||||
	local locales=("$@")
 | 
			
		||||
	for path in "${paths[@]}"; do
 | 
			
		||||
		for locale in "${locales[@]}"; do
 | 
			
		||||
			po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
 | 
			
		||||
		done
 | 
			
		||||
	done
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -35,11 +63,11 @@ help() {
 | 
			
		||||
	echo -e "\nDescription:"
 | 
			
		||||
	echo -e "\tCreate PO files to make easier the labor of translation"
 | 
			
		||||
	echo -e "\nUsage:"
 | 
			
		||||
	echo -e "\t./translation.sh [--stats] [--update <OPT_LOCALE>] [--update2 <OPT_LOCALE>]"
 | 
			
		||||
	echo -e "\t./translation.sh [--stats] [--update1 <OPT_LOCALE>] [--update2 <OPT_LOCALE>]"
 | 
			
		||||
	echo -e "\nFlags:"
 | 
			
		||||
	echo -e "  --clear\n\tClear all po-* directories"
 | 
			
		||||
	echo -e "  --stats\n\tPrint the number of existing strings on the JSON files for each locale"
 | 
			
		||||
	echo -e "  --update <LOCALE>\n\tUpdate PO files from English and localized JSON files as source"
 | 
			
		||||
	echo -e "  --update1 <LOCALE>\n\tUpdate PO files from English and localized JSON files as source"
 | 
			
		||||
	echo -e "  --update2 <LOCALE>\n\tRecover translation from PO files to localized JSON files"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -51,7 +79,7 @@ file_path="$(
 | 
			
		||||
	pwd -P
 | 
			
		||||
)"
 | 
			
		||||
paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/")
 | 
			
		||||
locales=(cn es fr ro)
 | 
			
		||||
locales=(cn de es fr pt_br ro tw)
 | 
			
		||||
 | 
			
		||||
if [ $# -eq 1 ]; then
 | 
			
		||||
	if [ "$1" == "--clear" ]; then
 | 
			
		||||
@@ -62,34 +90,18 @@ if [ $# -eq 1 ]; then
 | 
			
		||||
		done
 | 
			
		||||
	elif [ "$1" == "--stats" ]; then
 | 
			
		||||
		stats
 | 
			
		||||
	elif [ "$1" == "--update" ]; then
 | 
			
		||||
		# Update PO files from English and localized JSON files as source
 | 
			
		||||
		for path in "${paths[@]}"; do
 | 
			
		||||
			for locale in "${locales[@]}"; do
 | 
			
		||||
				json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
 | 
			
		||||
			done
 | 
			
		||||
		done
 | 
			
		||||
	elif [ "$1" == "--update1" ]; then
 | 
			
		||||
		update_1 "${locales[@]}"
 | 
			
		||||
	elif [ "$1" == "--update2" ]; then
 | 
			
		||||
		# Recover translation from PO files to localized JSON files
 | 
			
		||||
		for path in "${paths[@]}"; do
 | 
			
		||||
			for locale in "${locales[@]}"; do
 | 
			
		||||
				po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
 | 
			
		||||
			done
 | 
			
		||||
		done
 | 
			
		||||
		update_2 "${locales[@]}"
 | 
			
		||||
	else
 | 
			
		||||
		help
 | 
			
		||||
	fi
 | 
			
		||||
elif [ $# -eq 2 ]; then
 | 
			
		||||
	if [ "$1" == "--update" ]; then
 | 
			
		||||
		locale="$2"
 | 
			
		||||
		for path in "${paths[@]}"; do
 | 
			
		||||
			json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
 | 
			
		||||
		done
 | 
			
		||||
	if [ "$1" == "--update1" ]; then
 | 
			
		||||
		update_1 "$2"
 | 
			
		||||
	elif [ "$1" == "--update2" ]; then
 | 
			
		||||
		locale="$2"
 | 
			
		||||
		for path in "${paths[@]}"; do
 | 
			
		||||
			po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
 | 
			
		||||
		done
 | 
			
		||||
		update_2 "$2"
 | 
			
		||||
	else
 | 
			
		||||
		help
 | 
			
		||||
	fi
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								db/demo.zip
									
									
									
									
									
								
							
							
						
						
									
										
											BIN
										
									
								
								db/demo.zip
									
									
									
									
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										1
									
								
								libraries/mermaid-elk/elk.min.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										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
									
								
							
							
						
						
									
										13
									
								
								libraries/mermaid-elk/package-lock.json
									
									
									
										generated
									
									
									
										Normal 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"
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										13
									
								
								libraries/mermaid-elk/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								libraries/mermaid-elk/package.json
									
									
									
									
									
										Normal 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": {}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								libraries/mermaid-elk/webpack.config.cjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								libraries/mermaid-elk/webpack.config.cjs
									
									
									
									
									
										Normal 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
									
									
									
								
							
							
						
						
									
										31
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -1,18 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "version": "0.90.11-beta",
 | 
			
		||||
  "version": "0.90.12",
 | 
			
		||||
  "lockfileVersion": 3,
 | 
			
		||||
  "requires": true,
 | 
			
		||||
  "packages": {
 | 
			
		||||
    "": {
 | 
			
		||||
      "name": "trilium",
 | 
			
		||||
      "version": "0.90.11-beta",
 | 
			
		||||
      "version": "0.90.12",
 | 
			
		||||
      "license": "AGPL-3.0-only",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@braintree/sanitize-url": "7.1.0",
 | 
			
		||||
        "@electron/remote": "2.1.2",
 | 
			
		||||
        "@excalidraw/excalidraw": "0.17.6",
 | 
			
		||||
        "@highlightjs/cdn-assets": "11.10.0",
 | 
			
		||||
        "@mermaid-js/layout-elk": "0.1.5",
 | 
			
		||||
        "archiver": "7.0.1",
 | 
			
		||||
        "async-mutex": "0.5.0",
 | 
			
		||||
        "autocomplete.js": "0.38.1",
 | 
			
		||||
@@ -47,7 +48,7 @@
 | 
			
		||||
        "html2plaintext": "2.1.4",
 | 
			
		||||
        "http-proxy-agent": "7.0.2",
 | 
			
		||||
        "https-proxy-agent": "7.0.5",
 | 
			
		||||
        "i18next": "23.16.4",
 | 
			
		||||
        "i18next": "23.16.8",
 | 
			
		||||
        "i18next-fs-backend": "2.3.2",
 | 
			
		||||
        "i18next-http-backend": "2.6.2",
 | 
			
		||||
        "image-type": "4.1.0",
 | 
			
		||||
@@ -3072,6 +3073,18 @@
 | 
			
		||||
        "url": "https://github.com/malept/cross-spawn-promise?sponsor=1"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mermaid-js/layout-elk": {
 | 
			
		||||
      "version": "0.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mermaid-js/layout-elk/-/layout-elk-0.1.5.tgz",
 | 
			
		||||
      "integrity": "sha512-6ML4iWdVdyIkSW47KiID9runHzaomLxdMfNo9U60LJvfcQkB/FAjg0Vjc4AZEQnnBq7ibAoAknAWlT1XetwXSg==",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "d3": "^7.9.0",
 | 
			
		||||
        "elkjs": "^0.9.3"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "mermaid": "^11.0.0"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@mermaid-js/parser": {
 | 
			
		||||
      "version": "0.3.0",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.3.0.tgz",
 | 
			
		||||
@@ -7904,6 +7917,11 @@
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
 | 
			
		||||
      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/elkjs": {
 | 
			
		||||
      "version": "0.9.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz",
 | 
			
		||||
      "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ=="
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/emitter-listener": {
 | 
			
		||||
      "version": "1.1.2",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz",
 | 
			
		||||
@@ -10024,9 +10042,9 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/i18next": {
 | 
			
		||||
      "version": "23.16.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.4.tgz",
 | 
			
		||||
      "integrity": "sha512-9NIYBVy9cs4wIqzurf7nLXPyf3R78xYbxExVqHLK9od3038rjpyOEzW+XB130kZ1N4PZ9inTtJ471CRJ4Ituyg==",
 | 
			
		||||
      "version": "23.16.8",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz",
 | 
			
		||||
      "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==",
 | 
			
		||||
      "funding": [
 | 
			
		||||
        {
 | 
			
		||||
          "type": "individual",
 | 
			
		||||
@@ -10041,6 +10059,7 @@
 | 
			
		||||
          "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
 | 
			
		||||
        }
 | 
			
		||||
      ],
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@babel/runtime": "^7.23.2"
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@
 | 
			
		||||
  "name": "trilium",
 | 
			
		||||
  "productName": "TriliumNext Notes",
 | 
			
		||||
  "description": "Build your personal knowledge base with TriliumNext Notes",
 | 
			
		||||
  "version": "0.90.11-beta",
 | 
			
		||||
  "version": "0.90.12",
 | 
			
		||||
  "license": "AGPL-3.0-only",
 | 
			
		||||
  "main": "./dist/electron-main.js",
 | 
			
		||||
  "author": {
 | 
			
		||||
@@ -54,6 +54,7 @@
 | 
			
		||||
    "@electron/remote": "2.1.2",
 | 
			
		||||
    "@excalidraw/excalidraw": "0.17.6",
 | 
			
		||||
    "@highlightjs/cdn-assets": "11.10.0",
 | 
			
		||||
    "@mermaid-js/layout-elk": "0.1.5",
 | 
			
		||||
    "archiver": "7.0.1",
 | 
			
		||||
    "async-mutex": "0.5.0",
 | 
			
		||||
    "autocomplete.js": "0.38.1",
 | 
			
		||||
@@ -88,7 +89,7 @@
 | 
			
		||||
    "html2plaintext": "2.1.4",
 | 
			
		||||
    "http-proxy-agent": "7.0.2",
 | 
			
		||||
    "https-proxy-agent": "7.0.5",
 | 
			
		||||
    "i18next": "23.16.4",
 | 
			
		||||
    "i18next": "23.16.8",
 | 
			
		||||
    "i18next-fs-backend": "2.3.2",
 | 
			
		||||
    "i18next-http-backend": "2.6.2",
 | 
			
		||||
    "image-type": "4.1.0",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										10
									
								
								renovate.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								renovate.json
									
									
									
									
									
										Normal 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
 | 
			
		||||
}
 | 
			
		||||
@@ -88,8 +88,11 @@ export default class Component {
 | 
			
		||||
 | 
			
		||||
        if (fun) {
 | 
			
		||||
            return this.callMethod(fun, data);
 | 
			
		||||
        } else {
 | 
			
		||||
            if (!this.parent) {
 | 
			
		||||
                throw new Error(`Component "${this.componentId}" does not have a parent attached to propagate a command.`);
 | 
			
		||||
            }
 | 
			
		||||
        else {
 | 
			
		||||
 | 
			
		||||
            return this.parent.triggerCommand(name, data);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -586,6 +586,11 @@ export default class TabManager extends Component {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async copyTabToNewWindowCommand({ntxId}) {
 | 
			
		||||
        const {notePath, hoistedNoteId} = this.getNoteContextById(ntxId);
 | 
			
		||||
        this.triggerCommand('openInWindow', {notePath, hoistedNoteId});
 | 
			
		||||
    } 
 | 
			
		||||
 | 
			
		||||
    async reopenLastTabCommand() {
 | 
			
		||||
        let closeLastEmptyTab = null;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3
									
								
								src/public/app/doc_notes/cn/hidden.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/public/app/doc_notes/cn/hidden.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
			
		||||
<p>隐藏树用于记录各种应用层数据,这些数据大部分时间可能对用户不可见。</p>
 | 
			
		||||
 | 
			
		||||
<p>确保你知道自己在做什么。对这个子树的错误更改可能会导致应用程序崩溃。</p>
 | 
			
		||||
@@ -0,0 +1 @@
 | 
			
		||||
<p>此启动器操作的键盘快捷键可以在“选项”->“快捷键”中进行配置。</p>
 | 
			
		||||
@@ -0,0 +1,3 @@
 | 
			
		||||
<p>“后退”和“前进”按钮允许您在导航历史中移动。</p>
 | 
			
		||||
 | 
			
		||||
<p>这些启动器仅在桌面版本中有效,在服务器版本中将被忽略,您可以使用浏览器的原生导航按钮代替。</p>
 | 
			
		||||
							
								
								
									
										11
									
								
								src/public/app/doc_notes/cn/launchbar_intro.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/public/app/doc_notes/cn/launchbar_intro.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
<p>欢迎来到启动栏配置界面。</p>
 | 
			
		||||
 | 
			
		||||
<p>您可以在此处执行以下操作:</p>
 | 
			
		||||
 | 
			
		||||
<ul>
 | 
			
		||||
    <li>通过拖动将可用的启动器移动到可见列表中(从而将它们放入启动栏)</li>
 | 
			
		||||
    <li>通过拖动将可见的启动器移动到可用列表中(从而将它们从启动栏中隐藏)</li>
 | 
			
		||||
    <li>您可以通过拖动重新排列列表中的项目</li>
 | 
			
		||||
    <li>通过右键点击“可见启动器”文件夹来创建新的启动器</li>
 | 
			
		||||
    <li>如果您想恢复默认设置,可以在右键菜单中找到“重置”选项。</li>
 | 
			
		||||
</ul>
 | 
			
		||||
							
								
								
									
										9
									
								
								src/public/app/doc_notes/cn/launchbar_note_launcher.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/public/app/doc_notes/cn/launchbar_note_launcher.html
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										12
									
								
								src/public/app/doc_notes/cn/launchbar_script_launcher.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/public/app/doc_notes/cn/launchbar_script_launcher.html
									
									
									
									
									
										Normal 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>
 | 
			
		||||
							
								
								
									
										6
									
								
								src/public/app/doc_notes/cn/launchbar_spacer.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/public/app/doc_notes/cn/launchbar_spacer.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
<p>间隔器允许您在视觉上将启动器分组。您可以在提升的属性中进行配置:</p>
 | 
			
		||||
 | 
			
		||||
<ul>
 | 
			
		||||
    <li><code>baseSize</code> - 定义以像素为单位的大小(如果有足够的空间)</li>
 | 
			
		||||
    <li><code>growthFactor</code> - 如果您希望间隔器保持恒定的 <code>baseSize</code>,则设置为 0;如果设置为正值,它将增长。</li>
 | 
			
		||||
</ul>
 | 
			
		||||
							
								
								
									
										34
									
								
								src/public/app/doc_notes/cn/launchbar_widget_launcher.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								src/public/app/doc_notes/cn/launchbar_widget_launcher.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
<p>请在提升的属性中定义目标小部件笔记。该小部件将用于渲染启动栏图标。</p>
 | 
			
		||||
 | 
			
		||||
<h4>示例启动栏小部件</h4>
 | 
			
		||||
 | 
			
		||||
<pre>
 | 
			
		||||
const TPL = `<div style="height: 53px; width: 53px;"></div>`;
 | 
			
		||||
 | 
			
		||||
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>
 | 
			
		||||
							
								
								
									
										1
									
								
								src/public/app/doc_notes/cn/share.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/public/app/doc_notes/cn/share.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<p>在这里您可以找到所有分享的笔记。</p>
 | 
			
		||||
							
								
								
									
										1
									
								
								src/public/app/doc_notes/cn/user_hidden.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/public/app/doc_notes/cn/user_hidden.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
<p>此笔记作为一个子树,用于存储由用户脚本生成的数据,这些数据本应避免在隐藏子树中随意创建。</p>
 | 
			
		||||
@@ -83,6 +83,7 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
 | 
			
		||||
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
 | 
			
		||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
 | 
			
		||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
 | 
			
		||||
import options from "../services/options.js";
 | 
			
		||||
 | 
			
		||||
export default class DesktopLayout {
 | 
			
		||||
    constructor(customWidgets) {
 | 
			
		||||
@@ -92,24 +93,31 @@ export default class DesktopLayout {
 | 
			
		||||
    getRootWidget(appContext) {
 | 
			
		||||
        appContext.noteTreeWidget = new NoteTreeWidget();
 | 
			
		||||
 | 
			
		||||
        return new RootContainer()
 | 
			
		||||
        const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
 | 
			
		||||
        const launcherPane = this.#buildLauncherPane(launcherPaneIsHorizontal);
 | 
			
		||||
 | 
			
		||||
        return new RootContainer(launcherPaneIsHorizontal)
 | 
			
		||||
            .setParent(appContext)
 | 
			
		||||
            .optChild(launcherPaneIsHorizontal, new FlexContainer('row')               
 | 
			
		||||
                .child(new TabRowWidget().class("full-width"))
 | 
			
		||||
                .child(new TitleBarButtonsWidget())
 | 
			
		||||
                .css('height', '40px')
 | 
			
		||||
                .css('background-color', 'var(--launcher-pane-background-color)')
 | 
			
		||||
                .setParent(appContext)
 | 
			
		||||
            .child(new FlexContainer("column")
 | 
			
		||||
                .id("launcher-pane")
 | 
			
		||||
                .css("width", "53px")
 | 
			
		||||
                .child(new GlobalMenuWidget())
 | 
			
		||||
                .child(new LauncherContainer())
 | 
			
		||||
                .child(new LeftPaneToggleWidget())
 | 
			
		||||
            )
 | 
			
		||||
            .optChild(launcherPaneIsHorizontal, launcherPane)
 | 
			
		||||
            .child(new FlexContainer('row')
 | 
			
		||||
                .css("flex-grow", "1")
 | 
			
		||||
                .optChild(!launcherPaneIsHorizontal, launcherPane)
 | 
			
		||||
                .child(new LeftPaneContainer()
 | 
			
		||||
                .child(new QuickSearchWidget())
 | 
			
		||||
                    .optChild(!launcherPaneIsHorizontal, new QuickSearchWidget())
 | 
			
		||||
                    .child(appContext.noteTreeWidget)
 | 
			
		||||
                    .child(...this.customWidgets.get('left-pane'))
 | 
			
		||||
                )
 | 
			
		||||
                .child(new FlexContainer('column')
 | 
			
		||||
                    .id('rest-pane')
 | 
			
		||||
                    .css("flex-grow", "1")
 | 
			
		||||
                .child(new FlexContainer('row')
 | 
			
		||||
                    .optChild(!launcherPaneIsHorizontal, new FlexContainer('row')
 | 
			
		||||
                        .child(new TabRowWidget())
 | 
			
		||||
                        .child(new TitleBarButtonsWidget())
 | 
			
		||||
                        .css('height', '40px')
 | 
			
		||||
@@ -201,6 +209,7 @@ export default class DesktopLayout {
 | 
			
		||||
                        )
 | 
			
		||||
                    )
 | 
			
		||||
                )
 | 
			
		||||
            )
 | 
			
		||||
            .child(new BulkActionsDialog())
 | 
			
		||||
            .child(new AboutDialog())
 | 
			
		||||
            .child(new HelpDialog())
 | 
			
		||||
@@ -225,4 +234,27 @@ export default class DesktopLayout {
 | 
			
		||||
            .child(new ConfirmDialog())
 | 
			
		||||
            .child(new PromptDialog());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #buildLauncherPane(isHorizontal) {
 | 
			
		||||
        let launcherPane;        
 | 
			
		||||
 | 
			
		||||
        if (isHorizontal) {
 | 
			
		||||
            launcherPane = new FlexContainer("row")
 | 
			
		||||
                .css("height", "53px")
 | 
			
		||||
                .class("horizontal")
 | 
			
		||||
                .child(new LeftPaneToggleWidget(true))
 | 
			
		||||
                .child(new LauncherContainer(true))
 | 
			
		||||
                .child(new GlobalMenuWidget(true))
 | 
			
		||||
        } else {
 | 
			
		||||
            launcherPane = new FlexContainer("column")
 | 
			
		||||
                .css("width", "53px")
 | 
			
		||||
                .class("vertical")
 | 
			
		||||
                .child(new GlobalMenuWidget(false))
 | 
			
		||||
                .child(new LauncherContainer(false))
 | 
			
		||||
                .child(new LeftPaneToggleWidget(false));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        launcherPane.id("launcher-pane");
 | 
			
		||||
        return launcherPane;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ import RootContainer from "../widgets/containers/root_container.js";
 | 
			
		||||
import SharedInfoWidget from "../widgets/shared_info.js";
 | 
			
		||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
 | 
			
		||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
 | 
			
		||||
import options from "../services/options.js";
 | 
			
		||||
 | 
			
		||||
const MOBILE_CSS = `
 | 
			
		||||
<style>
 | 
			
		||||
@@ -112,15 +113,12 @@ span.fancytree-expander {
 | 
			
		||||
 | 
			
		||||
export default class MobileLayout {
 | 
			
		||||
    getRootWidget(appContext) {
 | 
			
		||||
        return new RootContainer()
 | 
			
		||||
        const launcherPaneIsHorizontal = (options.get("layoutOrientation") === "horizontal");
 | 
			
		||||
 | 
			
		||||
        return new RootContainer(launcherPaneIsHorizontal)
 | 
			
		||||
            .setParent(appContext)
 | 
			
		||||
            .cssBlock(MOBILE_CSS)
 | 
			
		||||
            .child(new FlexContainer("column")
 | 
			
		||||
                .id("launcher-pane")
 | 
			
		||||
                .css("width", "53px")
 | 
			
		||||
                .child(new GlobalMenuWidget())
 | 
			
		||||
                .child(new LauncherContainer())
 | 
			
		||||
            )
 | 
			
		||||
            .child(this.#buildLauncherPane(launcherPaneIsHorizontal))
 | 
			
		||||
            .child(new FlexContainer("row")
 | 
			
		||||
                .filling()
 | 
			
		||||
                .child(new ScreenContainer("tree", 'column')
 | 
			
		||||
@@ -140,12 +138,14 @@ export default class MobileLayout {
 | 
			
		||||
                    .child(new FlexContainer('row').contentSized()
 | 
			
		||||
                        .css('font-size', 'larger')
 | 
			
		||||
                        .css('align-items', 'center')
 | 
			
		||||
                        .child(new MobileDetailMenuWidget().contentSized())
 | 
			
		||||
                        .optChild(!launcherPaneIsHorizontal, new MobileDetailMenuWidget(false).contentSized())
 | 
			
		||||
                        .child(new NoteTitleWidget()
 | 
			
		||||
                            .contentSized()
 | 
			
		||||
                            .css("position: relative;")
 | 
			
		||||
                            .css("top: 5px;")
 | 
			
		||||
                            .optCss(launcherPaneIsHorizontal, "padding-left", "0.5em")
 | 
			
		||||
                        )
 | 
			
		||||
                        .optChild(launcherPaneIsHorizontal, new MobileDetailMenuWidget(true).contentSized())
 | 
			
		||||
                        .child(new CloseDetailButtonWidget().contentSized()))
 | 
			
		||||
                    .child(new SharedInfoWidget())
 | 
			
		||||
                    .child(new FloatingButtons()
 | 
			
		||||
@@ -174,4 +174,25 @@ export default class MobileLayout {
 | 
			
		||||
                .child(new ConfirmDialog())
 | 
			
		||||
            );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    #buildLauncherPane(isHorizontal) {
 | 
			
		||||
        let launcherPane;
 | 
			
		||||
 | 
			
		||||
        if (isHorizontal) {
 | 
			
		||||
            launcherPane = new FlexContainer("row")
 | 
			
		||||
                .class("horizontal")
 | 
			
		||||
                .css("height", "53px")
 | 
			
		||||
                .child(new LauncherContainer(true))
 | 
			
		||||
                .child(new GlobalMenuWidget(true));
 | 
			
		||||
        } else {
 | 
			
		||||
            launcherPane = new FlexContainer("column")                
 | 
			
		||||
                .class("vertical")
 | 
			
		||||
                .css("width", "53px")
 | 
			
		||||
                .child(new GlobalMenuWidget(false))
 | 
			
		||||
                .child(new LauncherContainer(false));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        launcherPane.id("launcher-pane");
 | 
			
		||||
        return launcherPane;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import keyboardActionService from '../services/keyboard_actions.js';
 | 
			
		||||
class ContextMenu {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        this.$widget = $("#context-menu-container");
 | 
			
		||||
        this.$widget.addClass("dropend");
 | 
			
		||||
        this.dateContextMenuOpenedMs = 0;
 | 
			
		||||
 | 
			
		||||
        $(document).on('click', () => this.hide());
 | 
			
		||||
@@ -11,6 +12,12 @@ class ContextMenu {
 | 
			
		||||
    async show(options) {
 | 
			
		||||
        this.options = options;
 | 
			
		||||
 | 
			
		||||
        if (this.$widget.hasClass("show")) {
 | 
			
		||||
            // The menu is already visible. Hide the menu then open it again
 | 
			
		||||
            // at the new location to re-trigger the opening animation.
 | 
			
		||||
            await this.hide();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$widget.empty();
 | 
			
		||||
 | 
			
		||||
        this.addItems(this.$widget, options.items);
 | 
			
		||||
@@ -96,6 +103,10 @@ class ContextMenu {
 | 
			
		||||
                    .append("   ") // some space between icon and text
 | 
			
		||||
                    .append(item.title);
 | 
			
		||||
 | 
			
		||||
                if (item.shortcut) {
 | 
			
		||||
                    $link.append($("<kbd>").text(item.shortcut));
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                const $item = $("<li>")
 | 
			
		||||
                    .addClass("dropdown-item")
 | 
			
		||||
                    .append($link)
 | 
			
		||||
@@ -142,18 +153,26 @@ class ContextMenu {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hide() {
 | 
			
		||||
    async hide() {
 | 
			
		||||
        // this date checking comes from change in FF66 - https://github.com/zadam/trilium/issues/468
 | 
			
		||||
        // "contextmenu" event also triggers "click" event which depending on the timing can close the just opened context menu
 | 
			
		||||
        // we might filter out right clicks, but then it's better if even right clicks close the context menu
 | 
			
		||||
        if (Date.now() - this.dateContextMenuOpenedMs > 300) {
 | 
			
		||||
            // seems like if we hide the menu immediately, some clicks can get propagated to the underlying component
 | 
			
		||||
            // see https://github.com/zadam/trilium/pull/3805 for details
 | 
			
		||||
            setTimeout(() => this.$widget.hide(), 100);
 | 
			
		||||
            await timeout(100);
 | 
			
		||||
            this.$widget.removeClass("show");
 | 
			
		||||
            this.$widget.hide()
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function timeout(ms) {
 | 
			
		||||
    return new Promise((accept, reject) => {
 | 
			
		||||
        setTimeout(accept, ms);
 | 
			
		||||
    });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const contextMenu = new ContextMenu();
 | 
			
		||||
 | 
			
		||||
export default contextMenu;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@ import utils from "../services/utils.js";
 | 
			
		||||
import options from "../services/options.js";
 | 
			
		||||
import zoomService from "../components/zoom.js";
 | 
			
		||||
import contextMenu from "./context_menu.js";
 | 
			
		||||
import { t } from "../services/i18n.js";
 | 
			
		||||
 | 
			
		||||
function setupContextMenu() {
 | 
			
		||||
    const electron = utils.dynamicRequire('electron');
 | 
			
		||||
@@ -28,7 +29,7 @@ function setupContextMenu() {
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            items.push({
 | 
			
		||||
                title: `Add "${params.misspelledWord}" to dictionary`,
 | 
			
		||||
                title: t("electron_context_menu.add-term-to-dictionary", { term: params.misspelledWord }),
 | 
			
		||||
                uiIcon: "bx bx-plus",
 | 
			
		||||
                handler: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord)
 | 
			
		||||
            });
 | 
			
		||||
@@ -39,7 +40,8 @@ function setupContextMenu() {
 | 
			
		||||
        if (params.isEditable) {
 | 
			
		||||
            items.push({
 | 
			
		||||
                enabled: editFlags.canCut && hasText,
 | 
			
		||||
                title: `Cut <kbd>${platformModifier}+X`,
 | 
			
		||||
                title: t("electron_context_menu.cut"),
 | 
			
		||||
                shortcut: `${platformModifier}+X`,
 | 
			
		||||
                uiIcon: "bx bx-cut",
 | 
			
		||||
                handler: () => webContents.cut()
 | 
			
		||||
            });
 | 
			
		||||
@@ -48,7 +50,8 @@ function setupContextMenu() {
 | 
			
		||||
        if (params.isEditable || hasText) {
 | 
			
		||||
            items.push({
 | 
			
		||||
                enabled: editFlags.canCopy && hasText,
 | 
			
		||||
                title: `Copy <kbd>${platformModifier}+C`,
 | 
			
		||||
                title: t("electron_context_menu.copy"),
 | 
			
		||||
                shortcut: `${platformModifier}+C`,
 | 
			
		||||
                uiIcon: "bx bx-copy",
 | 
			
		||||
                handler: () => webContents.copy()
 | 
			
		||||
            });
 | 
			
		||||
@@ -56,7 +59,7 @@ function setupContextMenu() {
 | 
			
		||||
 | 
			
		||||
        if (!["", "javascript:", "about:blank#blocked"].includes(params.linkURL) && params.mediaType === 'none') {
 | 
			
		||||
            items.push({
 | 
			
		||||
                title: `Copy link`,
 | 
			
		||||
                title: t("electron_context_menu.copy-link"),
 | 
			
		||||
                uiIcon: "bx bx-copy",
 | 
			
		||||
                handler: () => {
 | 
			
		||||
                    electron.clipboard.write({
 | 
			
		||||
@@ -70,7 +73,8 @@ function setupContextMenu() {
 | 
			
		||||
        if (params.isEditable) {
 | 
			
		||||
            items.push({
 | 
			
		||||
                enabled: editFlags.canPaste,
 | 
			
		||||
                title: `Paste <kbd>${platformModifier}+V`,
 | 
			
		||||
                title: t("electron_context_menu.paste"),
 | 
			
		||||
                shortcut: `${platformModifier}+V`,
 | 
			
		||||
                uiIcon: "bx bx-paste",
 | 
			
		||||
                handler: () => webContents.paste()
 | 
			
		||||
            });
 | 
			
		||||
@@ -79,7 +83,8 @@ function setupContextMenu() {
 | 
			
		||||
        if (params.isEditable) {
 | 
			
		||||
            items.push({
 | 
			
		||||
                enabled: editFlags.canPaste,
 | 
			
		||||
                title: `Paste as plain text <kbd>${platformModifier}+Shift+V`,
 | 
			
		||||
                title: t("electron_context_menu.paste-as-plain-text"),
 | 
			
		||||
                shortcut: `${platformModifier}+Shift+V`,
 | 
			
		||||
                uiIcon: "bx bx-paste",
 | 
			
		||||
                handler: () => webContents.pasteAndMatchStyle()
 | 
			
		||||
            });
 | 
			
		||||
@@ -106,9 +111,11 @@ function setupContextMenu() {
 | 
			
		||||
            // Replace the placeholder with the real search keyword.
 | 
			
		||||
            let searchUrl = searchEngineUrl.replace("{keyword}", encodeURIComponent(params.selectionText));
 | 
			
		||||
 | 
			
		||||
            items.push({title: "----"});
 | 
			
		||||
 | 
			
		||||
            items.push({
 | 
			
		||||
                enabled: editFlags.canPaste,
 | 
			
		||||
                title: `Search for "${shortenedSelection}" with ${searchEngineName}`,
 | 
			
		||||
                title: t("electron_context_menu.search_online", { term: shortenedSelection, searchEngine: searchEngineName }),
 | 
			
		||||
                uiIcon: "bx bx-search-alt",
 | 
			
		||||
                handler: () => electron.shell.openExternal(searchUrl)
 | 
			
		||||
            });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { t } from '../services/i18n.js';
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import contextMenu from "./context_menu.js";
 | 
			
		||||
import imageService from "../services/image.js";
 | 
			
		||||
@@ -18,11 +19,15 @@ function setupContextMenu($image) {
 | 
			
		||||
            y: e.pageY,
 | 
			
		||||
            items: [
 | 
			
		||||
                {
 | 
			
		||||
                    title: "Copy reference to clipboard",
 | 
			
		||||
                    title: t("image_context_menu.copy_reference_to_clipboard"),
 | 
			
		||||
                    command: "copyImageReferenceToClipboard",
 | 
			
		||||
                    uiIcon: "bx bx-empty"
 | 
			
		||||
                    uiIcon: "bx bx-directions"
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                    title: t("image_context_menu.copy_image_to_clipboard"),
 | 
			
		||||
                    command: "copyImageToClipboard",
 | 
			
		||||
                    uiIcon: "bx bx-copy"
 | 
			
		||||
                },
 | 
			
		||||
                { title: "Copy image to clipboard", command: "copyImageToClipboard", uiIcon: "bx bx-empty" },
 | 
			
		||||
            ],
 | 
			
		||||
            selectMenuItemHandler: async ({ command }) => {
 | 
			
		||||
                if (command === 'copyImageReferenceToClipboard') {
 | 
			
		||||
 
 | 
			
		||||
@@ -37,18 +37,22 @@ export default class LauncherContextMenu {
 | 
			
		||||
        const canBeReset = !canBeDeleted && note.isLaunchBarConfig();
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-plus" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-plus" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-plus" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-note" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-code-curly" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-customize" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-dots-horizontal" } : null,
 | 
			
		||||
            (isVisibleRoot || isAvailableRoot) ? { title: "----" } : null,
 | 
			
		||||
            { title: `${t("launcher_context_menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted },
 | 
			
		||||
            { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-empty", enabled: canBeReset},
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
 | 
			
		||||
            isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
 | 
			
		||||
            isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
 | 
			
		||||
            { title: `${t("launcher_context_menu.duplicate-launcher")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
 | 
			
		||||
                enabled: isItem }
 | 
			
		||||
            (isVisibleItem || isAvailableItem) ? { title: "----" } : null,
 | 
			
		||||
 | 
			
		||||
            { title: `${t("launcher_context_menu.duplicate-launcher")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: isItem },
 | 
			
		||||
            { title: `${t("launcher_context_menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon", enabled: canBeDeleted },
 | 
			
		||||
           
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
           
 | 
			
		||||
            { title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-reset destructive-action-icon", enabled: canBeReset}
 | 
			
		||||
        ].filter(row => row !== null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { t } from "../services/i18n.js";
 | 
			
		||||
import contextMenu from "./context_menu.js";
 | 
			
		||||
import appContext from "../components/app_context.js";
 | 
			
		||||
 | 
			
		||||
@@ -6,9 +7,9 @@ function openContextMenu(notePath, e, viewScope = {}, hoistedNoteId = null) {
 | 
			
		||||
        x: e.pageX,
 | 
			
		||||
        y: e.pageY,
 | 
			
		||||
        items: [
 | 
			
		||||
            {title: "Open note in a new tab", command: "openNoteInNewTab", uiIcon: "bx bx-empty"},
 | 
			
		||||
            {title: "Open note in a new split", command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
 | 
			
		||||
            {title: "Open note in a new window", command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
 | 
			
		||||
            {title: t("link_context_menu.open_note_in_new_tab"), command: "openNoteInNewTab", uiIcon: "bx bx-link-external"},
 | 
			
		||||
            {title: t("link_context_menu.open_note_in_new_split"), command: "openNoteInNewSplit", uiIcon: "bx bx-dock-right"},
 | 
			
		||||
            {title: t("link_context_menu.open_note_in_new_window"), command: "openNoteInNewWindow", uiIcon: "bx bx-window-open"}
 | 
			
		||||
        ],
 | 
			
		||||
        selectMenuItemHandler: ({command}) => {
 | 
			
		||||
            if (!hoistedNoteId) {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,56 +49,98 @@ export default class TreeContextMenu {
 | 
			
		||||
        const insertNoteAfterEnabled = isNotRoot && !isHoisted && parentNotSearch;
 | 
			
		||||
 | 
			
		||||
        return [
 | 
			
		||||
            { title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-empty", enabled: noSelectedNotes },
 | 
			
		||||
            { title: `${t("tree-context-menu.open-in-a-new-tab")} <kbd>Ctrl+Click</kbd>`, command: "openInTab", uiIcon: "bx bx-link-external", enabled: noSelectedNotes },
 | 
			
		||||
            
 | 
			
		||||
            { title: t("tree-context-menu.open-in-a-new-split"), command: "openNoteInSplit", uiIcon: "bx bx-dock-right", enabled: noSelectedNotes },
 | 
			
		||||
            { title: `${t("tree-context-menu.insert-note-after")} <kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
 | 
			
		||||
            
 | 
			
		||||
            isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bxs-chevrons-up", enabled: noSelectedNotes && notSearch },
 | 
			
		||||
            !isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.insert-note-after")}<kbd data-command="createNoteAfter"></kbd>`, command: "insertNoteAfter", uiIcon: "bx bx-plus",
 | 
			
		||||
                items: insertNoteAfterEnabled ? await noteTypesService.getNoteTypeItems("insertNoteAfter") : null,
 | 
			
		||||
                enabled: insertNoteAfterEnabled && noSelectedNotes && notOptions },
 | 
			
		||||
            { title: `${t("tree-context-menu.insert-child-note")} <kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.insert-child-note")}<kbd data-command="createNoteInto"></kbd>`, command: "insertChildNote", uiIcon: "bx bx-plus",
 | 
			
		||||
                items: notSearch ? await noteTypesService.getNoteTypeItems("insertChildNote") : null,
 | 
			
		||||
                enabled: notSearch && noSelectedNotes && notOptions },
 | 
			
		||||
            { title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
            { title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
 | 
			
		||||
                enabled: notSearch && noSelectedNotes },
 | 
			
		||||
            isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
 | 
			
		||||
            !isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
 | 
			
		||||
            { title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-empty",
 | 
			
		||||
                enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
 | 
			
		||||
            { title: t("tree-context-menu.advanced"), uiIcon: "bx bx-empty", enabled: true, items: [
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            { title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
 | 
			
		||||
 | 
			
		||||
            { title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            { title: t("tree-context-menu.advanced"), uiIcon: "bx bxs-wrench", enabled: true, items: [
 | 
			
		||||
                { title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus", enabled: true },
 | 
			
		||||
 | 
			
		||||
                { title: "----" },
 | 
			
		||||
 | 
			
		||||
                { title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-rename", enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
 | 
			
		||||
                { title: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-paperclip", enabled: isNotRoot && !isHoisted && notOptions },
 | 
			
		||||
                { title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-outline", enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
 | 
			
		||||
 | 
			
		||||
                { title: "----" },
 | 
			
		||||
 | 
			
		||||
                { title: `${t("tree-context-menu.expand-subtree")} <kbd data-command="expandSubtree"></kbd>`, command: "expandSubtree", uiIcon: "bx bx-expand", enabled: noSelectedNotes },
 | 
			
		||||
                { title: `${t("tree-context-menu.collapse-subtree")} <kbd data-command="collapseSubtree"></kbd>`, command: "collapseSubtree", uiIcon: "bx bx-collapse", enabled: noSelectedNotes },
 | 
			
		||||
                    { title: `${t("tree-context-menu.sort-by")} <kbd data-command="sortChildNotes"></kbd>`, command: "sortChildNotes", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
 | 
			
		||||
                { 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: t("tree-context-menu.convert-to-attachment"), command: "convertNoteToAttachment", uiIcon: "bx bx-empty", enabled: isNotRoot && !isHoisted && notOptions },
 | 
			
		||||
                    { title: t("tree-context-menu.copy-note-path-to-clipboard"), command: "copyNotePathToClipboard", uiIcon: "bx bx-empty", enabled: true }
 | 
			
		||||
            ] },
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
            { title: t("tree-context-menu.protect-subtree"), command: "protectSubtree", uiIcon: "bx bx-check-shield", enabled: noSelectedNotes },
 | 
			
		||||
            { title: t("tree-context-menu.unprotect-subtree"), command: "unprotectSubtree", uiIcon: "bx bx-shield", enabled: noSelectedNotes },
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
            { title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted },
 | 
			
		||||
            { title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-empty",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted },
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.cut")} <kbd data-command="cutNotesToClipboard"></kbd>`, command: "cutNotesToClipboard", uiIcon: "bx bx-cut",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted && parentNotSearch },
 | 
			
		||||
            { title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-empty",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted && parentNotSearch },
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.copy-clone")} <kbd data-command="copyNotesToClipboard"></kbd>`, command: "copyNotesToClipboard", uiIcon: "bx bx-copy",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted },
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.paste-into")} <kbd data-command="pasteNotesFromClipboard"></kbd>`, command: "pasteNotesFromClipboard", uiIcon: "bx bx-paste",
 | 
			
		||||
                enabled: !clipboard.isClipboardEmpty() && notSearch && noSelectedNotes },
 | 
			
		||||
            
 | 
			
		||||
            { title: t("tree-context-menu.paste-after"), command: "pasteNotesAfterFromClipboard", uiIcon: "bx bx-paste",
 | 
			
		||||
                enabled: !clipboard.isClipboardEmpty() && isNotRoot && !isHoisted && parentNotSearch && noSelectedNotes },
 | 
			
		||||
            { title: `${t("tree-context-menu.duplicate-subtree")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
 | 
			
		||||
                enabled: parentNotSearch && isNotRoot && !isHoisted && notOptions },
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.move-to")} <kbd data-command="moveNotesTo"></kbd>`, command: "moveNotesTo", uiIcon: "bx bx-transfer",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted && parentNotSearch },
 | 
			
		||||
            
 | 
			
		||||
            { title: `${t("tree-context-menu.clone-to")} <kbd data-command="cloneNotesTo"></kbd>`, command: "cloneNotesTo", uiIcon: "bx bx-duplicate",
 | 
			
		||||
                enabled: isNotRoot && !isHoisted },
 | 
			
		||||
 | 
			
		||||
            { title: `${t("tree-context-menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash destructive-action-icon",
 | 
			
		||||
            enabled: isNotRoot && !isHoisted && parentNotSearch && notOptions },
 | 
			
		||||
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
            { title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-empty",
 | 
			
		||||
 | 
			
		||||
            { title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-import",
 | 
			
		||||
                enabled: notSearch && noSelectedNotes && notOptions },
 | 
			
		||||
            { title: t("tree-context-menu.import-into-note"), command: "importIntoNote", uiIcon: "bx bx-empty",
 | 
			
		||||
 | 
			
		||||
            { title: t("tree-context-menu.export"), command: "exportNote", uiIcon: "bx bx-export",
 | 
			
		||||
                enabled: notSearch && noSelectedNotes && notOptions },
 | 
			
		||||
            { title: t("tree-context-menu.apply-bulk-actions"), command: "openBulkActionsDialog", uiIcon: "bx bx-list-plus",
 | 
			
		||||
                enabled: true }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            { title: "----" },
 | 
			
		||||
 | 
			
		||||
            
 | 
			
		||||
            { title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
 | 
			
		||||
            enabled: notSearch && noSelectedNotes },
 | 
			
		||||
 | 
			
		||||
        ].filter(row => row !== null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,22 +13,23 @@ import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js";
 | 
			
		||||
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
 | 
			
		||||
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
 | 
			
		||||
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
 | 
			
		||||
import { t } from "./i18n.js";
 | 
			
		||||
 | 
			
		||||
const ACTION_GROUPS = [
 | 
			
		||||
    {
 | 
			
		||||
        title: 'Labels',
 | 
			
		||||
        title: t("bulk_actions.labels"),
 | 
			
		||||
        actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        title: 'Relations',
 | 
			
		||||
        title: t("bulk_actions.relations"),
 | 
			
		||||
        actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        title: 'Notes',
 | 
			
		||||
        title: t("bulk_actions.notes"),
 | 
			
		||||
        actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction],
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        title: 'Other',
 | 
			
		||||
        title: t("bulk_actions.other"),
 | 
			
		||||
        actions: [ExecuteScriptBulkAction]
 | 
			
		||||
    }
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,7 @@ import FAttachment from "../entities/fattachment.js";
 | 
			
		||||
import imageContextMenuService from "../menus/image_context_menu.js";
 | 
			
		||||
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
 | 
			
		||||
import mime_types from "./mime_types.js";
 | 
			
		||||
import { loadElkIfNeeded } from "./mermaid.js";
 | 
			
		||||
 | 
			
		||||
let idCounter = 1;
 | 
			
		||||
 | 
			
		||||
@@ -237,6 +238,7 @@ async function renderMermaid(note, $renderedContent) {
 | 
			
		||||
    mermaid.mermaidAPI.initialize({startOnLoad: false, theme: mermaidTheme.trim(), securityLevel: 'antiscript'});
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
        await loadElkIfNeeded(content);
 | 
			
		||||
        const {svg} = await mermaid.mermaidAPI.render("in-mermaid-graph-" + idCounter++, content);
 | 
			
		||||
 | 
			
		||||
        $renderedContent.append($(svg));
 | 
			
		||||
 
 | 
			
		||||
@@ -58,7 +58,19 @@ const FORCE_GRAPH = {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const MERMAID = {
 | 
			
		||||
    js: [ "node_modules/mermaid/dist/mermaid.min.js" ]
 | 
			
		||||
    js: [
 | 
			
		||||
        "node_modules/mermaid/dist/mermaid.min.js"        
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * The ELK extension of Mermaid.js, which supports more advanced layouts.
 | 
			
		||||
 * See https://www.npmjs.com/package/@mermaid-js/layout-elk for more information.
 | 
			
		||||
 */
 | 
			
		||||
const MERMAID_ELK = {
 | 
			
		||||
    js: [
 | 
			
		||||
        "libraries/mermaid-elk/elk.min.js"
 | 
			
		||||
    ]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const EXCALIDRAW = {
 | 
			
		||||
@@ -197,6 +209,7 @@ export default {
 | 
			
		||||
    WHEEL_ZOOM,
 | 
			
		||||
    FORCE_GRAPH,
 | 
			
		||||
    MERMAID,
 | 
			
		||||
    MERMAID_ELK,
 | 
			
		||||
    EXCALIDRAW,
 | 
			
		||||
    MARKJS,
 | 
			
		||||
    I18NEXT,
 | 
			
		||||
 
 | 
			
		||||
@@ -254,8 +254,15 @@ function goToLinkExt(evt, hrefLink, $link) {
 | 
			
		||||
                window.open(hrefLink, '_blank');
 | 
			
		||||
            } else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
 | 
			
		||||
                const electron = utils.dynamicRequire('electron');
 | 
			
		||||
 | 
			
		||||
                electron.shell.openPath(hrefLink);
 | 
			
		||||
            } else {
 | 
			
		||||
                // Enable protocols supported by CKEditor 5 to be clickable. 
 | 
			
		||||
                // Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts.
 | 
			
		||||
                // Adding `:` to these links might be safer.
 | 
			
		||||
                const otherAllowedProtocols = ['mailto:', 'tel:', 'sms:', 'sftp:', 'smb:', 'slack:', 'zotero:'];
 | 
			
		||||
                if (otherAllowedProtocols.some(protocol => hrefLink.toLowerCase().startsWith(protocol))){
 | 
			
		||||
                    window.open(hrefLink, '_blank');
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										28
									
								
								src/public/app/services/mermaid.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/public/app/services/mermaid.js
									
									
									
									
									
										Normal 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);
 | 
			
		||||
    }    
 | 
			
		||||
}
 | 
			
		||||
@@ -45,6 +45,16 @@ async function autocompleteSource(term, cb, options = {}) {
 | 
			
		||||
        ].concat(results);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (term.trim().length >= 1 && options.allowSearchNotes) {
 | 
			
		||||
        results = results.concat([
 | 
			
		||||
            {
 | 
			
		||||
                action: 'search-notes',
 | 
			
		||||
                noteTitle: term,
 | 
			
		||||
                highlightedNotePathTitle: `Search for "${utils.escapeHtml(term)}" <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
 | 
			
		||||
            }
 | 
			
		||||
        ]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
 | 
			
		||||
        results = [
 | 
			
		||||
            {
 | 
			
		||||
@@ -138,6 +148,17 @@ function initNoteAutocomplete($el, options) {
 | 
			
		||||
        autocompleteOptions.debug = true;   // don't close on blur
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (options.allowSearchNotes) {
 | 
			
		||||
        $el.on('keydown', (event) => {
 | 
			
		||||
            if (event.ctrlKey && event.key === 'Enter') {
 | 
			
		||||
                // Prevent Ctrl + Enter from triggering autoComplete.
 | 
			
		||||
                event.stopImmediatePropagation();
 | 
			
		||||
                event.preventDefault();
 | 
			
		||||
                $el.trigger('autocomplete:selected', { action: 'search-notes', noteTitle: $el.autocomplete("val")});
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    $el.autocomplete({
 | 
			
		||||
        ...autocompleteOptions,
 | 
			
		||||
        appendTo: document.querySelector('body'),
 | 
			
		||||
@@ -192,6 +213,12 @@ function initNoteAutocomplete($el, options) {
 | 
			
		||||
            suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (suggestion.action === 'search-notes') {
 | 
			
		||||
            const searchString = suggestion.noteTitle;
 | 
			
		||||
            appContext.triggerCommand('searchNotes', { searchString });
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        $el.setSelectedNotePath(suggestion.notePath);
 | 
			
		||||
        $el.setSelectedExternalLink(null);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ function parse(value) {
 | 
			
		||||
        if (token === 'promoted') {
 | 
			
		||||
            defObj.isPromoted = true;
 | 
			
		||||
        }
 | 
			
		||||
        else if (['text', 'number', 'boolean', 'date', 'datetime', 'url'].includes(token)) {
 | 
			
		||||
        else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
 | 
			
		||||
            defObj.labelType = token;
 | 
			
		||||
        }
 | 
			
		||||
        else if (['single', 'multi'].includes(token)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -125,6 +125,7 @@ const TPL = `
 | 
			
		||||
                  <option value="boolean">${t('attribute_detail.boolean')}</option>
 | 
			
		||||
                  <option value="date">${t('attribute_detail.date')}</option>
 | 
			
		||||
                  <option value="datetime">${t('attribute_detail.date_time')}</option>
 | 
			
		||||
                  <option value="time">${t('attribute_detail.time')}</option>
 | 
			
		||||
                  <option value="url">${t('attribute_detail.url')}</option>
 | 
			
		||||
                </select>
 | 
			
		||||
            </td>
 | 
			
		||||
 
 | 
			
		||||
@@ -40,6 +40,21 @@ class BasicWidget extends Component {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Conditionally adds the given components as children to this component.
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {boolean} condition whether to add the components.
 | 
			
		||||
     * @param  {...any} components the components to be added as children to this component provided the condition is truthy. 
 | 
			
		||||
     * @returns self for chaining.
 | 
			
		||||
     */
 | 
			
		||||
    optChild(condition, ...components) {
 | 
			
		||||
        if (condition) {
 | 
			
		||||
            return this.child(...components);
 | 
			
		||||
        } else {
 | 
			
		||||
            return this;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    id(id) {
 | 
			
		||||
        this.attrs.id = id;
 | 
			
		||||
        return this;
 | 
			
		||||
@@ -50,11 +65,34 @@ class BasicWidget extends Component {
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the CSS attribute of the given name to the given value.
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
 | 
			
		||||
     * @param {string} value the value of the CSS attribute to set (e.g. `12px`).
 | 
			
		||||
     * @returns self for chaining.
 | 
			
		||||
     */
 | 
			
		||||
    css(name, value) {
 | 
			
		||||
        this.attrs.style += `${name}: ${value};`;
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Sets the CSS attribute of the given name to the given value, but only if the condition provided is truthy.
 | 
			
		||||
     * 
 | 
			
		||||
     * @param {boolean} condition `true` in order to apply the CSS, `false` to ignore it.
 | 
			
		||||
     * @param {string} name the name of the CSS attribute to set (e.g. `padding-left`).
 | 
			
		||||
     * @param {string} value the value of the CSS attribute to set (e.g. `12px`).
 | 
			
		||||
     * @returns self for chaining.
 | 
			
		||||
     */
 | 
			
		||||
    optCss(condition, name, value) {
 | 
			
		||||
        if (condition) {
 | 
			
		||||
            return this.css(name, value);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    contentSized() {
 | 
			
		||||
        this.css("contain", "none");
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@ import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
 | 
			
		||||
import froca from "../services/froca.js";
 | 
			
		||||
 | 
			
		||||
export default class BookmarkButtons extends FlexContainer {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super("column");
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
        super(isHorizontalLayout ? "row" : "column");
 | 
			
		||||
 | 
			
		||||
        this.contentSized();
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,11 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.tooltip = new bootstrap.Tooltip(this.$widget, {
 | 
			
		||||
            html: true, title: () => this.getTitle(), trigger: 'hover'
 | 
			
		||||
            html: true,
 | 
			
		||||
            title: () => this.getTitle(),
 | 
			
		||||
            trigger: 'hover',
 | 
			
		||||
            placement: this.settings.titlePlacement,
 | 
			
		||||
            fallbackPlacements: [ this.settings.titlePlacement ]
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
        if (this.settings.onContextMenu) {
 | 
			
		||||
@@ -36,8 +40,6 @@ export default class AbstractButtonWidget extends NoteContextAwareWidget {
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$widget.attr("data-placement", this.settings.titlePlacement);
 | 
			
		||||
 | 
			
		||||
        super.doRender();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,13 @@ const TPL = `
 | 
			
		||||
        width: 20em;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .attachment-actions .dropdown-item .bx {
 | 
			
		||||
        position: relative;
 | 
			
		||||
        top: 3px;
 | 
			
		||||
        font-size: 120%;
 | 
			
		||||
        margin-right: 5px;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .attachment-actions .dropdown-item[disabled], .attachment-actions .dropdown-item[disabled]:hover {
 | 
			
		||||
        color: var(--muted-text-color) !important;
 | 
			
		||||
        background-color: transparent !important;
 | 
			
		||||
@@ -32,16 +39,39 @@ const TPL = `
 | 
			
		||||
        style="position: relative; top: 3px;"></button>
 | 
			
		||||
 | 
			
		||||
    <div class="dropdown-menu dropdown-menu-right">
 | 
			
		||||
        <a data-trigger-command="openAttachment" class="dropdown-item"
 | 
			
		||||
            title="${t('attachments_actions.open_externally_title')}">${t('attachments_actions.open_externally')}</a>
 | 
			
		||||
        <a data-trigger-command="openAttachmentCustom" class="dropdown-item"
 | 
			
		||||
            title="${t('attachments_actions.open_custom_title')}">${t('attachments_actions.open_custom')}</a>
 | 
			
		||||
        <a data-trigger-command="downloadAttachment" class="dropdown-item">${t('attachments_actions.download')}</a>
 | 
			
		||||
        <a data-trigger-command="renameAttachment" class="dropdown-item">${t('attachments_actions.rename_attachment')}</a>
 | 
			
		||||
        <a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">${t('attachments_actions.upload_new_revision')}</a>
 | 
			
		||||
        <a data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item">${t('attachments_actions.copy_link_to_clipboard')}</a>
 | 
			
		||||
        <a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">${t('attachments_actions.convert_attachment_into_note')}</a>
 | 
			
		||||
        <a data-trigger-command="deleteAttachment" class="dropdown-item">${t('attachments_actions.delete_attachment')}</a>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="openAttachment" class="dropdown-item"
 | 
			
		||||
            title="${t('attachments_actions.open_externally_title')}"><span class="bx bx-file-find"></span> ${t('attachments_actions.open_externally')}</li>
 | 
			
		||||
        
 | 
			
		||||
        <li data-trigger-command="openAttachmentCustom" class="dropdown-item"
 | 
			
		||||
            title="${t('attachments_actions.open_custom_title')}"><span class="bx bx-customize"></span> ${t('attachments_actions.open_custom')}</li>
 | 
			
		||||
        
 | 
			
		||||
        <li data-trigger-command="downloadAttachment" class="dropdown-item">
 | 
			
		||||
            <span class="bx bx-download"></span> ${t('attachments_actions.download')}</li>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-link">
 | 
			
		||||
            </span> ${t('attachments_actions.copy_link_to_clipboard')}</li>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="dropdown-divider"></div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item"><span class="bx bx-upload">
 | 
			
		||||
            </span> ${t('attachments_actions.upload_new_revision')}</li>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="renameAttachment" class="dropdown-item">
 | 
			
		||||
            <span class="bx bx-rename"></span> ${t('attachments_actions.rename_attachment')}</li>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="deleteAttachment" class="dropdown-item">
 | 
			
		||||
            <span class="bx bx-trash destructive-action-icon"></span> ${t('attachments_actions.delete_attachment')}</li>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <div class="dropdown-divider"></div>
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note">
 | 
			
		||||
            </span> ${t('attachments_actions.convert_attachment_into_note')}</li>
 | 
			
		||||
        
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <input type="file" class="attachment-upload-new-revision-input" style="display: none">
 | 
			
		||||
@@ -83,14 +113,14 @@ export default class AttachmentActionsWidget extends BasicWidget {
 | 
			
		||||
            const $openAttachmentButton = this.$widget.find("[data-trigger-command='openAttachment']");
 | 
			
		||||
            $openAttachmentButton
 | 
			
		||||
                .addClass("disabled")
 | 
			
		||||
                .append($('<span class="disabled-tooltip"> (?)</span>')
 | 
			
		||||
                .append($('<span class="bx bx-info-circle disabled-tooltip" />')
 | 
			
		||||
                    .attr("title", t('attachments_actions.open_externally_detail_page'))
 | 
			
		||||
                );
 | 
			
		||||
            if (isElectron) {
 | 
			
		||||
                const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
 | 
			
		||||
                $openAttachmentCustomButton
 | 
			
		||||
                    .addClass("disabled")
 | 
			
		||||
                    .append($('<span class="disabled-tooltip"> (?)</span>')
 | 
			
		||||
                    .append($('<span class="bx bx-info-circle disabled-tooltip" />')
 | 
			
		||||
                        .attr("title", t('attachments_actions.open_externally_detail_page'))
 | 
			
		||||
                    );
 | 
			
		||||
            }
 | 
			
		||||
@@ -99,7 +129,7 @@ export default class AttachmentActionsWidget extends BasicWidget {
 | 
			
		||||
            const $openAttachmentCustomButton = this.$widget.find("[data-trigger-command='openAttachmentCustom']");
 | 
			
		||||
            $openAttachmentCustomButton
 | 
			
		||||
                .addClass("disabled")
 | 
			
		||||
                .append($('<span class="disabled-tooltip"> (?)</span>')
 | 
			
		||||
                .append($('<span class="bx bx-info-circle disabled-tooltip" />')
 | 
			
		||||
                    .attr("title", t('attachments_actions.open_custom_client_only'))
 | 
			
		||||
                );
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import UpdateAvailableWidget from "./update_available.js";
 | 
			
		||||
import options from "../../services/options.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="dropdown global-menu dropend">
 | 
			
		||||
<div class="dropdown global-menu">
 | 
			
		||||
    <style>
 | 
			
		||||
    .global-menu {
 | 
			
		||||
        width: 53px;
 | 
			
		||||
@@ -100,53 +100,31 @@ const TPL = `
 | 
			
		||||
        position: relative;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        top: 5px;
 | 
			
		||||
        --dropdown-shadow-opacity: 0;
 | 
			
		||||
        --submenu-opening-delay: 0;
 | 
			
		||||
    }
 | 
			
		||||
    </style>    
 | 
			
		||||
 | 
			
		||||
    <button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
 | 
			
		||||
            aria-expanded="false" class="icon-action global-menu-button">
 | 
			
		||||
        <svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
 | 
			
		||||
            <g>
 | 
			
		||||
                <path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
 | 
			
		||||
                <path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
 | 
			
		||||
                <path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
 | 
			
		||||
            
 | 
			
		||||
                <path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
 | 
			
		||||
                <path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
 | 
			
		||||
                <path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
 | 
			
		||||
    
 | 
			
		||||
                <path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
 | 
			
		||||
                <path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
 | 
			
		||||
                <path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
 | 
			
		||||
            </g>
 | 
			
		||||
        </svg>
 | 
			
		||||
 | 
			
		||||
        <div class="global-menu-button-update-available"></div>
 | 
			
		||||
    </button>
 | 
			
		||||
 | 
			
		||||
    <ul class="dropdown-menu dropdown-menu-right">
 | 
			
		||||
        <li class="dropdown-item" data-trigger-command="showOptions">
 | 
			
		||||
            <span class="bx bx-cog"></span>
 | 
			
		||||
            ${t('global_menu.options')}
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item" data-trigger-command="openNewWindow">
 | 
			
		||||
            <span class="bx bx-window-open"></span>
 | 
			
		||||
            ${t('global_menu.open_new_window')}
 | 
			
		||||
            <kbd data-command="openNewWindow"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
 | 
			
		||||
            <span class="bx bx-mobile"></span>
 | 
			
		||||
            ${t('global_menu.switch_to_mobile_version')}
 | 
			
		||||
        <li class="dropdown-item" data-trigger-command="showShareSubtree">
 | 
			
		||||
            <span class="bx bx-share-alt"></span>
 | 
			
		||||
            ${t('global_menu.show_shared_notes_subtree')}
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
 | 
			
		||||
            <span class="bx bx-desktop"></span>
 | 
			
		||||
            ${t('global_menu.switch_to_desktop_version')}
 | 
			
		||||
        </li>
 | 
			
		||||
        <div class="dropdown-divider"></div>
 | 
			
		||||
 | 
			
		||||
        <span class="zoom-container dropdown-item">
 | 
			
		||||
        <span class="zoom-container dropdown-item dropdown-item-container">
 | 
			
		||||
            <div>
 | 
			
		||||
                <span class="bx bx-empty"></span>
 | 
			
		||||
                ${t('global_menu.zoom')}
 | 
			
		||||
@@ -165,16 +143,23 @@ const TPL = `
 | 
			
		||||
            </div>
 | 
			
		||||
        </span>
 | 
			
		||||
 | 
			
		||||
        <div class="dropdown-divider zoom-container-separator"></div>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
 | 
			
		||||
            <span class="bx bx-mobile"></span>
 | 
			
		||||
            ${t('global_menu.switch_to_mobile_version')}
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
 | 
			
		||||
            <span class="bx bx-desktop"></span>
 | 
			
		||||
            ${t('global_menu.switch_to_desktop_version')}
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
 | 
			
		||||
            <span class="bx bx-sidebar"></span>
 | 
			
		||||
            ${t('global_menu.configure_launchbar')}
 | 
			
		||||
        </li>
 | 
			
		||||
        
 | 
			
		||||
        <li class="dropdown-item" data-trigger-command="showShareSubtree">
 | 
			
		||||
            <span class="bx bx-share-alt"></span>
 | 
			
		||||
            ${t('global_menu.show_shared_notes_subtree')}
 | 
			
		||||
        </li>
 | 
			
		||||
        
 | 
			
		||||
        <li class="dropdown-item dropdown-submenu">
 | 
			
		||||
            <span class="dropdown-toggle">
 | 
			
		||||
                <span class="bx bx-chip"></span>
 | 
			
		||||
@@ -182,10 +167,22 @@ const TPL = `
 | 
			
		||||
            </span>
 | 
			
		||||
            
 | 
			
		||||
            <ul class="dropdown-menu">
 | 
			
		||||
                <li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
 | 
			
		||||
                    <span class="bx bx-bug-alt"></span>
 | 
			
		||||
                    ${t('global_menu.open_dev_tools')}
 | 
			
		||||
                    <kbd data-command="openDevTools"></kbd>
 | 
			
		||||
                <li class="dropdown-item" data-trigger-command="showHiddenSubtree">
 | 
			
		||||
                    <span class="bx bx-hide"></span>
 | 
			
		||||
                    ${t('global_menu.show_hidden_subtree')}
 | 
			
		||||
                </li>
 | 
			
		||||
 | 
			
		||||
                <li class="dropdown-item" data-trigger-command="showSearchHistory">
 | 
			
		||||
                    <span class="bx bx-search-alt"></span>
 | 
			
		||||
                    ${t('global_menu.open_search_history')}
 | 
			
		||||
                </li>
 | 
			
		||||
 | 
			
		||||
                <div class="dropdown-divider"></div>
 | 
			
		||||
 | 
			
		||||
                <li class="dropdown-item" data-trigger-command="showBackendLog">
 | 
			
		||||
                    <span class="bx bx-detail"></span>
 | 
			
		||||
                    ${t('global_menu.show_backend_log')}
 | 
			
		||||
                    <kbd data-command="showBackendLog"></kbd>
 | 
			
		||||
                </li>
 | 
			
		||||
        
 | 
			
		||||
                <li class="dropdown-item" data-trigger-command="showSQLConsole">
 | 
			
		||||
@@ -199,15 +196,12 @@ const TPL = `
 | 
			
		||||
                    ${t('global_menu.open_sql_console_history')}
 | 
			
		||||
                </li>
 | 
			
		||||
 | 
			
		||||
                <li class="dropdown-item" data-trigger-command="showSearchHistory">
 | 
			
		||||
                    <span class="bx bx-search-alt"></span>
 | 
			
		||||
                    ${t('global_menu.open_search_history')}
 | 
			
		||||
                </li>
 | 
			
		||||
                <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 class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
 | 
			
		||||
                    <span class="bx bx-bug-alt"></span>
 | 
			
		||||
                    ${t('global_menu.open_dev_tools')}
 | 
			
		||||
                    <kbd data-command="openDevTools"></kbd>
 | 
			
		||||
                </li>
 | 
			
		||||
                
 | 
			
		||||
                <li class="dropdown-item" data-trigger-command="reloadFrontendApp" 
 | 
			
		||||
@@ -217,13 +211,16 @@ const TPL = `
 | 
			
		||||
                    <kbd data-command="reloadFrontendApp"></kbd>
 | 
			
		||||
                </li>
 | 
			
		||||
                
 | 
			
		||||
                <li class="dropdown-item" data-trigger-command="showHiddenSubtree">
 | 
			
		||||
                    <span class="bx bx-hide"></span>
 | 
			
		||||
                    ${t('global_menu.show_hidden_subtree')}
 | 
			
		||||
                </li>
 | 
			
		||||
            </ul>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item" data-trigger-command="showOptions">
 | 
			
		||||
            <span class="bx bx-cog"></span>
 | 
			
		||||
            ${t('global_menu.options')}
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <div class="dropdown-divider desktop-only"></div>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item show-help-button" data-trigger-command="showHelp">
 | 
			
		||||
            <span class="bx bx-help-circle"></span>
 | 
			
		||||
            ${t('global_menu.show_help')}
 | 
			
		||||
@@ -241,6 +238,8 @@ const TPL = `
 | 
			
		||||
            <span class="version-text"></span>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <div class="dropdown-divider logout-button-separator"></div>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item logout-button" data-trigger-command="logout">
 | 
			
		||||
            <span class="bx bx-log-out"></span>
 | 
			
		||||
            ${t('global_menu.logout')}
 | 
			
		||||
@@ -250,24 +249,54 @@ const TPL = `
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default class GlobalMenuWidget extends BasicWidget {
 | 
			
		||||
    constructor() {
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.updateAvailableWidget = new UpdateAvailableWidget();
 | 
			
		||||
        this.isHorizontalLayout = isHorizontalLayout;        
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
 | 
			
		||||
        this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
 | 
			
		||||
        if (!this.isHorizontalLayout) {
 | 
			
		||||
            this.$widget.addClass("dropend");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const $globalMenuButton = this.$widget.find(".global-menu-button")
 | 
			
		||||
        if (!this.isHorizontalLayout) {
 | 
			
		||||
            $globalMenuButton.prepend($(`\
 | 
			
		||||
                <svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t('global_menu.menu')}">
 | 
			
		||||
                    <g>
 | 
			
		||||
                        <path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
 | 
			
		||||
                        <path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
 | 
			
		||||
                        <path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
 | 
			
		||||
                    
 | 
			
		||||
                        <path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
 | 
			
		||||
                        <path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
 | 
			
		||||
                        <path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
 | 
			
		||||
            
 | 
			
		||||
                        <path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
 | 
			
		||||
                        <path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
 | 
			
		||||
                        <path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
 | 
			
		||||
                    </g>
 | 
			
		||||
                </svg>`));
 | 
			
		||||
            this.tooltip = new bootstrap.Tooltip(this.$widget.find("[data-bs-toggle='tooltip']"), { trigger: "hover" });
 | 
			
		||||
        } else {
 | 
			
		||||
            $globalMenuButton.toggleClass("bx bx-menu");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"), {
 | 
			
		||||
            alignment: "bottom"
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".show-about-dialog-button").on('click', () => this.triggerCommand("openAboutDialog"));
 | 
			
		||||
 | 
			
		||||
        const isElectron = utils.isElectron();
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".logout-button").toggle(!isElectron);
 | 
			
		||||
        this.$widget.find(".logout-button-separator").toggle(!isElectron);
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".open-dev-tools-button").toggle(isElectron);
 | 
			
		||||
        this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop());
 | 
			
		||||
        this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile());
 | 
			
		||||
@@ -293,15 +322,20 @@ export default class GlobalMenuWidget extends BasicWidget {
 | 
			
		||||
 | 
			
		||||
        if (!utils.isElectron()) {
 | 
			
		||||
            this.$widget.find(".zoom-container").hide();
 | 
			
		||||
            this.$widget.find(".zoom-container-separator").hide();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$zoomState = this.$widget.find(".zoom-state");
 | 
			
		||||
        this.$widget.on('show.bs.dropdown', () => {
 | 
			
		||||
            this.updateZoomState();
 | 
			
		||||
            if (this.tooltip) {
 | 
			
		||||
                this.tooltip.hide();
 | 
			
		||||
                this.tooltip.disable();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        if (this.tooltip) {
 | 
			
		||||
            this.$widget.on('hide.bs.dropdown', () => this.tooltip.enable());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".zoom-buttons").on("click",
 | 
			
		||||
            // delay to wait for the actual zoom change
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ import CommandButtonWidget from "./command_button.js";
 | 
			
		||||
import { t } from "../../services/i18n.js";
 | 
			
		||||
 | 
			
		||||
export default class LeftPaneToggleWidget extends CommandButtonWidget {
 | 
			
		||||
    constructor() {
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.class("launcher-button");
 | 
			
		||||
@@ -20,6 +20,10 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
 | 
			
		||||
        this.settings.command = () => options.is('leftPaneVisible')
 | 
			
		||||
            ? "hideLeftPane"
 | 
			
		||||
            : "showLeftPane";
 | 
			
		||||
 | 
			
		||||
        if (isHorizontalLayout) {
 | 
			
		||||
            this.settings.titlePlacement = "bottom";
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    refreshIcon() {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,33 +20,82 @@ const TPL = `
 | 
			
		||||
            min-width: 15em;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .note-actions .dropdown-item .bx {
 | 
			
		||||
            position: relative;
 | 
			
		||||
            top: 3px;
 | 
			
		||||
            font-size: 120%;
 | 
			
		||||
            margin-right: 5px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
 | 
			
		||||
            color: var(--muted-text-color) !important;
 | 
			
		||||
            background-color: transparent !important;
 | 
			
		||||
            pointer-events: none; /* makes it unclickable */
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" 
 | 
			
		||||
        aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button>
 | 
			
		||||
    <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
 | 
			
		||||
      class="icon-action bx bx-dots-vertical-rounded"></button>
 | 
			
		||||
 | 
			
		||||
    <div class="dropdown-menu dropdown-menu-right">
 | 
			
		||||
        <a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">${t('note_actions.convert_into_attachment')}</a>
 | 
			
		||||
        <a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> ${t('note_actions.re_render_note')}</a>
 | 
			
		||||
        <a data-trigger-command="findInText" class="dropdown-item find-in-text-button">${t('note_actions.search_in_note')} <kbd data-command="findInText"></kbd></a>
 | 
			
		||||
        <a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> ${t('note_actions.note_source')}</a>
 | 
			
		||||
        <a data-trigger-command="showAttachments" class="dropdown-item show-attachments-button"><kbd data-command="showAttachments"></kbd> ${t('note_actions.note_attachments')}</a>
 | 
			
		||||
        <a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"
 | 
			
		||||
           title="${t('note_actions.open_note_externally_title')}">
 | 
			
		||||
            <kbd data-command="openNoteExternally"></kbd> 
 | 
			
		||||
            ${t('note_actions.open_note_externally')}
 | 
			
		||||
        </a>
 | 
			
		||||
        <a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> ${t('note_actions.open_note_custom')}</a>
 | 
			
		||||
        <a class="dropdown-item import-files-button">${t('note_actions.import_files')}</a>
 | 
			
		||||
        <a class="dropdown-item export-note-button">${t('note_actions.export_note')}</a>
 | 
			
		||||
        <a class="dropdown-item delete-note-button">${t('note_actions.delete_note')}</a>
 | 
			
		||||
        <a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> ${t('note_actions.print_note')}</a>
 | 
			
		||||
        <a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> ${t('note_actions.save_revision')}</a>
 | 
			
		||||
        <li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
 | 
			
		||||
            <span class="bx bx-paperclip"></span> ${t('note_actions.convert_into_attachment')}
 | 
			
		||||
        </li>
 | 
			
		||||
        
 | 
			
		||||
        <li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
 | 
			
		||||
            <span class="bx bx-extension"></span> ${t('note_actions.re_render_note')}<kbd data-command="renderActiveNote"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
 | 
			
		||||
            <span class='bx bx-search'></span> ${t('note_actions.search_in_note')}<kbd data-command="findInText"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
 | 
			
		||||
            <span class="bx bx-printer"></span> ${t('note_actions.print_note')}<kbd data-command="printActiveNote"></kbd></li>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="dropdown-divider"></div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t('note_actions.import_files')}</li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t('note_actions.export_note')}</li>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="dropdown-divider"></div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" title="${t('note_actions.open_note_externally_title')}">
 | 
			
		||||
            <span class="bx bx-file-find"></span> ${t('note_actions.open_note_externally')}<kbd data-command="openNoteExternally"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button">
 | 
			
		||||
            <span class="bx bx-customize"></span> ${t('note_actions.open_note_custom')}<kbd data-command="openNoteCustom"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="showNoteSource" class="dropdown-item show-source-button">
 | 
			
		||||
            <span class="bx bx-code"></span> ${t('note_actions.note_source')}<kbd data-command="showNoteSource"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="dropdown-divider"></div>
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        <li data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button">
 | 
			
		||||
            <span class="bx bx-save"></span> ${t('note_actions.save_revision')}<kbd data-command="forceSaveRevision"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
 | 
			
		||||
        <li class="dropdown-item delete-note-button"><span class="bx bx-trash destructive-action-icon"></span> ${t('note_actions.delete_note')}</li>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <div class="dropdown-divider"></div>
 | 
			
		||||
 | 
			
		||||
        
 | 
			
		||||
        <li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
 | 
			
		||||
            <span class="bx bx-paperclip"></span> ${t('note_actions.note_attachments')}<kbd data-command="showAttachments"></kbd>
 | 
			
		||||
        </li>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,7 @@ import BasicWidget from "../basic_widget.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="dropdown right-dropdown-widget dropend">
 | 
			
		||||
    <style>
 | 
			
		||||
    .right-dropdown-widget {
 | 
			
		||||
        height: 53px;
 | 
			
		||||
    }
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <button type="button" data-bs-toggle="dropdown" data-placement="right"
 | 
			
		||||
    <button type="button" data-bs-toggle="dropdown"
 | 
			
		||||
            aria-haspopup="true" aria-expanded="false" 
 | 
			
		||||
            class="bx right-dropdown-button launcher-button"></button>
 | 
			
		||||
    
 | 
			
		||||
@@ -25,6 +19,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
 | 
			
		||||
        this.iconClass = iconClass;
 | 
			
		||||
        this.title = title;
 | 
			
		||||
        this.dropdownTpl = dropdownTpl;
 | 
			
		||||
 | 
			
		||||
        this.settings = {
 | 
			
		||||
            titlePlacement: "right"    
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
@@ -33,7 +31,10 @@ export default class RightDropdownButtonWidget extends BasicWidget {
 | 
			
		||||
        this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
 | 
			
		||||
 | 
			
		||||
        this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
 | 
			
		||||
        this.tooltip = new bootstrap.Tooltip(this.$tooltip);
 | 
			
		||||
        this.tooltip = new bootstrap.Tooltip(this.$tooltip, {
 | 
			
		||||
            placement: this.settings.titlePlacement,
 | 
			
		||||
            fallbackPlacements: [ this.settings.titlePlacement ]
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(".right-dropdown-button")
 | 
			
		||||
            .addClass(this.iconClass)
 | 
			
		||||
 
 | 
			
		||||
@@ -10,12 +10,14 @@ import CommandButtonWidget from "../buttons/command_button.js";
 | 
			
		||||
import utils from "../../services/utils.js";
 | 
			
		||||
import TodayLauncher from "../buttons/launcher/today_launcher.js";
 | 
			
		||||
import HistoryNavigationButton from "../buttons/history_navigation.js";
 | 
			
		||||
import QuickSearchLauncherWidget from "../quick_search_launcher.js";
 | 
			
		||||
 | 
			
		||||
export default class LauncherWidget extends BasicWidget {
 | 
			
		||||
    constructor() {
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
        super();
 | 
			
		||||
 | 
			
		||||
        this.innerWidget = null;
 | 
			
		||||
        this.isHorizontalLayout = isHorizontalLayout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
@@ -63,6 +65,9 @@ export default class LauncherWidget extends BasicWidget {
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        this.child(this.innerWidget);
 | 
			
		||||
        if (this.isHorizontalLayout && this.innerWidget.settings) {
 | 
			
		||||
            this.innerWidget.settings.titlePlacement = "bottom";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
@@ -86,28 +91,30 @@ export default class LauncherWidget extends BasicWidget {
 | 
			
		||||
 | 
			
		||||
    initBuiltinWidget(note) {
 | 
			
		||||
        const builtinWidget = note.getLabelValue("builtinWidget");
 | 
			
		||||
 | 
			
		||||
        if (builtinWidget === 'calendar') {
 | 
			
		||||
        switch (builtinWidget) {
 | 
			
		||||
            case "calendar":
 | 
			
		||||
                return new CalendarWidget(note.title, note.getIcon());
 | 
			
		||||
        } else if (builtinWidget === 'spacer') {
 | 
			
		||||
            case "spacer":
 | 
			
		||||
                // || has to be inside since 0 is a valid value
 | 
			
		||||
                const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
 | 
			
		||||
                const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
 | 
			
		||||
        
 | 
			
		||||
                return new SpacerWidget(baseSize, growthFactor);
 | 
			
		||||
        } else if (builtinWidget === 'bookmarks') {
 | 
			
		||||
            return new BookmarkButtons();
 | 
			
		||||
        } else if (builtinWidget === 'protectedSession') {
 | 
			
		||||
            case "bookmarks":
 | 
			
		||||
                return new BookmarkButtons(this.isHorizontalLayout);
 | 
			
		||||
            case "protectedSession":
 | 
			
		||||
                return new ProtectedSessionStatusWidget();
 | 
			
		||||
        } else if (builtinWidget === 'syncStatus') {
 | 
			
		||||
            case "syncStatus":
 | 
			
		||||
                return new SyncStatusWidget();
 | 
			
		||||
        } else if (builtinWidget === 'backInHistoryButton') {
 | 
			
		||||
            case "backInHistoryButton":
 | 
			
		||||
                return new HistoryNavigationButton(note, "backInNoteHistory");
 | 
			
		||||
        } else if (builtinWidget === 'forwardInHistoryButton') {
 | 
			
		||||
            case "forwardInHistoryButton":
 | 
			
		||||
                return new HistoryNavigationButton(note, "forwardInNoteHistory");
 | 
			
		||||
        } else if (builtinWidget === 'todayInJournal') {
 | 
			
		||||
            case "todayInJournal":
 | 
			
		||||
                return new TodayLauncher(note);
 | 
			
		||||
        } else {
 | 
			
		||||
            case "quickSearch":
 | 
			
		||||
                return new QuickSearchLauncherWidget(this.isHorizontalLayout);
 | 
			
		||||
            default:
 | 
			
		||||
                throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,12 +4,13 @@ import appContext from "../../components/app_context.js";
 | 
			
		||||
import LauncherWidget from "./launcher.js";
 | 
			
		||||
 | 
			
		||||
export default class LauncherContainer extends FlexContainer {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('column');
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
        super(isHorizontalLayout ? "row" : "column");
 | 
			
		||||
 | 
			
		||||
        this.id('launcher-container');
 | 
			
		||||
        this.css('height', '100%');
 | 
			
		||||
        this.css(isHorizontalLayout ? "width" : 'height', '100%');
 | 
			
		||||
        this.filling();
 | 
			
		||||
        this.isHorizontalLayout = isHorizontalLayout;
 | 
			
		||||
 | 
			
		||||
        this.load();
 | 
			
		||||
    }
 | 
			
		||||
@@ -29,7 +30,7 @@ export default class LauncherContainer extends FlexContainer {
 | 
			
		||||
 | 
			
		||||
        for (const launcherNote of await visibleLaunchersRoot.getChildNotes()) {
 | 
			
		||||
            try {
 | 
			
		||||
                const launcherWidget = new LauncherWidget();
 | 
			
		||||
                const launcherWidget = new LauncherWidget(this.isHorizontalLayout);
 | 
			
		||||
                const success = await launcherWidget.initLauncher(launcherNote);
 | 
			
		||||
 | 
			
		||||
                if (success) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
     * 
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
import FlexContainer from "./flex_container.js";
 | 
			
		||||
 | 
			
		||||
export default class RootContainer extends FlexContainer {
 | 
			
		||||
    constructor() {
 | 
			
		||||
        super('row');
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
        super(isHorizontalLayout ? "column" : "row");
 | 
			
		||||
 | 
			
		||||
        this.id('root-widget');
 | 
			
		||||
        this.css('height', '100%');
 | 
			
		||||
 
 | 
			
		||||
@@ -58,6 +58,7 @@ export default class JumpToNoteDialog extends BasicWidget {
 | 
			
		||||
        noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
 | 
			
		||||
            allowCreatingNotes: true,
 | 
			
		||||
            hideGoToSelectedNoteButton: true,
 | 
			
		||||
            allowSearchNotes: true,
 | 
			
		||||
            container: this.$results
 | 
			
		||||
        })
 | 
			
		||||
            // clear any event listener added in previous invocation of this function
 | 
			
		||||
 
 | 
			
		||||
@@ -54,7 +54,7 @@ const TPL = `
 | 
			
		||||
                            data-bs-toggle="dropdown" data-bs-display="static">
 | 
			
		||||
                    </button>
 | 
			
		||||
 | 
			
		||||
                    <div class="revision-list dropdown-menu" style="position: static; height: 100%; overflow: auto;"></div>
 | 
			
		||||
                    <div class="revision-list dropdown-menu static" style="position: static; height: 100%; overflow: auto;"></div>
 | 
			
		||||
                </div>
 | 
			
		||||
 | 
			
		||||
                <div class="revision-content-wrapper">
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@
 | 
			
		||||
 | 
			
		||||
import { t } from "../services/i18n.js";
 | 
			
		||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
			
		||||
import attributeService from "../services/attributes.js";
 | 
			
		||||
import FindInText from "./find_in_text.js";
 | 
			
		||||
import FindInCode from "./find_in_code.js";
 | 
			
		||||
import FindInHtml from "./find_in_html.js";
 | 
			
		||||
@@ -16,19 +17,18 @@ const waitForEnter = (findWidgetDelayMillis < 0);
 | 
			
		||||
// the focusout handler is called with relatedTarget equal to the label instead
 | 
			
		||||
// of undefined. It's -1 instead of > 0, so they don't tabstop
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div style="contain: none;">
 | 
			
		||||
<div class='find-replace-widget' style="contain: none; border-top: 1px solid var(--main-border-color);">
 | 
			
		||||
    <style>
 | 
			
		||||
        .find-widget-box {
 | 
			
		||||
            padding: 10px;
 | 
			
		||||
            border-top: 1px solid var(--main-border-color); 
 | 
			
		||||
        .find-widget-box, .replace-widget-box {
 | 
			
		||||
            padding: 2px 10px 2px 10px;
 | 
			
		||||
            align-items: center;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .find-widget-box > * {
 | 
			
		||||
        .find-widget-box > *, .replace-widget-box > *{
 | 
			
		||||
            margin-right: 15px;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .find-widget-box {
 | 
			
		||||
        .find-widget-box, .replace-widget-box {
 | 
			
		||||
            display: flex;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -36,7 +36,7 @@ const TPL = `
 | 
			
		||||
            font-weight: bold;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        .find-widget-search-term-input-group {
 | 
			
		||||
        .find-widget-search-term-input-group, .replace-widget-replacetext-input {
 | 
			
		||||
            max-width: 300px;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
@@ -47,19 +47,23 @@ const TPL = `
 | 
			
		||||
 | 
			
		||||
    <div class="find-widget-box">
 | 
			
		||||
        <div class="input-group find-widget-search-term-input-group">
 | 
			
		||||
            <input type="text" class="form-control find-widget-search-term-input">
 | 
			
		||||
            <input type="text" class="form-control find-widget-search-term-input" placeholder="${t('find.find_placeholder')}">
 | 
			
		||||
            <button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button>
 | 
			
		||||
            <button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="form-check">
 | 
			
		||||
            <label tabIndex="-1" class="form-check-label">
 | 
			
		||||
                <input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox"> 
 | 
			
		||||
            <label tabIndex="-1" class="form-check-label">${t('find.case_sensitive')}</label>
 | 
			
		||||
                ${t('find.case_sensitive')}
 | 
			
		||||
            </label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div class="form-check">
 | 
			
		||||
            <label tabIndex="-1" class="form-check-label">
 | 
			
		||||
                <input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
 | 
			
		||||
            <label tabIndex="-1" class="form-check-label">${t('find.match_words')}</label>
 | 
			
		||||
                ${t('find.match_words')}
 | 
			
		||||
            </label>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="find-widget-found-wrapper">
 | 
			
		||||
@@ -72,6 +76,12 @@ const TPL = `
 | 
			
		||||
        
 | 
			
		||||
        <div class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="replace-widget-box" style='display: none'>
 | 
			
		||||
        <input type="text" class="form-control replace-widget-replacetext-input" placeholder="${t('find.replace_placeholder')}">
 | 
			
		||||
        <button class="btn btn-sm replace-widget-replaceall-button" type="button">${t('find.replace_all')}</button>
 | 
			
		||||
        <button class="btn btn-sm  replace-widget-replace-button" type="button">${t('find.replace')}</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class FindWidget extends NoteContextAwareWidget {
 | 
			
		||||
@@ -93,8 +103,7 @@ export default class FindWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$findBox = this.$widget.find('.find-widget-box');
 | 
			
		||||
        this.$findBox.hide();
 | 
			
		||||
        this.$widget.hide();
 | 
			
		||||
        this.$input = this.$widget.find('.find-widget-search-term-input');
 | 
			
		||||
        this.$currentFound = this.$widget.find('.find-widget-current-found');
 | 
			
		||||
        this.$totalFound = this.$widget.find('.find-widget-total-found');
 | 
			
		||||
@@ -109,6 +118,13 @@ export default class FindWidget extends NoteContextAwareWidget {
 | 
			
		||||
        this.$closeButton = this.$widget.find(".find-widget-close-button");
 | 
			
		||||
        this.$closeButton.on("click", () => this.closeSearch());
 | 
			
		||||
 | 
			
		||||
        this.$replaceWidgetBox = this.$widget.find(".replace-widget-box");
 | 
			
		||||
        this.$replaceTextInput = this.$widget.find(".replace-widget-replacetext-input");
 | 
			
		||||
        this.$replaceAllButton = this.$widget.find(".replace-widget-replaceall-button");
 | 
			
		||||
        this.$replaceAllButton.on("click", () => this.replaceAll());
 | 
			
		||||
        this.$replaceButton = this.$widget.find(".replace-widget-replace-button");
 | 
			
		||||
        this.$replaceButton.on("click", () => this.replace());
 | 
			
		||||
 | 
			
		||||
        this.$input.keydown(async e => {
 | 
			
		||||
            if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) {
 | 
			
		||||
                // If ctrl+f is pressed when the findbox is shown, select the
 | 
			
		||||
@@ -121,7 +137,7 @@ export default class FindWidget extends NoteContextAwareWidget {
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.$findBox.keydown(async e => {
 | 
			
		||||
        this.$widget.keydown(async e => {
 | 
			
		||||
            if (e.key === 'Escape') {
 | 
			
		||||
                await this.closeSearch();
 | 
			
		||||
            }
 | 
			
		||||
@@ -143,12 +159,24 @@ export default class FindWidget extends NoteContextAwareWidget {
 | 
			
		||||
 | 
			
		||||
        this.handler = await this.getHandler();
 | 
			
		||||
        
 | 
			
		||||
        const selectedText = window.getSelection().toString() || "";
 | 
			
		||||
        const isReadOnly = await this.noteContext.isReadOnly();
 | 
			
		||||
 | 
			
		||||
        this.$findBox.show();
 | 
			
		||||
        let selectedText = '';
 | 
			
		||||
        if (this.note.type === 'code' && !isReadOnly){
 | 
			
		||||
            const codeEditor = await this.noteContext.getCodeEditor();
 | 
			
		||||
            selectedText = codeEditor.getSelection();
 | 
			
		||||
        }else{
 | 
			
		||||
            selectedText = window.getSelection().toString() || "";
 | 
			
		||||
        }
 | 
			
		||||
        this.$widget.show();
 | 
			
		||||
        this.$input.focus();
 | 
			
		||||
        if (['text', 'code'].includes(this.note.type) && !isReadOnly) {
 | 
			
		||||
            this.$replaceWidgetBox.show();
 | 
			
		||||
        }else{
 | 
			
		||||
            this.$replaceWidgetBox.hide();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const isAlreadyVisible = this.$findBox.is(":visible");
 | 
			
		||||
        const isAlreadyVisible = this.$widget.is(":visible");
 | 
			
		||||
 | 
			
		||||
        if (isAlreadyVisible) {
 | 
			
		||||
            if (selectedText) {
 | 
			
		||||
@@ -254,8 +282,8 @@ export default class FindWidget extends NoteContextAwareWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async closeSearch() {
 | 
			
		||||
        if (this.$findBox.is(":visible")) {
 | 
			
		||||
            this.$findBox.hide();
 | 
			
		||||
        if (this.$widget.is(":visible")) {
 | 
			
		||||
            this.$widget.hide();
 | 
			
		||||
 | 
			
		||||
            // Restore any state, if there's a current occurrence clear markers
 | 
			
		||||
            // and scroll to and select the last occurrence
 | 
			
		||||
@@ -268,13 +296,27 @@ export default class FindWidget extends NoteContextAwareWidget {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async replace() {
 | 
			
		||||
        const replaceText = this.$replaceTextInput.val();
 | 
			
		||||
        await this.handler.replace(replaceText);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async replaceAll() {
 | 
			
		||||
        const replaceText = this.$replaceTextInput.val();
 | 
			
		||||
        await this.handler.replaceAll(replaceText);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    isEnabled() {
 | 
			
		||||
        return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async entitiesReloadedEvent({loadResults}) {
 | 
			
		||||
    async entitiesReloadedEvent({ loadResults }) {
 | 
			
		||||
        if (loadResults.isNoteContentReloaded(this.noteId)) {
 | 
			
		||||
            this.$totalFound.text("?")
 | 
			
		||||
        } else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
 | 
			
		||||
            && (attr.name.toLowerCase().includes('readonly'))
 | 
			
		||||
            && attributeService.isAffecting(attr, this.note))) {
 | 
			
		||||
            this.closeSearch();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -170,4 +170,55 @@ export default class FindInCode {
 | 
			
		||||
 | 
			
		||||
        codeEditor.focus();
 | 
			
		||||
    }
 | 
			
		||||
    async replace(replaceText) {
 | 
			
		||||
        // this.findResult may be undefined and null
 | 
			
		||||
        if (!this.findResult || this.findResult.length===0){
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        let currentFound = -1;
 | 
			
		||||
        this.findResult.forEach((marker, index) => {
 | 
			
		||||
            const pos = marker.find();
 | 
			
		||||
            if (pos) {
 | 
			
		||||
                if (marker.className === FIND_RESULT_SELECTED_CSS_CLASSNAME) {
 | 
			
		||||
                    currentFound = index;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        if (currentFound >= 0) {
 | 
			
		||||
            let marker = this.findResult[currentFound];
 | 
			
		||||
            let pos = marker.find();
 | 
			
		||||
            const codeEditor = await this.getCodeEditor();
 | 
			
		||||
            const doc = codeEditor.doc;
 | 
			
		||||
            doc.replaceRange(replaceText, pos.from, pos.to);
 | 
			
		||||
            marker.clear();
 | 
			
		||||
 | 
			
		||||
            let nextFound;
 | 
			
		||||
            if (currentFound === this.findResult.length - 1) {
 | 
			
		||||
                nextFound = 0;
 | 
			
		||||
            } else {
 | 
			
		||||
                nextFound = currentFound;
 | 
			
		||||
            }
 | 
			
		||||
            this.findResult.splice(currentFound, 1);
 | 
			
		||||
            if (this.findResult.length > 0) {
 | 
			
		||||
                this.findNext(0, nextFound, nextFound);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    async replaceAll(replaceText) {
 | 
			
		||||
        if (!this.findResult || this.findResult.length===0){
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        const codeEditor = await this.getCodeEditor();
 | 
			
		||||
        const doc = codeEditor.doc;
 | 
			
		||||
        codeEditor.operation(() => {
 | 
			
		||||
            for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
 | 
			
		||||
                let marker = this.findResult[currentFound];
 | 
			
		||||
                let pos = marker.find();
 | 
			
		||||
                doc.replaceRange(replaceText, pos.from, pos.to);
 | 
			
		||||
                marker.clear();
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        this.findResult = [];
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,7 @@ export default class FindInText {
 | 
			
		||||
        const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing');
 | 
			
		||||
        findAndReplaceEditing.state.clear(model);
 | 
			
		||||
        findAndReplaceEditing.stop();
 | 
			
		||||
        this.editingState = findAndReplaceEditing.state;
 | 
			
		||||
        if (searchTerm !== "") {
 | 
			
		||||
            // Parameters are callback/text, options.matchCase=false, options.wholeWords=false
 | 
			
		||||
            // See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44
 | 
			
		||||
@@ -29,7 +30,7 @@ export default class FindInText {
 | 
			
		||||
            // let re = new RegExp(searchTerm, 'gi');
 | 
			
		||||
            // let m = text.match(re);
 | 
			
		||||
            // totalFound = m ? m.length : 0;
 | 
			
		||||
            const options = { "matchCase" : matchCase, "wholeWords" : wholeWord };
 | 
			
		||||
            const options = { "matchCase": matchCase, "wholeWords": wholeWord };
 | 
			
		||||
            findResult = textEditor.execute('find', searchTerm, options);
 | 
			
		||||
            totalFound = findResult.results.length;
 | 
			
		||||
            // Find the result beyond the cursor
 | 
			
		||||
@@ -102,4 +103,18 @@ export default class FindInText {
 | 
			
		||||
 | 
			
		||||
        textEditor.focus();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async replace(replaceText) {
 | 
			
		||||
        if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
 | 
			
		||||
            const textEditor = await this.getTextEditor();
 | 
			
		||||
            textEditor.execute('replace', replaceText, this.editingState.highlightedResult);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async replaceAll(replaceText) {
 | 
			
		||||
        if (this.editingState !== undefined  && this.editingState.results.length > 0) {
 | 
			
		||||
            const textEditor = await this.getTextEditor();
 | 
			
		||||
            textEditor.execute('replaceAll', replaceText, this.editingState.results);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5171,7 +5171,7 @@ const icons = [
 | 
			
		||||
        "type_of_icon": "REGULAR"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "name": '_share',
 | 
			
		||||
        "name": "share",
 | 
			
		||||
        "slug": "share-regular",
 | 
			
		||||
        "category_id": 101,
 | 
			
		||||
        "type_of_icon": "REGULAR"
 | 
			
		||||
@@ -6826,7 +6826,7 @@ const icons = [
 | 
			
		||||
        "type_of_icon": "SOLID"
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        "name": '_share',
 | 
			
		||||
        "name": "share",
 | 
			
		||||
        "slug": "share-solid",
 | 
			
		||||
        "category_id": 101,
 | 
			
		||||
        "type_of_icon": "SOLID"
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import libraryLoader from "../services/library_loader.js";
 | 
			
		||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
			
		||||
import server from "../services/server.js";
 | 
			
		||||
import utils from "../services/utils.js";
 | 
			
		||||
import { loadElkIfNeeded } from "../services/mermaid.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `<div class="mermaid-widget">
 | 
			
		||||
    <style>
 | 
			
		||||
@@ -111,6 +112,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
 | 
			
		||||
                zoomOnClick: false
 | 
			
		||||
            });
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            console.warn(e);
 | 
			
		||||
            this.$errorMessage.text(e.message);
 | 
			
		||||
            this.$errorContainer.show();
 | 
			
		||||
        }
 | 
			
		||||
@@ -122,6 +124,7 @@ export default class MermaidWidget extends NoteContextAwareWidget {
 | 
			
		||||
        const blob = await this.note.getBlob();
 | 
			
		||||
        const content = blob.content || "";
 | 
			
		||||
 | 
			
		||||
        await loadElkIfNeeded(content);
 | 
			
		||||
        const {svg} = await mermaid.mermaidAPI.render(`mermaid-graph-${idCounter}`, content);
 | 
			
		||||
        return svg;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,12 +6,20 @@ import branchService from "../../services/branches.js";
 | 
			
		||||
import treeService from "../../services/tree.js";
 | 
			
		||||
import { t } from "../../services/i18n.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `<button type="button" class="action-button bx bx-menu" style="padding-top: 10px;"></button>`;
 | 
			
		||||
const TPL = `<button type="button" class="action-button bx" style="padding-top: 10px;"></button>`;
 | 
			
		||||
 | 
			
		||||
class MobileDetailMenuWidget extends BasicWidget {
 | 
			
		||||
 | 
			
		||||
    constructor(isHorizontalLayout) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.isHorizontalLayout = isHorizontalLayout;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
 | 
			
		||||
        this.$widget.addClass(this.isHorizontalLayout ? "bx-dots-vertical-rounded" : "bx-menu");
 | 
			
		||||
 | 
			
		||||
        this.$widget.on("click", async e => {
 | 
			
		||||
            const note = appContext.tabManager.getActiveContextNote();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										33
									
								
								src/public/app/widgets/quick_search_launcher.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/public/app/widgets/quick_search_launcher.js
									
									
									
									
									
										Normal 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();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -60,7 +60,7 @@ export default class ClassicEditorToolbar extends NoteContextAwareWidget {
 | 
			
		||||
            show: await this.#shouldDisplay(),
 | 
			
		||||
            activate: true,
 | 
			
		||||
            title: t("classic_editor_toolbar.title"),
 | 
			
		||||
            icon: "bx bx-edit-alt"
 | 
			
		||||
            icon: "bx bx-text"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -233,6 +233,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
 | 
			
		||||
            else if (definition.labelType === 'datetime') {
 | 
			
		||||
                $input.prop('type', 'datetime-local')
 | 
			
		||||
            }
 | 
			
		||||
            else if (definition.labelType === 'time') {
 | 
			
		||||
                $input.prop('type', 'time')
 | 
			
		||||
            }
 | 
			
		||||
            else if (definition.labelType === 'url') {
 | 
			
		||||
                $input.prop("placeholder", t("promoted_attributes.url_placeholder"));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -56,6 +56,10 @@ const TAB_ROW_TPL = `
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .tab-row-widget.full-width {
 | 
			
		||||
        background: var(--launcher-pane-background-color);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    .tab-row-widget * {
 | 
			
		||||
        box-sizing: inherit;
 | 
			
		||||
        font: inherit;
 | 
			
		||||
@@ -263,8 +267,15 @@ export default class TabRowWidget extends BasicWidget {
 | 
			
		||||
                    {title: t('tab_row.close_other_tabs'), command: "closeOtherTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.length !== 1},
 | 
			
		||||
                    {title: t('tab_row.close_right_tabs'), command: "closeRightTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.at(-1).ntxId !== ntxId},
 | 
			
		||||
                    {title: t('tab_row.close_all_tabs'), command: "closeAllTabs", uiIcon: "bx bx-empty"},
 | 
			
		||||
                    { title: "----" },
 | 
			
		||||
                    {title: t('tab_row.move_tab_to_new_window'), command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"}
 | 
			
		||||
 | 
			
		||||
                    {title: "----"},
 | 
			
		||||
 | 
			
		||||
                    {title: t('tab_row.reopen_last_tab'), command: "reopenLastTab", uiIcon: "bx bx-undo", enabled: appContext.tabManager.recentlyClosedTabs.length !== 0},
 | 
			
		||||
 | 
			
		||||
                    {title: "----"},
 | 
			
		||||
                    
 | 
			
		||||
                    {title: t('tab_row.move_tab_to_new_window'), command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"},
 | 
			
		||||
                    {title: t('tab_row.copy_tab_to_new_window'), command: "copyTabToNewWindow", uiIcon: "bx bx-empty"}
 | 
			
		||||
                ],
 | 
			
		||||
                selectMenuItemHandler: ({command}) => {
 | 
			
		||||
                    this.triggerCommand(command, {ntxId});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,45 +1,53 @@
 | 
			
		||||
import NoteContextAwareWidget from "../../note_context_aware_widget.js";
 | 
			
		||||
import server from "../../../services/server.js";
 | 
			
		||||
import { t } from "../../../services/i18n.js";
 | 
			
		||||
import AbstractCodeTypeWidget from "../abstract_code_type_widget.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `<div style="height: 100%; display: flex; flex-direction: column;">
 | 
			
		||||
    <style>
 | 
			
		||||
        .backend-log-textarea {
 | 
			
		||||
        .backend-log-editor {
 | 
			
		||||
            flex-grow: 1; 
 | 
			
		||||
            width: 100%;
 | 
			
		||||
            border: none;
 | 
			
		||||
            resize: none;
 | 
			
		||||
        }   
 | 
			
		||||
    </style>
 | 
			
		||||
 | 
			
		||||
    <textarea class="backend-log-textarea" readonly="readonly"></textarea>
 | 
			
		||||
    <pre class="backend-log-editor"></pre>
 | 
			
		||||
    
 | 
			
		||||
    <div style="display: flex; justify-content: space-around; margin-top: 10px;">
 | 
			
		||||
        <button class="refresh-backend-log-button btn btn-primary">${t("backend_log.refresh")}</button>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class BackendLogWidget extends NoteContextAwareWidget {
 | 
			
		||||
export default class BackendLogWidget extends AbstractCodeTypeWidget {
 | 
			
		||||
    doRender() {
 | 
			
		||||
        super.doRender();
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$backendLogTextArea = this.$widget.find(".backend-log-textarea");
 | 
			
		||||
        this.$editor = this.$widget.find(".backend-log-editor");
 | 
			
		||||
 | 
			
		||||
        this.$refreshBackendLog = this.$widget.find(".refresh-backend-log-button");
 | 
			
		||||
 | 
			
		||||
        this.$refreshBackendLog.on('click', () => this.load());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    scrollToBottom() {
 | 
			
		||||
        this.$backendLogTextArea.scrollTop(this.$backendLogTextArea[0].scrollHeight);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async refresh() {
 | 
			
		||||
        await this.load();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    getExtraOpts() {
 | 
			
		||||
        return {
 | 
			
		||||
            lineWrapping: false,
 | 
			
		||||
            readOnly: true
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async load() {
 | 
			
		||||
        const backendLog = await server.get('backend-log');
 | 
			
		||||
        const content = await server.get('backend-log');
 | 
			
		||||
        await this.initialized;
 | 
			
		||||
 | 
			
		||||
        this.$backendLogTextArea.text(backendLog);
 | 
			
		||||
 | 
			
		||||
        this.scrollToBottom();
 | 
			
		||||
        this._update({
 | 
			
		||||
            mime: "text/plain"            
 | 
			
		||||
        }, content);
 | 
			
		||||
        this.show();
 | 
			
		||||
        this.scrollToEnd();
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,14 @@ export default class DocTypeWidget extends TypeWidget {
 | 
			
		||||
        const docName = note.getLabelValue('docName');
 | 
			
		||||
 | 
			
		||||
        if (docName) {
 | 
			
		||||
            this.$content.load(`${window.glob.appPath}/doc_notes/${docName}.html`);
 | 
			
		||||
            // find doc based on language
 | 
			
		||||
            const lng = i18next.language;
 | 
			
		||||
            this.$content.load(`${window.glob.appPath}/doc_notes/${lng}/${docName}.html`, (response, status) => {
 | 
			
		||||
                // fallback to english doc if no translation available
 | 
			
		||||
                if (status === 'error') {
 | 
			
		||||
                    this.$content.load(`${window.glob.appPath}/doc_notes/en/${docName}.html`);
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
        } else {
 | 
			
		||||
            this.$content.empty();
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ export default class EmptyTypeWidget extends TypeWidget {
 | 
			
		||||
        noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
 | 
			
		||||
            hideGoToSelectedNoteButton: true,
 | 
			
		||||
            allowCreatingNotes: true,
 | 
			
		||||
            allowSearchNotes: true,
 | 
			
		||||
            container: this.$results
 | 
			
		||||
        })
 | 
			
		||||
            .on('autocomplete:noteselected', function(event, suggestion, dataset) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@ import OptionsWidget from "../options_widget.js";
 | 
			
		||||
import utils from "../../../../services/utils.js";
 | 
			
		||||
import { t } from "../../../../services/i18n.js";
 | 
			
		||||
 | 
			
		||||
const MIN_VALUE = 640;
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="options-section">
 | 
			
		||||
    <h4>${t("max_content_width.title")}</h4>
 | 
			
		||||
@@ -11,7 +13,7 @@ const TPL = `
 | 
			
		||||
    <div class="form-group row">
 | 
			
		||||
        <div class="col-6">
 | 
			
		||||
            <label>${t("max_content_width.max_width_label")}</label>
 | 
			
		||||
            <input type="number" min="200" step="10" class="max-content-width form-control options-number-input">
 | 
			
		||||
            <input type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
@@ -34,6 +36,6 @@ export default class MaxContentWidthOptions extends OptionsWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async optionsLoaded(options) {
 | 
			
		||||
        this.$maxContentWidth.val(options.maxContentWidth);
 | 
			
		||||
        this.$maxContentWidth.val(Math.max(MIN_VALUE, options.maxContentWidth));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,26 @@ import { t } from "../../../../services/i18n.js";
 | 
			
		||||
 | 
			
		||||
const TPL = `
 | 
			
		||||
<div class="options-section">
 | 
			
		||||
    <h4>${t("theme.layout")}</h4>
 | 
			
		||||
 | 
			
		||||
    <div class="form-group row">
 | 
			
		||||
        <div>
 | 
			
		||||
            <label>
 | 
			
		||||
                <input type="radio" name="layout-orientation" value="vertical" />
 | 
			
		||||
                <strong>${t("theme.layout-vertical-title")}</strong>
 | 
			
		||||
                - ${t("theme.layout-vertical-description")}
 | 
			
		||||
            </label>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
        <div>
 | 
			
		||||
            <label>
 | 
			
		||||
                <input type="radio" name="layout-orientation" value="horizontal" />
 | 
			
		||||
                <strong>${t("theme.layout-horizontal-title")}</strong>
 | 
			
		||||
                - ${t("theme.layout-horizontal-description")}
 | 
			
		||||
            </label>
 | 
			
		||||
        </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <h4>${t("theme.title")}</h4>
 | 
			
		||||
    
 | 
			
		||||
    <div class="form-group row">
 | 
			
		||||
@@ -27,6 +47,11 @@ export default class ThemeOptions extends OptionsWidget {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$themeSelect = this.$widget.find(".theme-select");
 | 
			
		||||
        this.$overrideThemeFonts = this.$widget.find(".override-theme-fonts");
 | 
			
		||||
        this.$layoutOrientation = this.$widget.find(`input[name="layout-orientation"]`).on("change", async () => {
 | 
			
		||||
            const newLayoutOrientation = this.$widget.find(`input[name="layout-orientation"]:checked`).val();
 | 
			
		||||
            await this.updateOption("layoutOrientation", newLayoutOrientation);
 | 
			
		||||
            utils.reloadFrontendApp("layout orientation change");
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        this.$themeSelect.on('change', async () => {
 | 
			
		||||
            const newTheme = this.$themeSelect.val();
 | 
			
		||||
@@ -57,5 +82,8 @@ export default class ThemeOptions extends OptionsWidget {
 | 
			
		||||
        this.$themeSelect.val(options.theme);
 | 
			
		||||
 | 
			
		||||
        this.setCheckboxState(this.$overrideThemeFonts, options.overrideThemeFonts);
 | 
			
		||||
 | 
			
		||||
        this.$widget.find(`input[name="layout-orientation"][value="${options.layoutOrientation}"]`)
 | 
			
		||||
            .prop("checked", "true");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,21 @@ const TPL = `
 | 
			
		||||
<div class="options-section">
 | 
			
		||||
    <h4>${t('backup.existing_backups')}</h4>
 | 
			
		||||
    
 | 
			
		||||
    <ul class="existing-backup-list"></ul>
 | 
			
		||||
    <table class="table table-stripped">
 | 
			
		||||
        <colgroup>
 | 
			
		||||
            <col width="33%" />
 | 
			
		||||
            <col />
 | 
			
		||||
        </colgroup>
 | 
			
		||||
        <thead>
 | 
			
		||||
            <tr>
 | 
			
		||||
                <th>${t("backup.date-and-time")}</th>
 | 
			
		||||
                <th>${t("backup.path")}</th>
 | 
			
		||||
            </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody class="existing-backup-list-items">
 | 
			
		||||
        </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
 | 
			
		||||
</div>
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
@@ -73,7 +87,7 @@ export default class BackupOptions extends OptionsWidget {
 | 
			
		||||
        this.$monthlyBackupEnabled.on('change', () =>
 | 
			
		||||
            this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled));
 | 
			
		||||
 | 
			
		||||
        this.$existingBackupList = this.$widget.find(".existing-backup-list");
 | 
			
		||||
        this.$existingBackupList = this.$widget.find(".existing-backup-list-items");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    optionsLoaded(options) {
 | 
			
		||||
@@ -85,11 +99,34 @@ export default class BackupOptions extends OptionsWidget {
 | 
			
		||||
            this.$existingBackupList.empty();
 | 
			
		||||
 | 
			
		||||
            if (!backupFiles.length) {
 | 
			
		||||
                backupFiles = [{filePath: t('backup.no_backup_yet'), mtime: ''}];
 | 
			
		||||
                this.$existingBackupList.append($(`
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td class="empty-table-placeholder" colspan="2">${t('backup.no_backup_yet')}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                `));
 | 
			
		||||
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Sort the backup files by modification date & time in a desceding order
 | 
			
		||||
            backupFiles.sort((a, b) => {
 | 
			
		||||
                if (a.mtime < b.mtime) return 1;
 | 
			
		||||
                if (a.mtime > b.mtime) return -1;
 | 
			
		||||
                return 0;
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const dateTimeFormatter = new Intl.DateTimeFormat(navigator.language, {
 | 
			
		||||
                dateStyle: "medium",
 | 
			
		||||
                timeStyle: "medium"
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            for (const {filePath, mtime} of backupFiles) {
 | 
			
		||||
                this.$existingBackupList.append($("<li>").text(`${filePath} ${mtime ? ` - ${mtime}` : ''}`));
 | 
			
		||||
                this.$existingBackupList.append($(`
 | 
			
		||||
                    <tr>
 | 
			
		||||
                        <td>${(mtime) ? dateTimeFormatter.format(new Date(mtime)) : "-"}</td>
 | 
			
		||||
                        <td>${filePath}</td>
 | 
			
		||||
                    </tr>
 | 
			
		||||
                `));
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -95,9 +95,9 @@ export default class EtapiOptions extends OptionsWidget {
 | 
			
		||||
                    .append($("<td>").text(token.name))
 | 
			
		||||
                    .append($("<td>").text(token.utcDateCreated))
 | 
			
		||||
                    .append($("<td>").append(
 | 
			
		||||
                        $('<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>')
 | 
			
		||||
                        $(`<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>`)
 | 
			
		||||
                            .on("click", () => this.renameToken(token.etapiTokenId, token.name)),
 | 
			
		||||
                        $('<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>')
 | 
			
		||||
                        $(`<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>`)
 | 
			
		||||
                            .on("click", () => this.deleteToken(token.etapiTokenId, token.name))
 | 
			
		||||
                    ))
 | 
			
		||||
            );
 | 
			
		||||
 
 | 
			
		||||
@@ -6,25 +6,37 @@ const TPL = `
 | 
			
		||||
<div class="options-section">
 | 
			
		||||
    <h4>${t("editing.editor_type.label")}</h4>
 | 
			
		||||
    
 | 
			
		||||
    <select class="editor-type-select form-select">
 | 
			
		||||
        <option value="ckeditor-balloon">${t("editing.editor_type.floating")}</option>
 | 
			
		||||
        <option value="ckeditor-classic">${t("editing.editor_type.fixed")}</option>
 | 
			
		||||
    </select>
 | 
			
		||||
    <div>
 | 
			
		||||
        <label>
 | 
			
		||||
            <input type="radio" name="editor-type" value="ckeditor-balloon" />
 | 
			
		||||
            <strong>${t("editing.editor_type.floating.title")}</strong>
 | 
			
		||||
            - ${t("editing.editor_type.floating.description")}
 | 
			
		||||
        </label>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div>
 | 
			
		||||
        <label>
 | 
			
		||||
            <input type="radio" name="editor-type" value="ckeditor-classic" />
 | 
			
		||||
            <strong>${t("editing.editor_type.fixed.title")}</strong>
 | 
			
		||||
            - ${t("editing.editor_type.fixed.description")}
 | 
			
		||||
        </label>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
</div>`;
 | 
			
		||||
 | 
			
		||||
export default class EditorOptions extends OptionsWidget {
 | 
			
		||||
    doRender() {
 | 
			
		||||
        this.$widget = $(TPL);
 | 
			
		||||
        this.$body = $("body");
 | 
			
		||||
        this.$editorType = this.$widget.find(".editor-type-select");
 | 
			
		||||
        this.$editorType.on('change', async () => {
 | 
			
		||||
            const newEditorType = this.$editorType.val();
 | 
			
		||||
        this.$widget.find(`input[name="editor-type"]`).on('change', async () => {
 | 
			
		||||
            const newEditorType = this.$widget.find(`input[name="editor-type"]:checked`).val();
 | 
			
		||||
            await this.updateOption('textNoteEditorType', newEditorType);
 | 
			
		||||
            utils.reloadFrontendApp("editor type change");
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async optionsLoaded(options) {
 | 
			
		||||
        this.$editorType.val(options.textNoteEditorType);
 | 
			
		||||
        this.$widget.find(`input[name="editor-type"][value="${options.textNoteEditorType}"]`)
 | 
			
		||||
                    .prop("checked", "true");
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
 | 
			
		||||
 | 
			
		||||
        await this.initialized;
 | 
			
		||||
 | 
			
		||||
        resolve(this.$content);
 | 
			
		||||
        resolve(this.$editor);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    format(html) {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,10 @@
 | 
			
		||||
    --bs-table-bg: transparent !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
:root {
 | 
			
		||||
    --submenu-opening-delay: 300ms;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html {
 | 
			
		||||
    /* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
 | 
			
		||||
    height: 100%;
 | 
			
		||||
@@ -36,6 +40,10 @@ body {
 | 
			
		||||
    font-size: var(--main-font-size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body.mobile .desktop-only {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
a {
 | 
			
		||||
    text-decoration: none;
 | 
			
		||||
}
 | 
			
		||||
@@ -229,7 +237,15 @@ div.ui-tooltip {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-divider {
 | 
			
		||||
    background-color: var(--menu-text-color);
 | 
			
		||||
    border-color: var(--dropdown-border-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes dropdown-menu-opening {
 | 
			
		||||
    from {
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
    } to {
 | 
			
		||||
        opacity: 1;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu {
 | 
			
		||||
@@ -237,6 +253,26 @@ div.ui-tooltip {
 | 
			
		||||
    color: var(--menu-text-color) !important;
 | 
			
		||||
    background-color: var(--menu-background-color) !important;
 | 
			
		||||
    font-size: inherit;
 | 
			
		||||
    box-shadow: 0px 10px 20px rgba(0, 0, 0, var(--dropdown-shadow-opacity));
 | 
			
		||||
    animation: dropdown-menu-opening 100ms ease-in;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@supports(animation-fill-mode: forwards) {
 | 
			
		||||
    /* Delay the opening of submenus */
 | 
			
		||||
    .dropdown-submenu .dropdown-menu {
 | 
			
		||||
        opacity: 0;
 | 
			
		||||
        animation-fill-mode: forwards;
 | 
			
		||||
        animation-delay: var(--submenu-opening-delay);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu.static {
 | 
			
		||||
    box-shadow: unset;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropend .dropdown-toggle::after {
 | 
			
		||||
    margin-left: .5em;
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu .disabled {
 | 
			
		||||
@@ -246,17 +282,29 @@ div.ui-tooltip {
 | 
			
		||||
 | 
			
		||||
.dropdown-menu .disabled .disabled-tooltip {
 | 
			
		||||
    pointer-events: all;
 | 
			
		||||
    color: var(--menu-text-color);
 | 
			
		||||
    margin-left: 8px;
 | 
			
		||||
    font-size: .5em;
 | 
			
		||||
    color: var(--disabled-tooltip-icon-color);
 | 
			
		||||
    cursor: help;
 | 
			
		||||
    opacity: .75;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu a:hover:not(.disabled), .dropdown-item:hover:not(.disabled) {
 | 
			
		||||
.dropdown-menu .disabled .disabled-tooltip:hover {
 | 
			
		||||
    opacity: 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu a:hover:not(.disabled), .dropdown-item:hover:not(.disabled, .dropdown-item-container) {
 | 
			
		||||
    color: var(--hover-item-text-color) !important;
 | 
			
		||||
    background-color: var(--hover-item-background-color) !important;
 | 
			
		||||
    border-color: var(--hover-item-border-color) !important;
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-item-container, .dropdown-item-container:hover, .dropdown-item-container:active {
 | 
			
		||||
    background: transparent;
 | 
			
		||||
    cursor: default;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-menu a:not(.selected) .check {
 | 
			
		||||
    visibility: hidden;
 | 
			
		||||
}
 | 
			
		||||
@@ -288,6 +336,10 @@ div.ui-tooltip {
 | 
			
		||||
    outline: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.dropdown-item .destructive-action-icon {
 | 
			
		||||
    color: var(--dropdown-item-icon-destructive-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.CodeMirror {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    background: inherit;
 | 
			
		||||
@@ -974,6 +1026,18 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
 | 
			
		||||
    left: calc(-100% + 10px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#launcher-pane.horizontal .right-dropdown-widget {
 | 
			
		||||
    width: 53px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#launcher-pane.vertical .right-dropdown-widget {
 | 
			
		||||
    height: 53px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* rotate caret on hover */
 | 
			
		||||
.dropdown-menu > li > a:hover:after {
 | 
			
		||||
    text-decoration: underline;
 | 
			
		||||
@@ -1076,8 +1140,20 @@ li.dropdown-submenu:hover > ul.dropdown-menu {
 | 
			
		||||
    border: none;
 | 
			
		||||
    color: var(--launcher-pane-text-color);
 | 
			
		||||
    background-color: var(--launcher-pane-background-color);    
 | 
			
		||||
    height: 53px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#launcher-pane.vertical .launcher-button {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 53px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#launcher-pane.horizontal .launcher-button {
 | 
			
		||||
    width: 53px;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#launcher-pane.horizontal .quick-search {
 | 
			
		||||
    width: 350px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#launcher-pane .icon-action:hover {
 | 
			
		||||
@@ -1238,3 +1314,8 @@ textarea {
 | 
			
		||||
    padding: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.empty-table-placeholder {
 | 
			
		||||
    text-align: center;
 | 
			
		||||
    color: var(--muted-text-color);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,10 @@
 | 
			
		||||
    --main-text-color: #ccc;
 | 
			
		||||
    --main-border-color: #aaa;
 | 
			
		||||
    --dropdown-border-color: #555;
 | 
			
		||||
    --dropdown-shadow-opacity: .4;
 | 
			
		||||
    --dropdown-item-icon-destructive-color: #de6e5b;
 | 
			
		||||
    --disabled-tooltip-icon-color: #7fd2ef;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    --accented-background-color: #555;
 | 
			
		||||
    --more-accented-background-color: #777;
 | 
			
		||||
 
 | 
			
		||||
@@ -21,6 +21,9 @@ html {
 | 
			
		||||
    --main-text-color: black;
 | 
			
		||||
    --main-border-color: #ccc;
 | 
			
		||||
    --dropdown-border-color: #ccc;
 | 
			
		||||
    --dropdown-shadow-opacity: .2;
 | 
			
		||||
    --dropdown-item-icon-destructive-color: #ec5138;
 | 
			
		||||
    --disabled-tooltip-icon-color: #004382;
 | 
			
		||||
 | 
			
		||||
    --accented-background-color: #f5f5f5;
 | 
			
		||||
    --more-accented-background-color: #ddd;
 | 
			
		||||
 
 | 
			
		||||
@@ -138,13 +138,13 @@ span.fancytree-node.protected > span.fancytree-custom-icon {
 | 
			
		||||
span.fancytree-node.multiple-parents.shared .fancytree-title::after {
 | 
			
		||||
    font-family: 'boxicons' !important;
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
    content: " \ec27  \ec03";
 | 
			
		||||
    content: " \eb3d  \ec03";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
span.fancytree-node.multiple-parents .fancytree-title::after {
 | 
			
		||||
    font-family: 'boxicons' !important;
 | 
			
		||||
    font-size: smaller;
 | 
			
		||||
    content: " \ec27"; /* lookup code for "star" in boxicons.css */
 | 
			
		||||
    content: " \eb3d"; /* lookup code for "link-alt" in boxicons.css */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
span.fancytree-node.shared .fancytree-title::after {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,13 @@
 | 
			
		||||
      "message": "发生了严重错误,导致客户端应用程序无法启动:\n\n{{message}}\n\n这很可能是由于脚本以意外的方式失败引起的。请尝试以安全模式启动应用程序并解决问题。"
 | 
			
		||||
    },
 | 
			
		||||
    "widget-error": {
 | 
			
		||||
      "title": "小部件初始化失败"
 | 
			
		||||
      "title": "小部件初始化失败",
 | 
			
		||||
      "message-custom": "来自 ID 为 \"{{id}}\"、标题为 \"{{title}}\" 的笔记的自定义小部件因以下原因无法初始化:\n\n{{message}}",
 | 
			
		||||
      "message-unknown": "未知小部件因以下原因无法初始化:\n\n{{message}}"
 | 
			
		||||
    },
 | 
			
		||||
    "bundle-error": {
 | 
			
		||||
      "title": "加载自定义脚本失败",
 | 
			
		||||
      "message": "来自 ID 为 \"{{id}}\"、标题为 \"{{title}}\" 的笔记的脚本因以下原因无法执行:\n\n{{message}}"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "add_link": {
 | 
			
		||||
@@ -45,7 +51,11 @@
 | 
			
		||||
    "chosen_actions": "选择的操作",
 | 
			
		||||
    "execute_bulk_actions": "执行批量操作",
 | 
			
		||||
    "bulk_actions_executed": "批量操作已成功执行。",
 | 
			
		||||
    "none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。"
 | 
			
		||||
    "none_yet": "暂无操作 ... 通过点击上方的可用操作添加一个操作。",
 | 
			
		||||
    "labels": "标签",
 | 
			
		||||
    "relations": "关联关系",
 | 
			
		||||
    "notes": "笔记",
 | 
			
		||||
    "other": "其它"
 | 
			
		||||
  },
 | 
			
		||||
  "clone_to": {
 | 
			
		||||
    "clone_notes_to": "克隆笔记到...",
 | 
			
		||||
@@ -164,7 +174,8 @@
 | 
			
		||||
    "textImportedAsText": "如果元数据不明确,将HTML、Markdown和TXT导入为文本笔记",
 | 
			
		||||
    "codeImportedAsCode": "如果元数据不明确,将识别的代码文件(例如<code>.json</code>)导入为代码笔记",
 | 
			
		||||
    "replaceUnderscoresWithSpaces": "在导入的笔记名称中将下划线替换为空格",
 | 
			
		||||
    "import": "导入"
 | 
			
		||||
    "import": "导入",
 | 
			
		||||
    "failed": "导入失败: {{message}}."
 | 
			
		||||
  },
 | 
			
		||||
  "include_note": {
 | 
			
		||||
    "dialog_title": "包含笔记",
 | 
			
		||||
@@ -304,6 +315,7 @@
 | 
			
		||||
    "boolean": "布尔值",
 | 
			
		||||
    "date": "日期",
 | 
			
		||||
    "date_time": "日期和时间",
 | 
			
		||||
    "time": "时间",
 | 
			
		||||
    "url": "网址",
 | 
			
		||||
    "precision_title": "值设置界面中浮点数后的位数。",
 | 
			
		||||
    "precision": "精度",
 | 
			
		||||
@@ -630,7 +642,10 @@
 | 
			
		||||
    "export_note": "导出笔记",
 | 
			
		||||
    "delete_note": "删除笔记",
 | 
			
		||||
    "print_note": "打印笔记",
 | 
			
		||||
    "save_revision": "保存笔记历史"
 | 
			
		||||
    "save_revision": "保存笔记历史",
 | 
			
		||||
    "convert_into_attachment_failed": "笔记 '{{title}}' 转换失败。",
 | 
			
		||||
    "convert_into_attachment_successful": "笔记 '{{title}}' 已成功转换为附件。",
 | 
			
		||||
    "convert_into_attachment_prompt": "确定要将笔记 '{{title}}' 转换为父笔记的附件吗?"
 | 
			
		||||
  },
 | 
			
		||||
  "onclick_button": {
 | 
			
		||||
    "no_click_handler": "按钮组件'{{componentId}}'没有定义点击处理程序"
 | 
			
		||||
@@ -885,7 +900,8 @@
 | 
			
		||||
    "label_rock_or_pop": "只需一个标签存在即可",
 | 
			
		||||
    "label_year_comparison": "数字比较(也包括>,>=,<)。",
 | 
			
		||||
    "label_date_created": "上个月创建的笔记",
 | 
			
		||||
    "error": "搜索错误:{{error}}"
 | 
			
		||||
    "error": "搜索错误:{{error}}",
 | 
			
		||||
    "search_prefix": "搜索:"
 | 
			
		||||
  },
 | 
			
		||||
  "attachment_detail": {
 | 
			
		||||
    "open_help_page": "打开附件帮助页面",
 | 
			
		||||
@@ -919,7 +935,15 @@
 | 
			
		||||
  },
 | 
			
		||||
  "protected_session": {
 | 
			
		||||
    "enter_password_instruction": "显示受保护的笔记需要输入您的密码:",
 | 
			
		||||
    "start_session_button": "开始受保护的会话"
 | 
			
		||||
    "start_session_button": "开始受保护的会话",
 | 
			
		||||
    "started": "受保护的会话已启动。",
 | 
			
		||||
    "wrong_password": "密码错误。",
 | 
			
		||||
    "protecting-finished-successfully": "保护操作已成功完成。",
 | 
			
		||||
    "unprotecting-finished-successfully": "解除保护操作已成功完成。",
 | 
			
		||||
    "protecting-in-progress": "保护进行中:{{count}}",
 | 
			
		||||
    "unprotecting-in-progress-count": "解除保护进行中:{{count}}",
 | 
			
		||||
    "protecting-title": "保护状态",
 | 
			
		||||
    "unprotecting-title": "解除保护状态"
 | 
			
		||||
  },
 | 
			
		||||
  "relation_map": {
 | 
			
		||||
    "open_in_new_tab": "在新标签页中打开",
 | 
			
		||||
@@ -990,7 +1014,9 @@
 | 
			
		||||
    "fill_entity_changes_button": "填充实体变更记录",
 | 
			
		||||
    "full_sync_triggered": "全量同步已触发",
 | 
			
		||||
    "filling_entity_changes": "正在填充实体变更行...",
 | 
			
		||||
    "sync_rows_filled_successfully": "同步行填充成功"
 | 
			
		||||
    "sync_rows_filled_successfully": "同步行填充成功",
 | 
			
		||||
    "finished-successfully": "同步已完成。",
 | 
			
		||||
    "failed": "同步失败:{{message}}"
 | 
			
		||||
  },
 | 
			
		||||
  "vacuum_database": {
 | 
			
		||||
    "title": "数据库清理",
 | 
			
		||||
@@ -1036,7 +1062,12 @@
 | 
			
		||||
    "theme_label": "主题",
 | 
			
		||||
    "override_theme_fonts_label": "覆盖主题字体",
 | 
			
		||||
    "light_theme": "浅色",
 | 
			
		||||
    "dark_theme": "深色"
 | 
			
		||||
    "dark_theme": "深色",
 | 
			
		||||
    "layout": "布局",
 | 
			
		||||
    "layout-vertical-title": "垂直",
 | 
			
		||||
    "layout-horizontal-title": "水平",
 | 
			
		||||
    "layout-vertical-description": "启动栏位于左侧(默认)",
 | 
			
		||||
    "layout-horizontal-description": "启动栏位于标签栏下方,标签栏现在是全宽的。"
 | 
			
		||||
 },
 | 
			
		||||
  "zoom_factor": {
 | 
			
		||||
    "title": "缩放系数(仅桌面客户端有效)",
 | 
			
		||||
@@ -1162,6 +1193,8 @@
 | 
			
		||||
    "backup_now": "立即备份",
 | 
			
		||||
    "backup_database_now": "立即备份数据库",
 | 
			
		||||
    "existing_backups": "现有备份",
 | 
			
		||||
    "date-and-time": "日期和时间",
 | 
			
		||||
    "path": "路径",
 | 
			
		||||
    "database_backed_up_to": "数据库已备份到",
 | 
			
		||||
    "no_backup_yet": "尚无备份"
 | 
			
		||||
  },
 | 
			
		||||
@@ -1309,7 +1342,9 @@
 | 
			
		||||
    "duplicate-subtree": "复制子树",
 | 
			
		||||
    "export": "导出",
 | 
			
		||||
    "import-into-note": "导入到笔记",
 | 
			
		||||
    "apply-bulk-actions": "应用批量操作"
 | 
			
		||||
    "apply-bulk-actions": "应用批量操作",
 | 
			
		||||
    "converted-to-attachments": "{{count}} 个笔记已被转换为附件。",
 | 
			
		||||
    "convert-to-attachment-confirm": "确定要将选中的笔记转换为其父笔记的附件吗?"
 | 
			
		||||
  },
 | 
			
		||||
  "shared_info": {
 | 
			
		||||
    "shared_publicly": "此笔记已公开分享在",
 | 
			
		||||
@@ -1332,7 +1367,8 @@
 | 
			
		||||
    "image": "图片",
 | 
			
		||||
    "launcher": "启动器",
 | 
			
		||||
    "doc": "文档",
 | 
			
		||||
    "widget": "小部件"
 | 
			
		||||
    "widget": "小部件",
 | 
			
		||||
    "confirm-change": "当笔记内容不为空时,不建议更改笔记类型。您仍然要继续吗?"
 | 
			
		||||
  },
 | 
			
		||||
  "protect_note": {
 | 
			
		||||
    "toggle-on": "保护笔记",
 | 
			
		||||
@@ -1355,7 +1391,11 @@
 | 
			
		||||
  "open-help-page": "打开帮助页面",
 | 
			
		||||
  "find": {
 | 
			
		||||
    "case_sensitive": "区分大小写",
 | 
			
		||||
    "match_words": "匹配单词"
 | 
			
		||||
    "match_words": "匹配单词",
 | 
			
		||||
    "find_placeholder": "在文本中查找...",
 | 
			
		||||
    "replace_placeholder": "替换为...",
 | 
			
		||||
    "replace": "替换",
 | 
			
		||||
    "replace_all": "全部替换"
 | 
			
		||||
  },
 | 
			
		||||
  "highlights_list_2": {
 | 
			
		||||
    "title": "高亮列表",
 | 
			
		||||
@@ -1378,7 +1418,9 @@
 | 
			
		||||
    "hide-archived-notes": "隐藏已归档笔记",
 | 
			
		||||
    "automatically-collapse-notes": "自动折叠笔记",
 | 
			
		||||
    "automatically-collapse-notes-title": "笔记在一段时间内未使用将被折叠,以减少树形结构的杂乱。",
 | 
			
		||||
    "save-changes": "保存并应用更改"
 | 
			
		||||
    "save-changes": "保存并应用更改",
 | 
			
		||||
    "auto-collapsing-notes-after-inactivity": "在不活动后自动折叠笔记...",
 | 
			
		||||
    "saved-search-note-refreshed": "已保存的搜索笔记已刷新。"
 | 
			
		||||
  },
 | 
			
		||||
  "title_bar_buttons": {
 | 
			
		||||
    "window-on-top": "保持此窗口置顶"
 | 
			
		||||
@@ -1407,8 +1449,11 @@
 | 
			
		||||
    "add_new_tab": "添加新标签页",
 | 
			
		||||
    "close": "关闭",
 | 
			
		||||
    "close_other_tabs": "关闭其他标签页",
 | 
			
		||||
    "close_right_tabs": "关闭右侧标签页",
 | 
			
		||||
    "close_all_tabs": "关闭所有标签页",
 | 
			
		||||
    "reopen_last_tab": "重新打开最后一个关闭的标签页",
 | 
			
		||||
    "move_tab_to_new_window": "将此标签页移动到新窗口",
 | 
			
		||||
    "copy_tab_to_new_window": "将此标签页复制到新窗口",
 | 
			
		||||
    "new_tab": "新标签页"
 | 
			
		||||
  },
 | 
			
		||||
  "toc": {
 | 
			
		||||
@@ -1422,5 +1467,101 @@
 | 
			
		||||
  },
 | 
			
		||||
  "app_context": {
 | 
			
		||||
    "please_wait_for_save": "请等待几秒钟以完成保存,然后您可以尝试再操作一次。"
 | 
			
		||||
  },
 | 
			
		||||
  "note_create": {
 | 
			
		||||
    "duplicated": "笔记 \"{{title}}\" 已被复制。"
 | 
			
		||||
  },
 | 
			
		||||
  "image": {
 | 
			
		||||
    "copied-to-clipboard": "图片的引用已复制到剪贴板,可以粘贴到任何文本笔记中。",
 | 
			
		||||
    "cannot-copy": "无法将图片引用复制到剪贴板。"
 | 
			
		||||
  },
 | 
			
		||||
  "clipboard": {
 | 
			
		||||
    "cut": "笔记已剪切到剪贴板。",
 | 
			
		||||
    "copied": "笔记已复制到剪贴板。"
 | 
			
		||||
  },
 | 
			
		||||
  "entrypoints": {
 | 
			
		||||
    "note-revision-created": "笔记修订已创建。",
 | 
			
		||||
    "note-executed": "笔记已执行。",
 | 
			
		||||
    "sql-error": "执行 SQL 查询时发生错误:{{message}}"
 | 
			
		||||
  },
 | 
			
		||||
  "branches": {
 | 
			
		||||
    "cannot-move-notes-here": "无法将笔记移动到这里。",
 | 
			
		||||
    "delete-status": "删除状态",
 | 
			
		||||
    "delete-notes-in-progress": "正在删除笔记:{{count}}",
 | 
			
		||||
    "delete-finished-successfully": "删除成功完成。",
 | 
			
		||||
    "undeleting-notes-in-progress": "正在恢复删除的笔记:{{count}}",
 | 
			
		||||
    "undeleting-notes-finished-successfully": "恢复删除的笔记已成功完成。"
 | 
			
		||||
  },
 | 
			
		||||
  "frontend_script_api": {
 | 
			
		||||
    "async_warning": "您正在将一个异步函数传递给 `api.runOnBackend()`,这可能无法按预期工作。\\n要么使该函数同步(通过移除 `async` 关键字),要么使用 `api.runAsyncOnBackendWithManualTransactionHandling()`。",
 | 
			
		||||
    "sync_warning": "您正在将一个同步函数传递给 `api.runAsyncOnBackendWithManualTransactionHandling()`,\\n而您可能应该使用 `api.runOnBackend()`。"
 | 
			
		||||
  },
 | 
			
		||||
  "ws": {
 | 
			
		||||
    "sync-check-failed": "同步检查失败!",
 | 
			
		||||
    "consistency-checks-failed": "一致性检查失败!请查看日志了解详细信息。",
 | 
			
		||||
    "encountered-error": "遇到错误 \"{{message}}\",请查看控制台。"
 | 
			
		||||
  },
 | 
			
		||||
  "hoisted_note": {
 | 
			
		||||
    "confirm_unhoisting": "请求的笔记 '{{requestedNote}}' 位于提升的笔记 '{{hoistedNote}}' 的子树之外,您必须取消提升才能访问该笔记。是否继续取消提升?"
 | 
			
		||||
  },
 | 
			
		||||
  "launcher_context_menu": {
 | 
			
		||||
    "reset_launcher_confirm": "您确定要重置 \"{{title}}\" 吗?此笔记(及其子项)中的所有数据/设置将丢失,且启动器将恢复到其原始位置。",
 | 
			
		||||
    "add-note-launcher": "添加笔记启动器",
 | 
			
		||||
    "add-script-launcher": "添加脚本启动器",
 | 
			
		||||
    "add-custom-widget": "添加自定义小部件",
 | 
			
		||||
    "add-spacer": "添加间隔",
 | 
			
		||||
    "delete": "删除",
 | 
			
		||||
    "reset": "重置",
 | 
			
		||||
    "move-to-visible-launchers": "移动到可见启动器",
 | 
			
		||||
    "move-to-available-launchers": "移动到可用启动器",
 | 
			
		||||
    "duplicate-launcher": "复制启动器"
 | 
			
		||||
  },
 | 
			
		||||
  "editable-text": {
 | 
			
		||||
    "auto-detect-language": "自动检测"
 | 
			
		||||
  },
 | 
			
		||||
  "highlighting": {
 | 
			
		||||
    "title": "文本笔记的代码语法高亮",
 | 
			
		||||
    "description": "控制文本笔记中代码块的语法高亮,代码笔记不会受到影响。",
 | 
			
		||||
    "color-scheme": "颜色方案"
 | 
			
		||||
  },
 | 
			
		||||
  "code_block": {
 | 
			
		||||
    "word_wrapping": "自动换行"
 | 
			
		||||
  },
 | 
			
		||||
  "classic_editor_toolbar": {
 | 
			
		||||
    "title": "格式化"
 | 
			
		||||
  },
 | 
			
		||||
  "editor": {
 | 
			
		||||
    "title": "编辑器"
 | 
			
		||||
  },
 | 
			
		||||
  "editing": {
 | 
			
		||||
    "editor_type": {
 | 
			
		||||
      "label": "格式化工具栏",
 | 
			
		||||
      "floating": {
 | 
			
		||||
        "title": "浮动",
 | 
			
		||||
        "description": "编辑工具出现在光标附近;"
 | 
			
		||||
      },
 | 
			
		||||
      "fixed": {
 | 
			
		||||
        "title": "固定",
 | 
			
		||||
        "description": "编辑工具出现在 \"格式化\" 功能区标签中。"
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "electron_context_menu": {
 | 
			
		||||
    "add-term-to-dictionary": "将 \"{{term}}\" 添加到字典",
 | 
			
		||||
    "cut": "剪切",
 | 
			
		||||
    "copy": "复制",
 | 
			
		||||
    "copy-link": "复制链接",
 | 
			
		||||
    "paste": "粘贴",
 | 
			
		||||
    "paste-as-plain-text": "以纯文本粘贴",
 | 
			
		||||
    "search_online": "用 {{searchEngine}} 搜索 \"{{term}}\""
 | 
			
		||||
  },
 | 
			
		||||
  "image_context_menu": {
 | 
			
		||||
    "copy_reference_to_clipboard": "复制引用到剪贴板",
 | 
			
		||||
    "copy_image_to_clipboard": "复制图片到剪贴板"
 | 
			
		||||
  },
 | 
			
		||||
  "link_context_menu": {
 | 
			
		||||
    "open_note_in_new_tab": "在新标签页中打开笔记",
 | 
			
		||||
    "open_note_in_new_split": "在新分屏中打开笔记",
 | 
			
		||||
    "open_note_in_new_window": "在新窗口中打开笔记"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -51,7 +51,11 @@
 | 
			
		||||
    "chosen_actions": "Chosen actions",
 | 
			
		||||
    "execute_bulk_actions": "Execute bulk actions",
 | 
			
		||||
    "bulk_actions_executed": "Bulk actions have been executed successfully.",
 | 
			
		||||
    "none_yet": "None yet... add an action by clicking one of the available ones above."
 | 
			
		||||
    "none_yet": "None yet... add an action by clicking one of the available ones above.",
 | 
			
		||||
    "labels": "Labels",
 | 
			
		||||
    "relations": "Relations",
 | 
			
		||||
    "notes": "Notes",
 | 
			
		||||
    "other": "Other"
 | 
			
		||||
  },
 | 
			
		||||
  "clone_to": {
 | 
			
		||||
    "clone_notes_to": "Clone notes to...",
 | 
			
		||||
@@ -238,23 +242,23 @@
 | 
			
		||||
    "confirm_undelete": "Do you want to undelete this note and its sub-notes?"
 | 
			
		||||
  },
 | 
			
		||||
  "revisions": {
 | 
			
		||||
    "note_revisions": "Note revisions",
 | 
			
		||||
    "note_revisions": "Note Revisions",
 | 
			
		||||
    "delete_all_revisions": "Delete all revisions of this note",
 | 
			
		||||
    "delete_all_button": "Delete all revisions",
 | 
			
		||||
    "help_title": "Help on Note revisions",
 | 
			
		||||
    "help_title": "Help on Note Revisions",
 | 
			
		||||
    "revision_last_edited": "This revision was last edited on {{date}}",
 | 
			
		||||
    "confirm_delete_all": "Do you want to delete all revisions of this note? This action will erase revision title and content, but still preserve revision metadata.",
 | 
			
		||||
    "confirm_delete_all": "Do you want to delete all revisions of this note? This action will erase the revision title and content, but still preserve the revision metadata.",
 | 
			
		||||
    "no_revisions": "No revisions for this note yet...",
 | 
			
		||||
    "restore_button": "Restore this revision",
 | 
			
		||||
    "confirm_restore": "Do you want to restore this revision? This will overwrite current title and content of the note with this revision.",
 | 
			
		||||
    "confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.",
 | 
			
		||||
    "delete_button": "Delete this revision",
 | 
			
		||||
    "confirm_delete": "Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.",
 | 
			
		||||
    "revisions_deleted": "Note revisions has been deleted.",
 | 
			
		||||
    "confirm_delete": "Do you want to delete this revision? This action will delete the revision title and content, but still preserve the revision metadata.",
 | 
			
		||||
    "revisions_deleted": "Note revisions have been deleted.",
 | 
			
		||||
    "revision_restored": "Note revision has been restored.",
 | 
			
		||||
    "revision_deleted": "Note revision has been deleted.",
 | 
			
		||||
    "snapshot_interval": "Note Revisions Snapshot Interval: {{seconds}}s.",
 | 
			
		||||
    "maximum_revisions": "Maximum revisions for current note: {{number}}.",
 | 
			
		||||
    "settings": "Settings for Note revisions",
 | 
			
		||||
    "snapshot_interval": "Note Revision Snapshot Interval: {{seconds}}s.",
 | 
			
		||||
    "maximum_revisions": "Note Revision Snapshot Limit: {{number}}.",
 | 
			
		||||
    "settings": "Note Revision Settings",
 | 
			
		||||
    "download_button": "Download",
 | 
			
		||||
    "mime": "MIME: ",
 | 
			
		||||
    "file_size": "File size:",
 | 
			
		||||
@@ -311,6 +315,7 @@
 | 
			
		||||
    "boolean": "Boolean",
 | 
			
		||||
    "date": "Date",
 | 
			
		||||
    "date_time": "Date & Time",
 | 
			
		||||
    "time": "Time",
 | 
			
		||||
    "url": "URL",
 | 
			
		||||
    "precision_title": "What number of digits after floating point should be available in the value setting interface.",
 | 
			
		||||
    "precision": "Precision",
 | 
			
		||||
@@ -1057,7 +1062,12 @@
 | 
			
		||||
    "theme_label": "Theme",
 | 
			
		||||
    "override_theme_fonts_label": "Override theme fonts",
 | 
			
		||||
    "light_theme": "Light",
 | 
			
		||||
    "dark_theme": "Dark"
 | 
			
		||||
    "dark_theme": "Dark",
 | 
			
		||||
    "layout": "Layout",
 | 
			
		||||
    "layout-vertical-title": "Vertical",
 | 
			
		||||
    "layout-horizontal-title": "Horizontal",
 | 
			
		||||
    "layout-vertical-description": "launcher bar is on the left (default)",
 | 
			
		||||
    "layout-horizontal-description": "launcher bar is underneath the tab bar, the tab bar is now full width."
 | 
			
		||||
  },
 | 
			
		||||
  "zoom_factor": {
 | 
			
		||||
    "title": "Zoom Factor (desktop build only)",
 | 
			
		||||
@@ -1108,12 +1118,12 @@
 | 
			
		||||
    "deleted_notes_erased": "Deleted notes have been erased."
 | 
			
		||||
  },
 | 
			
		||||
  "revisions_snapshot_interval": {
 | 
			
		||||
    "note_revisions_snapshot_interval_title": "Note Revisions Snapshot Interval",
 | 
			
		||||
    "note_revisions_snapshot_description": "Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
 | 
			
		||||
    "note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
 | 
			
		||||
    "note_revisions_snapshot_description": "The Note revision snapshot interval is the time in seconds after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
 | 
			
		||||
    "snapshot_time_interval_label": "Note revision snapshot time interval (in seconds):"
 | 
			
		||||
  },
 | 
			
		||||
  "revisions_snapshot_limit": {
 | 
			
		||||
    "note_revisions_snapshot_limit_title": "Note Revision Snapshots Limit",
 | 
			
		||||
    "note_revisions_snapshot_limit_title": "Note Revision Snapshot Limit",
 | 
			
		||||
    "note_revisions_snapshot_limit_description": "The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions. You can set the maximum revisions for a single note through the #versioningLimit label.",
 | 
			
		||||
    "snapshot_number_limit_label": "Note revision snapshot number limit:",
 | 
			
		||||
    "erase_excess_revision_snapshots": "Erase excess revision snapshots now",
 | 
			
		||||
@@ -1183,6 +1193,8 @@
 | 
			
		||||
    "backup_now": "Backup now",
 | 
			
		||||
    "backup_database_now": "Backup database now",
 | 
			
		||||
    "existing_backups": "Existing backups",
 | 
			
		||||
    "date-and-time": "Date & time",
 | 
			
		||||
    "path": "Path",
 | 
			
		||||
    "database_backed_up_to": "Database has been backed up to",
 | 
			
		||||
    "no_backup_yet": "no backup yet"
 | 
			
		||||
  },
 | 
			
		||||
@@ -1378,8 +1390,12 @@
 | 
			
		||||
  },
 | 
			
		||||
  "open-help-page": "Open help page",
 | 
			
		||||
  "find": {
 | 
			
		||||
    "case_sensitive": "case sensitive",
 | 
			
		||||
    "match_words": "match words"
 | 
			
		||||
    "case_sensitive": "Case sensitive",
 | 
			
		||||
    "match_words": "Match words",
 | 
			
		||||
    "find_placeholder": "Find in text...",
 | 
			
		||||
    "replace_placeholder": "Replace with...",
 | 
			
		||||
    "replace": "Replace",
 | 
			
		||||
    "replace_all": "Replace all"
 | 
			
		||||
  },
 | 
			
		||||
  "highlights_list_2": {
 | 
			
		||||
    "title": "Highlights List",
 | 
			
		||||
@@ -1435,7 +1451,9 @@
 | 
			
		||||
    "close_other_tabs": "Close other tabs",
 | 
			
		||||
    "close_right_tabs": "Close tabs to the right",
 | 
			
		||||
    "close_all_tabs": "Close all tabs",
 | 
			
		||||
    "reopen_last_tab": "Reopen last closed tab",
 | 
			
		||||
    "move_tab_to_new_window": "Move this tab to a new window",
 | 
			
		||||
    "copy_tab_to_new_window": "Copy this tab to a new window",
 | 
			
		||||
    "new_tab": "New tab"
 | 
			
		||||
  },
 | 
			
		||||
  "toc": {
 | 
			
		||||
@@ -1518,8 +1536,32 @@
 | 
			
		||||
  "editing": {
 | 
			
		||||
    "editor_type": {
 | 
			
		||||
      "label": "Formatting toolbar",
 | 
			
		||||
      "floating": "Floating (editing tools appear near the cursor)",
 | 
			
		||||
      "fixed": "Fixed (editing tools appear in the \"Formatting\" ribbon tab)"
 | 
			
		||||
      "floating": {
 | 
			
		||||
        "title": "Floating",
 | 
			
		||||
        "description": "editing tools appear near the cursor;"
 | 
			
		||||
      },
 | 
			
		||||
      "fixed": {
 | 
			
		||||
        "title": "Fixed",
 | 
			
		||||
        "description": "editing tools appear in the \"Formatting\" ribbon tab."
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "electron_context_menu": {
 | 
			
		||||
    "add-term-to-dictionary": "Add \"{{term}}\" to dictionary",
 | 
			
		||||
    "cut": "Cut",
 | 
			
		||||
    "copy": "Copy",
 | 
			
		||||
    "copy-link": "Copy link",
 | 
			
		||||
    "paste": "Paste",
 | 
			
		||||
    "paste-as-plain-text": "Paste as plain text",
 | 
			
		||||
    "search_online": "Search for \"{{term}}\" with {{searchEngine}}"
 | 
			
		||||
  },
 | 
			
		||||
  "image_context_menu": {
 | 
			
		||||
    "copy_reference_to_clipboard": "Copy reference to clipboard",
 | 
			
		||||
    "copy_image_to_clipboard": "Copy image to clipboard"
 | 
			
		||||
  },
 | 
			
		||||
  "link_context_menu": {
 | 
			
		||||
    "open_note_in_new_tab": "Open note in a new tab",
 | 
			
		||||
    "open_note_in_new_split": "Open note in a new split",
 | 
			
		||||
    "open_note_in_new_window": "Open note in a new window"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,11 @@
 | 
			
		||||
    "chosen_actions": "Acciones elegidas",
 | 
			
		||||
    "execute_bulk_actions": "Ejecutar acciones en bloque",
 | 
			
		||||
    "bulk_actions_executed": "Las acciones en bloque se han ejecutado con éxito.",
 | 
			
		||||
    "none_yet": "Ninguna todavía... agrega una acción haciendo clic en una de las disponibles arriba."
 | 
			
		||||
    "none_yet": "Ninguna todavía... agregue una acción haciendo clic en una de las disponibles arriba.",
 | 
			
		||||
    "labels": "Etiquetas",
 | 
			
		||||
    "relations": "Relaciones",
 | 
			
		||||
    "notes": "Notas",
 | 
			
		||||
    "other": "Otro"
 | 
			
		||||
  },
 | 
			
		||||
  "clone_to": {
 | 
			
		||||
    "clone_notes_to": "Clonar notas a...",
 | 
			
		||||
@@ -311,6 +315,7 @@
 | 
			
		||||
    "boolean": "Booleano",
 | 
			
		||||
    "date": "Fecha",
 | 
			
		||||
    "date_time": "Fecha y hora",
 | 
			
		||||
    "time": "Hora",
 | 
			
		||||
    "url": "URL",
 | 
			
		||||
    "precision_title": "Cantidad de dígitos después del punto flotante que deben estar disponibles en la interfaz de configuración del valor.",
 | 
			
		||||
    "precision": "Precisión",
 | 
			
		||||
@@ -1057,7 +1062,12 @@
 | 
			
		||||
    "theme_label": "Tema",
 | 
			
		||||
    "override_theme_fonts_label": "Sobreescribir fuentes de tema",
 | 
			
		||||
    "light_theme": "Claro",
 | 
			
		||||
    "dark_theme": "Oscuro"
 | 
			
		||||
    "dark_theme": "Oscuro",
 | 
			
		||||
    "layout": "Disposición",
 | 
			
		||||
    "layout-vertical-title": "Vertical",
 | 
			
		||||
    "layout-horizontal-title": "Horizontal",
 | 
			
		||||
    "layout-vertical-description": "la barra del lanzador está en la izquierda (por defecto)",
 | 
			
		||||
    "layout-horizontal-description": "la barra de lanzamiento está debajo de la barra de pestañas, la barra de pestañas ahora tiene ancho completo."
 | 
			
		||||
  },
 | 
			
		||||
  "zoom_factor": {
 | 
			
		||||
    "title": "Factor de zoom (solo versión de escritorio)",
 | 
			
		||||
@@ -1183,6 +1193,8 @@
 | 
			
		||||
    "backup_now": "Realizar copia de seguridad ahora",
 | 
			
		||||
    "backup_database_now": "Realizar copia de seguridad de la base de datos ahora",
 | 
			
		||||
    "existing_backups": "Copias de seguridad existentes",
 | 
			
		||||
    "date-and-time": "Fecha y hora",
 | 
			
		||||
    "path": "Ruta",
 | 
			
		||||
    "database_backed_up_to": "Se ha realizado una copia de seguridad de la base de datos en",
 | 
			
		||||
    "no_backup_yet": "no hay copia de seguridad todavía"
 | 
			
		||||
  },
 | 
			
		||||
@@ -1378,8 +1390,12 @@
 | 
			
		||||
  },
 | 
			
		||||
  "open-help-page": "Abrir página de ayuda",
 | 
			
		||||
  "find": {
 | 
			
		||||
    "case_sensitive": "distingue entre mayúsculas y minúsculas",
 | 
			
		||||
    "match_words": "coincidir palabras"
 | 
			
		||||
    "case_sensitive": "Distingue entre mayúsculas y minúsculas",
 | 
			
		||||
    "match_words": "Coincidir palabras",
 | 
			
		||||
    "find_placeholder": "Encontrar en texto...",
 | 
			
		||||
    "replace_placeholder": "Reemplazar con...",
 | 
			
		||||
    "replace": "Reemplazar",
 | 
			
		||||
    "replace_all": "Reemplazar todo"
 | 
			
		||||
  },
 | 
			
		||||
  "highlights_list_2": {
 | 
			
		||||
    "title": "Lista de destacados",
 | 
			
		||||
@@ -1435,7 +1451,9 @@
 | 
			
		||||
    "close_other_tabs": "Cerrar otras pestañas",
 | 
			
		||||
    "close_right_tabs": "Cerrar pestañas a la derecha",
 | 
			
		||||
    "close_all_tabs": "Cerras todas las pestañas",
 | 
			
		||||
    "reopen_last_tab": "Reabrir última pestaña cerrada",
 | 
			
		||||
    "move_tab_to_new_window": "Mover esta pestaña a una nueva ventana",
 | 
			
		||||
    "copy_tab_to_new_window": "Copiar esta pestaña a una ventana nueva",
 | 
			
		||||
    "new_tab": "Nueva pestaña"
 | 
			
		||||
  },
 | 
			
		||||
  "toc": {
 | 
			
		||||
@@ -1518,8 +1536,23 @@
 | 
			
		||||
  "editing": {
 | 
			
		||||
    "editor_type": {
 | 
			
		||||
      "label": "Barra de herramientas de formato",
 | 
			
		||||
      "floating": "Flotante (las herramientas de edición aparecen cerca del cursor)",
 | 
			
		||||
      "fixed": "Fijo (las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")"
 | 
			
		||||
      "floating": {
 | 
			
		||||
        "title": "Flotante",
 | 
			
		||||
        "description": "las herramientas de edición aparecen cerca del cursor;"
 | 
			
		||||
      },
 | 
			
		||||
      "fixed": {
 | 
			
		||||
        "title": "Fijo",
 | 
			
		||||
        "description": "las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")."
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "electron_context_menu": {
 | 
			
		||||
    "add-term-to-dictionary": "Agregar \"{{term}}\" al diccionario.",
 | 
			
		||||
    "cut": "Cortar",
 | 
			
		||||
    "copy": "Copiar",
 | 
			
		||||
    "copy-link": "Copiar enlace",
 | 
			
		||||
    "paste": "Pegar",
 | 
			
		||||
    "paste-as-plain-text": "Pegar como texto plano",
 | 
			
		||||
    "search_online": "Buscar \"{{term}}\" con {{searchEngine}}"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -311,6 +311,7 @@
 | 
			
		||||
    "boolean": "Booléen",
 | 
			
		||||
    "date": "Date",
 | 
			
		||||
    "date_time": "Date et heure",
 | 
			
		||||
    "time": "Heure",
 | 
			
		||||
    "url": "URL",
 | 
			
		||||
    "precision_title": "Nombre de chiffres après la virgule devant être disponible dans l'interface définissant la valeur.",
 | 
			
		||||
    "precision": "Précision",
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								src/public/translations/pt_br/translation.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/public/translations/pt_br/translation.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{}
 | 
			
		||||
@@ -127,6 +127,7 @@
 | 
			
		||||
    "custom_resource_provider": "a se vedea <a href=\"javascript:\" data-help-page=\"custom-request-handler.html\">Custom request handler</a>",
 | 
			
		||||
    "date": "Dată",
 | 
			
		||||
    "date_time": "Dată și timp",
 | 
			
		||||
    "time": "Timp",
 | 
			
		||||
    "delete": "Șterge",
 | 
			
		||||
    "digits": "număr de zecimale",
 | 
			
		||||
    "disable_inclusion": "script-urile cu această etichetă nu vor fi incluse în execuția scriptului părinte.",
 | 
			
		||||
@@ -254,6 +255,8 @@
 | 
			
		||||
    "enable_monthly_backup": "Activează copia de siguranță lunară",
 | 
			
		||||
    "enable_weekly_backup": "Activează copia de siguranță săptămânală",
 | 
			
		||||
    "existing_backups": "Copii de siguranță existente",
 | 
			
		||||
    "date-and-time": "Data și ora",
 | 
			
		||||
    "path": "Calea fișierului",
 | 
			
		||||
    "no_backup_yet": "nu există încă nicio copie de siguranță"
 | 
			
		||||
  },
 | 
			
		||||
  "basic_properties": {
 | 
			
		||||
@@ -297,7 +300,11 @@
 | 
			
		||||
    "close": "Închide",
 | 
			
		||||
    "execute_bulk_actions": "Execută acțiunile în masă",
 | 
			
		||||
    "include_descendants": "Include descendenții notiței selectate",
 | 
			
		||||
    "none_yet": "Nicio acțiune... adaugați una printr-un click pe cele disponibile mai jos."
 | 
			
		||||
    "none_yet": "Nicio acțiune... adăugați una printr-un click pe cele disponibile mai jos.",
 | 
			
		||||
    "labels": "Etichete",
 | 
			
		||||
    "notes": "Notițe",
 | 
			
		||||
    "other": "Altele",
 | 
			
		||||
    "relations": "Relații"
 | 
			
		||||
  },
 | 
			
		||||
  "calendar": {
 | 
			
		||||
    "april": "Aprilie",
 | 
			
		||||
@@ -1349,7 +1356,11 @@
 | 
			
		||||
  "open-help-page": "Deschide pagina de informații",
 | 
			
		||||
  "find": {
 | 
			
		||||
    "match_words": "doar cuvinte întregi",
 | 
			
		||||
    "case_sensitive": "ține cont de majuscule"
 | 
			
		||||
    "case_sensitive": "ține cont de majuscule",
 | 
			
		||||
    "replace_all": "Înlocuiește totul",
 | 
			
		||||
    "replace_placeholder": "Înlocuiește cu...",
 | 
			
		||||
    "replace": "Înlocuiește",
 | 
			
		||||
    "find_placeholder": "Căutați în text..."
 | 
			
		||||
  },
 | 
			
		||||
  "highlights_list_2": {
 | 
			
		||||
    "options": "Setări",
 | 
			
		||||
@@ -1439,7 +1450,9 @@
 | 
			
		||||
    "close_tab": "Închide tab",
 | 
			
		||||
    "move_tab_to_new_window": "Mută acest tab în altă fereastră",
 | 
			
		||||
    "new_tab": "Tab nou",
 | 
			
		||||
    "close_right_tabs": "Închide taburile din dreapta"
 | 
			
		||||
    "close_right_tabs": "Închide taburile din dreapta",
 | 
			
		||||
    "copy_tab_to_new_window": "Copiază tab-ul într-o fereastră nouă",
 | 
			
		||||
    "reopen_last_tab": "Redeschide ultimul tab închis"
 | 
			
		||||
  },
 | 
			
		||||
  "toc": {
 | 
			
		||||
    "options": "Setări",
 | 
			
		||||
@@ -1514,12 +1527,27 @@
 | 
			
		||||
  },
 | 
			
		||||
  "editing": {
 | 
			
		||||
    "editor_type": {
 | 
			
		||||
      "fixed": "Editor cu bară fixă (uneltele de editare vor apărea în tab-ul „Formatare” din panglică)",
 | 
			
		||||
      "floating": "Editor cu bară flotantă (uneltele de editare vor apărea lângă cursor)",
 | 
			
		||||
      "label": "Bară de formatare"
 | 
			
		||||
      "label": "Bară de formatare",
 | 
			
		||||
      "floating": {
 | 
			
		||||
        "title": "Editor cu bară flotantă",
 | 
			
		||||
        "description": "uneltele de editare vor apărea lângă cursor;"
 | 
			
		||||
      },
 | 
			
		||||
      "fixed": {
 | 
			
		||||
        "title": "Editor cu bară fixă",
 | 
			
		||||
        "description": "uneltele de editare vor apărea în tab-ul „Formatare” din panglică."
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "editor": {
 | 
			
		||||
    "title": "Editor"
 | 
			
		||||
  },
 | 
			
		||||
  "electron_context_menu": {
 | 
			
		||||
    "add-term-to-dictionary": "Adaugă „{{term}}” în dicționar",
 | 
			
		||||
    "copy": "Copiază",
 | 
			
		||||
    "copy-link": "Copiază legătura",
 | 
			
		||||
    "cut": "Decupează",
 | 
			
		||||
    "paste": "Lipește",
 | 
			
		||||
    "paste-as-plain-text": "Lipește doar textul",
 | 
			
		||||
    "search_online": "Caută „{{term}}” cu {{searchEngine}}"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								src/public/translations/tw/translation.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/public/translations/tw/translation.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
{}
 | 
			
		||||
@@ -66,7 +66,8 @@ const ALLOWED_OPTIONS = new Set([
 | 
			
		||||
    'editedNotesOpenInRibbon',
 | 
			
		||||
    'locale',
 | 
			
		||||
    'firstDayOfWeek',
 | 
			
		||||
    'textNoteEditorType'
 | 
			
		||||
    'textNoteEditorType',
 | 
			
		||||
    'layoutOrientation'
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
function getOptions() {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ function index(req: Request, res: Response) {
 | 
			
		||||
        isDev: env.isDev(),
 | 
			
		||||
        isMainWindow: !req.query.extraWindow,
 | 
			
		||||
        isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
 | 
			
		||||
        maxContentWidth: parseInt(options.maxContentWidth),
 | 
			
		||||
        maxContentWidth: Math.max(640, parseInt(options.maxContentWidth)),
 | 
			
		||||
        triliumVersion: packageJson.version,
 | 
			
		||||
        assetPath: assetPath,
 | 
			
		||||
        appPath: appPath
 | 
			
		||||
 
 | 
			
		||||
@@ -34,8 +34,17 @@ interface Item {
 | 
			
		||||
    baseSize?: string;
 | 
			
		||||
    growthFactor?: string;
 | 
			
		||||
    targetNoteId?: "_backendLog" | "_globalNoteMap";
 | 
			
		||||
    builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar";
 | 
			
		||||
    command?: "jumpToNote" | "searchNotes" | "createNoteIntoInbox" | "showRecentChanges";
 | 
			
		||||
    builtinWidget?: "bookmarks" | "spacer" | "backInHistoryButton" | "forwardInHistoryButton" | "syncStatus" | "protectedSession" | "todayInJournal" | "calendar" | "quickSearch";
 | 
			
		||||
    command?: keyof typeof Command;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: Move this into a commons project once the client/server architecture is well split.
 | 
			
		||||
enum Command {
 | 
			
		||||
    jumpToNote,
 | 
			
		||||
    searchNotes,
 | 
			
		||||
    createNoteIntoInbox,
 | 
			
		||||
    showRecentChanges,
 | 
			
		||||
    showOptions
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
@@ -94,7 +103,8 @@ const HIDDEN_SUBTREE_DEFINITION: Item = {
 | 
			
		||||
            type: 'contentWidget',
 | 
			
		||||
            icon: 'bx-terminal',
 | 
			
		||||
            attributes: [
 | 
			
		||||
                { type: 'label', name: 'keepCurrentHoisting' }
 | 
			
		||||
                { type: 'label', name: 'keepCurrentHoisting' },
 | 
			
		||||
                { type: 'label', name: 'fullContentWidth' }
 | 
			
		||||
            ]
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
@@ -231,8 +241,10 @@ const HIDDEN_SUBTREE_DEFINITION: Item = {
 | 
			
		||||
                        { id: '_lbBookmarks', title: 'Bookmarks', type: 'launcher', builtinWidget: 'bookmarks', icon: 'bx bx-bookmark' },
 | 
			
		||||
                        { id: '_lbToday', title: "Open Today's Journal Note", type: 'launcher', builtinWidget: 'todayInJournal', icon: 'bx bx-calendar-star' },
 | 
			
		||||
                        { id: '_lbSpacer2', title: 'Spacer', type: 'launcher', builtinWidget: 'spacer', baseSize: "0", growthFactor: "1" },
 | 
			
		||||
                        { id: '_lbQuickSearch', title: "Quick Search", type: "launcher", builtinWidget: "quickSearch", icon: "bx bx-rectangle" },
 | 
			
		||||
                        { id: '_lbProtectedSession', title: 'Protected Session', type: 'launcher', builtinWidget: 'protectedSession', icon: 'bx bx bx-shield-quarter' },
 | 
			
		||||
                        { id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' }
 | 
			
		||||
                        { id: '_lbSyncStatus', title: 'Sync Status', type: 'launcher', builtinWidget: 'syncStatus', icon: 'bx bx-wifi' },
 | 
			
		||||
                        { id: '_lbSettings', title: 'Settings', type: 'launcher', command: 'showOptions', icon: 'bx bx-cog' }
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
            ]
 | 
			
		||||
 
 | 
			
		||||
@@ -134,7 +134,9 @@ const defaultOptions: DefaultOption[] = [
 | 
			
		||||
    { name: "codeBlockWordWrap", value: "false", isSynced: true },
 | 
			
		||||
 | 
			
		||||
    // Text note configuration
 | 
			
		||||
    { name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true }
 | 
			
		||||
    { name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true },
 | 
			
		||||
 | 
			
		||||
    { name: "layoutOrientation", value: "vertical", isSynced: false }
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ function parse(value: string): DefinitionObject {
 | 
			
		||||
        if (token === 'promoted') {
 | 
			
		||||
            defObj.isPromoted = true;
 | 
			
		||||
        }
 | 
			
		||||
        else if (['text', 'number', 'boolean', 'date', 'datetime', 'url'].includes(token)) {
 | 
			
		||||
        else if (['text', 'number', 'boolean', 'date', 'datetime', 'time', 'url'].includes(token)) {
 | 
			
		||||
            defObj.labelType = token;
 | 
			
		||||
        }
 | 
			
		||||
        else if (['single', 'multi'].includes(token)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -27,43 +27,52 @@ class SearchResult {
 | 
			
		||||
        this.score = 0;
 | 
			
		||||
 | 
			
		||||
        const note = becca.notes[this.noteId];
 | 
			
		||||
        const normalizedQuery = fulltextQuery.toLowerCase();
 | 
			
		||||
        const normalizedTitle = note.title.toLowerCase();
 | 
			
		||||
 | 
			
		||||
        // Note ID exact match, much higher score
 | 
			
		||||
        if (note.noteId.toLowerCase() === fulltextQuery) {
 | 
			
		||||
            this.score += 100;
 | 
			
		||||
            this.score += 1000;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (note.title.toLowerCase() === fulltextQuery) {
 | 
			
		||||
            this.score += 100; // high reward for exact match #3470
 | 
			
		||||
        // Title matching scores, make sure to always win
 | 
			
		||||
        if (normalizedTitle === normalizedQuery) {
 | 
			
		||||
            this.score += 2000; // Increased from 1000 to ensure exact matches always win
 | 
			
		||||
        }
 | 
			
		||||
        else if (normalizedTitle.startsWith(normalizedQuery)) {
 | 
			
		||||
            this.score += 500;  // Increased to give more weight to prefix matches
 | 
			
		||||
        }
 | 
			
		||||
        else if (normalizedTitle.includes(` ${normalizedQuery} `) || 
 | 
			
		||||
                normalizedTitle.startsWith(`${normalizedQuery} `) || 
 | 
			
		||||
                normalizedTitle.endsWith(` ${normalizedQuery}`)) {
 | 
			
		||||
            this.score += 300;  // Increased to better distinguish word matches
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // notes with matches on its own note title as opposed to ancestors or descendants
 | 
			
		||||
        this.addScoreForStrings(tokens, note.title, 1.5);
 | 
			
		||||
 | 
			
		||||
        // matches in attributes don't get extra points and thus are implicitly valued less than note path matches
 | 
			
		||||
 | 
			
		||||
        this.addScoreForStrings(tokens, this.notePathTitle, 1);
 | 
			
		||||
        // Add scores for partial matches with adjusted weights
 | 
			
		||||
        this.addScoreForStrings(tokens, note.title, 2.0);  // Increased to give more weight to title matches
 | 
			
		||||
        this.addScoreForStrings(tokens, this.notePathTitle, 0.3); // Reduced to further de-emphasize path matches
 | 
			
		||||
 | 
			
		||||
        if (note.isInHiddenSubtree()) {
 | 
			
		||||
            this.score = this.score / 2;
 | 
			
		||||
            this.score = this.score / 3; // Increased penalty for hidden notes
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    addScoreForStrings(tokens: string[], str: string, factor: number) {
 | 
			
		||||
        const chunks = str.toLowerCase().split(" ");
 | 
			
		||||
 | 
			
		||||
        this.score = 0;
 | 
			
		||||
 | 
			
		||||
        let tokenScore = 0;
 | 
			
		||||
        for (const chunk of chunks) {
 | 
			
		||||
            for (const token of tokens) {
 | 
			
		||||
                if (chunk === token) {
 | 
			
		||||
                    this.score += 4 * token.length * factor;
 | 
			
		||||
                    tokenScore += 4 * token.length * factor;
 | 
			
		||||
                } else if (chunk.startsWith(token)) {
 | 
			
		||||
                    this.score += 2 * token.length * factor;
 | 
			
		||||
                    tokenScore += 2 * token.length * factor;
 | 
			
		||||
                } else if (chunk.includes(token)) {
 | 
			
		||||
                    this.score += token.length * factor;
 | 
			
		||||
                    tokenScore += token.length * factor;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        this.score += tokenScore;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user