Merge branch 'develop' of https://github.com/TriliumNext/Notes into style/next/forms
							
								
								
									
										3
									
								
								.github/ISSUE_TEMPLATE/bug_report.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,7 +1,6 @@
 | 
				
			|||||||
name: Bug Report
 | 
					name: Bug Report
 | 
				
			||||||
description: Report a bug
 | 
					description: Report a bug
 | 
				
			||||||
title: "(Bug report) "
 | 
					type: "Bug"
 | 
				
			||||||
labels: "Type: Bug"
 | 
					 | 
				
			||||||
body:
 | 
					body:
 | 
				
			||||||
- type: textarea
 | 
					- type: textarea
 | 
				
			||||||
  attributes:
 | 
					  attributes:
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								.github/ISSUE_TEMPLATE/feature_request.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,12 +1,11 @@
 | 
				
			|||||||
name: Feature Request
 | 
					name: Feature Request
 | 
				
			||||||
description: Ask for a new feature to be added
 | 
					description: Ask for a new feature to be added
 | 
				
			||||||
title: "(Feature request) "
 | 
					type: "Feature"
 | 
				
			||||||
labels: "Type: Enhancement"
 | 
					 | 
				
			||||||
body:
 | 
					body:
 | 
				
			||||||
- type: textarea
 | 
					- type: textarea
 | 
				
			||||||
  attributes:
 | 
					  attributes:
 | 
				
			||||||
    label: Describe feature
 | 
					    label: Describe feature
 | 
				
			||||||
    description: A clear and concise description of what you want to be added..
 | 
					    description: A clear and concise description of what you want to be added.
 | 
				
			||||||
  validations:
 | 
					  validations:
 | 
				
			||||||
    required: true
 | 
					    required: true
 | 
				
			||||||
- type: textarea
 | 
					- type: textarea
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										24
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -92,7 +92,16 @@ jobs:
 | 
				
			|||||||
          asset_content_type: application/zip # required by GitHub API
 | 
					          asset_content_type: application/zip # required by GitHub API
 | 
				
			||||||
  nightly-server:
 | 
					  nightly-server:
 | 
				
			||||||
    name: Deploy server nightly
 | 
					    name: Deploy server nightly
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        arch: [x64, arm64]
 | 
				
			||||||
 | 
					        include:
 | 
				
			||||||
 | 
					          - arch: x64
 | 
				
			||||||
 | 
					            runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					          - arch: arm64
 | 
				
			||||||
 | 
					            runs-on: ubuntu-24.04-arm
 | 
				
			||||||
 | 
					    runs-on: ${{ matrix.runs-on }}
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - name: Set up node & dependencies
 | 
					      - name: Set up node & dependencies
 | 
				
			||||||
@@ -102,22 +111,21 @@ jobs:
 | 
				
			|||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
      - name: Install dependencies
 | 
					      - name: Install dependencies
 | 
				
			||||||
        run: npm ci
 | 
					        run: npm ci
 | 
				
			||||||
      - name: Run Linux server build (x86_64)
 | 
					      - name: Run Linux server build
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          MATRIX_ARCH: ${{ matrix.arch }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          npm run update-build-info
 | 
					          npm run update-build-info
 | 
				
			||||||
          npm run ci-update-nightly-version
 | 
					 | 
				
			||||||
          ./bin/build-server.sh
 | 
					          ./bin/build-server.sh
 | 
				
			||||||
      - name: Prepare artifacts
 | 
					      - name: Prepare artifacts
 | 
				
			||||||
        if: runner.os != 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          mkdir -p upload
 | 
					          mkdir -p upload
 | 
				
			||||||
          file=$(find dist -name '*.tar.xz' -print -quit)
 | 
					          file=$(find dist -name '*.tar.xz' -print -quit)
 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz"
 | 
					          cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
 | 
				
			||||||
      - uses: actions/upload-artifact@v4
 | 
					      - uses: actions/upload-artifact@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: TriliumNextNotes linux server x64
 | 
					          name: TriliumNextNotes linux server ${{ matrix.arch }}
 | 
				
			||||||
          path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz
 | 
					          path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz
 | 
				
			||||||
          overwrite: true
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Deploy release
 | 
					      - name: Deploy release
 | 
				
			||||||
        uses: WebFreak001/deploy-nightly@v3.2.0
 | 
					        uses: WebFreak001/deploy-nightly@v3.2.0
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -66,8 +66,17 @@ jobs:
 | 
				
			|||||||
          fail_on_unmatched_files: true
 | 
					          fail_on_unmatched_files: true
 | 
				
			||||||
          files: upload/*.*
 | 
					          files: upload/*.*
 | 
				
			||||||
  build_linux_server-x64:
 | 
					  build_linux_server-x64:
 | 
				
			||||||
    name: Build Linux Server x86_64
 | 
					    name: Build Linux Server
 | 
				
			||||||
    runs-on: ubuntu-latest
 | 
					    strategy:
 | 
				
			||||||
 | 
					      fail-fast: false
 | 
				
			||||||
 | 
					      matrix:
 | 
				
			||||||
 | 
					        arch: [x64, arm64]
 | 
				
			||||||
 | 
					        include:
 | 
				
			||||||
 | 
					          - arch: x64
 | 
				
			||||||
 | 
					            runs-on: ubuntu-latest
 | 
				
			||||||
 | 
					          - arch: arm64
 | 
				
			||||||
 | 
					            runs-on: ubuntu-24.04-arm
 | 
				
			||||||
 | 
					    runs-on: ${{ matrix.runs-on }}
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - uses: actions/checkout@v4
 | 
					      - uses: actions/checkout@v4
 | 
				
			||||||
      - name: Set up node & dependencies
 | 
					      - name: Set up node & dependencies
 | 
				
			||||||
@@ -77,16 +86,17 @@ jobs:
 | 
				
			|||||||
          cache: "npm"
 | 
					          cache: "npm"
 | 
				
			||||||
      - name: Install dependencies
 | 
					      - name: Install dependencies
 | 
				
			||||||
        run: npm ci
 | 
					        run: npm ci
 | 
				
			||||||
      - name: Run Linux server build (x86_64)
 | 
					      - name: Run Linux server build
 | 
				
			||||||
 | 
					        env:
 | 
				
			||||||
 | 
					          MATRIX_ARCH: ${{ matrix.arch }}
 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          npm run update-build-info
 | 
					          npm run update-build-info
 | 
				
			||||||
          ./bin/build-server.sh
 | 
					          ./bin/build-server.sh
 | 
				
			||||||
      - name: Prepare artifacts
 | 
					      - name: Prepare artifacts
 | 
				
			||||||
        if: runner.os != 'windows'
 | 
					 | 
				
			||||||
        run: |
 | 
					        run: |
 | 
				
			||||||
          mkdir -p upload
 | 
					          mkdir -p upload
 | 
				
			||||||
          file=$(find dist -name '*.tar.xz' -print -quit)
 | 
					          file=$(find dist -name '*.tar.xz' -print -quit)
 | 
				
			||||||
          cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-server-linux-x64.tar.xz"
 | 
					          cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz"
 | 
				
			||||||
      - name: Publish release
 | 
					      - name: Publish release
 | 
				
			||||||
        uses: softprops/action-gh-release@v2
 | 
					        uses: softprops/action-gh-release@v2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
# Build stage
 | 
					# Build stage
 | 
				
			||||||
FROM node:22.13.0-bullseye-slim AS builder
 | 
					FROM node:22.13.1-bullseye-slim AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configure build dependencies in a single layer
 | 
					# Configure build dependencies in a single layer
 | 
				
			||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
					RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
				
			||||||
@@ -28,7 +28,6 @@ RUN cp -R build/src/* src/. && \
 | 
				
			|||||||
    npm run webpack && \
 | 
					    npm run webpack && \
 | 
				
			||||||
    npm prune --omit=dev && \
 | 
					    npm prune --omit=dev && \
 | 
				
			||||||
    npm cache clean --force && \
 | 
					    npm cache clean --force && \
 | 
				
			||||||
    cp src/public/app/share.js src/public/app-dist/. && \
 | 
					 | 
				
			||||||
    cp -r src/public/app/doc_notes src/public/app-dist/. && \
 | 
					    cp -r src/public/app/doc_notes src/public/app-dist/. && \
 | 
				
			||||||
    rm -rf src/public/app/* && \
 | 
					    rm -rf src/public/app/* && \
 | 
				
			||||||
    mkdir -p src/public/app/services && \
 | 
					    mkdir -p src/public/app/services && \
 | 
				
			||||||
@@ -37,7 +36,7 @@ RUN cp -R build/src/* src/. && \
 | 
				
			|||||||
    rm -r build
 | 
					    rm -r build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Runtime stage
 | 
					# Runtime stage
 | 
				
			||||||
FROM node:22.13.0-bullseye-slim
 | 
					FROM node:22.13.1-bullseye-slim
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install only runtime dependencies
 | 
					# Install only runtime dependencies
 | 
				
			||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
					RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
# Build stage
 | 
					# Build stage
 | 
				
			||||||
FROM node:22.13.0-alpine AS builder
 | 
					FROM node:22.13.1-alpine AS builder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Configure build dependencies
 | 
					# Configure build dependencies
 | 
				
			||||||
RUN apk add --no-cache --virtual .build-dependencies \
 | 
					RUN apk add --no-cache --virtual .build-dependencies \
 | 
				
			||||||
@@ -27,7 +27,6 @@ RUN cp -R build/src/* src/. && \
 | 
				
			|||||||
    npm run webpack && \
 | 
					    npm run webpack && \
 | 
				
			||||||
    npm prune --omit=dev && \
 | 
					    npm prune --omit=dev && \
 | 
				
			||||||
    npm cache clean --force && \
 | 
					    npm cache clean --force && \
 | 
				
			||||||
    cp src/public/app/share.js src/public/app-dist/. && \
 | 
					 | 
				
			||||||
    cp -r src/public/app/doc_notes src/public/app-dist/. && \
 | 
					    cp -r src/public/app/doc_notes src/public/app-dist/. && \
 | 
				
			||||||
    rm -rf src/public/app && \
 | 
					    rm -rf src/public/app && \
 | 
				
			||||||
    mkdir -p src/public/app/services && \
 | 
					    mkdir -p src/public/app/services && \
 | 
				
			||||||
@@ -36,7 +35,7 @@ RUN cp -R build/src/* src/. && \
 | 
				
			|||||||
    rm -r build
 | 
					    rm -r build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Runtime stage
 | 
					# Runtime stage
 | 
				
			||||||
FROM node:22.13.0-alpine
 | 
					FROM node:22.13.1-alpine
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install runtime dependencies
 | 
					# Install runtime dependencies
 | 
				
			||||||
RUN apk add --no-cache su-exec shadow
 | 
					RUN apk add --no-cache su-exec shadow
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,7 +68,6 @@ find $DIR -name "*.ts" -type f -delete
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
d="$DIR"/src/public
 | 
					d="$DIR"/src/public
 | 
				
			||||||
[[ -d "$d"/app-dist ]] || mkdir -pv "$d"/app-dist
 | 
					[[ -d "$d"/app-dist ]] || mkdir -pv "$d"/app-dist
 | 
				
			||||||
cp "$d"/app/share.js "$d"/app-dist/
 | 
					 | 
				
			||||||
cp -r "$d"/app/doc_notes "$d"/app-dist/
 | 
					cp -r "$d"/app/doc_notes "$d"/app-dist/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
rm -rf "$d"/app
 | 
					rm -rf "$d"/app
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,3 +27,8 @@ keyPath=
 | 
				
			|||||||
# once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients.
 | 
					# once set, expressjs will use the X-Forwarded-For header set by the rev. proxy to determinate the real IPs of clients.
 | 
				
			||||||
# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
 | 
					# expressjs shortcuts are supported: loopback(127.0.0.1/8, ::1/128), linklocal(169.254.0.0/16, fe80::/10), uniquelocal(10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
 | 
				
			||||||
trustedReverseProxy=false
 | 
					trustedReverseProxy=false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[Sync]
 | 
				
			||||||
 | 
					#syncServerHost=
 | 
				
			||||||
 | 
					#syncServerTimeout=
 | 
				
			||||||
 | 
					#syncServerProxy=
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.4 KiB  | 
@@ -1,8 +1,5 @@
 | 
				
			|||||||
import http from "http";
 | 
					import http from "http";
 | 
				
			||||||
import ini from "ini";
 | 
					import config from "./src/services/config.js";
 | 
				
			||||||
import fs from "fs";
 | 
					 | 
				
			||||||
import dataDir from "./src/services/data_dir.js";
 | 
					 | 
				
			||||||
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (config.Network.https) {
 | 
					if (config.Network.https) {
 | 
				
			||||||
    // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
 | 
					    // built-in TLS (terminated by trilium) is not supported yet, PRs are welcome
 | 
				
			||||||
 
 | 
				
			|||||||
| 
		 Before Width: | Height: | Size: 61 KiB After Width: | Height: | Size: 16 KiB  | 
| 
		 Before Width: | Height: | Size: 8.4 KiB After Width: | Height: | Size: 3.9 KiB  | 
| 
		 Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 4.1 KiB  | 
| 
		 Before Width: | Height: | Size: 93 KiB After Width: | Height: | Size: 30 KiB  | 
| 
		 Before Width: | Height: | Size: 107 KiB After Width: | Height: | Size: 32 KiB  | 
| 
		 Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 3.2 KiB  | 
| 
		 Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 5.6 KiB  | 
| 
		 Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 5.5 KiB  | 
| 
		 Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 13 KiB  | 
| 
		 Before Width: | Height: | Size: 6.7 KiB After Width: | Height: | Size: 6.4 KiB  | 
@@ -1,5 +0,0 @@
 | 
				
			|||||||
For bug reports, **PLEASE mention version of Trilium you're using** and also include **log files** from following location:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
* `/home/[user]/.local/share/trilium-data/log` for Linux
 | 
					 | 
				
			||||||
* `C:\Users\[user]\AppData\Roaming\trilium-data\log` for Windows Vista and up
 | 
					 | 
				
			||||||
* `/Users/[user]/Library/Application Support/trilium-data/log` for Mac OS
 | 
					 | 
				
			||||||
							
								
								
									
										734
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
							
								
								
									
										36
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -2,7 +2,7 @@
 | 
				
			|||||||
  "name": "trilium",
 | 
					  "name": "trilium",
 | 
				
			||||||
  "productName": "TriliumNext Notes",
 | 
					  "productName": "TriliumNext Notes",
 | 
				
			||||||
  "description": "Build your personal knowledge base with TriliumNext Notes",
 | 
					  "description": "Build your personal knowledge base with TriliumNext Notes",
 | 
				
			||||||
  "version": "0.91.2-beta",
 | 
					  "version": "0.91.4-beta",
 | 
				
			||||||
  "license": "AGPL-3.0-only",
 | 
					  "license": "AGPL-3.0-only",
 | 
				
			||||||
  "main": "./dist/electron-main.js",
 | 
					  "main": "./dist/electron-main.js",
 | 
				
			||||||
  "author": {
 | 
					  "author": {
 | 
				
			||||||
@@ -26,9 +26,9 @@
 | 
				
			|||||||
    "start-test-server": "npm run switch-server && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
 | 
					    "start-test-server": "npm run switch-server && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
 | 
				
			||||||
    "qstart-server": "npm run switch-server && npm run start-server",
 | 
					    "qstart-server": "npm run switch-server && npm run start-server",
 | 
				
			||||||
    "start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .",
 | 
					    "start-electron": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./dist/electron-main.js --inspect=5858 .",
 | 
				
			||||||
    "start-electron-nix": "npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
 | 
					    "start-electron-nix": "electron-rebuild --version 33.3.1 && npm run prepare-dist && cross-env TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
 | 
				
			||||||
    "start-electron-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
 | 
					    "start-electron-no-dir": "npm run prepare-dist && cross-env TRILIUM_ENV=dev electron --inspect=5858 .",
 | 
				
			||||||
    "start-electron-no-dir-nix": "npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
 | 
					    "start-electron-no-dir-nix": "electron-rebuild --version 33.3.1 && npm run prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
 | 
				
			||||||
    "qstart-electron": "npm run switch-electron && npm run start-electron",
 | 
					    "qstart-electron": "npm run switch-electron && npm run start-electron",
 | 
				
			||||||
    "switch-server": "rimraf ./node_modules/better-sqlite3 && npm install",
 | 
					    "switch-server": "rimraf ./node_modules/better-sqlite3 && npm install",
 | 
				
			||||||
    "switch-electron": "electron-rebuild",
 | 
					    "switch-electron": "electron-rebuild",
 | 
				
			||||||
@@ -59,7 +59,7 @@
 | 
				
			|||||||
    "@excalidraw/excalidraw": "0.17.6",
 | 
					    "@excalidraw/excalidraw": "0.17.6",
 | 
				
			||||||
    "@highlightjs/cdn-assets": "11.11.1",
 | 
					    "@highlightjs/cdn-assets": "11.11.1",
 | 
				
			||||||
    "@mermaid-js/layout-elk": "0.1.7",
 | 
					    "@mermaid-js/layout-elk": "0.1.7",
 | 
				
			||||||
    "@mind-elixir/node-menu": "1.0.3",
 | 
					    "@mind-elixir/node-menu": "1.0.4",
 | 
				
			||||||
    "@triliumnext/express-partial-content": "1.0.1",
 | 
					    "@triliumnext/express-partial-content": "1.0.1",
 | 
				
			||||||
    "@types/leaflet": "1.9.16",
 | 
					    "@types/leaflet": "1.9.16",
 | 
				
			||||||
    "@types/react-dom": "18.3.5",
 | 
					    "@types/react-dom": "18.3.5",
 | 
				
			||||||
@@ -97,9 +97,9 @@
 | 
				
			|||||||
    "html2plaintext": "2.1.4",
 | 
					    "html2plaintext": "2.1.4",
 | 
				
			||||||
    "http-proxy-agent": "7.0.2",
 | 
					    "http-proxy-agent": "7.0.2",
 | 
				
			||||||
    "https-proxy-agent": "7.0.6",
 | 
					    "https-proxy-agent": "7.0.6",
 | 
				
			||||||
    "i18next": "24.2.1",
 | 
					    "i18next": "24.2.2",
 | 
				
			||||||
    "i18next-fs-backend": "2.6.0",
 | 
					    "i18next-fs-backend": "2.6.0",
 | 
				
			||||||
    "i18next-http-backend": "3.0.1",
 | 
					    "i18next-http-backend": "3.0.2",
 | 
				
			||||||
    "image-type": "5.2.0",
 | 
					    "image-type": "5.2.0",
 | 
				
			||||||
    "ini": "5.0.0",
 | 
					    "ini": "5.0.0",
 | 
				
			||||||
    "is-animated": "2.0.2",
 | 
					    "is-animated": "2.0.2",
 | 
				
			||||||
@@ -148,14 +148,14 @@
 | 
				
			|||||||
    "yauzl": "3.2.0"
 | 
					    "yauzl": "3.2.0"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@electron-forge/cli": "7.6.0",
 | 
					    "@electron-forge/cli": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-deb": "7.6.0",
 | 
					    "@electron-forge/maker-deb": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-dmg": "7.6.0",
 | 
					    "@electron-forge/maker-dmg": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-squirrel": "7.6.0",
 | 
					    "@electron-forge/maker-squirrel": "7.6.1",
 | 
				
			||||||
    "@electron-forge/maker-zip": "7.6.0",
 | 
					    "@electron-forge/maker-zip": "7.6.1",
 | 
				
			||||||
    "@electron-forge/plugin-auto-unpack-natives": "7.6.0",
 | 
					    "@electron-forge/plugin-auto-unpack-natives": "7.6.1",
 | 
				
			||||||
    "@electron/rebuild": "3.7.1",
 | 
					    "@electron/rebuild": "3.7.1",
 | 
				
			||||||
    "@playwright/test": "1.49.1",
 | 
					    "@playwright/test": "1.50.0",
 | 
				
			||||||
    "@types/archiver": "6.0.3",
 | 
					    "@types/archiver": "6.0.3",
 | 
				
			||||||
    "@types/better-sqlite3": "7.6.12",
 | 
					    "@types/better-sqlite3": "7.6.12",
 | 
				
			||||||
    "@types/bootstrap": "5.2.10",
 | 
					    "@types/bootstrap": "5.2.10",
 | 
				
			||||||
@@ -177,7 +177,7 @@
 | 
				
			|||||||
    "@types/jsdom": "21.1.7",
 | 
					    "@types/jsdom": "21.1.7",
 | 
				
			||||||
    "@types/mime-types": "2.1.4",
 | 
					    "@types/mime-types": "2.1.4",
 | 
				
			||||||
    "@types/multer": "1.4.12",
 | 
					    "@types/multer": "1.4.12",
 | 
				
			||||||
    "@types/node": "22.10.7",
 | 
					    "@types/node": "22.12.0",
 | 
				
			||||||
    "@types/react": "18.3.18",
 | 
					    "@types/react": "18.3.18",
 | 
				
			||||||
    "@types/safe-compare": "1.1.2",
 | 
					    "@types/safe-compare": "1.1.2",
 | 
				
			||||||
    "@types/sanitize-html": "2.13.0",
 | 
					    "@types/sanitize-html": "2.13.0",
 | 
				
			||||||
@@ -189,12 +189,12 @@
 | 
				
			|||||||
    "@types/stream-throttle": "0.1.4",
 | 
					    "@types/stream-throttle": "0.1.4",
 | 
				
			||||||
    "@types/tmp": "0.2.6",
 | 
					    "@types/tmp": "0.2.6",
 | 
				
			||||||
    "@types/turndown": "5.0.5",
 | 
					    "@types/turndown": "5.0.5",
 | 
				
			||||||
    "@types/ws": "8.5.13",
 | 
					    "@types/ws": "8.5.14",
 | 
				
			||||||
    "@types/xml2js": "0.4.14",
 | 
					    "@types/xml2js": "0.4.14",
 | 
				
			||||||
    "@types/yargs": "17.0.33",
 | 
					    "@types/yargs": "17.0.33",
 | 
				
			||||||
    "@vitest/coverage-v8": "3.0.3",
 | 
					    "@vitest/coverage-v8": "3.0.4",
 | 
				
			||||||
    "cross-env": "7.0.3",
 | 
					    "cross-env": "7.0.3",
 | 
				
			||||||
    "electron": "34.0.0",
 | 
					    "electron": "34.0.1",
 | 
				
			||||||
    "esm": "3.2.25",
 | 
					    "esm": "3.2.25",
 | 
				
			||||||
    "jasmine": "5.5.0",
 | 
					    "jasmine": "5.5.0",
 | 
				
			||||||
    "jsdoc": "4.0.4",
 | 
					    "jsdoc": "4.0.4",
 | 
				
			||||||
@@ -207,7 +207,7 @@
 | 
				
			|||||||
    "tsx": "4.19.2",
 | 
					    "tsx": "4.19.2",
 | 
				
			||||||
    "typedoc": "0.27.6",
 | 
					    "typedoc": "0.27.6",
 | 
				
			||||||
    "typescript": "5.7.3",
 | 
					    "typescript": "5.7.3",
 | 
				
			||||||
    "vitest": "3.0.3",
 | 
					    "vitest": "3.0.4",
 | 
				
			||||||
    "webpack": "5.97.1",
 | 
					    "webpack": "5.97.1",
 | 
				
			||||||
    "webpack-cli": "6.0.1",
 | 
					    "webpack-cli": "6.0.1",
 | 
				
			||||||
    "webpack-dev-middleware": "7.4.2"
 | 
					    "webpack-dev-middleware": "7.4.2"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,6 +36,12 @@ interface DateLimits {
 | 
				
			|||||||
    maxDate: string;
 | 
					    maxDate: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SimilarNote {
 | 
				
			||||||
 | 
					    score: number;
 | 
				
			||||||
 | 
					    notePath: string[];
 | 
				
			||||||
 | 
					    noteId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function filterUrlValue(value: string) {
 | 
					function filterUrlValue(value: string) {
 | 
				
			||||||
    return value
 | 
					    return value
 | 
				
			||||||
        .replace(/https?:\/\//gi, "")
 | 
					        .replace(/https?:\/\//gi, "")
 | 
				
			||||||
@@ -247,7 +253,7 @@ function hasConnectingRelation(sourceNote: BNote, targetNote: BNote) {
 | 
				
			|||||||
    return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId);
 | 
					    return sourceNote.getAttributes().find((attr) => attr.type === "relation" && ["includenotelink", "imagelink"].includes(attr.name) && attr.value === targetNote.noteId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async function findSimilarNotes(noteId: string) {
 | 
					async function findSimilarNotes(noteId: string): Promise<SimilarNote[] | undefined> {
 | 
				
			||||||
    const results = [];
 | 
					    const results = [];
 | 
				
			||||||
    let i = 0;
 | 
					    let i = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -417,6 +423,7 @@ async function findSimilarNotes(noteId: string) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            // this takes care of note hoisting
 | 
					            // this takes care of note hoisting
 | 
				
			||||||
            if (!notePath) {
 | 
					            if (!notePath) {
 | 
				
			||||||
 | 
					                // TODO: This return is suspicious, it should probably be continue
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,7 +71,7 @@ export interface ExecuteCommandData extends CommandData {
 | 
				
			|||||||
export type CommandMappings = {
 | 
					export type CommandMappings = {
 | 
				
			||||||
    "api-log-messages": CommandData;
 | 
					    "api-log-messages": CommandData;
 | 
				
			||||||
    focusTree: CommandData,
 | 
					    focusTree: CommandData,
 | 
				
			||||||
    focusOnDetail: Required<CommandData>;
 | 
					    focusOnDetail: CommandData;
 | 
				
			||||||
    focusOnSearchDefinition: Required<CommandData>;
 | 
					    focusOnSearchDefinition: Required<CommandData>;
 | 
				
			||||||
    searchNotes: CommandData & {
 | 
					    searchNotes: CommandData & {
 | 
				
			||||||
        searchString?: string;
 | 
					        searchString?: string;
 | 
				
			||||||
@@ -104,6 +104,8 @@ export type CommandMappings = {
 | 
				
			|||||||
    openNoteInNewTab: CommandData;
 | 
					    openNoteInNewTab: CommandData;
 | 
				
			||||||
    openNoteInNewSplit: CommandData;
 | 
					    openNoteInNewSplit: CommandData;
 | 
				
			||||||
    openNoteInNewWindow: CommandData;
 | 
					    openNoteInNewWindow: CommandData;
 | 
				
			||||||
 | 
					    hideLeftPane: CommandData;
 | 
				
			||||||
 | 
					    showLeftPane: CommandData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    openInTab: ContextMenuCommandData;
 | 
					    openInTab: ContextMenuCommandData;
 | 
				
			||||||
    openNoteInSplit: ContextMenuCommandData;
 | 
					    openNoteInSplit: ContextMenuCommandData;
 | 
				
			||||||
@@ -236,6 +238,9 @@ type EventMappings = {
 | 
				
			|||||||
    beforeNoteSwitch: {
 | 
					    beforeNoteSwitch: {
 | 
				
			||||||
        noteContext: NoteContext;
 | 
					        noteContext: NoteContext;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					    beforeNoteContextRemove: {
 | 
				
			||||||
 | 
					        ntxIds: string[];
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    noteSwitched: {
 | 
					    noteSwitched: {
 | 
				
			||||||
        noteContext: NoteContext;
 | 
					        noteContext: NoteContext;
 | 
				
			||||||
        notePath: string | null;
 | 
					        notePath: string | null;
 | 
				
			||||||
@@ -286,6 +291,9 @@ type EventMappings = {
 | 
				
			|||||||
    tabReorder: {
 | 
					    tabReorder: {
 | 
				
			||||||
        ntxIdsInOrder: string[]
 | 
					        ntxIdsInOrder: string[]
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					    refreshNoteList: {
 | 
				
			||||||
 | 
					        noteId: string;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type EventListener<T extends EventNames> = {
 | 
					export type EventListener<T extends EventNames> = {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,7 +46,7 @@ export class TypedComponent<ChildT extends TypedComponent<ChildT>> {
 | 
				
			|||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null {
 | 
					    handleEvent<T extends EventNames>(name: T, data: EventData<T>): Promise<unknown[] | unknown> | null | undefined {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
            const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
 | 
					            const callMethodPromise = this.initialized ? this.initialized.then(() => this.callMethod((this as any)[`${name}Event`], data)) : this.callMethod((this as any)[`${name}Event`], data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,8 @@ import electronContextMenu from "./menus/electron_context_menu.js";
 | 
				
			|||||||
import glob from "./services/glob.js";
 | 
					import glob from "./services/glob.js";
 | 
				
			||||||
import { t } from "./services/i18n.js";
 | 
					import { t } from "./services/i18n.js";
 | 
				
			||||||
import options from "./services/options.js";
 | 
					import options from "./services/options.js";
 | 
				
			||||||
 | 
					import type ElectronRemote from "@electron/remote";
 | 
				
			||||||
 | 
					import type Electron from "electron";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
await appContext.earlyInit();
 | 
					await appContext.earlyInit();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -44,10 +46,9 @@ if (utils.isElectron()) {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function initOnElectron() {
 | 
					function initOnElectron() {
 | 
				
			||||||
    const electron = utils.dynamicRequire("electron");
 | 
					    const electron: typeof Electron = utils.dynamicRequire("electron");
 | 
				
			||||||
    electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
 | 
					    electron.ipcRenderer.on("globalShortcut", async (event, actionName) => appContext.triggerCommand(actionName));
 | 
				
			||||||
 | 
					    const electronRemote: typeof ElectronRemote = utils.dynamicRequire("@electron/remote");
 | 
				
			||||||
    const electronRemote = utils.dynamicRequire("@electron/remote");
 | 
					 | 
				
			||||||
    const currentWindow = electronRemote.getCurrentWindow();
 | 
					    const currentWindow = electronRemote.getCurrentWindow();
 | 
				
			||||||
    const style = window.getComputedStyle(document.body);
 | 
					    const style = window.getComputedStyle(document.body);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -58,7 +59,7 @@ function initOnElectron() {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function initTitleBarButtons(style, currentWindow) {
 | 
					function initTitleBarButtons(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
 | 
				
			||||||
    if (window.glob.platform === "win32") {
 | 
					    if (window.glob.platform === "win32") {
 | 
				
			||||||
        const applyWindowsOverlay = () => {
 | 
					        const applyWindowsOverlay = () => {
 | 
				
			||||||
            const color = style.getPropertyValue("--native-titlebar-background");
 | 
					            const color = style.getPropertyValue("--native-titlebar-background");
 | 
				
			||||||
@@ -81,9 +82,14 @@ function initTitleBarButtons(style, currentWindow) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function initTransparencyEffects(style, currentWindow) {
 | 
					function initTransparencyEffects(style: CSSStyleDeclaration, currentWindow: Electron.BrowserWindow) {
 | 
				
			||||||
    if (window.glob.platform === "win32") {
 | 
					    if (window.glob.platform === "win32") {
 | 
				
			||||||
        const material = style.getPropertyValue("--background-material");
 | 
					        const material = style.getPropertyValue("--background-material");
 | 
				
			||||||
        currentWindow.setBackgroundMaterial(material);
 | 
					        // TriliumNextTODO: find a nicer way to make TypeScript happy – unfortunately TS did not like Array.includes here
 | 
				
			||||||
 | 
					        const bgMaterialOptions = ["auto", "none", "mica", "acrylic", "tabbed"] as const;
 | 
				
			||||||
 | 
					        const foundBgMaterialOption = bgMaterialOptions.find((bgMaterialOption) => material === bgMaterialOption);
 | 
				
			||||||
 | 
					        if (foundBgMaterialOption) {
 | 
				
			||||||
 | 
					            currentWindow.setBackgroundMaterial(foundBgMaterialOption);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -36,12 +36,12 @@ const NOTE_TYPE_ICONS = {
 | 
				
			|||||||
 * end user. Those types should be used only for checking against, they are
 | 
					 * end user. Those types should be used only for checking against, they are
 | 
				
			||||||
 * not for direct use.
 | 
					 * not for direct use.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap";
 | 
					export type NoteType = "file" | "image" | "search" | "noteMap" | "launcher" | "doc" | "contentWidget" | "text" | "relationMap" | "render" | "canvas" | "mermaid" | "book" | "webView" | "code" | "mindMap" | "geoMap";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface NotePathRecord {
 | 
					export interface NotePathRecord {
 | 
				
			||||||
    isArchived: boolean;
 | 
					    isArchived: boolean;
 | 
				
			||||||
    isInHoistedSubTree: boolean;
 | 
					    isInHoistedSubTree: boolean;
 | 
				
			||||||
    isSearch: boolean;
 | 
					    isSearch?: boolean;
 | 
				
			||||||
    notePath: string[];
 | 
					    notePath: string[];
 | 
				
			||||||
    isHidden: boolean;
 | 
					    isHidden: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -402,14 +402,14 @@ class FNote {
 | 
				
			|||||||
        return notePaths;
 | 
					        return notePaths;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getSortedNotePathRecords(hoistedNoteId = "root") {
 | 
					    getSortedNotePathRecords(hoistedNoteId = "root"): NotePathRecord[] {
 | 
				
			||||||
        const isHoistedRoot = hoistedNoteId === "root";
 | 
					        const isHoistedRoot = hoistedNoteId === "root";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const notePaths = this.getAllNotePaths().map((path) => ({
 | 
					        const notePaths: NotePathRecord[] = this.getAllNotePaths().map((path) => ({
 | 
				
			||||||
            notePath: path,
 | 
					            notePath: path,
 | 
				
			||||||
            isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
 | 
					            isInHoistedSubTree: isHoistedRoot || path.includes(hoistedNoteId),
 | 
				
			||||||
            isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
 | 
					            isArchived: path.some((noteId) => froca.notes[noteId].isArchived),
 | 
				
			||||||
            isSearch: path.find((noteId) => froca.notes[noteId].type === "search"),
 | 
					            isSearch: path.some((noteId) => froca.notes[noteId].type === "search"),
 | 
				
			||||||
            isHidden: path.includes("_hidden")
 | 
					            isHidden: path.includes("_hidden")
 | 
				
			||||||
        }));
 | 
					        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,6 +8,7 @@ interface NoteRow {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface BranchRow {
 | 
					interface BranchRow {
 | 
				
			||||||
 | 
					    noteId?: string;
 | 
				
			||||||
    branchId: string;
 | 
					    branchId: string;
 | 
				
			||||||
    componentId: string;
 | 
					    componentId: string;
 | 
				
			||||||
    parentNoteId?: string;
 | 
					    parentNoteId?: string;
 | 
				
			||||||
@@ -157,7 +158,7 @@ export default class LoadResults {
 | 
				
			|||||||
        return Object.keys(this.noteIdToComponentId);
 | 
					        return Object.keys(this.noteIdToComponentId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isNoteReloaded(noteId: string, componentId = null) {
 | 
					    isNoteReloaded(noteId: string | undefined, componentId: string | null = null) {
 | 
				
			||||||
        if (!noteId) {
 | 
					        if (!noteId) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -124,6 +124,10 @@ function escapeHtml(str: string) {
 | 
				
			|||||||
    return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
 | 
					    return str.replace(/[&<>"'`=\/]/g, (s) => entityMap[s]);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export function escapeQuotes(value: string) {
 | 
				
			||||||
 | 
					    return value.replaceAll("\"", """);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function formatSize(size: number) {
 | 
					function formatSize(size: number) {
 | 
				
			||||||
    size = Math.max(Math.round(size / 1024), 1);
 | 
					    size = Math.max(Math.round(size / 1024), 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,7 +3,7 @@
 | 
				
			|||||||
 *
 | 
					 *
 | 
				
			||||||
 * @param noteId of the given note to be fetched. If false, fetches current note.
 | 
					 * @param noteId of the given note to be fetched. If false, fetches current note.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
async function fetchNote(noteId = null) {
 | 
					async function fetchNote(noteId: string | null = null) {
 | 
				
			||||||
    if (!noteId) {
 | 
					    if (!noteId) {
 | 
				
			||||||
        noteId = document.body.getAttribute("data-note-id");
 | 
					        noteId = document.body.getAttribute("data-note-id");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -25,3 +25,9 @@ document.addEventListener(
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    false
 | 
					    false
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// workaround to prevent webpack from removing "fetchNote" as dead code:
 | 
				
			||||||
 | 
					// add fetchNote as property to the window object
 | 
				
			||||||
 | 
					Object.defineProperty(window, "fetchNote", {
 | 
				
			||||||
 | 
					    value: fetchNote
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/public/app/types.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -43,6 +43,7 @@ interface CustomGlobals {
 | 
				
			|||||||
    appCssNoteIds: string[];
 | 
					    appCssNoteIds: string[];
 | 
				
			||||||
    triliumVersion: string;
 | 
					    triliumVersion: string;
 | 
				
			||||||
    TRILIUM_SAFE_MODE: boolean;
 | 
					    TRILIUM_SAFE_MODE: boolean;
 | 
				
			||||||
 | 
					    platform?: typeof process.platform;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type RequireMethod = (moduleName: string) => any;
 | 
					type RequireMethod = (moduleName: string) => any;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,6 +14,7 @@ import type AttributeDetailWidget from "./attribute_detail.js";
 | 
				
			|||||||
import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js";
 | 
					import type { CommandData, EventData, EventListener, FilteredCommandNames } from "../../components/app_context.js";
 | 
				
			||||||
import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
 | 
					import type { default as FAttribute, AttributeType } from "../../entities/fattribute.js";
 | 
				
			||||||
import type FNote from "../../entities/fnote.js";
 | 
					import type FNote from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					import { escapeQuotes } from "../../services/utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const HELP_TEXT = `
 | 
					const HELP_TEXT = `
 | 
				
			||||||
<p>${t("attribute_editor.help_text_body1")}</p>
 | 
					<p>${t("attribute_editor.help_text_body1")}</p>
 | 
				
			||||||
@@ -76,8 +77,8 @@ const TPL = `
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    <div class="attribute-list-editor" tabindex="200"></div>
 | 
					    <div class="attribute-list-editor" tabindex="200"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="bx bx-save save-attributes-button" title="${t("attribute_editor.save_attributes")}"></div>
 | 
					    <div class="bx bx-save save-attributes-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div>
 | 
				
			||||||
    <div class="bx bx-plus add-new-attribute-button" title="${t("attribute_editor.add_a_new_attribute")}"></div>
 | 
					    <div class="bx bx-plus add-new-attribute-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="attribute-errors" style="display: none;"></div>
 | 
					    <div class="attribute-errors" style="display: none;"></div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -193,7 +193,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
 | 
				
			|||||||
     * Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
 | 
					     * Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
 | 
				
			||||||
     * @returns whether the widget is enabled.
 | 
					     * @returns whether the widget is enabled.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    isEnabled() {
 | 
					    isEnabled(): boolean | null | undefined {
 | 
				
			||||||
        return true;
 | 
					        return true;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -205,7 +205,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    doRender() {}
 | 
					    doRender() {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    toggleInt(show: boolean) {
 | 
					    toggleInt(show: boolean | null | undefined) {
 | 
				
			||||||
        this.$widget.toggleClass("hidden-int", !show);
 | 
					        this.$widget.toggleClass("hidden-int", !show);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,9 +2,11 @@ import options from "../../services/options.js";
 | 
				
			|||||||
import splitService from "../../services/resizer.js";
 | 
					import splitService from "../../services/resizer.js";
 | 
				
			||||||
import CommandButtonWidget from "./command_button.js";
 | 
					import CommandButtonWidget from "./command_button.js";
 | 
				
			||||||
import { t } from "../../services/i18n.js";
 | 
					import { t } from "../../services/i18n.js";
 | 
				
			||||||
 | 
					import type { EventData } from "../../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class LeftPaneToggleWidget extends CommandButtonWidget {
 | 
					export default class LeftPaneToggleWidget extends CommandButtonWidget {
 | 
				
			||||||
    constructor(isHorizontalLayout) {
 | 
					
 | 
				
			||||||
 | 
					    constructor(isHorizontalLayout: boolean) {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
 | 
					        this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
 | 
				
			||||||
@@ -32,7 +34,7 @@ export default class LeftPaneToggleWidget extends CommandButtonWidget {
 | 
				
			|||||||
        splitService.setupLeftPaneResizer(options.is("leftPaneVisible"));
 | 
					        splitService.setupLeftPaneResizer(options.is("leftPaneVisible"));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (loadResults.isOptionReloaded("leftPaneVisible")) {
 | 
					        if (loadResults.isOptionReloaded("leftPaneVisible")) {
 | 
				
			||||||
            this.refreshIcon();
 | 
					            this.refreshIcon();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -1,6 +1,10 @@
 | 
				
			|||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
					import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
				
			||||||
import keyboardActionsService from "../../services/keyboard_actions.js";
 | 
					import keyboardActionsService from "../../services/keyboard_actions.js";
 | 
				
			||||||
import attributeService from "../../services/attributes.js";
 | 
					import attributeService from "../../services/attributes.js";
 | 
				
			||||||
 | 
					import type CommandButtonWidget from "../buttons/command_button.js";
 | 
				
			||||||
 | 
					import type FNote from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					import type { NoteType } from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					import type { EventData, EventNames } from "../../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="ribbon-container">
 | 
					<div class="ribbon-container">
 | 
				
			||||||
@@ -8,11 +12,11 @@ const TPL = `
 | 
				
			|||||||
    .ribbon-container {
 | 
					    .ribbon-container {
 | 
				
			||||||
        margin-bottom: 5px;
 | 
					        margin-bottom: 5px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-top-row {
 | 
					    .ribbon-top-row {
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-container {
 | 
					    .ribbon-tab-container {
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
        flex-direction: row;
 | 
					        flex-direction: row;
 | 
				
			||||||
@@ -21,10 +25,10 @@ const TPL = `
 | 
				
			|||||||
        flex-grow: 1;
 | 
					        flex-grow: 1;
 | 
				
			||||||
        flex-flow: row wrap;
 | 
					        flex-flow: row wrap;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-title {
 | 
					    .ribbon-tab-title {
 | 
				
			||||||
        color: var(--muted-text-color);
 | 
					        color: var(--muted-text-color);
 | 
				
			||||||
        border-bottom: 1px solid var(--main-border-color); 
 | 
					        border-bottom: 1px solid var(--main-border-color);
 | 
				
			||||||
        min-width: 24px;
 | 
					        min-width: 24px;
 | 
				
			||||||
        flex-basis: 24px;
 | 
					        flex-basis: 24px;
 | 
				
			||||||
        max-width: max-content;
 | 
					        max-width: max-content;
 | 
				
			||||||
@@ -36,7 +40,7 @@ const TPL = `
 | 
				
			|||||||
        position: relative;
 | 
					        position: relative;
 | 
				
			||||||
        top: 3px;
 | 
					        top: 3px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-title.active {
 | 
					    .ribbon-tab-title.active {
 | 
				
			||||||
        color: var(--main-text-color);
 | 
					        color: var(--main-text-color);
 | 
				
			||||||
        border-bottom: 3px solid var(--main-text-color);
 | 
					        border-bottom: 3px solid var(--main-text-color);
 | 
				
			||||||
@@ -44,7 +48,7 @@ const TPL = `
 | 
				
			|||||||
        overflow: hidden;
 | 
					        overflow: hidden;
 | 
				
			||||||
        text-overflow: ellipsis;
 | 
					        text-overflow: ellipsis;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-title:hover {
 | 
					    .ribbon-tab-title:hover {
 | 
				
			||||||
        cursor: pointer;
 | 
					        cursor: pointer;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -52,11 +56,11 @@ const TPL = `
 | 
				
			|||||||
    .ribbon-tab-title:hover {
 | 
					    .ribbon-tab-title:hover {
 | 
				
			||||||
        color: var(--main-text-color);
 | 
					        color: var(--main-text-color);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-title:first-of-type {
 | 
					    .ribbon-tab-title:first-of-type {
 | 
				
			||||||
        padding-left: 10px;
 | 
					        padding-left: 10px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-spacer {
 | 
					    .ribbon-tab-spacer {
 | 
				
			||||||
        flex-basis: 0;
 | 
					        flex-basis: 0;
 | 
				
			||||||
        min-width: 0;
 | 
					        min-width: 0;
 | 
				
			||||||
@@ -64,41 +68,41 @@ const TPL = `
 | 
				
			|||||||
        flex-grow: 1;
 | 
					        flex-grow: 1;
 | 
				
			||||||
        border-bottom: 1px solid var(--main-border-color);
 | 
					        border-bottom: 1px solid var(--main-border-color);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
    .ribbon-tab-spacer:last-of-type {
 | 
					    .ribbon-tab-spacer:last-of-type {
 | 
				
			||||||
        flex-grow: 1;
 | 
					        flex-grow: 1;
 | 
				
			||||||
        flex-basis: 0;
 | 
					        flex-basis: 0;
 | 
				
			||||||
        min-width: 0;
 | 
					        min-width: 0;
 | 
				
			||||||
        max-width: 10000px;
 | 
					        max-width: 10000px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-button-container {
 | 
					    .ribbon-button-container {
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
        border-bottom: 1px solid var(--main-border-color); 
 | 
					        border-bottom: 1px solid var(--main-border-color);
 | 
				
			||||||
        margin-right: 5px;
 | 
					        margin-right: 5px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-button-container > * {
 | 
					    .ribbon-button-container > * {
 | 
				
			||||||
        position: relative;
 | 
					        position: relative;
 | 
				
			||||||
        top: -3px;
 | 
					        top: -3px;
 | 
				
			||||||
        margin-left: 10px;
 | 
					        margin-left: 10px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-body {
 | 
					    .ribbon-body {
 | 
				
			||||||
        display: none;
 | 
					        display: none;
 | 
				
			||||||
        border-bottom: 1px solid var(--main-border-color);
 | 
					        border-bottom: 1px solid var(--main-border-color);
 | 
				
			||||||
        margin-left: 10px;
 | 
					        margin-left: 10px;
 | 
				
			||||||
        margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
 | 
					        margin-right: 5px; /* needs to have this value so that the bottom border is the same width as the top one */
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-body.active {
 | 
					    .ribbon-body.active {
 | 
				
			||||||
        display: block;
 | 
					        display: block;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-title-label {
 | 
					    .ribbon-tab-title-label {
 | 
				
			||||||
        display: none;
 | 
					        display: none;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .ribbon-tab-title.active .ribbon-tab-title-label {
 | 
					    .ribbon-tab-title.active .ribbon-tab-title-label {
 | 
				
			||||||
        display: inline;
 | 
					        display: inline;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -108,11 +112,21 @@ const TPL = `
 | 
				
			|||||||
        <div class="ribbon-tab-container"></div>
 | 
					        <div class="ribbon-tab-container"></div>
 | 
				
			||||||
        <div class="ribbon-button-container"></div>
 | 
					        <div class="ribbon-button-container"></div>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    <div class="ribbon-body-container"></div>
 | 
					    <div class="ribbon-body-container"></div>
 | 
				
			||||||
</div>`;
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class RibbonContainer extends NoteContextAwareWidget {
 | 
					export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private lastActiveComponentId?: string | null;
 | 
				
			||||||
 | 
					    private lastNoteType?: NoteType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private ribbonWidgets: NoteContextAwareWidget[];
 | 
				
			||||||
 | 
					    private buttonWidgets: CommandButtonWidget[];
 | 
				
			||||||
 | 
					    private $tabContainer!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $buttonContainer!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $bodyContainer!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -122,10 +136,10 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isEnabled() {
 | 
					    isEnabled() {
 | 
				
			||||||
        return super.isEnabled() && this.noteContext.viewScope.viewMode === "default";
 | 
					        return super.isEnabled() && this.noteContext?.viewScope?.viewMode === "default";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ribbon(widget) {
 | 
					    ribbon(widget: NoteContextAwareWidget) { // TODO: Base class
 | 
				
			||||||
        super.child(widget);
 | 
					        super.child(widget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.ribbonWidgets.push(widget);
 | 
					        this.ribbonWidgets.push(widget);
 | 
				
			||||||
@@ -133,7 +147,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    button(widget) {
 | 
					    button(widget: CommandButtonWidget) {
 | 
				
			||||||
        super.child(widget);
 | 
					        super.child(widget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.buttonWidgets.push(widget);
 | 
					        this.buttonWidgets.push(widget);
 | 
				
			||||||
@@ -163,7 +177,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    toggleRibbonTab($ribbonTitle, refreshActiveTab = true) {
 | 
					    toggleRibbonTab($ribbonTitle: JQuery<HTMLElement>, refreshActiveTab = true) {
 | 
				
			||||||
        const activate = !$ribbonTitle.hasClass("active");
 | 
					        const activate = !$ribbonTitle.hasClass("active");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
 | 
					        this.$tabContainer.find(".ribbon-tab-title").removeClass("active");
 | 
				
			||||||
@@ -181,14 +195,15 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const activeChild = this.getActiveRibbonWidget();
 | 
					            const activeChild = this.getActiveRibbonWidget();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (activeChild && (refreshActiveTab || !wasAlreadyActive)) {
 | 
					            if (activeChild && (refreshActiveTab || !wasAlreadyActive) && this.noteContext && this.notePath) {
 | 
				
			||||||
                const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
 | 
					                const handleEventPromise = activeChild.handleEvent("noteSwitched", { noteContext: this.noteContext, notePath: this.notePath });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (refreshActiveTab) {
 | 
					                if (refreshActiveTab) {
 | 
				
			||||||
                    if (handleEventPromise) {
 | 
					                    if (handleEventPromise) {
 | 
				
			||||||
                        handleEventPromise.then(() => activeChild.focus?.());
 | 
					                        handleEventPromise.then(() => (activeChild as any).focus()); // TODO: Base class
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        activeChild.focus?.();
 | 
					                        // TODO: Base class
 | 
				
			||||||
 | 
					                        (activeChild as any)?.focus();
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -203,7 +218,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        await super.noteSwitched();
 | 
					        await super.noteSwitched();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshWithNote(note, noExplicitActivation = false) {
 | 
					    async refreshWithNote(note: FNote, noExplicitActivation = false) {
 | 
				
			||||||
        this.lastNoteType = note.type;
 | 
					        this.lastNoteType = note.type;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        let $ribbonTabToActivate, $lastActiveRibbon;
 | 
					        let $ribbonTabToActivate, $lastActiveRibbon;
 | 
				
			||||||
@@ -211,7 +226,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        this.$tabContainer.empty();
 | 
					        this.$tabContainer.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        for (const ribbonWidget of this.ribbonWidgets) {
 | 
					        for (const ribbonWidget of this.ribbonWidgets) {
 | 
				
			||||||
            const ret = await ribbonWidget.getTitle(note);
 | 
					            // TODO: Base class for ribbon widget
 | 
				
			||||||
 | 
					            const ret = await (ribbonWidget as any).getTitle(note);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!ret.show) {
 | 
					            if (!ret.show) {
 | 
				
			||||||
                continue;
 | 
					                continue;
 | 
				
			||||||
@@ -219,8 +235,8 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const $ribbonTitle = $('<div class="ribbon-tab-title">')
 | 
					            const $ribbonTitle = $('<div class="ribbon-tab-title">')
 | 
				
			||||||
                .attr("data-ribbon-component-id", ribbonWidget.componentId)
 | 
					                .attr("data-ribbon-component-id", ribbonWidget.componentId)
 | 
				
			||||||
                .attr("data-ribbon-component-name", ribbonWidget.name)
 | 
					                .attr("data-ribbon-component-name", (ribbonWidget as any).name as string) // TODO: base class for ribbon widgets
 | 
				
			||||||
                .append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", ribbonWidget.toggleCommand))
 | 
					                .append($('<span class="ribbon-tab-title-icon">').addClass(ret.icon).attr("title", ret.title).attr("data-toggle-command", (ribbonWidget as any).toggleCommand)) // TODO: base class
 | 
				
			||||||
                .append(" ")
 | 
					                .append(" ")
 | 
				
			||||||
                .append($('<span class="ribbon-tab-title-label">').text(ret.title));
 | 
					                .append($('<span class="ribbon-tab-title-label">').text(ret.title));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -238,7 +254,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        keyboardActionsService.getActions().then((actions) => {
 | 
					        keyboardActionsService.getActions().then((actions) => {
 | 
				
			||||||
            this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
 | 
					            this.$tabContainer.find(".ribbon-tab-title-icon").tooltip({
 | 
				
			||||||
                title: function () {
 | 
					                title: () => {
 | 
				
			||||||
                    const toggleCommandName = $(this).attr("data-toggle-command");
 | 
					                    const toggleCommandName = $(this).attr("data-toggle-command");
 | 
				
			||||||
                    const action = actions.find((act) => act.actionName === toggleCommandName);
 | 
					                    const action = actions.find((act) => act.actionName === toggleCommandName);
 | 
				
			||||||
                    const title = $(this).attr("data-title");
 | 
					                    const title = $(this).attr("data-title");
 | 
				
			||||||
@@ -246,7 +262,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
                    if (action && action.effectiveShortcuts.length > 0) {
 | 
					                    if (action && action.effectiveShortcuts.length > 0) {
 | 
				
			||||||
                        return `${title} (${action.effectiveShortcuts.join(", ")})`;
 | 
					                        return `${title} (${action.effectiveShortcuts.join(", ")})`;
 | 
				
			||||||
                    } else {
 | 
					                    } else {
 | 
				
			||||||
                        return title;
 | 
					                        return title ?? "";
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
@@ -263,27 +279,27 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isRibbonTabActive(name) {
 | 
					    isRibbonTabActive(name: string) {
 | 
				
			||||||
        const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
 | 
					        const $ribbonComponent = this.$widget.find(`.ribbon-tab-title[data-ribbon-component-name='${name}']`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return $ribbonComponent.hasClass("active");
 | 
					        return $ribbonComponent.hasClass("active");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ensureOwnedAttributesAreOpen(ntxId) {
 | 
					    ensureOwnedAttributesAreOpen(ntxId: string | null | undefined) {
 | 
				
			||||||
        if (this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
 | 
					        if (ntxId && this.isNoteContext(ntxId) && !this.isRibbonTabActive("ownedAttributes")) {
 | 
				
			||||||
            this.toggleRibbonTabWithName("ownedAttributes", ntxId);
 | 
					            this.toggleRibbonTabWithName("ownedAttributes", ntxId);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    addNewLabelEvent({ ntxId }) {
 | 
					    addNewLabelEvent({ ntxId }: EventData<"addNewLabel">) {
 | 
				
			||||||
        this.ensureOwnedAttributesAreOpen(ntxId);
 | 
					        this.ensureOwnedAttributesAreOpen(ntxId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    addNewRelationEvent({ ntxId }) {
 | 
					    addNewRelationEvent({ ntxId }: EventData<"addNewRelation">) {
 | 
				
			||||||
        this.ensureOwnedAttributesAreOpen(ntxId);
 | 
					        this.ensureOwnedAttributesAreOpen(ntxId);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    toggleRibbonTabWithName(name, ntxId) {
 | 
					    toggleRibbonTabWithName(name: string, ntxId?: string) {
 | 
				
			||||||
        if (!this.isNoteContext(ntxId)) {
 | 
					        if (!this.isNoteContext(ntxId)) {
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -295,23 +311,23 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    handleEvent(name, data) {
 | 
					    handleEvent<T extends EventNames>(name: T, data: EventData<T>) {
 | 
				
			||||||
        const PREFIX = "toggleRibbonTab";
 | 
					        const PREFIX = "toggleRibbonTab";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (name.startsWith(PREFIX)) {
 | 
					        if (name.startsWith(PREFIX)) {
 | 
				
			||||||
            let componentName = name.substr(PREFIX.length);
 | 
					            let componentName = name.substr(PREFIX.length);
 | 
				
			||||||
            componentName = componentName[0].toLowerCase() + componentName.substr(1);
 | 
					            componentName = componentName[0].toLowerCase() + componentName.substr(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.toggleRibbonTabWithName(componentName, data.ntxId);
 | 
					            this.toggleRibbonTabWithName(componentName, (data as any).ntxId);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            return super.handleEvent(name, data);
 | 
					            return super.handleEvent(name, data);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async handleEventInChildren(name, data) {
 | 
					    async handleEventInChildren<T extends EventNames>(name: T, data: EventData<T>) {
 | 
				
			||||||
        if (["activeContextChanged", "setNoteContext"].includes(name)) {
 | 
					        if (["activeContextChanged", "setNoteContext"].includes(name)) {
 | 
				
			||||||
            // won't trigger .refresh();
 | 
					            // won't trigger .refresh();
 | 
				
			||||||
            await super.handleEventInChildren("setNoteContext", data);
 | 
					            await super.handleEventInChildren("setNoteContext", data as EventData<"activeContextChanged" | "setNoteContext">);
 | 
				
			||||||
        } else if (this.isEnabled() || name === "initialRenderComplete") {
 | 
					        } else if (this.isEnabled() || name === "initialRenderComplete") {
 | 
				
			||||||
            const activeRibbonWidget = this.getActiveRibbonWidget();
 | 
					            const activeRibbonWidget = this.getActiveRibbonWidget();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -326,8 +342,12 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
 | 
					        if (!this.note) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (this.noteId && loadResults.isNoteReloaded(this.noteId) && this.lastNoteType !== this.note.type) {
 | 
				
			||||||
            // note type influences the list of available ribbon tabs the most
 | 
					            // note type influences the list of available ribbon tabs the most
 | 
				
			||||||
            // check for the type is so that we don't update on each title rename
 | 
					            // check for the type is so that we don't update on each title rename
 | 
				
			||||||
            this.lastNoteType = this.note.type;
 | 
					            this.lastNoteType = this.note.type;
 | 
				
			||||||
@@ -338,7 +358,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    noteTypeMimeChangedEvent() {
 | 
					    async noteTypeMimeChangedEvent() {
 | 
				
			||||||
        // We are ignoring the event which triggers a refresh since it is usually already done by a different
 | 
					        // 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.
 | 
					        // event and causing a race condition in which the items appear twice.
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import utils from "../../services/utils.js";
 | 
					import utils, { escapeQuotes } from "../../services/utils.js";
 | 
				
			||||||
import treeService from "../../services/tree.js";
 | 
					import treeService from "../../services/tree.js";
 | 
				
			||||||
import importService from "../../services/import.js";
 | 
					import importService from "../../services/import.js";
 | 
				
			||||||
import options from "../../services/options.js";
 | 
					import options from "../../services/options.js";
 | 
				
			||||||
@@ -27,21 +27,21 @@ const TPL = `
 | 
				
			|||||||
                        <strong>${t("import.options")}:</strong>
 | 
					                        <strong>${t("import.options")}:</strong>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        <div class="checkbox">
 | 
					                        <div class="checkbox">
 | 
				
			||||||
                            <label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.safeImportTooltip")}">
 | 
					                            <label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.safeImportTooltip"))}">
 | 
				
			||||||
                                <input class="safe-import-checkbox" value="1" type="checkbox" checked>
 | 
					                                <input class="safe-import-checkbox" value="1" type="checkbox" checked>
 | 
				
			||||||
                                <span>${t("import.safeImport")}</span>
 | 
					                                <span>${t("import.safeImport")}</span>
 | 
				
			||||||
                            </label>
 | 
					                            </label>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        <div class="checkbox">
 | 
					                        <div class="checkbox">
 | 
				
			||||||
                            <label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.explodeArchivesTooltip")}">
 | 
					                            <label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.explodeArchivesTooltip"))}">
 | 
				
			||||||
                                <input class="explode-archives-checkbox" value="1" type="checkbox" checked>
 | 
					                                <input class="explode-archives-checkbox" value="1" type="checkbox" checked>
 | 
				
			||||||
                                <span>${t("import.explodeArchives")}</span>
 | 
					                                <span>${t("import.explodeArchives")}</span>
 | 
				
			||||||
                            </label>
 | 
					                            </label>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        <div class="checkbox">
 | 
					                        <div class="checkbox">
 | 
				
			||||||
                            <label class="tn-checkbox" data-bs-toggle="tooltip" title="${t("import.shrinkImagesTooltip")}">
 | 
					                            <label class="tn-checkbox" data-bs-toggle="tooltip" title="${escapeQuotes(t("import.shrinkImagesTooltip"))}">
 | 
				
			||||||
                                <input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>${t("import.shrinkImages")}</span>
 | 
					                                <input class="shrink-images-checkbox" value="1" type="checkbox" checked> <span>${t("import.shrinkImages")}</span>
 | 
				
			||||||
                            </label>
 | 
					                            </label>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { t } from "../../services/i18n.js";
 | 
					import { t } from "../../services/i18n.js";
 | 
				
			||||||
import utils from "../../services/utils.js";
 | 
					import utils, { escapeQuotes } from "../../services/utils.js";
 | 
				
			||||||
import treeService from "../../services/tree.js";
 | 
					import treeService from "../../services/tree.js";
 | 
				
			||||||
import importService from "../../services/import.js";
 | 
					import importService from "../../services/import.js";
 | 
				
			||||||
import options from "../../services/options.js";
 | 
					import options from "../../services/options.js";
 | 
				
			||||||
@@ -24,7 +24,7 @@ const TPL = `
 | 
				
			|||||||
                    <div class="form-group">
 | 
					                    <div class="form-group">
 | 
				
			||||||
                        <strong>${t("upload_attachments.options")}:</strong>
 | 
					                        <strong>${t("upload_attachments.options")}:</strong>
 | 
				
			||||||
                        <div class="checkbox">
 | 
					                        <div class="checkbox">
 | 
				
			||||||
                            <label data-bs-toggle="tooltip" title="${t("upload_attachments.tooltip")}">
 | 
					                            <label data-bs-toggle="tooltip" title="${escapeQuotes(t("upload_attachments.tooltip"))}">
 | 
				
			||||||
                                <input class="shrink-images-checkbox form-check-input" value="1" type="checkbox" checked> <span>${t("upload_attachments.shrink_images")}</span>
 | 
					                                <input class="shrink-images-checkbox form-check-input" value="1" type="checkbox" checked> <span>${t("upload_attachments.shrink_images")}</span>
 | 
				
			||||||
                            </label>
 | 
					                            </label>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,10 @@
 | 
				
			|||||||
import attributeService from "../services/attributes.js";
 | 
					import attributeService from "../services/attributes.js";
 | 
				
			||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
					import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
				
			||||||
import { t } from "../services/i18n.js";
 | 
					import { t } from "../services/i18n.js";
 | 
				
			||||||
 | 
					import type FNote from "../entities/fnote.js";
 | 
				
			||||||
 | 
					import type { EventData } from "../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type Editability = "auto" | "readOnly" | "autoReadOnlyDisabled";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="dropdown editability-select-widget">
 | 
					<div class="dropdown editability-select-widget">
 | 
				
			||||||
@@ -9,13 +13,17 @@ const TPL = `
 | 
				
			|||||||
        width: 300px;
 | 
					        width: 300px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .editability-dropdown .dropdown-item {
 | 
				
			||||||
 | 
					        display: block !important;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .editability-dropdown .dropdown-item div {
 | 
					    .editability-dropdown .dropdown-item div {
 | 
				
			||||||
        font-size: small;
 | 
					        font-size: small;
 | 
				
			||||||
        color: var(--muted-text-color);
 | 
					        color: var(--muted-text-color);
 | 
				
			||||||
        white-space: normal;
 | 
					        white-space: normal;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
    <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle select-button editability-button">
 | 
					    <button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" class="btn btn-sm dropdown-toggle editability-button">
 | 
				
			||||||
        <span class="editability-active-desc">${t("editability_select.auto")}</span>
 | 
					        <span class="editability-active-desc">${t("editability_select.auto")}</span>
 | 
				
			||||||
        <span class="caret"></span>
 | 
					        <span class="caret"></span>
 | 
				
			||||||
    </button>
 | 
					    </button>
 | 
				
			||||||
@@ -40,9 +48,15 @@ const TPL = `
 | 
				
			|||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class EditabilitySelectWidget extends NoteContextAwareWidget {
 | 
					export default class EditabilitySelectWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private dropdown!: bootstrap.Dropdown;
 | 
				
			||||||
 | 
					    private $editabilityActiveDesc!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    doRender() {
 | 
					    doRender() {
 | 
				
			||||||
        this.$widget = $(TPL);
 | 
					        this.$widget = $(TPL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // TODO: Remove once bootstrap is added to webpack.
 | 
				
			||||||
 | 
					        //@ts-ignore
 | 
				
			||||||
        this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
 | 
					        this.dropdown = bootstrap.Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
 | 
					        this.$editabilityActiveDesc = this.$widget.find(".editability-active-desc");
 | 
				
			||||||
@@ -52,24 +66,28 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            const editability = $(e.target).closest("[data-editability]").attr("data-editability");
 | 
					            const editability = $(e.target).closest("[data-editability]").attr("data-editability");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!this.note || !this.noteId) {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            for (const ownedAttr of this.note.getOwnedLabels()) {
 | 
					            for (const ownedAttr of this.note.getOwnedLabels()) {
 | 
				
			||||||
                if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
 | 
					                if (["readOnly", "autoReadOnlyDisabled"].includes(ownedAttr.name)) {
 | 
				
			||||||
                    await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
 | 
					                    await attributeService.removeAttributeById(this.noteId, ownedAttr.attributeId);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (editability !== "auto") {
 | 
					            if (editability && editability !== "auto") {
 | 
				
			||||||
                await attributeService.addLabel(this.noteId, editability);
 | 
					                await attributeService.addLabel(this.noteId, editability);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshWithNote(note) {
 | 
					    async refreshWithNote(note: FNote) {
 | 
				
			||||||
        let editability = "auto";
 | 
					        let editability: Editability = "auto";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.note.isLabelTruthy("readOnly")) {
 | 
					        if (this.note?.isLabelTruthy("readOnly")) {
 | 
				
			||||||
            editability = "readOnly";
 | 
					            editability = "readOnly";
 | 
				
			||||||
        } else if (this.note.isLabelTruthy("autoReadOnlyDisabled")) {
 | 
					        } else if (this.note?.isLabelTruthy("autoReadOnlyDisabled")) {
 | 
				
			||||||
            editability = "autoReadOnlyDisabled";
 | 
					            editability = "autoReadOnlyDisabled";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -85,7 +103,7 @@ export default class EditabilitySelectWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        this.$editabilityActiveDesc.text(labels[editability]);
 | 
					        this.$editabilityActiveDesc.text(labels[editability]);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
 | 
					        if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId)) {
 | 
				
			||||||
            this.refresh();
 | 
					            this.refresh();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -7,6 +7,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
				
			|||||||
import linkService from "../../services/link.js";
 | 
					import linkService from "../../services/link.js";
 | 
				
			||||||
import server from "../../services/server.js";
 | 
					import server from "../../services/server.js";
 | 
				
			||||||
import froca from "../../services/froca.js";
 | 
					import froca from "../../services/froca.js";
 | 
				
			||||||
 | 
					import type FNote from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="backlinks-widget">
 | 
					<div class="backlinks-widget">
 | 
				
			||||||
@@ -14,7 +15,7 @@ const TPL = `
 | 
				
			|||||||
        .backlinks-widget {
 | 
					        .backlinks-widget {
 | 
				
			||||||
            position: relative;
 | 
					            position: relative;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
        .backlinks-ticker {
 | 
					        .backlinks-ticker {
 | 
				
			||||||
            border-radius: 10px;
 | 
					            border-radius: 10px;
 | 
				
			||||||
            border-color: var(--main-border-color);
 | 
					            border-color: var(--main-border-color);
 | 
				
			||||||
@@ -25,11 +26,11 @@ const TPL = `
 | 
				
			|||||||
            justify-content: space-between;
 | 
					            justify-content: space-between;
 | 
				
			||||||
            align-items: center;
 | 
					            align-items: center;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        .backlinks-count {
 | 
					        .backlinks-count {
 | 
				
			||||||
            cursor: pointer;
 | 
					            cursor: pointer;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
                        
 | 
					
 | 
				
			||||||
        .backlinks-items {
 | 
					        .backlinks-items {
 | 
				
			||||||
            z-index: 10;
 | 
					            z-index: 10;
 | 
				
			||||||
            position: absolute;
 | 
					            position: absolute;
 | 
				
			||||||
@@ -42,29 +43,41 @@ const TPL = `
 | 
				
			|||||||
            padding: 20px;
 | 
					            padding: 20px;
 | 
				
			||||||
            overflow-y: auto;
 | 
					            overflow-y: auto;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        .backlink-excerpt {
 | 
					        .backlink-excerpt {
 | 
				
			||||||
            border-left: 2px solid var(--main-border-color);
 | 
					            border-left: 2px solid var(--main-border-color);
 | 
				
			||||||
            padding-left: 10px;
 | 
					            padding-left: 10px;
 | 
				
			||||||
            opacity: 80%;
 | 
					            opacity: 80%;
 | 
				
			||||||
            font-size: 90%;
 | 
					            font-size: 90%;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        .backlink-excerpt .backlink-link { /* the actual backlink */
 | 
					        .backlink-excerpt .backlink-link { /* the actual backlink */
 | 
				
			||||||
            font-weight: bold;
 | 
					            font-weight: bold;
 | 
				
			||||||
            background-color: yellow;
 | 
					            background-color: yellow;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    <div class="backlinks-ticker">
 | 
					    <div class="backlinks-ticker">
 | 
				
			||||||
        <span class="backlinks-count"></span>
 | 
					        <span class="backlinks-count"></span>
 | 
				
			||||||
    </div>   
 | 
					    </div>
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    <div class="backlinks-items" style="display: none;"></div>
 | 
					    <div class="backlinks-items" style="display: none;"></div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: Deduplicate with server
 | 
				
			||||||
 | 
					interface Backlink {
 | 
				
			||||||
 | 
					    noteId: string;
 | 
				
			||||||
 | 
					    relationName?: string;
 | 
				
			||||||
 | 
					    excerpts?: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
					export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $count!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $items!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $ticker!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    doRender() {
 | 
					    doRender() {
 | 
				
			||||||
        this.$widget = $(TPL);
 | 
					        this.$widget = $(TPL);
 | 
				
			||||||
        this.$count = this.$widget.find(".backlinks-count");
 | 
					        this.$count = this.$widget.find(".backlinks-count");
 | 
				
			||||||
@@ -73,7 +86,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        this.$count.on("click", () => {
 | 
					        this.$count.on("click", () => {
 | 
				
			||||||
            this.$items.toggle();
 | 
					            this.$items.toggle();
 | 
				
			||||||
            this.$items.css("max-height", $(window).height() - this.$items.offset().top - 10);
 | 
					            this.$items.css("max-height", ($(window).height() ?? 0) - (this.$items.offset()?.top ?? 0) - 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (this.$items.is(":visible")) {
 | 
					            if (this.$items.is(":visible")) {
 | 
				
			||||||
                this.renderBacklinks();
 | 
					                this.renderBacklinks();
 | 
				
			||||||
@@ -83,7 +96,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        this.contentSized();
 | 
					        this.contentSized();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshWithNote(note) {
 | 
					    async refreshWithNote(note: FNote) {
 | 
				
			||||||
        this.clearItems();
 | 
					        this.clearItems();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.noteContext?.viewScope?.viewMode !== "default") {
 | 
					        if (this.noteContext?.viewScope?.viewMode !== "default") {
 | 
				
			||||||
@@ -92,7 +105,8 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // can't use froca since that would count only relations from loaded notes
 | 
					        // can't use froca since that would count only relations from loaded notes
 | 
				
			||||||
        const resp = await server.get(`note-map/${this.noteId}/backlink-count`);
 | 
					        // TODO: Deduplicate response type
 | 
				
			||||||
 | 
					        const resp = await server.get<{ count: number }>(`note-map/${this.noteId}/backlink-count`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!resp || !resp.count) {
 | 
					        if (!resp || !resp.count) {
 | 
				
			||||||
            this.toggle(false);
 | 
					            this.toggle(false);
 | 
				
			||||||
@@ -106,7 +120,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    toggle(show) {
 | 
					    toggle(show: boolean) {
 | 
				
			||||||
        this.$widget.toggleClass("hidden-no-content", !show);
 | 
					        this.$widget.toggleClass("hidden-no-content", !show);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -121,7 +135,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        this.$items.empty();
 | 
					        this.$items.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const backlinks = await server.get(`note-map/${this.noteId}/backlinks`);
 | 
					        const backlinks = await server.get<Backlink[]>(`note-map/${this.noteId}/backlinks`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!backlinks.length) {
 | 
					        if (!backlinks.length) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@@ -143,7 +157,7 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            if (backlink.relationName) {
 | 
					            if (backlink.relationName) {
 | 
				
			||||||
                $item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
 | 
					                $item.append($("<p>").text(`${t("zpetne_odkazy.relation")}: ${backlink.relationName}`));
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                $item.append(...backlink.excerpts);
 | 
					                $item.append(...backlink.excerpts ?? []);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.$items.append($item);
 | 
					            this.$items.append($item);
 | 
				
			||||||
@@ -40,7 +40,7 @@ export default class GeoMapWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
                const L = (await import("leaflet")).default;
 | 
					                const L = (await import("leaflet")).default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                const map = L.map(this.$container[0], {
 | 
					                const map = L.map(this.$container[0], {
 | 
				
			||||||
 | 
					                    worldCopyJump: true
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                this.map = map;
 | 
					                this.map = map;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,9 +10,9 @@ import type NoteContext from "../components/note_context.js";
 | 
				
			|||||||
class NoteContextAwareWidget extends BasicWidget {
 | 
					class NoteContextAwareWidget extends BasicWidget {
 | 
				
			||||||
    protected noteContext?: NoteContext;
 | 
					    protected noteContext?: NoteContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isNoteContext(ntxId: string | null | undefined) {
 | 
					    isNoteContext(ntxId: string | string[] | null | undefined) {
 | 
				
			||||||
        if (Array.isArray(ntxId)) {
 | 
					        if (Array.isArray(ntxId)) {
 | 
				
			||||||
            return this.noteContext && ntxId.includes(this.noteContext.ntxId);
 | 
					            return this.noteContext && this.noteContext.ntxId && ntxId.includes(this.noteContext.ntxId);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            return this.noteContext && this.noteContext.ntxId === ntxId;
 | 
					            return this.noteContext && this.noteContext.ntxId === ntxId;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -54,7 +54,7 @@ class NoteContextAwareWidget extends BasicWidget {
 | 
				
			|||||||
     *
 | 
					     *
 | 
				
			||||||
     * @returns true when an active note exists
 | 
					     * @returns true when an active note exists
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    isEnabled() {
 | 
					    isEnabled(): boolean | null | undefined {
 | 
				
			||||||
        return !!this.note;
 | 
					        return !!this.note;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -147,11 +147,14 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
     */
 | 
					     */
 | 
				
			||||||
    checkFullHeight() {
 | 
					    checkFullHeight() {
 | 
				
			||||||
        // https://github.com/zadam/trilium/issues/2522
 | 
					        // https://github.com/zadam/trilium/issues/2522
 | 
				
			||||||
        this.$widget.toggleClass(
 | 
					        const isBackendNote = this.noteContext?.noteId === "_backendLog";
 | 
				
			||||||
            "full-height",
 | 
					        const isSqlNote = this.mime === "text/x-sqlite;schema=trilium";
 | 
				
			||||||
            (!this.noteContext.hasNoteList() && ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type) && this.mime !== "text/x-sqlite;schema=trilium") ||
 | 
					        const isFullHeightNoteType = ["canvas", "webView", "noteMap", "mindMap", "geoMap"].includes(this.type);
 | 
				
			||||||
                this.noteContext.viewScope.viewMode === "attachments"
 | 
					        const isFullHeight = (!this.noteContext.hasNoteList() && isFullHeightNoteType && !isSqlNote)
 | 
				
			||||||
        );
 | 
					            || this.noteContext.viewScope.viewMode === "attachments"
 | 
				
			||||||
 | 
					            || isBackendNote;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.$widget.toggleClass("full-height", isFullHeight);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getTypeWidget() {
 | 
					    getTypeWidget() {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,7 @@
 | 
				
			|||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
					import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
				
			||||||
import NoteListRenderer from "../services/note_list_renderer.js";
 | 
					import NoteListRenderer from "../services/note_list_renderer.js";
 | 
				
			||||||
 | 
					import type FNote from "../entities/fnote.js";
 | 
				
			||||||
 | 
					import type { EventData } from "../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="note-list-widget">
 | 
					<div class="note-list-widget">
 | 
				
			||||||
@@ -8,19 +10,25 @@ const TPL = `
 | 
				
			|||||||
        min-height: 0;
 | 
					        min-height: 0;
 | 
				
			||||||
        overflow: auto;
 | 
					        overflow: auto;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .note-list-widget .note-list {
 | 
					    .note-list-widget .note-list {
 | 
				
			||||||
        padding: 10px;
 | 
					        padding: 10px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    <div class="note-list-widget-content">
 | 
					    <div class="note-list-widget-content">
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>`;
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class NoteListWidget extends NoteContextAwareWidget {
 | 
					export default class NoteListWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $content!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private isIntersecting?: boolean;
 | 
				
			||||||
 | 
					    private noteIdRefreshed?: string;
 | 
				
			||||||
 | 
					    private shownNoteId?: string | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isEnabled() {
 | 
					    isEnabled() {
 | 
				
			||||||
        return super.isEnabled() && this.noteContext.hasNoteList();
 | 
					        return super.isEnabled() && this.noteContext?.hasNoteList();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    doRender() {
 | 
					    doRender() {
 | 
				
			||||||
@@ -50,13 +58,13 @@ export default class NoteListWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
 | 
					        // console.log(`${this.noteIdRefreshed} === ${this.noteId}`, this.noteIdRefreshed === this.noteId);
 | 
				
			||||||
        // console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId);
 | 
					        // console.log("this.shownNoteId !== this.noteId", this.shownNoteId !== this.noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.isIntersecting && this.noteIdRefreshed === this.noteId && this.shownNoteId !== this.noteId) {
 | 
					        if (this.note && this.isIntersecting && this.noteIdRefreshed === this.noteId && this.shownNoteId !== this.noteId) {
 | 
				
			||||||
            this.shownNoteId = this.noteId;
 | 
					            this.shownNoteId = this.noteId;
 | 
				
			||||||
            this.renderNoteList(this.note);
 | 
					            this.renderNoteList(this.note);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async renderNoteList(note) {
 | 
					    async renderNoteList(note: FNote) {
 | 
				
			||||||
        const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
 | 
					        const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
 | 
				
			||||||
        await noteListRenderer.renderList();
 | 
					        await noteListRenderer.renderList();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -67,8 +75,8 @@ export default class NoteListWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        await super.refresh();
 | 
					        await super.refresh();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshNoteListEvent({ noteId }) {
 | 
					    async refreshNoteListEvent({ noteId }: EventData<"refreshNoteList">) {
 | 
				
			||||||
        if (this.isNote(noteId)) {
 | 
					        if (this.isNote(noteId) && this.note) {
 | 
				
			||||||
            await this.renderNoteList(this.note);
 | 
					            await this.renderNoteList(this.note);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -78,7 +86,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
     * If it's evaluated before note detail, then it's clearly intersected (visible) although after note detail load
 | 
					     * If it's evaluated before note detail, then it's clearly intersected (visible) although after note detail load
 | 
				
			||||||
     * it is not intersected (visible) anymore.
 | 
					     * it is not intersected (visible) anymore.
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    noteDetailRefreshedEvent({ ntxId }) {
 | 
					    noteDetailRefreshedEvent({ ntxId }: EventData<"noteDetailRefreshed">) {
 | 
				
			||||||
        if (!this.isNoteContext(ntxId)) {
 | 
					        if (!this.isNoteContext(ntxId)) {
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -88,14 +96,14 @@ export default class NoteListWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        setTimeout(() => this.checkRenderStatus(), 100);
 | 
					        setTimeout(() => this.checkRenderStatus(), 100);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    notesReloadedEvent({ noteIds }) {
 | 
					    notesReloadedEvent({ noteIds }: EventData<"notesReloaded">) {
 | 
				
			||||||
        if (noteIds.includes(this.noteId)) {
 | 
					        if (this.noteId && noteIds.includes(this.noteId)) {
 | 
				
			||||||
            this.refresh();
 | 
					            this.refresh();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
 | 
					        if (loadResults.getAttributeRows().find((attr) => attr.noteId === this.noteId && attr.name && ["viewType", "expanded", "pageSize"].includes(attr.name))) {
 | 
				
			||||||
            this.shownNoteId = null; // force render
 | 
					            this.shownNoteId = null; // force render
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            this.checkRenderStatus();
 | 
					            this.checkRenderStatus();
 | 
				
			||||||
@@ -163,7 +163,7 @@ export default class NoteMapWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
    private themeStyle!: string;
 | 
					    private themeStyle!: string;
 | 
				
			||||||
    private $container!: JQuery<HTMLElement>;
 | 
					    private $container!: JQuery<HTMLElement>;
 | 
				
			||||||
    private $styleResolver!: JQuery<HTMLElement>;
 | 
					    private $styleResolver!: JQuery<HTMLElement>;
 | 
				
			||||||
    private graph!: ForceGraph;
 | 
					    graph!: ForceGraph;
 | 
				
			||||||
    private noteIdToSizeMap!: Record<string, number>;
 | 
					    private noteIdToSizeMap!: Record<string, number>;
 | 
				
			||||||
    private zoomLevel!: number;
 | 
					    private zoomLevel!: number;
 | 
				
			||||||
    private nodes!: Node[];
 | 
					    private nodes!: Node[];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,10 +3,11 @@ import NoteContextAwareWidget from "./note_context_aware_widget.js";
 | 
				
			|||||||
import protectedSessionHolder from "../services/protected_session_holder.js";
 | 
					import protectedSessionHolder from "../services/protected_session_holder.js";
 | 
				
			||||||
import server from "../services/server.js";
 | 
					import server from "../services/server.js";
 | 
				
			||||||
import SpacedUpdate from "../services/spaced_update.js";
 | 
					import SpacedUpdate from "../services/spaced_update.js";
 | 
				
			||||||
import appContext from "../components/app_context.js";
 | 
					import appContext, { type EventData } from "../components/app_context.js";
 | 
				
			||||||
import branchService from "../services/branches.js";
 | 
					import branchService from "../services/branches.js";
 | 
				
			||||||
import shortcutService from "../services/shortcuts.js";
 | 
					import shortcutService from "../services/shortcuts.js";
 | 
				
			||||||
import utils from "../services/utils.js";
 | 
					import utils from "../services/utils.js";
 | 
				
			||||||
 | 
					import type FNote from "../entities/fnote.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="note-title-widget">
 | 
					<div class="note-title-widget">
 | 
				
			||||||
@@ -33,13 +34,20 @@ const TPL = `
 | 
				
			|||||||
</div>`;
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class NoteTitleWidget extends NoteContextAwareWidget {
 | 
					export default class NoteTitleWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $noteTitle!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private deleteNoteOnEscape: boolean;
 | 
				
			||||||
 | 
					    private spacedUpdate: SpacedUpdate;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.spacedUpdate = new SpacedUpdate(async () => {
 | 
					        this.spacedUpdate = new SpacedUpdate(async () => {
 | 
				
			||||||
            const title = this.$noteTitle.val();
 | 
					            const title = this.$noteTitle.val();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
 | 
					            if (this.note) {
 | 
				
			||||||
 | 
					                protectedSessionHolder.touchProtectedSessionIfNecessary(this.note);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await server.put(`notes/${this.noteId}/title`, { title }, this.componentId);
 | 
					            await server.put(`notes/${this.noteId}/title`, { title }, this.componentId);
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@@ -62,37 +70,36 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        shortcutService.bindElShortcut(this.$noteTitle, "esc", () => {
 | 
					        shortcutService.bindElShortcut(this.$noteTitle, "esc", () => {
 | 
				
			||||||
            if (this.deleteNoteOnEscape && this.noteContext.isActive()) {
 | 
					            if (this.deleteNoteOnEscape && this.noteContext?.isActive() && this.noteContext?.note) {
 | 
				
			||||||
                branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch));
 | 
					                branchService.deleteNotes(Object.values(this.noteContext.note.parentToBranch));
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        shortcutService.bindElShortcut(this.$noteTitle, "return", () => {
 | 
					        shortcutService.bindElShortcut(this.$noteTitle, "return", () => {
 | 
				
			||||||
            this.triggerCommand("focusOnDetail", { ntxId: this.noteContext.ntxId });
 | 
					            this.triggerCommand("focusOnDetail", { ntxId: this.noteContext?.ntxId });
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshWithNote(note) {
 | 
					    async refreshWithNote(note: FNote) {
 | 
				
			||||||
        const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext.viewScope.viewMode !== "default";
 | 
					        const isReadOnly = (note.isProtected && !protectedSessionHolder.isProtectedSessionAvailable()) || utils.isLaunchBarConfig(note.noteId) || this.noteContext?.viewScope?.viewMode !== "default";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$noteTitle.val(isReadOnly ? await this.noteContext.getNavigationTitle() : note.title);
 | 
					        this.$noteTitle.val(isReadOnly ? await this.noteContext?.getNavigationTitle() || "" : note.title);
 | 
				
			||||||
        this.$noteTitle.prop("readonly", isReadOnly);
 | 
					        this.$noteTitle.prop("readonly", isReadOnly);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.setProtectedStatus(note);
 | 
					        this.setProtectedStatus(note);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** @param {FNote} note */
 | 
					    setProtectedStatus(note: FNote) {
 | 
				
			||||||
    setProtectedStatus(note) {
 | 
					 | 
				
			||||||
        this.$noteTitle.toggleClass("protected", !!note.isProtected);
 | 
					        this.$noteTitle.toggleClass("protected", !!note.isProtected);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async beforeNoteSwitchEvent({ noteContext }) {
 | 
					    async beforeNoteSwitchEvent({ noteContext }: EventData<"beforeNoteSwitch">) {
 | 
				
			||||||
        if (this.isNoteContext(noteContext.ntxId)) {
 | 
					        if (this.isNoteContext(noteContext.ntxId)) {
 | 
				
			||||||
            await this.spacedUpdate.updateNowIfNecessary();
 | 
					            await this.spacedUpdate.updateNowIfNecessary();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async beforeNoteContextRemoveEvent({ ntxIds }) {
 | 
					    async beforeNoteContextRemoveEvent({ ntxIds }: EventData<"beforeNoteContextRemove">) {
 | 
				
			||||||
        if (this.isNoteContext(ntxIds)) {
 | 
					        if (this.isNoteContext(ntxIds)) {
 | 
				
			||||||
            await this.spacedUpdate.updateNowIfNecessary();
 | 
					            await this.spacedUpdate.updateNowIfNecessary();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -112,8 +119,8 @@ export default class NoteTitleWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (loadResults.isNoteReloaded(this.noteId)) {
 | 
					        if (loadResults.isNoteReloaded(this.noteId) && this.note) {
 | 
				
			||||||
            // not updating the title specifically since the synced title might be older than what the user is currently typing
 | 
					            // not updating the title specifically since the synced title might be older than what the user is currently typing
 | 
				
			||||||
            this.setProtectedStatus(this.note);
 | 
					            this.setProtectedStatus(this.note);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -21,6 +21,7 @@ const NOTE_TYPES = [
 | 
				
			|||||||
    { type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true },
 | 
					    { type: "mermaid", mime: "text/mermaid", title: t("note_types.mermaid-diagram"), selectable: true },
 | 
				
			||||||
    { type: "book", mime: "", title: t("note_types.book"), selectable: true },
 | 
					    { type: "book", mime: "", title: t("note_types.book"), selectable: true },
 | 
				
			||||||
    { type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
 | 
					    { type: "webView", mime: "", title: t("note_types.web-view"), selectable: true },
 | 
				
			||||||
 | 
					    { type: "geoMap", mime: "application/json", title: t("note_types.geo-map"), selectable: true },
 | 
				
			||||||
    { type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true }
 | 
					    { type: "code", mime: "text/plain", title: t("note_types.code"), selectable: true }
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ import { t } from "../../services/i18n.js";
 | 
				
			|||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
					import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
				
			||||||
import server from "../../services/server.js";
 | 
					import server from "../../services/server.js";
 | 
				
			||||||
import utils from "../../services/utils.js";
 | 
					import utils from "../../services/utils.js";
 | 
				
			||||||
 | 
					import type { EventData } from "../../components/app_context.js";
 | 
				
			||||||
 | 
					import type FNote from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="note-info-widget">
 | 
					<div class="note-info-widget">
 | 
				
			||||||
@@ -10,18 +12,18 @@ const TPL = `
 | 
				
			|||||||
        .note-info-widget {
 | 
					        .note-info-widget {
 | 
				
			||||||
            padding: 12px;
 | 
					            padding: 12px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        .note-info-widget-table {
 | 
					        .note-info-widget-table {
 | 
				
			||||||
            max-width: 100%;            
 | 
					            max-width: 100%;
 | 
				
			||||||
            display: block;
 | 
					            display: block;
 | 
				
			||||||
            overflow-x: auto;
 | 
					            overflow-x: auto;
 | 
				
			||||||
            white-space: nowrap;
 | 
					            white-space: nowrap;
 | 
				
			||||||
        } 
 | 
					        }
 | 
				
			||||||
   
 | 
					
 | 
				
			||||||
        .note-info-widget-table td, .note-info-widget-table th {
 | 
					        .note-info-widget-table td, .note-info-widget-table th {
 | 
				
			||||||
            padding: 5px;
 | 
					            padding: 5px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        .note-info-mime {
 | 
					        .note-info-mime {
 | 
				
			||||||
            max-width: 13em;
 | 
					            max-width: 13em;
 | 
				
			||||||
            overflow: hidden;
 | 
					            overflow: hidden;
 | 
				
			||||||
@@ -61,7 +63,33 @@ const TPL = `
 | 
				
			|||||||
</div>
 | 
					</div>
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: Deduplicate with server
 | 
				
			||||||
 | 
					interface NoteSizeResponse {
 | 
				
			||||||
 | 
					    noteSize: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface SubtreeSizeResponse {
 | 
				
			||||||
 | 
					    subTreeNoteCount: number;
 | 
				
			||||||
 | 
					    subTreeSize: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MetadataResponse {
 | 
				
			||||||
 | 
					    dateCreated: number;
 | 
				
			||||||
 | 
					    dateModified: number;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
					export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $noteId!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $dateCreated!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $dateModified!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $type!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $mime!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $noteSizesWrapper!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $noteSize!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $subTreeSize!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $calculateButton!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get name() {
 | 
					    get name() {
 | 
				
			||||||
        return "noteInfo";
 | 
					        return "noteInfo";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -71,7 +99,7 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isEnabled() {
 | 
					    isEnabled() {
 | 
				
			||||||
        return this.note;
 | 
					        return !!this.note;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getTitle() {
 | 
					    getTitle() {
 | 
				
			||||||
@@ -104,10 +132,10 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
            this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
 | 
					            this.$noteSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
 | 
				
			||||||
            this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
 | 
					            this.$subTreeSize.empty().append($('<span class="bx bx-loader bx-spin"></span>'));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const noteSizeResp = await server.get(`stats/note-size/${this.noteId}`);
 | 
					            const noteSizeResp = await server.get<NoteSizeResponse>(`stats/note-size/${this.noteId}`);
 | 
				
			||||||
            this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize));
 | 
					            this.$noteSize.text(utils.formatSize(noteSizeResp.noteSize));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            const subTreeResp = await server.get(`stats/subtree-size/${this.noteId}`);
 | 
					            const subTreeResp = await server.get<SubtreeSizeResponse>(`stats/subtree-size/${this.noteId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (subTreeResp.subTreeNoteCount > 1) {
 | 
					            if (subTreeResp.subTreeNoteCount > 1) {
 | 
				
			||||||
                this.$subTreeSize.text(t("note_info_widget.subtree_size", { size: utils.formatSize(subTreeResp.subTreeSize), count: subTreeResp.subTreeNoteCount }));
 | 
					                this.$subTreeSize.text(t("note_info_widget.subtree_size", { size: utils.formatSize(subTreeResp.subTreeSize), count: subTreeResp.subTreeNoteCount }));
 | 
				
			||||||
@@ -117,8 +145,8 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshWithNote(note) {
 | 
					    async refreshWithNote(note: FNote) {
 | 
				
			||||||
        const metadata = await server.get(`notes/${this.noteId}/metadata`);
 | 
					        const metadata = await server.get<MetadataResponse>(`notes/${this.noteId}/metadata`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$noteId.text(note.noteId);
 | 
					        this.$noteId.text(note.noteId);
 | 
				
			||||||
        this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated);
 | 
					        this.$dateCreated.text(formatDateTime(metadata.dateCreated)).attr("title", metadata.dateCreated);
 | 
				
			||||||
@@ -137,8 +165,8 @@ export default class NoteInfoWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        this.$noteSizesWrapper.hide();
 | 
					        this.$noteSizesWrapper.hide();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId)) {
 | 
					        if (this.noteId && (loadResults.isNoteReloaded(this.noteId) || loadResults.isNoteContentReloaded(this.noteId))) {
 | 
				
			||||||
            this.refresh();
 | 
					            this.refresh();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -8,18 +8,18 @@ const TPL = `
 | 
				
			|||||||
        .note-map-ribbon-widget {
 | 
					        .note-map-ribbon-widget {
 | 
				
			||||||
            position: relative;
 | 
					            position: relative;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        .note-map-ribbon-widget .note-map-container {
 | 
					        .note-map-ribbon-widget .note-map-container {
 | 
				
			||||||
            height: 300px;
 | 
					            height: 300px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
        .open-full-button, .collapse-button {
 | 
					        .open-full-button, .collapse-button {
 | 
				
			||||||
            position: absolute;
 | 
					            position: absolute;
 | 
				
			||||||
            right: 5px;
 | 
					            right: 5px;
 | 
				
			||||||
            bottom: 5px;
 | 
					            bottom: 5px;
 | 
				
			||||||
            z-index: 1000;
 | 
					            z-index: 1000;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        
 | 
					
 | 
				
			||||||
        .style-resolver {
 | 
					        .style-resolver {
 | 
				
			||||||
            color: var(--muted-text-color);
 | 
					            color: var(--muted-text-color);
 | 
				
			||||||
            display: none;
 | 
					            display: none;
 | 
				
			||||||
@@ -33,6 +33,13 @@ const TPL = `
 | 
				
			|||||||
</div>`;
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
 | 
					export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private openState!: "small" | "full";
 | 
				
			||||||
 | 
					    private noteMapWidget: NoteMapWidget;
 | 
				
			||||||
 | 
					    private $container!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $openFullButton!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $collapseButton!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor() {
 | 
					    constructor() {
 | 
				
			||||||
        super();
 | 
					        super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -106,7 +113,7 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    setSmallSize() {
 | 
					    setSmallSize() {
 | 
				
			||||||
        const SMALL_SIZE_HEIGHT = 300;
 | 
					        const SMALL_SIZE_HEIGHT = 300;
 | 
				
			||||||
        const width = this.$widget.width();
 | 
					        const width = this.$widget.width() ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$widget.find(".note-map-container").height(SMALL_SIZE_HEIGHT).width(width);
 | 
					        this.$widget.find(".note-map-container").height(SMALL_SIZE_HEIGHT).width(width);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -114,9 +121,11 @@ export default class NoteMapRibbonWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
    setFullHeight() {
 | 
					    setFullHeight() {
 | 
				
			||||||
        const { top } = this.$widget[0].getBoundingClientRect();
 | 
					        const { top } = this.$widget[0].getBoundingClientRect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const height = $(window).height() - top;
 | 
					        const height = ($(window).height() ?? 0) - top;
 | 
				
			||||||
        const width = this.$widget.width();
 | 
					        const width = (this.$widget.width() ?? 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.$widget.find(".note-map-container").height(height).width(width);
 | 
					        this.$widget.find(".note-map-container")
 | 
				
			||||||
 | 
					            .height(height)
 | 
				
			||||||
 | 
					            .width(width);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -2,6 +2,9 @@ import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
				
			|||||||
import treeService from "../../services/tree.js";
 | 
					import treeService from "../../services/tree.js";
 | 
				
			||||||
import linkService from "../../services/link.js";
 | 
					import linkService from "../../services/link.js";
 | 
				
			||||||
import { t } from "../../services/i18n.js";
 | 
					import { t } from "../../services/i18n.js";
 | 
				
			||||||
 | 
					import type FNote from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					import type { NotePathRecord } from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					import type { EventData } from "../../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="note-paths-widget">
 | 
					<div class="note-paths-widget">
 | 
				
			||||||
@@ -37,6 +40,10 @@ const TPL = `
 | 
				
			|||||||
</div>`;
 | 
					</div>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
					export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $notePathIntro!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private $notePathList!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get name() {
 | 
					    get name() {
 | 
				
			||||||
        return "notePaths";
 | 
					        return "notePaths";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -59,13 +66,12 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        this.$notePathIntro = this.$widget.find(".note-path-intro");
 | 
					        this.$notePathIntro = this.$widget.find(".note-path-intro");
 | 
				
			||||||
        this.$notePathList = this.$widget.find(".note-path-list");
 | 
					        this.$notePathList = this.$widget.find(".note-path-list");
 | 
				
			||||||
        this.$widget.on("show.bs.dropdown", () => this.renderDropdown());
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshWithNote(note) {
 | 
					    async refreshWithNote(note: FNote) {
 | 
				
			||||||
        this.$notePathList.empty();
 | 
					        this.$notePathList.empty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (this.noteId === "root") {
 | 
					        if (!this.note || this.noteId === "root") {
 | 
				
			||||||
            this.$notePathList.empty().append(await this.getRenderedPath("root"));
 | 
					            this.$notePathList.empty().append(await this.getRenderedPath("root"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
@@ -90,7 +96,7 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        this.$notePathList.empty().append(...renderedPaths);
 | 
					        this.$notePathList.empty().append(...renderedPaths);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async getRenderedPath(notePath, notePathRecord = null) {
 | 
					    async getRenderedPath(notePath: string, notePathRecord: NotePathRecord | null = null) {
 | 
				
			||||||
        const title = await treeService.getNotePathTitle(notePath);
 | 
					        const title = await treeService.getNotePathTitle(notePath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const $noteLink = await linkService.createLink(notePath, { title });
 | 
					        const $noteLink = await linkService.createLink(notePath, { title });
 | 
				
			||||||
@@ -128,8 +134,9 @@ export default class NotePathsWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        return $("<li>").append($noteLink);
 | 
					        return $("<li>").append($noteLink);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) || loadResults.isNoteReloaded(this.noteId)) {
 | 
					        if (loadResults.getBranchRows().find((branch) => branch.noteId === this.noteId) ||
 | 
				
			||||||
 | 
					            (this.noteId != null && loadResults.isNoteReloaded(this.noteId))) {
 | 
				
			||||||
            this.refresh();
 | 
					            this.refresh();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -3,10 +3,12 @@ import linkService from "../../services/link.js";
 | 
				
			|||||||
import server from "../../services/server.js";
 | 
					import server from "../../services/server.js";
 | 
				
			||||||
import froca from "../../services/froca.js";
 | 
					import froca from "../../services/froca.js";
 | 
				
			||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
					import NoteContextAwareWidget from "../note_context_aware_widget.js";
 | 
				
			||||||
 | 
					import type FNote from "../../entities/fnote.js";
 | 
				
			||||||
 | 
					import type { EventData } from "../../components/app_context.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="similar-notes-widget">
 | 
					<div class="similar-notes-widget">
 | 
				
			||||||
    <style>    
 | 
					    <style>
 | 
				
			||||||
    .similar-notes-wrapper {
 | 
					    .similar-notes-wrapper {
 | 
				
			||||||
        max-height: 200px;
 | 
					        max-height: 200px;
 | 
				
			||||||
        overflow: auto;
 | 
					        overflow: auto;
 | 
				
			||||||
@@ -31,7 +33,20 @@ const TPL = `
 | 
				
			|||||||
</div>
 | 
					</div>
 | 
				
			||||||
`;
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO: Deduplicate with server
 | 
				
			||||||
 | 
					interface SimilarNote {
 | 
				
			||||||
 | 
					    score: number;
 | 
				
			||||||
 | 
					    notePath: string[];
 | 
				
			||||||
 | 
					    noteId: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
					export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private $similarNotesWrapper!: JQuery<HTMLElement>;
 | 
				
			||||||
 | 
					    private title?: string;
 | 
				
			||||||
 | 
					    private rendered?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    get name() {
 | 
					    get name() {
 | 
				
			||||||
        return "similarNotes";
 | 
					        return "similarNotes";
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -41,7 +56,7 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    isEnabled() {
 | 
					    isEnabled() {
 | 
				
			||||||
        return super.isEnabled() && this.note.type !== "search" && !this.note.isLabelTruthy("similarNotesWidgetDisabled");
 | 
					        return super.isEnabled() && this.note?.type !== "search" && !this.note?.isLabelTruthy("similarNotesWidgetDisabled");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    getTitle() {
 | 
					    getTitle() {
 | 
				
			||||||
@@ -59,11 +74,15 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper");
 | 
					        this.$similarNotesWrapper = this.$widget.find(".similar-notes-wrapper");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async refreshWithNote(note) {
 | 
					    async refreshWithNote(note: FNote) {
 | 
				
			||||||
 | 
					        if (!this.note) {
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // remember which title was when we found the similar notes
 | 
					        // remember which title was when we found the similar notes
 | 
				
			||||||
        this.title = this.note.title;
 | 
					        this.title = this.note.title;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const similarNotes = await server.get(`similar-notes/${this.noteId}`);
 | 
					        const similarNotes = await server.get<SimilarNote[]>(`similar-notes/${this.noteId}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (similarNotes.length === 0) {
 | 
					        if (similarNotes.length === 0) {
 | 
				
			||||||
            this.$similarNotesWrapper.empty().append(t("similar_notes.no_similar_notes_found"));
 | 
					            this.$similarNotesWrapper.empty().append(t("similar_notes.no_similar_notes_found"));
 | 
				
			||||||
@@ -92,7 +111,7 @@ export default class SimilarNotesWidget extends NoteContextAwareWidget {
 | 
				
			|||||||
        this.$similarNotesWrapper.empty().append($list);
 | 
					        this.$similarNotesWrapper.empty().append($list);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    entitiesReloadedEvent({ loadResults }) {
 | 
					    entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
 | 
				
			||||||
        if (this.note && this.title !== this.note.title) {
 | 
					        if (this.note && this.title !== this.note.title) {
 | 
				
			||||||
            this.rendered = false;
 | 
					            this.rendered = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -3,35 +3,36 @@ import BasicWidget from "./basic_widget.js";
 | 
				
			|||||||
import ws from "../services/ws.js";
 | 
					import ws from "../services/ws.js";
 | 
				
			||||||
import options from "../services/options.js";
 | 
					import options from "../services/options.js";
 | 
				
			||||||
import syncService from "../services/sync.js";
 | 
					import syncService from "../services/sync.js";
 | 
				
			||||||
 | 
					import { escapeQuotes } from "../services/utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const TPL = `
 | 
					const TPL = `
 | 
				
			||||||
<div class="sync-status-widget launcher-button">
 | 
					<div class="sync-status-widget launcher-button">
 | 
				
			||||||
    <style>
 | 
					    <style>
 | 
				
			||||||
    .sync-status-widget {
 | 
					    .sync-status-widget {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .sync-status {
 | 
					    .sync-status {
 | 
				
			||||||
        box-sizing: border-box;
 | 
					        box-sizing: border-box;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .sync-status .sync-status-icon {
 | 
					    .sync-status .sync-status-icon {
 | 
				
			||||||
        display: inline-block;
 | 
					        display: inline-block;
 | 
				
			||||||
        position: relative;
 | 
					        position: relative;
 | 
				
			||||||
        top: -5px;
 | 
					        top: -5px;
 | 
				
			||||||
        font-size: 110%;
 | 
					        font-size: 110%;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .sync-status .sync-status-sub-icon {
 | 
					    .sync-status .sync-status-sub-icon {
 | 
				
			||||||
        font-size: 40%; 
 | 
					        font-size: 40%;
 | 
				
			||||||
        position: absolute; 
 | 
					        position: absolute;
 | 
				
			||||||
        left: 0;
 | 
					        left: 0;
 | 
				
			||||||
        top: 16px;
 | 
					        top: 16px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .sync-status .sync-status-icon span {
 | 
					    .sync-status .sync-status-icon span {
 | 
				
			||||||
        border: none !important;
 | 
					        border: none !important;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					
 | 
				
			||||||
    .sync-status-icon:not(.sync-status-in-progress):hover {
 | 
					    .sync-status-icon:not(.sync-status-in-progress):hover {
 | 
				
			||||||
        background-color: var(--hover-item-background-color);
 | 
					        background-color: var(--hover-item-background-color);
 | 
				
			||||||
        cursor: pointer;
 | 
					        cursor: pointer;
 | 
				
			||||||
@@ -39,31 +40,31 @@ const TPL = `
 | 
				
			|||||||
    </style>
 | 
					    </style>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="sync-status">
 | 
					    <div class="sync-status">
 | 
				
			||||||
        <span class="sync-status-icon sync-status-unknown bx bx-time" 
 | 
					        <span class="sync-status-icon sync-status-unknown bx bx-time"
 | 
				
			||||||
              data-bs-toggle="tooltip" 
 | 
					              data-bs-toggle="tooltip"
 | 
				
			||||||
              title="${t("sync_status.unknown")}">
 | 
					              title="${escapeQuotes(t("sync_status.unknown"))}">
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <span class="sync-status-icon sync-status-connected-with-changes bx bx-wifi"
 | 
					        <span class="sync-status-icon sync-status-connected-with-changes bx bx-wifi"
 | 
				
			||||||
              data-bs-toggle="tooltip" 
 | 
					              data-bs-toggle="tooltip"
 | 
				
			||||||
              title="${t("sync_status.connected_with_changes")}">
 | 
					              title="${escapeQuotes(t("sync_status.connected_with_changes"))}">
 | 
				
			||||||
            <span class="bx bxs-star sync-status-sub-icon"></span>
 | 
					            <span class="bx bxs-star sync-status-sub-icon"></span>
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi" 
 | 
					        <span class="sync-status-icon sync-status-connected-no-changes bx bx-wifi"
 | 
				
			||||||
              data-bs-toggle="tooltip" 
 | 
					              data-bs-toggle="tooltip"
 | 
				
			||||||
              title="${t("sync_status.connected_no_changes")}">
 | 
					              title="${escapeQuotes(t("sync_status.connected_no_changes"))}">
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <span class="sync-status-icon sync-status-disconnected-with-changes bx bx-wifi-off"
 | 
					        <span class="sync-status-icon sync-status-disconnected-with-changes bx bx-wifi-off"
 | 
				
			||||||
              data-bs-toggle="tooltip" 
 | 
					              data-bs-toggle="tooltip"
 | 
				
			||||||
              title="${t("sync_status.disconnected_with_changes")}">
 | 
					              title="${escapeQuotes(t("sync_status.disconnected_with_changes"))}">
 | 
				
			||||||
            <span class="bx bxs-star sync-status-sub-icon"></span>
 | 
					            <span class="bx bxs-star sync-status-sub-icon"></span>
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <span class="sync-status-icon sync-status-disconnected-no-changes bx bx-wifi-off" 
 | 
					        <span class="sync-status-icon sync-status-disconnected-no-changes bx bx-wifi-off"
 | 
				
			||||||
              data-bs-toggle="tooltip"
 | 
					              data-bs-toggle="tooltip"
 | 
				
			||||||
              title="${t("sync_status.disconnected_no_changes")}">
 | 
					              title="${escapeQuotes(t("sync_status.disconnected_no_changes"))}">
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <span class="sync-status-icon sync-status-in-progress bx bx-analyse bx-spin" 
 | 
					        <span class="sync-status-icon sync-status-in-progress bx bx-analyse bx-spin"
 | 
				
			||||||
              data-bs-toggle="tooltip"
 | 
					              data-bs-toggle="tooltip"
 | 
				
			||||||
              title="${t("sync_status.in_progress")}">
 | 
					              title="${escapeQuotes(t("sync_status.in_progress"))}">
 | 
				
			||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
| 
		 Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 10 KiB  | 
@@ -26,6 +26,11 @@
 | 
				
			|||||||
        border-radius: 2pt !important;
 | 
					        border-radius: 2pt !important;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    span[style] {
 | 
				
			||||||
 | 
					        print-color-adjust: exact;
 | 
				
			||||||
 | 
					        -webkit-print-color-adjust: exact;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /* Fix visibility of checkbox checkmarks
 | 
					    /* Fix visibility of checkbox checkmarks
 | 
				
			||||||
       see https://github.com/TriliumNext/Notes/issues/901 */
 | 
					       see https://github.com/TriliumNext/Notes/issues/901 */
 | 
				
			||||||
    .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after {
 | 
					    .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable="false"] > input[checked]::after {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -396,6 +396,10 @@ body.desktop .dropdown-menu {
 | 
				
			|||||||
    color: var(--dropdown-item-icon-destructive-color);
 | 
					    color: var(--dropdown-item-icon-destructive-color);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.dropdown-item > span:not([class]) {
 | 
				
			||||||
 | 
					    width: 100%;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.CodeMirror {
 | 
					.CodeMirror {
 | 
				
			||||||
    height: 100%;
 | 
					    height: 100%;
 | 
				
			||||||
    background: inherit;
 | 
					    background: inherit;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,6 +31,7 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child {
 | 
				
			|||||||
    padding: 0px 10px;
 | 
					    padding: 0px 10px;
 | 
				
			||||||
    letter-spacing: 0.5px;
 | 
					    letter-spacing: 0.5px;
 | 
				
			||||||
    font-weight: bold;
 | 
					    font-weight: bold;
 | 
				
			||||||
 | 
					    top: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.attachment-content-wrapper pre code,
 | 
					.attachment-content-wrapper pre code,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1339,7 +1339,7 @@ body .calendar-dropdown-widget .calendar-body a:hover {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* Item title for deleted notes */
 | 
					/* Item title for deleted notes */
 | 
				
			||||||
.recent-changes-content ul li.deleted-note .note-title {
 | 
					.recent-changes-content ul li.deleted-note .note-title > .note-title {
 | 
				
			||||||
    text-decoration: line-through;
 | 
					    text-decoration: line-through;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1350,7 +1350,7 @@
 | 
				
			|||||||
    "mermaid-diagram": "Mermaid Diagram",
 | 
					    "mermaid-diagram": "Mermaid Diagram",
 | 
				
			||||||
    "canvas": "Canvas",
 | 
					    "canvas": "Canvas",
 | 
				
			||||||
    "web-view": "Webansicht",
 | 
					    "web-view": "Webansicht",
 | 
				
			||||||
    "mind-map": "Mind Map (Beta)",
 | 
					    "mind-map": "Mind Map",
 | 
				
			||||||
    "file": "Datei",
 | 
					    "file": "Datei",
 | 
				
			||||||
    "image": "Bild",
 | 
					    "image": "Bild",
 | 
				
			||||||
    "launcher": "Launcher",
 | 
					    "launcher": "Launcher",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1403,7 +1403,7 @@
 | 
				
			|||||||
    "mermaid-diagram": "Mermaid Diagram",
 | 
					    "mermaid-diagram": "Mermaid Diagram",
 | 
				
			||||||
    "canvas": "Canvas",
 | 
					    "canvas": "Canvas",
 | 
				
			||||||
    "web-view": "Web View",
 | 
					    "web-view": "Web View",
 | 
				
			||||||
    "mind-map": "Mind Map (Beta)",
 | 
					    "mind-map": "Mind Map",
 | 
				
			||||||
    "file": "File",
 | 
					    "file": "File",
 | 
				
			||||||
    "image": "Image",
 | 
					    "image": "Image",
 | 
				
			||||||
    "launcher": "Launcher",
 | 
					    "launcher": "Launcher",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1403,7 +1403,7 @@
 | 
				
			|||||||
    "mermaid-diagram": "Diagrama Mermaid",
 | 
					    "mermaid-diagram": "Diagrama Mermaid",
 | 
				
			||||||
    "canvas": "Lienzo",
 | 
					    "canvas": "Lienzo",
 | 
				
			||||||
    "web-view": "Vista Web",
 | 
					    "web-view": "Vista Web",
 | 
				
			||||||
    "mind-map": "Mapa Mental (beta)",
 | 
					    "mind-map": "Mapa Mental",
 | 
				
			||||||
    "file": "Archivo",
 | 
					    "file": "Archivo",
 | 
				
			||||||
    "image": "Imagen",
 | 
					    "image": "Imagen",
 | 
				
			||||||
    "launcher": "Lanzador",
 | 
					    "launcher": "Lanzador",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1351,7 +1351,7 @@
 | 
				
			|||||||
    "mermaid-diagram": "Diagramme Mermaid",
 | 
					    "mermaid-diagram": "Diagramme Mermaid",
 | 
				
			||||||
    "canvas": "Canevas",
 | 
					    "canvas": "Canevas",
 | 
				
			||||||
    "web-view": "Affichage Web",
 | 
					    "web-view": "Affichage Web",
 | 
				
			||||||
    "mind-map": "Carte mentale (Beta)",
 | 
					    "mind-map": "Carte mentale",
 | 
				
			||||||
    "file": "Fichier",
 | 
					    "file": "Fichier",
 | 
				
			||||||
    "image": "Image",
 | 
					    "image": "Image",
 | 
				
			||||||
    "launcher": "Raccourci",
 | 
					    "launcher": "Raccourci",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1367,7 +1367,7 @@
 | 
				
			|||||||
    "canvas": "Schiță",
 | 
					    "canvas": "Schiță",
 | 
				
			||||||
    "code": "Cod sursă",
 | 
					    "code": "Cod sursă",
 | 
				
			||||||
    "mermaid-diagram": "Diagramă Mermaid",
 | 
					    "mermaid-diagram": "Diagramă Mermaid",
 | 
				
			||||||
    "mind-map": "Hartă mentală (beta)",
 | 
					    "mind-map": "Hartă mentală",
 | 
				
			||||||
    "note-map": "Hartă notițe",
 | 
					    "note-map": "Hartă notițe",
 | 
				
			||||||
    "relation-map": "Hartă relații",
 | 
					    "relation-map": "Hartă relații",
 | 
				
			||||||
    "render-note": "Randare notiță",
 | 
					    "render-note": "Randare notiță",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,12 @@ import type BNote from "../../becca/entities/bnote.js";
 | 
				
			|||||||
import type BAttribute from "../../becca/entities/battribute.js";
 | 
					import type BAttribute from "../../becca/entities/battribute.js";
 | 
				
			||||||
import type { Request } from "express";
 | 
					import type { Request } from "express";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Backlink {
 | 
				
			||||||
 | 
					    noteId: string;
 | 
				
			||||||
 | 
					    relationName?: string;
 | 
				
			||||||
 | 
					    excerpts?: string[];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function buildDescendantCountMap(noteIdsToCount: string[]) {
 | 
					function buildDescendantCountMap(noteIdsToCount: string[]) {
 | 
				
			||||||
    if (!Array.isArray(noteIdsToCount)) {
 | 
					    if (!Array.isArray(noteIdsToCount)) {
 | 
				
			||||||
        throw new Error("noteIdsToCount: type error");
 | 
					        throw new Error("noteIdsToCount: type error");
 | 
				
			||||||
@@ -325,7 +331,7 @@ function findExcerpts(sourceNote: BNote, referencedNoteId: string) {
 | 
				
			|||||||
    return excerpts;
 | 
					    return excerpts;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getFilteredBacklinks(note: BNote) {
 | 
					function getFilteredBacklinks(note: BNote): BAttribute[] {
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        note
 | 
					        note
 | 
				
			||||||
            .getTargetRelations()
 | 
					            .getTargetRelations()
 | 
				
			||||||
@@ -344,7 +350,7 @@ function getBacklinkCount(req: Request) {
 | 
				
			|||||||
    };
 | 
					    };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getBacklinks(req: Request) {
 | 
					function getBacklinks(req: Request): Backlink[] {
 | 
				
			||||||
    const { noteId } = req.params;
 | 
					    const { noteId } = req.params;
 | 
				
			||||||
    const note = becca.getNoteOrThrow(noteId);
 | 
					    const note = becca.getNoteOrThrow(noteId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ import fs from "fs";
 | 
				
			|||||||
import dataDir from "./data_dir.js";
 | 
					import dataDir from "./data_dir.js";
 | 
				
			||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
import resourceDir from "./resource_dir.js";
 | 
					import resourceDir from "./resource_dir.js";
 | 
				
			||||||
 | 
					import { envToBoolean } from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini");
 | 
					const configSampleFilePath = path.resolve(resourceDir.RESOURCE_DIR, "config-sample.ini");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -14,6 +15,79 @@ if (!fs.existsSync(dataDir.CONFIG_INI_PATH)) {
 | 
				
			|||||||
    fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample);
 | 
					    fs.writeFileSync(dataDir.CONFIG_INI_PATH, configSample);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const config = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
 | 
					const iniConfig = ini.parse(fs.readFileSync(dataDir.CONFIG_INI_PATH, "utf-8"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TriliumConfig {
 | 
				
			||||||
 | 
					    General: {
 | 
				
			||||||
 | 
					        instanceName: string;
 | 
				
			||||||
 | 
					        noAuthentication: boolean;
 | 
				
			||||||
 | 
					        noBackup: boolean;
 | 
				
			||||||
 | 
					        noDesktopIcon: boolean;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    Network: {
 | 
				
			||||||
 | 
					        host: string;
 | 
				
			||||||
 | 
					        port: string;
 | 
				
			||||||
 | 
					        https: boolean;
 | 
				
			||||||
 | 
					        certPath: string;
 | 
				
			||||||
 | 
					        keyPath: string;
 | 
				
			||||||
 | 
					        trustedReverseProxy: boolean | string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    Sync: {
 | 
				
			||||||
 | 
					        syncServerHost: string;
 | 
				
			||||||
 | 
					        syncServerTimeout: string;
 | 
				
			||||||
 | 
					        syncProxy: string;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//prettier-ignore
 | 
				
			||||||
 | 
					const config: TriliumConfig = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    General: {
 | 
				
			||||||
 | 
					        instanceName:
 | 
				
			||||||
 | 
					            process.env.TRILIUM_GENERAL_INSTANCENAME || iniConfig.General.instanceName || "",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        noAuthentication: 
 | 
				
			||||||
 | 
					            envToBoolean(process.env.TRILIUM_GENERAL_NOAUTHENTICATION) || iniConfig.General.noAuthentication || false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        noBackup: 
 | 
				
			||||||
 | 
					            envToBoolean(process.env.TRILIUM_GENERAL_NOBACKUP) || iniConfig.General.noBackup || false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        noDesktopIcon: 
 | 
				
			||||||
 | 
					            envToBoolean(process.env.TRILIUM_GENERAL_NODESKTOPICON) || iniConfig.General.noDesktopIcon || false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Network: {
 | 
				
			||||||
 | 
					        host:
 | 
				
			||||||
 | 
					            process.env.TRILIUM_NETWORK_HOST || iniConfig.Network.host || "0.0.0.0",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        port:
 | 
				
			||||||
 | 
					            process.env.TRILIUM_NETWORK_PORT || iniConfig.Network.port || "3000",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        https: 
 | 
				
			||||||
 | 
					            envToBoolean(process.env.TRILIUM_NETWORK_HTTPS) || iniConfig.Network.https || false,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        certPath: 
 | 
				
			||||||
 | 
					            process.env.TRILIUM_NETWORK_CERTPATH || iniConfig.Network.certPath || "",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        keyPath: 
 | 
				
			||||||
 | 
					            process.env.TRILIUM_NETWORK_KEYPATH  || iniConfig.Network.keyPath || "",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        trustedReverseProxy:
 | 
				
			||||||
 | 
					            process.env.TRILIUM_NETWORK_TRUSTEDREVERSEPROXY || iniConfig.Network.trustedReverseProxy || false
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Sync: {
 | 
				
			||||||
 | 
					        syncServerHost:
 | 
				
			||||||
 | 
					            process.env.TRILIUM_SYNC_SERVER_HOST || iniConfig?.Sync?.syncServerHost || "",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        syncServerTimeout:
 | 
				
			||||||
 | 
					            process.env.TRILIUM_SYNC_SERVER_TIMEOUT || iniConfig?.Sync?.syncServerTimeout || "120000",
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        syncProxy:
 | 
				
			||||||
 | 
					            // additionally checking in iniConfig for inconsistently named syncProxy for backwards compatibility
 | 
				
			||||||
 | 
					            process.env.TRILIUM_SYNC_SERVER_PROXY || iniConfig?.Sync?.syncProxy || iniConfig?.Sync?.syncServerProxy || ""
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default config;
 | 
					export default config;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										103
									
								
								src/services/import/utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,103 @@
 | 
				
			|||||||
 | 
					import { describe, it, expect } from "vitest";
 | 
				
			||||||
 | 
					import importUtils from "./utils.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#extractHtmlTitle", () => {
 | 
				
			||||||
 | 
					    const htmlWithNoTitle = `
 | 
				
			||||||
 | 
					  <html>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					      <div>abc</div>
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					  </html>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const htmlWithTitle = `
 | 
				
			||||||
 | 
					  <html><head>
 | 
				
			||||||
 | 
					    <title>Test Title</title>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div>abc</div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					  </html>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const htmlWithTitleWOpeningBracket = `
 | 
				
			||||||
 | 
					  <html><head>
 | 
				
			||||||
 | 
					  <title>Test < Title</title>
 | 
				
			||||||
 | 
					  </head>
 | 
				
			||||||
 | 
					  <body>
 | 
				
			||||||
 | 
					    <div>abc</div>
 | 
				
			||||||
 | 
					  </body>
 | 
				
			||||||
 | 
					  </html>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // prettier-ignore
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof importUtils.extractHtmlTitle>[] = [
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ existing <title> tag, it should return the content of the title tag",
 | 
				
			||||||
 | 
					            [htmlWithTitle],
 | 
				
			||||||
 | 
					            "Test Title"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            // @TriliumNextTODO: this seems more like an unwanted behaviour to me – check if this needs rather fixing
 | 
				
			||||||
 | 
					            "with existing <title> tag, that includes an opening HTML tag '<', it should return null",
 | 
				
			||||||
 | 
					            [htmlWithTitleWOpeningBracket], 
 | 
				
			||||||
 | 
					            null
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/o an existing <title> tag, it should reutrn null",
 | 
				
			||||||
 | 
					            [htmlWithNoTitle],
 | 
				
			||||||
 | 
					            null
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ empty string content, it should return null",
 | 
				
			||||||
 | 
					            [""],
 | 
				
			||||||
 | 
					            null
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach((testCase) => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        return it(desc, () => {
 | 
				
			||||||
 | 
					            const actual = importUtils.extractHtmlTitle(...fnParams);
 | 
				
			||||||
 | 
					            expect(actual).toStrictEqual(expected);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe("#handleH1", () => {
 | 
				
			||||||
 | 
					    // prettier-ignore
 | 
				
			||||||
 | 
					    const testCases: TestCase<typeof importUtils.handleH1>[] = [
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ single <h1> tag w/ identical text content as the title tag: the <h1> tag should be stripped",
 | 
				
			||||||
 | 
					            ["<h1>Title</h1>", "Title"],
 | 
				
			||||||
 | 
					            ""
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ multiple <h1> tags, with the fist matching the title tag: the first <h1> tag should be stripped and subsequent tags converted to <h2>",
 | 
				
			||||||
 | 
					            ["<h1>Title</h1><h1>Header 1</h1><h1>Header 2</h1>", "Title"],
 | 
				
			||||||
 | 
					            "<h2>Header 1</h2><h2>Header 2</h2>"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ no <h1> tag and only <h2> tags, it should not cause any changes and return the same content",
 | 
				
			||||||
 | 
					            ["<h2>Heading 1</h2><h2>Heading 2</h2>", "Title"],
 | 
				
			||||||
 | 
					            "<h2>Heading 1</h2><h2>Heading 2</h2>"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ multiple <h1> tags, and the 1st matching the title tag, it should strip ONLY the very first occurence of the <h1> tags in the returned content",
 | 
				
			||||||
 | 
					            ["<h1>Topic ABC</h1><h1>Heading 1</h1><h1>Topic ABC</h1>", "Topic ABC"],
 | 
				
			||||||
 | 
					            "<h2>Heading 1</h2><h2>Topic ABC</h2>"
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        [
 | 
				
			||||||
 | 
					            "w/ multiple <h1> tags, and the 1st matching NOT the title tag, it should NOT strip any other <h1> tags",
 | 
				
			||||||
 | 
					            ["<h1>Introduction</h1><h1>Topic ABC</h1><h1>Summary</h1>", "Topic ABC"],
 | 
				
			||||||
 | 
					            "<h2>Introduction</h2><h2>Topic ABC</h2><h2>Summary</h2>"
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    testCases.forEach((testCase) => {
 | 
				
			||||||
 | 
					        const [desc, fnParams, expected] = testCase;
 | 
				
			||||||
 | 
					        return it(desc, () => {
 | 
				
			||||||
 | 
					            const actual = importUtils.handleH1(...fnParams);
 | 
				
			||||||
 | 
					            expect(actual).toStrictEqual(expected);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@@ -1,14 +1,19 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function handleH1(content: string, title: string) {
 | 
					function handleH1(content: string, title: string) {
 | 
				
			||||||
    content = content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => {
 | 
					    let isFirstH1Handled = false;
 | 
				
			||||||
        if (title.trim() === text.trim()) {
 | 
					
 | 
				
			||||||
            return ""; // remove whole H1 tag
 | 
					    return content.replace(/<h1[^>]*>([^<]*)<\/h1>/gi, (match, text) => {
 | 
				
			||||||
        } else {
 | 
					        const convertedContent = `<h2>${text}</h2>`;
 | 
				
			||||||
            return `<h2>${text}</h2>`;
 | 
					
 | 
				
			||||||
 | 
					        // strip away very first found h1 tag, if it matches the title
 | 
				
			||||||
 | 
					        if (!isFirstH1Handled) {
 | 
				
			||||||
 | 
					            isFirstH1Handled = true;
 | 
				
			||||||
 | 
					            return title.trim() === text.trim() ? "" : convertedContent;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return convertedContent;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    return content;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function extractHtmlTitle(content: string): string | null {
 | 
					function extractHtmlTitle(content: string): string | null {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ function getRunAtHours(note: BNote): number[] {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function runNotesWithLabel(runAttrValue: string) {
 | 
					function runNotesWithLabel(runAttrValue: string) {
 | 
				
			||||||
    const instanceName = config.General ? config.General.instanceName : null;
 | 
					    const instanceName = config.General.instanceName;
 | 
				
			||||||
    const currentHours = new Date().getHours();
 | 
					    const currentHours = new Date().getHours();
 | 
				
			||||||
    const notes = attributeService.getNotesWithLabel("run", runAttrValue);
 | 
					    const notes = attributeService.getNotesWithLabel("run", runAttrValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,6 @@
 | 
				
			|||||||
"use strict";
 | 
					"use strict";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import optionService from "./options.js";
 | 
					import optionService from "./options.js";
 | 
				
			||||||
import type { OptionNames } from "./options_interface.js";
 | 
					 | 
				
			||||||
import config from "./config.js";
 | 
					import config from "./config.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/*
 | 
					/*
 | 
				
			||||||
@@ -11,14 +10,14 @@ import config from "./config.js";
 | 
				
			|||||||
 * to live sync server.
 | 
					 * to live sync server.
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function get(name: OptionNames) {
 | 
					function get(name: keyof typeof config.Sync) {
 | 
				
			||||||
    return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
 | 
					  return (config["Sync"] && config["Sync"][name]) || optionService.getOption(name);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default {
 | 
					export default {
 | 
				
			||||||
    // env variable is the easiest way to guarantee we won't overwrite prod data during development
 | 
					    // env variable is the easiest way to guarantee we won't overwrite prod data during development
 | 
				
			||||||
    // after copying prod document/data directory
 | 
					    // after copying prod document/data directory
 | 
				
			||||||
    getSyncServerHost: () => process.env.TRILIUM_SYNC_SERVER_HOST || get("syncServerHost"),
 | 
					    getSyncServerHost: () => get("syncServerHost"),
 | 
				
			||||||
    isSyncSetup: () => {
 | 
					    isSyncSetup: () => {
 | 
				
			||||||
        const syncServerHost = get("syncServerHost");
 | 
					        const syncServerHost = get("syncServerHost");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -295,6 +295,18 @@ export function isString(x: any) {
 | 
				
			|||||||
    return Object.prototype.toString.call(x) === "[object String]";
 | 
					    return Object.prototype.toString.call(x) === "[object String]";
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// try to turn 'true' and 'false' strings from process.env variables into boolean values or undefined
 | 
				
			||||||
 | 
					export function envToBoolean(val: string | undefined) {
 | 
				
			||||||
 | 
					    if (val === undefined || typeof val !== "string") return undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const valLc = val.toLowerCase().trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (valLc === "true") return true;
 | 
				
			||||||
 | 
					    if (valLc === "false") return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package.
 | 
					 * Returns the directory for resources. On Electron builds this corresponds to the `resources` subdirectory inside the distributable package.
 | 
				
			||||||
 * On development builds, this simply refers to the root directory of the application.
 | 
					 * On development builds, this simply refers to the root directory of the application.
 | 
				
			||||||
@@ -352,5 +364,6 @@ export default {
 | 
				
			|||||||
    isString,
 | 
					    isString,
 | 
				
			||||||
    getResourceDir,
 | 
					    getResourceDir,
 | 
				
			||||||
    isMac,
 | 
					    isMac,
 | 
				
			||||||
    isWindows
 | 
					    isWindows,
 | 
				
			||||||
 | 
					    envToBoolean
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import WebSocket from "ws";
 | 
					import { WebSocketServer as WebSocketServer, WebSocket } from "ws";
 | 
				
			||||||
import { isElectron, randomString } from "./utils.js";
 | 
					import { isElectron, randomString } from "./utils.js";
 | 
				
			||||||
import log from "./log.js";
 | 
					import log from "./log.js";
 | 
				
			||||||
import sql from "./sql.js";
 | 
					import sql from "./sql.js";
 | 
				
			||||||
@@ -10,7 +10,7 @@ import becca from "../becca/becca.js";
 | 
				
			|||||||
import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
 | 
					import AbstractBeccaEntity from "../becca/entities/abstract_becca_entity.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import env from "./env.js";
 | 
					import env from "./env.js";
 | 
				
			||||||
import type { IncomingMessage, Server } from "http";
 | 
					import type { IncomingMessage, Server as HttpServer } from "http";
 | 
				
			||||||
import type { EntityChange } from "./entity_changes_interface.js";
 | 
					import type { EntityChange } from "./entity_changes_interface.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (env.isDev()) {
 | 
					if (env.isDev()) {
 | 
				
			||||||
@@ -24,7 +24,7 @@ if (env.isDev()) {
 | 
				
			|||||||
        .on("unlink", debouncedReloadFrontend);
 | 
					        .on("unlink", debouncedReloadFrontend);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let webSocketServer!: WebSocket.Server;
 | 
					let webSocketServer!: WebSocketServer;
 | 
				
			||||||
let lastSyncedPush: number | null = null;
 | 
					let lastSyncedPush: number | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface Message {
 | 
					interface Message {
 | 
				
			||||||
@@ -58,8 +58,8 @@ interface Message {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void;
 | 
					type SessionParser = (req: IncomingMessage, params: {}, cb: () => void) => void;
 | 
				
			||||||
function init(httpServer: Server, sessionParser: SessionParser) {
 | 
					function init(httpServer: HttpServer, sessionParser: SessionParser) {
 | 
				
			||||||
    webSocketServer = new WebSocket.Server({
 | 
					    webSocketServer = new WebSocketServer({
 | 
				
			||||||
        verifyClient: (info, done) => {
 | 
					        verifyClient: (info, done) => {
 | 
				
			||||||
            sessionParser(info.req, {}, () => {
 | 
					            sessionParser(info.req, {}, () => {
 | 
				
			||||||
                const allowed = isElectron() || (info.req as any).session.loggedIn || (config.General && config.General.noAuthentication);
 | 
					                const allowed = isElectron() || (info.req as any).session.loggedIn || (config.General && config.General.noAuthentication);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,16 @@
 | 
				
			|||||||
import { fileURLToPath } from "url";
 | 
					import { fileURLToPath } from "url";
 | 
				
			||||||
import path from "path";
 | 
					import path from "path";
 | 
				
			||||||
import assetPath from "./src/services/asset_path.js";
 | 
					import assetPath from "./src/services/asset_path.js";
 | 
				
			||||||
 | 
					import type { Configuration } from "webpack";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const rootDir = path.dirname(fileURLToPath(import.meta.url));
 | 
					const rootDir = path.dirname(fileURLToPath(import.meta.url));
 | 
				
			||||||
export default {
 | 
					const config: Configuration = {
 | 
				
			||||||
    mode: "production",
 | 
					    mode: "production",
 | 
				
			||||||
    entry: {
 | 
					    entry: {
 | 
				
			||||||
        setup: "./src/public/app/setup.js",
 | 
					        setup: "./src/public/app/setup.js",
 | 
				
			||||||
        mobile: "./src/public/app/mobile.js",
 | 
					        mobile: "./src/public/app/mobile.js",
 | 
				
			||||||
        desktop: "./src/public/app/desktop.js"
 | 
					        desktop: "./src/public/app/desktop.js",
 | 
				
			||||||
 | 
					        share: "./src/public/app/share.js"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    output: {
 | 
					    output: {
 | 
				
			||||||
        publicPath: `${assetPath}/app-dist/`,
 | 
					        publicPath: `${assetPath}/app-dist/`,
 | 
				
			||||||
@@ -42,3 +44,5 @@ export default {
 | 
				
			|||||||
    devtool: "source-map",
 | 
					    devtool: "source-map",
 | 
				
			||||||
    target: "electron-renderer"
 | 
					    target: "electron-renderer"
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default config;
 | 
				
			||||||