mirror of
				https://github.com/TriliumNext/Notes.git
				synced 2025-10-31 21:11:30 +08:00 
			
		
		
		
	Merge remote-tracking branch 'origin/develop' into feature/in_app_help
This commit is contained in:
		
						commit
						7fa0ad336e
					
				| @ -15,3 +15,9 @@ indent_size = 2 | ||||
| indent_style = space | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
| 
 | ||||
| [*.yml] | ||||
| indent_size = 2 | ||||
| indent_style = space | ||||
| insert_final_newline = true | ||||
| trim_trailing_whitespace = true | ||||
							
								
								
									
										39
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								.github/actions/build-electron/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | ||||
| inputs: | ||||
|   os: | ||||
|     description: "One of the supported platforms: macos, linux, windows" | ||||
|     required: true | ||||
|   arch: | ||||
|     description: "The architecture to build for: x64, arm64" | ||||
|     required: true | ||||
|   extension: | ||||
|     description: "Platform specific extensions to copy in the output: dmg, deb, rpm, exe" | ||||
|     required: true | ||||
| runs: | ||||
|   using: composite | ||||
|   steps: | ||||
|     - name: Set up Python for appdmg to be installed | ||||
|       if: ${{ inputs.os == 'macos' }} | ||||
|       shell: bash | ||||
|       run: brew install python-setuptools | ||||
|     - name: Install rpm on Ubuntu for RPM package building | ||||
|       if: ${{ inputs.os == 'linux' }} | ||||
|       shell: bash | ||||
|       run: sudo apt install rpm | ||||
|     - name: Install dependencies | ||||
|       shell: bash | ||||
|       run: npm ci | ||||
|     - name: Update build info | ||||
|       shell: bash | ||||
|       run: npm run update-build-info | ||||
|     - name: Run electron-forge | ||||
|       shell: bash | ||||
|       run: npm run make-electron -- --arch=${{ inputs.arch }} | ||||
|     - name: Prepare artifacts | ||||
|       shell: bash | ||||
|       run: | | ||||
|         mkdir -p upload; | ||||
|         for ext in ${{ inputs.extension }}; | ||||
|         do | ||||
|           file=$(find out/make -name "*.$ext" -print -quit); | ||||
|           cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ inputs.os }}-${{ inputs.arch }}.$ext"; | ||||
|         done | ||||
							
								
								
									
										28
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								.github/actions/build-server/action.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,28 @@ | ||||
| inputs: | ||||
|   arch: | ||||
|     description: "The architecture to build for: x64, arm64" | ||||
|     required: true | ||||
| runs: | ||||
|   using: composite | ||||
|   steps: | ||||
|     - name: Set up node & dependencies | ||||
|       uses: actions/setup-node@v4 | ||||
|       with: | ||||
|         node-version: 20 | ||||
|         cache: "npm" | ||||
|     - name: Install dependencies | ||||
|       shell: bash | ||||
|       run: npm ci | ||||
|     - name: Run Linux server build | ||||
|       env: | ||||
|         MATRIX_ARCH: ${{ inputs.arch }} | ||||
|       shell: bash | ||||
|       run: | | ||||
|         npm run update-build-info | ||||
|         ./bin/build-server.sh | ||||
|     - name: Prepare artifacts | ||||
|       shell: bash | ||||
|       run: | | ||||
|         mkdir -p upload | ||||
|         file=$(find dist -name '*.tar.xz' -print -quit) | ||||
|         cp "$file" "upload/TriliumNextNotes-linux-${{ inputs.arch }}-${{ github.ref_name }}.tar.xz" | ||||
							
								
								
									
										55
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										55
									
								
								.github/workflows/main.yml
									
									
									
									
										vendored
									
									
								
							| @ -23,10 +23,10 @@ jobs: | ||||
|         os: | ||||
|           - name: macos | ||||
|             image: macos-latest | ||||
|             extension: dmg | ||||
|             extension: [dmg, zip] | ||||
|           - name: linux | ||||
|             image: ubuntu-latest | ||||
|             extension: deb | ||||
|             extension: [deb, rpm, zip] | ||||
|           - name: windows | ||||
|             image: windows-latest | ||||
|             extension: exe | ||||
| @ -37,31 +37,12 @@ jobs: | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|       - name: Set up Python for appdmg to be installed | ||||
|         if: ${{ matrix.os.name == 'macos' }} | ||||
|         run: brew install python-setuptools | ||||
|       - name: Install dependencies | ||||
|         run: npm ci | ||||
|       - name: Update build info | ||||
|         run: npm run update-build-info       | ||||
|       - name: Run electron-forge | ||||
|         run: npm run make-electron -- --arch=${{ matrix.arch }} | ||||
|       - name: Prepare artifacts (Unix) | ||||
|         if: runner.os != 'windows' | ||||
|         run: | | ||||
|           mkdir -p upload | ||||
|           file=$(find out/make -name '*.zip' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.zip" | ||||
|           file=$(find out/make -name '*.${{ matrix.os.extension }}' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.${{ matrix.os.extension }}" | ||||
|       - name: Prepare artifacts (Windows) | ||||
|         if: runner.os == 'windows' | ||||
|         run: | | ||||
|           mkdir upload | ||||
|           $file = Get-ChildItem -Path out/make -Filter '*.zip' -Recurse | Select-Object -First 1 | ||||
|           Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.zip" | ||||
|           $file = Get-ChildItem -Path out/make -Filter '*.${{ matrix.os.extension }}' -Recurse | Select-Object -First 1 | ||||
|           Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-${{ github.ref_name }}.${{ matrix.os.extension }}" | ||||
|       - name: Run the build | ||||
|         uses: ./.github/actions/build-electron | ||||
|         with: | ||||
|           os: ${{ matrix.os.name }} | ||||
|           arch: ${{ matrix.arch }} | ||||
|           extension: ${{ matrix.os.extension }} | ||||
|       - name: Publish artifacts | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
| @ -86,24 +67,10 @@ jobs: | ||||
|     runs-on: ${{ matrix.runs-on }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - name: Set up node & dependencies | ||||
|         uses: actions/setup-node@v4 | ||||
|       - name: Run the build | ||||
|         uses: ./.github/actions/build-server | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           cache: "npm" | ||||
|       - name: Install dependencies | ||||
|         run: npm ci | ||||
|       - name: Run Linux server build | ||||
|         env: | ||||
|           MATRIX_ARCH: ${{ matrix.arch }} | ||||
|         run: | | ||||
|           npm run update-build-info | ||||
|           ./bin/build-server.sh | ||||
|       - name: Prepare artifacts | ||||
|         run: | | ||||
|           mkdir -p upload | ||||
|           file=$(find dist -name '*.tar.xz' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz" | ||||
|           arch: ${{ matrix.arch }} | ||||
|       - uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: TriliumNextNotes linux server ${{ matrix.arch }} | ||||
|  | ||||
							
								
								
									
										107
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										107
									
								
								.github/workflows/nightly.yml
									
									
									
									
										vendored
									
									
								
							| @ -20,10 +20,10 @@ jobs: | ||||
|         os: | ||||
|           - name: macos | ||||
|             image: macos-latest | ||||
|             extension: dmg | ||||
|             extension: [dmg, zip] | ||||
|           - name: linux | ||||
|             image: ubuntu-latest | ||||
|             extension: deb | ||||
|             extension: [deb, rpm, zip] | ||||
|           - name: windows | ||||
|             image: windows-latest | ||||
|             extension: exe | ||||
| @ -34,62 +34,26 @@ jobs: | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|       - name: Set up Python for appdmg to be installed | ||||
|         if: ${{ matrix.os.name == 'macos' }} | ||||
|         run: brew install python-setuptools | ||||
|       - name: Install dependencies | ||||
|         shell: bash | ||||
|         run: npm ci | ||||
|       - name: Update build info | ||||
|         run: npm run update-build-info | ||||
|       - name: Update nightly version | ||||
|         run: npm run ci-update-nightly-version | ||||
|       - name: Run electron-forge | ||||
|         run: npm run make-electron -- --arch=${{ matrix.arch }} | ||||
|       - name: Prepare artifacts (Unix) | ||||
|         if: runner.os != 'windows' | ||||
|         run: | | ||||
|           mkdir -p upload | ||||
|           file=$(find out/make -name '*.zip' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.zip" | ||||
|           file=$(find out/make -name '*.${{ matrix.os.extension }}' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}" | ||||
|       - name: Prepare artifacts (Windows) | ||||
|         if: runner.os == 'windows' | ||||
|         run: | | ||||
|           mkdir upload | ||||
|           $file = Get-ChildItem -Path out/make -Filter '*.zip' -Recurse | Select-Object -First 1 | ||||
|           Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.zip" | ||||
|           $file = Get-ChildItem -Path out/make -Filter '*.${{ matrix.os.extension }}' -Recurse | Select-Object -First 1 | ||||
|           Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}" | ||||
|       - name: Publish artifacts | ||||
|         uses: actions/upload-artifact@v4 | ||||
|       - name: Run the build | ||||
|         uses: ./.github/actions/build-electron | ||||
|         with: | ||||
|           name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }} | ||||
|           path: upload/*.zip | ||||
|           overwrite: true | ||||
|       - name: Publish installer artifacts | ||||
|         uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: TriliumNextNotes ${{ matrix.os.name }} ${{ matrix.arch }} | ||||
|           path: upload/*.${{ matrix.os.extension }} | ||||
|           overwrite: true | ||||
|           os: ${{ matrix.os.name }} | ||||
|           arch: ${{ matrix.arch }} | ||||
|           extension: ${{ join(matrix.os.extension, ' ') }} | ||||
| 
 | ||||
|       - name: Deploy release | ||||
|         uses: WebFreak001/deploy-nightly@v3.2.0 | ||||
|       - name: Publish release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
|           upload_url: ${{ env.GITHUB_UPLOAD_URL }} | ||||
|           release_id: ${{ env.GITHUB_RELEASE_ID }} | ||||
|           asset_path: upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.zip # path to archive to upload | ||||
|           asset_name: TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-nightly.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash | ||||
|           asset_content_type: application/zip # required by GitHub API | ||||
|       - name: Deploy installer release | ||||
|         uses: WebFreak001/deploy-nightly@v3.2.0 | ||||
|         with: | ||||
|           upload_url: ${{ env.GITHUB_UPLOAD_URL }} | ||||
|           release_id: ${{ env.GITHUB_RELEASE_ID }} | ||||
|           asset_path: upload/TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }} # path to archive to upload | ||||
|           asset_name: TriliumNextNotes-${{ matrix.os.name }}-${{ matrix.arch }}-nightly.${{ matrix.os.extension }} # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash | ||||
|           asset_content_type: application/zip # required by GitHub API | ||||
|           draft: true | ||||
|           fail_on_unmatched_files: true | ||||
|           files: upload/*.* | ||||
|           tag_name: nightly | ||||
|           name: Nightly Build | ||||
|   nightly-server: | ||||
|     name: Deploy server nightly | ||||
|     strategy: | ||||
| @ -104,34 +68,17 @@ jobs: | ||||
|     runs-on: ${{ matrix.runs-on }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - name: Set up node & dependencies | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           cache: "npm" | ||||
|       - name: Install dependencies | ||||
|         run: npm ci | ||||
|       - name: Run Linux server build | ||||
|         env: | ||||
|           MATRIX_ARCH: ${{ matrix.arch }} | ||||
|         run: | | ||||
|           npm run update-build-info | ||||
|           ./bin/build-server.sh | ||||
|       - name: Prepare artifacts | ||||
|         run: | | ||||
|           mkdir -p upload | ||||
|           file=$(find dist -name '*.tar.xz' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz" | ||||
|       - uses: actions/upload-artifact@v4 | ||||
|         with: | ||||
|           name: TriliumNextNotes linux server ${{ matrix.arch }} | ||||
|           path: upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz | ||||
| 
 | ||||
|       - name: Deploy release | ||||
|         uses: WebFreak001/deploy-nightly@v3.2.0 | ||||
|       - name: Run the build | ||||
|         uses: ./.github/actions/build-server | ||||
|         with: | ||||
|           upload_url: ${{ env.GITHUB_UPLOAD_URL }} | ||||
|           release_id: ${{ env.GITHUB_RELEASE_ID }} | ||||
|           asset_path: upload/TriliumNextNotes-linux-x64-${{ github.ref_name }}.tar.xz # path to archive to upload | ||||
|           asset_name: TriliumNextNotes-linux-x64-nightly.zip # name to upload the release as, use $$ to insert date (YYYYMMDD) and 6 letter commit hash | ||||
|           asset_content_type: application/zip # required by GitHub API | ||||
|           arch: ${{ matrix.arch }} | ||||
| 
 | ||||
|       - name: Publish release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
|           draft: true | ||||
|           fail_on_unmatched_files: true | ||||
|           files: upload/*.* | ||||
|           tag_name: nightly | ||||
|           name: Nightly Build | ||||
|  | ||||
							
								
								
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										57
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @ -20,10 +20,10 @@ jobs: | ||||
|         os: | ||||
|           - name: macos | ||||
|             image: macos-latest | ||||
|             extension: dmg | ||||
|             extension: [dmg, zip] | ||||
|           - name: linux | ||||
|             image: ubuntu-latest | ||||
|             extension: deb | ||||
|             extension: [deb, rpm, zip] | ||||
|           - name: windows | ||||
|             image: windows-latest | ||||
|             extension: exe | ||||
| @ -34,31 +34,12 @@ jobs: | ||||
|         uses: actions/setup-node@v4 | ||||
|         with: | ||||
|           node-version: 20 | ||||
|       - name: Set up Python for appdmg to be installed | ||||
|         if: ${{ matrix.os.name == 'macos' }} | ||||
|         run: brew install python-setuptools | ||||
|       - name: Install dependencies | ||||
|         run: npm ci | ||||
|       - name: Update build info | ||||
|         run: npm run update-build-info | ||||
|       - name: Run electron-forge | ||||
|         run: npm run make-electron -- --arch=${{ matrix.arch }} | ||||
|       - name: Prepare artifacts (Unix) | ||||
|         if: runner.os != 'windows' | ||||
|         run: | | ||||
|           mkdir -p upload | ||||
|           file=$(find out/make -name '*.zip' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.zip" | ||||
|           file=$(find out/make -name '*.${{ matrix.os.extension }}' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}" | ||||
|       - name: Prepare artifacts (Windows) | ||||
|         if: runner.os == 'windows' | ||||
|         run: | | ||||
|           mkdir upload | ||||
|           $file = Get-ChildItem -Path out/make -Filter '*.zip' -Recurse | Select-Object -First 1 | ||||
|           Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.zip" | ||||
|           $file = Get-ChildItem -Path out/make -Filter '*.${{ matrix.os.extension }}' -Recurse | Select-Object -First 1 | ||||
|           Copy-Item -Path $file.FullName -Destination "upload/TriliumNextNotes-${{ github.ref_name }}-${{ matrix.os.name }}-${{ matrix.arch }}.${{ matrix.os.extension }}"       | ||||
|       - name: Run the build | ||||
|         uses: ./.github/actions/build-electron | ||||
|         with: | ||||
|           os: ${{ matrix.os.name }} | ||||
|           arch: ${{ matrix.arch }} | ||||
|           extension: ${{ join(matrix.os.extension, ' ') }} | ||||
|       - name: Publish release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
| @ -79,24 +60,12 @@ jobs: | ||||
|     runs-on: ${{ matrix.runs-on }} | ||||
|     steps: | ||||
|       - uses: actions/checkout@v4 | ||||
|       - name: Set up node & dependencies | ||||
|         uses: actions/setup-node@v4 | ||||
| 
 | ||||
|       - name: Run the build | ||||
|         uses: ./.github/actions/build-server | ||||
|         with: | ||||
|           node-version: 20 | ||||
|           cache: "npm" | ||||
|       - name: Install dependencies | ||||
|         run: npm ci | ||||
|       - name: Run Linux server build | ||||
|         env: | ||||
|           MATRIX_ARCH: ${{ matrix.arch }} | ||||
|         run: | | ||||
|           npm run update-build-info | ||||
|           ./bin/build-server.sh | ||||
|       - name: Prepare artifacts | ||||
|         run: | | ||||
|           mkdir -p upload | ||||
|           file=$(find dist -name '*.tar.xz' -print -quit) | ||||
|           cp "$file" "upload/TriliumNextNotes-linux-${{ matrix.arch }}-${{ github.ref_name }}.tar.xz" | ||||
|           arch: ${{ matrix.arch }} | ||||
| 
 | ||||
|       - name: Publish release | ||||
|         uses: softprops/action-gh-release@v2 | ||||
|         with: | ||||
|  | ||||
| @ -1,12 +1,17 @@ | ||||
| [Desktop Entry] | ||||
| <% if (productName) { %>Name=<%= productName %> | ||||
| <% } %><% if (description) { %>Comment=<%= description %> | ||||
| <% } %><% if (genericName) { %>GenericName=<%= genericName %> | ||||
| <% } %><% if (name) { %>Exec=<%= name %> %U | ||||
| Icon=<%= name %> | ||||
| <% } %>Type=Application | ||||
| StartupNotify=true | ||||
| <% if (productName) { %>StartupWMClass=<%= productName %> | ||||
| <% } if (categories && categories.length) { %>Categories=<%= categories.join(';') %>; | ||||
| <% } %><% if (mimeType && mimeType.length) { %>MimeType=<%= mimeType.join(';') %>; | ||||
| <% } %> | ||||
| <%= | ||||
| Object.entries({ | ||||
|     "Name": productName, | ||||
|     "Comment": description, | ||||
|     "GenericName": genericName, | ||||
|     "Exec": name ? `${name} %U` : undefined, | ||||
|     "Icon": name, | ||||
|     "Type": "Application", | ||||
|     "StartupNotify": "true", | ||||
|     "StartupWMClass": productName, | ||||
|     "Categories": categories?.length ? `${categories.join(";")};` : undefined, | ||||
|     "MimeType": mimeType?.length ? `${mimeType.join(";")};` : undefined | ||||
| }) | ||||
| .map(line => line[1] ? line.join("=") : undefined) | ||||
| .filter(line => !!line) | ||||
| .join("\n")%> | ||||
| @ -26,6 +26,8 @@ electronDl({ saveAs: true }); | ||||
| // needed for excalidraw export https://github.com/zadam/trilium/issues/4271
 | ||||
| electron.app.commandLine.appendSwitch("enable-experimental-web-platform-features"); | ||||
| 
 | ||||
| electron.app.userAgentFallback = `${electron.app.getName()} ${electron.app.getVersion()}`; | ||||
| 
 | ||||
| // Quit when all windows are closed, except on macOS. There, it's common
 | ||||
| // for applications and their menu bar to stay active until the user quits
 | ||||
| // explicitly with Cmd + Q.
 | ||||
|  | ||||
| @ -3,6 +3,12 @@ const fs = require("fs-extra"); | ||||
| 
 | ||||
| const APP_NAME = "TriliumNext Notes"; | ||||
| 
 | ||||
| const extraResourcesForPlatform = getExtraResourcesForPlatform(); | ||||
| const baseLinuxMakerConfigOptions = { | ||||
|   icon: "./images/app-icons/png/128x128.png", | ||||
|   desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs"), | ||||
| }; | ||||
| 
 | ||||
| module.exports = { | ||||
|     packagerConfig: { | ||||
|         executableName: "trilium", | ||||
| @ -12,7 +18,7 @@ module.exports = { | ||||
|         icon: "./images/app-icons/icon", | ||||
|         extraResource: [ | ||||
|             // Moved to root
 | ||||
|             ...getExtraResourcesForPlatform(), | ||||
|             ...extraResourcesForPlatform, | ||||
| 
 | ||||
|             // Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
 | ||||
|             "translations/", | ||||
| @ -20,22 +26,18 @@ module.exports = { | ||||
|         ], | ||||
|         afterComplete: [ | ||||
|             (buildPath, _electronVersion, platform, _arch, callback) => { | ||||
|                 const extraResources = getExtraResourcesForPlatform(); | ||||
|                 for (const resource of extraResources) { | ||||
|                 for (const resource of extraResourcesForPlatform) { | ||||
|                     const baseName = path.basename(resource); | ||||
|                     let sourcePath; | ||||
|                     if (platform === "darwin") { | ||||
|                         sourcePath = path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName); | ||||
|                     } else { | ||||
|                         sourcePath = path.join(buildPath, "resources", baseName); | ||||
|                     } | ||||
|                     let destPath; | ||||
| 
 | ||||
|                     if (baseName !== "256x256.png") { | ||||
|                         destPath = path.join(buildPath, baseName); | ||||
|                     } else { | ||||
|                         destPath = path.join(buildPath, "icon.png"); | ||||
|                     } | ||||
|                     // prettier-ignore
 | ||||
|                     const sourcePath = (platform === "darwin") | ||||
|                         ? path.join(buildPath, `${APP_NAME}.app`, "Contents", "Resources", baseName) | ||||
|                         : path.join(buildPath, "resources", baseName); | ||||
| 
 | ||||
|                     // prettier-ignore
 | ||||
|                     const destPath = (baseName !== "256x256.png") | ||||
|                         ? path.join(buildPath, baseName) | ||||
|                         : path.join(buildPath, "icon.png"); | ||||
| 
 | ||||
|                     // Copy files from resources folder to root
 | ||||
|                     fs.move(sourcePath, destPath) | ||||
| @ -53,8 +55,15 @@ module.exports = { | ||||
|             name: "@electron-forge/maker-deb", | ||||
|             config: { | ||||
|                 options: { | ||||
|                     icon: "./images/app-icons/png/128x128.png", | ||||
|                     desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs") | ||||
|                   ...baseLinuxMakerConfigOptions | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
|         { | ||||
|             name: "@electron-forge/maker-rpm", | ||||
|             config: { | ||||
|                 options: { | ||||
|                   ...baseLinuxMakerConfigOptions | ||||
|                 } | ||||
|             } | ||||
|         }, | ||||
| @ -91,21 +100,20 @@ module.exports = { | ||||
| }; | ||||
| 
 | ||||
| function getExtraResourcesForPlatform() { | ||||
|     let resources = ["dump-db/", "./bin/tpl/anonymize-database.sql"]; | ||||
|     const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"]; | ||||
|     const resources = ["dump-db/", "./bin/tpl/anonymize-database.sql"]; | ||||
| 
 | ||||
|     const getScriptRessources = () => { | ||||
|         const scripts = ["trilium-portable", "trilium-safe-mode", "trilium-no-cert-check"]; | ||||
|         const scriptExt = (process.platform === "win32") ? "bat" : "sh"; | ||||
|         return scripts.map(script => `./bin/tpl/${script}.${scriptExt}`); | ||||
|     } | ||||
| 
 | ||||
|     switch (process.platform) { | ||||
|         case "win32": | ||||
|             for (const script of scripts) { | ||||
|                 resources.push(`./bin/tpl/${script}.bat`); | ||||
|             } | ||||
|             break; | ||||
|         case "darwin": | ||||
|             resources.push(...getScriptRessources()) | ||||
|             break; | ||||
|         case "linux": | ||||
|             resources.push("images/app-icons/png/256x256.png"); | ||||
|             for (const script of scripts) { | ||||
|                 resources.push(`./bin/tpl/${script}.sh`); | ||||
|             } | ||||
|             resources.push(...getScriptRessources(), "images/app-icons/png/256x256.png"); | ||||
|             break; | ||||
|         default: | ||||
|             break; | ||||
|  | ||||
							
								
								
									
										374
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										374
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @ -1,12 +1,12 @@ | ||||
| { | ||||
|   "name": "trilium", | ||||
|   "version": "0.91.5", | ||||
|   "version": "0.91.6", | ||||
|   "lockfileVersion": 3, | ||||
|   "requires": true, | ||||
|   "packages": { | ||||
|     "": { | ||||
|       "name": "trilium", | ||||
|       "version": "0.91.5", | ||||
|       "version": "0.91.6", | ||||
|       "license": "AGPL-3.0-only", | ||||
|       "dependencies": { | ||||
|         "@braintree/sanitize-url": "7.1.1", | ||||
| @ -86,7 +86,6 @@ | ||||
|         "sanitize-filename": "1.6.3", | ||||
|         "sanitize-html": "2.14.0", | ||||
|         "sax": "1.4.1", | ||||
|         "semver": "7.7.0", | ||||
|         "serve-favicon": "2.5.0", | ||||
|         "session-file-store": "1.5.0", | ||||
|         "source-map-support": "0.5.21", | ||||
| @ -109,11 +108,12 @@ | ||||
|         "@electron-forge/cli": "7.6.1", | ||||
|         "@electron-forge/maker-deb": "7.6.1", | ||||
|         "@electron-forge/maker-dmg": "7.6.1", | ||||
|         "@electron-forge/maker-rpm": "7.6.1", | ||||
|         "@electron-forge/maker-squirrel": "7.6.1", | ||||
|         "@electron-forge/maker-zip": "7.6.1", | ||||
|         "@electron-forge/plugin-auto-unpack-natives": "7.6.1", | ||||
|         "@electron/rebuild": "3.7.1", | ||||
|         "@playwright/test": "1.50.0", | ||||
|         "@playwright/test": "1.50.1", | ||||
|         "@types/archiver": "6.0.3", | ||||
|         "@types/better-sqlite3": "7.6.12", | ||||
|         "@types/bootstrap": "5.2.10", | ||||
| @ -136,12 +136,11 @@ | ||||
|         "@types/leaflet-gpx": "1.3.7", | ||||
|         "@types/mime-types": "2.1.4", | ||||
|         "@types/multer": "1.4.12", | ||||
|         "@types/node": "22.12.0", | ||||
|         "@types/node": "22.13.1", | ||||
|         "@types/react": "18.3.18", | ||||
|         "@types/safe-compare": "1.1.2", | ||||
|         "@types/sanitize-html": "2.13.0", | ||||
|         "@types/sax": "1.2.7", | ||||
|         "@types/semver": "7.5.8", | ||||
|         "@types/serve-favicon": "2.5.7", | ||||
|         "@types/session-file-store": "1.2.5", | ||||
|         "@types/source-map-support": "0.5.10", | ||||
| @ -151,7 +150,7 @@ | ||||
|         "@types/ws": "8.5.14", | ||||
|         "@types/xml2js": "0.4.14", | ||||
|         "@types/yargs": "17.0.33", | ||||
|         "@vitest/coverage-v8": "3.0.4", | ||||
|         "@vitest/coverage-v8": "3.0.5", | ||||
|         "cross-env": "7.0.3", | ||||
|         "electron": "34.0.2", | ||||
|         "esm": "3.2.25", | ||||
| @ -166,7 +165,7 @@ | ||||
|         "tsx": "4.19.2", | ||||
|         "typedoc": "0.27.6", | ||||
|         "typescript": "5.7.3", | ||||
|         "vitest": "3.0.4", | ||||
|         "vitest": "3.0.5", | ||||
|         "webpack": "5.97.1", | ||||
|         "webpack-cli": "6.0.1", | ||||
|         "webpack-dev-middleware": "7.4.2" | ||||
| @ -782,6 +781,23 @@ | ||||
|         "graceful-fs": "^4.1.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@electron-forge/maker-rpm": { | ||||
|       "version": "7.6.1", | ||||
|       "resolved": "https://registry.npmjs.org/@electron-forge/maker-rpm/-/maker-rpm-7.6.1.tgz", | ||||
|       "integrity": "sha512-BShfmto+XTSA01pkZp10r2ktyruVfI24sGC+y4az1vbqkmX2qN9j0Xr+G/ZMHsm76XHju0N/e1Y2pqqu2JM8/A==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@electron-forge/maker-base": "7.6.1", | ||||
|         "@electron-forge/shared-types": "7.6.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 16.4.0" | ||||
|       }, | ||||
|       "optionalDependencies": { | ||||
|         "electron-installer-redhat": "^3.2.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@electron-forge/maker-squirrel": { | ||||
|       "version": "7.6.1", | ||||
|       "resolved": "https://registry.npmjs.org/@electron-forge/maker-squirrel/-/maker-squirrel-7.6.1.tgz", | ||||
| @ -2880,13 +2896,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@playwright/test": { | ||||
|       "version": "1.50.0", | ||||
|       "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.0.tgz", | ||||
|       "integrity": "sha512-ZGNXbt+d65EGjBORQHuYKj+XhCewlwpnSd/EDuLPZGSiEWmgOJB5RmMCCYGy5aMfTs9wx61RivfDKi8H/hcMvw==", | ||||
|       "version": "1.50.1", | ||||
|       "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.50.1.tgz", | ||||
|       "integrity": "sha512-Jii3aBg+CEDpgnuDxEp/h7BimHcUTDlpEtce89xEumlJ5ef2hqepZ+PWp1DDpYC/VO9fmWVI1IlEaoI5fK9FXQ==", | ||||
|       "dev": true, | ||||
|       "license": "Apache-2.0", | ||||
|       "dependencies": { | ||||
|         "playwright": "1.50.0" | ||||
|         "playwright": "1.50.1" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "playwright": "cli.js" | ||||
| @ -3909,9 +3925,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/node": { | ||||
|       "version": "22.12.0", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.12.0.tgz", | ||||
|       "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", | ||||
|       "version": "22.13.1", | ||||
|       "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.1.tgz", | ||||
|       "integrity": "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==", | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "undici-types": "~6.20.0" | ||||
| @ -4002,13 +4018,6 @@ | ||||
|         "@types/node": "*" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@types/semver": { | ||||
|       "version": "7.5.8", | ||||
|       "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", | ||||
|       "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/@types/send": { | ||||
|       "version": "0.17.4", | ||||
|       "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", | ||||
| @ -4163,9 +4172,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vitest/coverage-v8": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.4.tgz", | ||||
|       "integrity": "sha512-f0twgRCHgbs24Dp8cLWagzcObXMcuKtAwgxjJV/nnysPAJJk1JiKu/W0gIehZLmkljhJXU/E0/dmuQzsA/4jhA==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.0.5.tgz", | ||||
|       "integrity": "sha512-zOOWIsj5fHh3jjGwQg+P+J1FW3s4jBu1Zqga0qW60yutsBtqEqNEJKWYh7cYn1yGD+1bdPsPdC/eL4eVK56xMg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
| @ -4186,8 +4195,8 @@ | ||||
|         "url": "https://opencollective.com/vitest" | ||||
|       }, | ||||
|       "peerDependencies": { | ||||
|         "@vitest/browser": "3.0.4", | ||||
|         "vitest": "3.0.4" | ||||
|         "@vitest/browser": "3.0.5", | ||||
|         "vitest": "3.0.5" | ||||
|       }, | ||||
|       "peerDependenciesMeta": { | ||||
|         "@vitest/browser": { | ||||
| @ -4196,14 +4205,14 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vitest/expect": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.4.tgz", | ||||
|       "integrity": "sha512-Nm5kJmYw6P2BxhJPkO3eKKhGYKRsnqJqf+r0yOGRKpEP+bSCBDsjXgiu1/5QFrnPMEgzfC38ZEjvCFgaNBC0Eg==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.0.5.tgz", | ||||
|       "integrity": "sha512-nNIOqupgZ4v5jWuQx2DSlHLEs7Q4Oh/7AYwNyE+k0UQzG7tSmjPXShUikn1mpNGzYEN2jJbTvLejwShMitovBA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@vitest/spy": "3.0.4", | ||||
|         "@vitest/utils": "3.0.4", | ||||
|         "@vitest/spy": "3.0.5", | ||||
|         "@vitest/utils": "3.0.5", | ||||
|         "chai": "^5.1.2", | ||||
|         "tinyrainbow": "^2.0.0" | ||||
|       }, | ||||
| @ -4212,13 +4221,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vitest/mocker": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.4.tgz", | ||||
|       "integrity": "sha512-gEef35vKafJlfQbnyOXZ0Gcr9IBUsMTyTLXsEQwuyYAerpHqvXhzdBnDFuHLpFqth3F7b6BaFr4qV/Cs1ULx5A==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.0.5.tgz", | ||||
|       "integrity": "sha512-CLPNBFBIE7x6aEGbIjaQAX03ZZlBMaWwAjBdMkIf/cAn6xzLTiM3zYqO/WAbieEjsAZir6tO71mzeHZoodThvw==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@vitest/spy": "3.0.4", | ||||
|         "@vitest/spy": "3.0.5", | ||||
|         "estree-walker": "^3.0.3", | ||||
|         "magic-string": "^0.30.17" | ||||
|       }, | ||||
| @ -4239,9 +4248,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vitest/pretty-format": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.4.tgz", | ||||
|       "integrity": "sha512-ts0fba+dEhK2aC9PFuZ9LTpULHpY/nd6jhAQ5IMU7Gaj7crPCTdCFfgvXxruRBLFS+MLraicCuFXxISEq8C93g==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.0.5.tgz", | ||||
|       "integrity": "sha512-CjUtdmpOcm4RVtB+up8r2vVDLR16Mgm/bYdkGFe3Yj/scRfCpbSi2W/BDSDcFK7ohw8UXvjMbOp9H4fByd/cOA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
| @ -4252,13 +4261,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vitest/runner": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.4.tgz", | ||||
|       "integrity": "sha512-dKHzTQ7n9sExAcWH/0sh1elVgwc7OJ2lMOBrAm73J7AH6Pf9T12Zh3lNE1TETZaqrWFXtLlx3NVrLRb5hCK+iw==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.0.5.tgz", | ||||
|       "integrity": "sha512-BAiZFityFexZQi2yN4OX3OkJC6scwRo8EhRB0Z5HIGGgd2q+Nq29LgHU/+ovCtd0fOfXj5ZI6pwdlUmC5bpi8A==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@vitest/utils": "3.0.4", | ||||
|         "@vitest/utils": "3.0.5", | ||||
|         "pathe": "^2.0.2" | ||||
|       }, | ||||
|       "funding": { | ||||
| @ -4273,13 +4282,13 @@ | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/@vitest/snapshot": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.4.tgz", | ||||
|       "integrity": "sha512-+p5knMLwIk7lTQkM3NonZ9zBewzVp9EVkVpvNta0/PlFWpiqLaRcF4+33L1it3uRUCh0BGLOaXPPGEjNKfWb4w==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.0.5.tgz", | ||||
|       "integrity": "sha512-GJPZYcd7v8QNUJ7vRvLDmRwl+a1fGg4T/54lZXe+UOGy47F9yUfE18hRCtXL5aHN/AONu29NGzIXSVFh9K0feA==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@vitest/pretty-format": "3.0.4", | ||||
|         "@vitest/pretty-format": "3.0.5", | ||||
|         "magic-string": "^0.30.17", | ||||
|         "pathe": "^2.0.2" | ||||
|       }, | ||||
| @ -4295,9 +4304,9 @@ | ||||
|       "license": "MIT" | ||||
|     }, | ||||
|     "node_modules/@vitest/spy": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.4.tgz", | ||||
|       "integrity": "sha512-sXIMF0oauYyUy2hN49VFTYodzEAu744MmGcPR3ZBsPM20G+1/cSW/n1U+3Yu/zHxX2bIDe1oJASOkml+osTU6Q==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.0.5.tgz", | ||||
|       "integrity": "sha512-5fOzHj0WbUNqPK6blI/8VzZdkBlQLnT25knX0r4dbZI9qoZDf3qAdjoMmDcLG5A83W6oUUFJgUd0EYBc2P5xqg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
| @ -4308,13 +4317,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/@vitest/utils": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.4.tgz", | ||||
|       "integrity": "sha512-8BqC1ksYsHtbWH+DfpOAKrFw3jl3Uf9J7yeFh85Pz52IWuh1hBBtyfEbRNNZNjl8H8A5yMLH9/t+k7HIKzQcZQ==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.0.5.tgz", | ||||
|       "integrity": "sha512-N9AX0NUoUtVwKwy21JtwzaqR5L5R5A99GAbrHfCCXK1lp593i/3AZAXhSP43wRQuxYsflrdzEfXZFo1reR1Nkg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@vitest/pretty-format": "3.0.4", | ||||
|         "@vitest/pretty-format": "3.0.5", | ||||
|         "loupe": "^3.1.2", | ||||
|         "tinyrainbow": "^2.0.0" | ||||
|       }, | ||||
| @ -8029,6 +8038,211 @@ | ||||
|         "appdmg": "^0.6.4" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat": { | ||||
|       "version": "3.4.0", | ||||
|       "resolved": "https://registry.npmjs.org/electron-installer-redhat/-/electron-installer-redhat-3.4.0.tgz", | ||||
|       "integrity": "sha512-gEISr3U32Sgtj+fjxUAlSDo3wyGGq6OBx7rF5UdpIgbnpUvMN4W5uYb0ThpnAZ42VEJh/3aODQXHbFS4f5J3Iw==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "os": [ | ||||
|         "darwin", | ||||
|         "linux" | ||||
|       ], | ||||
|       "dependencies": { | ||||
|         "@malept/cross-spawn-promise": "^1.0.0", | ||||
|         "debug": "^4.1.1", | ||||
|         "electron-installer-common": "^0.10.2", | ||||
|         "fs-extra": "^9.0.0", | ||||
|         "lodash": "^4.17.15", | ||||
|         "word-wrap": "^1.2.3", | ||||
|         "yargs": "^16.0.2" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "electron-installer-redhat": "src/cli.js" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 10.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/@malept/cross-spawn-promise": { | ||||
|       "version": "1.1.1", | ||||
|       "resolved": "https://registry.npmjs.org/@malept/cross-spawn-promise/-/cross-spawn-promise-1.1.1.tgz", | ||||
|       "integrity": "sha512-RTBGWL5FWQcg9orDOCcp4LvItNzUPcyEU9bwaeJX0rJ1IQxzucC48Y0/sQLp/g6t99IQgAlGIaesJS+gTn7tVQ==", | ||||
|       "dev": true, | ||||
|       "funding": [ | ||||
|         { | ||||
|           "type": "individual", | ||||
|           "url": "https://github.com/sponsors/malept" | ||||
|         }, | ||||
|         { | ||||
|           "type": "tidelift", | ||||
|           "url": "https://tidelift.com/subscription/pkg/npm-.malept-cross-spawn-promise?utm_medium=referral&utm_source=npm_fund" | ||||
|         } | ||||
|       ], | ||||
|       "license": "Apache-2.0", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "cross-spawn": "^7.0.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">= 10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/ansi-regex": { | ||||
|       "version": "5.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", | ||||
|       "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "engines": { | ||||
|         "node": ">=8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/cliui": { | ||||
|       "version": "7.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", | ||||
|       "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", | ||||
|       "dev": true, | ||||
|       "license": "ISC", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "string-width": "^4.2.0", | ||||
|         "strip-ansi": "^6.0.0", | ||||
|         "wrap-ansi": "^7.0.0" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/emoji-regex": { | ||||
|       "version": "8.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", | ||||
|       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/fs-extra": { | ||||
|       "version": "9.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", | ||||
|       "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "at-least-node": "^1.0.0", | ||||
|         "graceful-fs": "^4.2.0", | ||||
|         "jsonfile": "^6.0.1", | ||||
|         "universalify": "^2.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/is-fullwidth-code-point": { | ||||
|       "version": "3.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", | ||||
|       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "engines": { | ||||
|         "node": ">=8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/jsonfile": { | ||||
|       "version": "6.1.0", | ||||
|       "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", | ||||
|       "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "universalify": "^2.0.0" | ||||
|       }, | ||||
|       "optionalDependencies": { | ||||
|         "graceful-fs": "^4.1.6" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/string-width": { | ||||
|       "version": "4.2.3", | ||||
|       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", | ||||
|       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "emoji-regex": "^8.0.0", | ||||
|         "is-fullwidth-code-point": "^3.0.0", | ||||
|         "strip-ansi": "^6.0.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/strip-ansi": { | ||||
|       "version": "6.0.1", | ||||
|       "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", | ||||
|       "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "ansi-regex": "^5.0.1" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=8" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/wrap-ansi": { | ||||
|       "version": "7.0.0", | ||||
|       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", | ||||
|       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "ansi-styles": "^4.0.0", | ||||
|         "string-width": "^4.1.0", | ||||
|         "strip-ansi": "^6.0.0" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       }, | ||||
|       "funding": { | ||||
|         "url": "https://github.com/chalk/wrap-ansi?sponsor=1" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/yargs": { | ||||
|       "version": "16.2.0", | ||||
|       "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", | ||||
|       "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "optional": true, | ||||
|       "dependencies": { | ||||
|         "cliui": "^7.0.2", | ||||
|         "escalade": "^3.1.1", | ||||
|         "get-caller-file": "^2.0.5", | ||||
|         "require-directory": "^2.1.1", | ||||
|         "string-width": "^4.2.0", | ||||
|         "y18n": "^5.0.5", | ||||
|         "yargs-parser": "^20.2.2" | ||||
|       }, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-installer-redhat/node_modules/yargs-parser": { | ||||
|       "version": "20.2.9", | ||||
|       "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", | ||||
|       "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", | ||||
|       "dev": true, | ||||
|       "license": "ISC", | ||||
|       "optional": true, | ||||
|       "engines": { | ||||
|         "node": ">=10" | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/electron-is-accelerator": { | ||||
|       "version": "0.1.2", | ||||
|       "resolved": "https://registry.npmjs.org/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz", | ||||
| @ -13320,13 +13534,13 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/playwright": { | ||||
|       "version": "1.50.0", | ||||
|       "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.0.tgz", | ||||
|       "integrity": "sha512-+GinGfGTrd2IfX1TA4N2gNmeIksSb+IAe589ZH+FlmpV3MYTx6+buChGIuDLQwrGNCw2lWibqV50fU510N7S+w==", | ||||
|       "version": "1.50.1", | ||||
|       "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.50.1.tgz", | ||||
|       "integrity": "sha512-G8rwsOQJ63XG6BbKj2w5rHeavFjy5zynBA9zsJMMtBoe/Uf757oG12NXz6e6OirF7RCrTVAKFXbLmn1RbL7Qaw==", | ||||
|       "dev": true, | ||||
|       "license": "Apache-2.0", | ||||
|       "dependencies": { | ||||
|         "playwright-core": "1.50.0" | ||||
|         "playwright-core": "1.50.1" | ||||
|       }, | ||||
|       "bin": { | ||||
|         "playwright": "cli.js" | ||||
| @ -13339,9 +13553,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/playwright-core": { | ||||
|       "version": "1.50.0", | ||||
|       "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.0.tgz", | ||||
|       "integrity": "sha512-CXkSSlr4JaZs2tZHI40DsZUN/NIwgaUPsyLuOAaIZp2CyF2sN5MM5NJsyB188lFSSozFxQ5fPT4qM+f0tH/6wQ==", | ||||
|       "version": "1.50.1", | ||||
|       "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.50.1.tgz", | ||||
|       "integrity": "sha512-ra9fsNWayuYumt+NiM069M6OkcRb1FZSK8bgi66AtpFoWkg2+y0bJSNmkFrWhMbEBbVKC/EruAHH3g0zmtwGmQ==", | ||||
|       "dev": true, | ||||
|       "license": "Apache-2.0", | ||||
|       "bin": { | ||||
| @ -14721,9 +14935,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/semver": { | ||||
|       "version": "7.7.0", | ||||
|       "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.0.tgz", | ||||
|       "integrity": "sha512-DrfFnPzblFmNrIZzg5RzHegbiRWg7KMR7btwi2yjHwx06zsUbO5g613sVwEV7FTwmzJu+Io0lJe2GJ3LxqpvBQ==", | ||||
|       "version": "7.7.1", | ||||
|       "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", | ||||
|       "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", | ||||
|       "license": "ISC", | ||||
|       "bin": { | ||||
|         "semver": "bin/semver.js" | ||||
| @ -16856,9 +17070,9 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/vite-node": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.4.tgz", | ||||
|       "integrity": "sha512-7JZKEzcYV2Nx3u6rlvN8qdo3QV7Fxyt6hx+CCKz9fbWxdX5IvUOmTWEAxMrWxaiSf7CKGLJQ5rFu8prb/jBjOA==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.0.5.tgz", | ||||
|       "integrity": "sha512-02JEJl7SbtwSDJdYS537nU6l+ktdvcREfLksk/NDAqtdKWGqHl+joXzEubHROmS3E6pip+Xgu2tFezMu75jH7A==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
| @ -17350,19 +17564,19 @@ | ||||
|       } | ||||
|     }, | ||||
|     "node_modules/vitest": { | ||||
|       "version": "3.0.4", | ||||
|       "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.4.tgz", | ||||
|       "integrity": "sha512-6XG8oTKy2gnJIFTHP6LD7ExFeNLxiTkK3CfMvT7IfR8IN+BYICCf0lXUQmX7i7JoxUP8QmeP4mTnWXgflu4yjw==", | ||||
|       "version": "3.0.5", | ||||
|       "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.0.5.tgz", | ||||
|       "integrity": "sha512-4dof+HvqONw9bvsYxtkfUp2uHsTN9bV2CZIi1pWgoFpL1Lld8LA1ka9q/ONSsoScAKG7NVGf2stJTI7XRkXb2Q==", | ||||
|       "dev": true, | ||||
|       "license": "MIT", | ||||
|       "dependencies": { | ||||
|         "@vitest/expect": "3.0.4", | ||||
|         "@vitest/mocker": "3.0.4", | ||||
|         "@vitest/pretty-format": "^3.0.4", | ||||
|         "@vitest/runner": "3.0.4", | ||||
|         "@vitest/snapshot": "3.0.4", | ||||
|         "@vitest/spy": "3.0.4", | ||||
|         "@vitest/utils": "3.0.4", | ||||
|         "@vitest/expect": "3.0.5", | ||||
|         "@vitest/mocker": "3.0.5", | ||||
|         "@vitest/pretty-format": "^3.0.5", | ||||
|         "@vitest/runner": "3.0.5", | ||||
|         "@vitest/snapshot": "3.0.5", | ||||
|         "@vitest/spy": "3.0.5", | ||||
|         "@vitest/utils": "3.0.5", | ||||
|         "chai": "^5.1.2", | ||||
|         "debug": "^4.4.0", | ||||
|         "expect-type": "^1.1.0", | ||||
| @ -17374,7 +17588,7 @@ | ||||
|         "tinypool": "^1.0.2", | ||||
|         "tinyrainbow": "^2.0.0", | ||||
|         "vite": "^5.0.0 || ^6.0.0", | ||||
|         "vite-node": "3.0.4", | ||||
|         "vite-node": "3.0.5", | ||||
|         "why-is-node-running": "^2.3.0" | ||||
|       }, | ||||
|       "bin": { | ||||
| @ -17390,8 +17604,8 @@ | ||||
|         "@edge-runtime/vm": "*", | ||||
|         "@types/debug": "^4.1.12", | ||||
|         "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", | ||||
|         "@vitest/browser": "3.0.4", | ||||
|         "@vitest/ui": "3.0.4", | ||||
|         "@vitest/browser": "3.0.5", | ||||
|         "@vitest/ui": "3.0.5", | ||||
|         "happy-dom": "*", | ||||
|         "jsdom": "*" | ||||
|       }, | ||||
|  | ||||
							
								
								
									
										13
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								package.json
									
									
									
									
									
								
							| @ -2,7 +2,7 @@ | ||||
|   "name": "trilium", | ||||
|   "productName": "TriliumNext Notes", | ||||
|   "description": "Build your personal knowledge base with TriliumNext Notes", | ||||
|   "version": "0.91.5", | ||||
|   "version": "0.91.6", | ||||
|   "license": "AGPL-3.0-only", | ||||
|   "main": "./dist/electron-main.js", | ||||
|   "author": { | ||||
| @ -131,7 +131,6 @@ | ||||
|     "sanitize-filename": "1.6.3", | ||||
|     "sanitize-html": "2.14.0", | ||||
|     "sax": "1.4.1", | ||||
|     "semver": "7.7.0", | ||||
|     "serve-favicon": "2.5.0", | ||||
|     "session-file-store": "1.5.0", | ||||
|     "source-map-support": "0.5.21", | ||||
| @ -151,11 +150,12 @@ | ||||
|     "@electron-forge/cli": "7.6.1", | ||||
|     "@electron-forge/maker-deb": "7.6.1", | ||||
|     "@electron-forge/maker-dmg": "7.6.1", | ||||
|     "@electron-forge/maker-rpm": "7.6.1", | ||||
|     "@electron-forge/maker-squirrel": "7.6.1", | ||||
|     "@electron-forge/maker-zip": "7.6.1", | ||||
|     "@electron-forge/plugin-auto-unpack-natives": "7.6.1", | ||||
|     "@electron/rebuild": "3.7.1", | ||||
|     "@playwright/test": "1.50.0", | ||||
|     "@playwright/test": "1.50.1", | ||||
|     "@types/archiver": "6.0.3", | ||||
|     "@types/better-sqlite3": "7.6.12", | ||||
|     "@types/bootstrap": "5.2.10", | ||||
| @ -178,12 +178,11 @@ | ||||
|     "@types/leaflet-gpx": "1.3.7", | ||||
|     "@types/mime-types": "2.1.4", | ||||
|     "@types/multer": "1.4.12", | ||||
|     "@types/node": "22.12.0", | ||||
|     "@types/node": "22.13.1", | ||||
|     "@types/react": "18.3.18", | ||||
|     "@types/safe-compare": "1.1.2", | ||||
|     "@types/sanitize-html": "2.13.0", | ||||
|     "@types/sax": "1.2.7", | ||||
|     "@types/semver": "7.5.8", | ||||
|     "@types/serve-favicon": "2.5.7", | ||||
|     "@types/session-file-store": "1.2.5", | ||||
|     "@types/source-map-support": "0.5.10", | ||||
| @ -193,7 +192,7 @@ | ||||
|     "@types/ws": "8.5.14", | ||||
|     "@types/xml2js": "0.4.14", | ||||
|     "@types/yargs": "17.0.33", | ||||
|     "@vitest/coverage-v8": "3.0.4", | ||||
|     "@vitest/coverage-v8": "3.0.5", | ||||
|     "cross-env": "7.0.3", | ||||
|     "electron": "34.0.2", | ||||
|     "esm": "3.2.25", | ||||
| @ -208,7 +207,7 @@ | ||||
|     "tsx": "4.19.2", | ||||
|     "typedoc": "0.27.6", | ||||
|     "typescript": "5.7.3", | ||||
|     "vitest": "3.0.4", | ||||
|     "vitest": "3.0.5", | ||||
|     "webpack": "5.97.1", | ||||
|     "webpack-cli": "6.0.1", | ||||
|     "webpack-dev-middleware": "7.4.2" | ||||
|  | ||||
| @ -4,7 +4,6 @@ import noteTooltipService from "./services/note_tooltip.js"; | ||||
| import bundleService from "./services/bundle.js"; | ||||
| import toastService from "./services/toast.js"; | ||||
| import noteAutocompleteService from "./services/note_autocomplete.js"; | ||||
| import macInit from "./services/mac_init.js"; | ||||
| import electronContextMenu from "./menus/electron_context_menu.js"; | ||||
| import glob from "./services/glob.js"; | ||||
| import { t } from "./services/i18n.js"; | ||||
| @ -35,8 +34,6 @@ if (utils.isElectron()) { | ||||
|     initOnElectron(); | ||||
| } | ||||
| 
 | ||||
| macInit.init(); | ||||
| 
 | ||||
| noteTooltipService.setupGlobalTooltip(); | ||||
| 
 | ||||
| noteAutocompleteService.init(); | ||||
|  | ||||
| @ -1,26 +0,0 @@ | ||||
| /** | ||||
|  * Mac specific initialization | ||||
|  */ | ||||
| import utils from "./utils.js"; | ||||
| import shortcutService from "./shortcuts.js"; | ||||
| 
 | ||||
| function init() { | ||||
|     if (utils.isElectron() && utils.isMac()) { | ||||
|         shortcutService.bindGlobalShortcut("meta+c", () => exec("copy")); | ||||
|         shortcutService.bindGlobalShortcut("meta+v", () => exec("paste")); | ||||
|         shortcutService.bindGlobalShortcut("meta+x", () => exec("cut")); | ||||
|         shortcutService.bindGlobalShortcut("meta+a", () => exec("selectAll")); | ||||
|         shortcutService.bindGlobalShortcut("meta+z", () => exec("undo")); | ||||
|         shortcutService.bindGlobalShortcut("meta+y", () => exec("redo")); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| function exec(cmd: string) { | ||||
|     document.execCommand(cmd); | ||||
| 
 | ||||
|     return false; | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     init | ||||
| }; | ||||
| @ -1,9 +1,6 @@ | ||||
| import utils from "./services/utils.js"; | ||||
| import macInit from "./services/mac_init.js"; | ||||
| import ko from "knockout"; | ||||
| 
 | ||||
| macInit.init(); | ||||
| 
 | ||||
| // TriliumNextTODO: properly make use of below types
 | ||||
| // type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
 | ||||
| // type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
 | ||||
|  | ||||
| @ -67,6 +67,10 @@ const TPL = ` | ||||
|         .attr-detail input[readonly] { | ||||
|             background-color: var(--accented-background-color) !important; | ||||
|         } | ||||
| 
 | ||||
|         .attr-edit-table td { | ||||
|             padding: 4px 0; | ||||
|         } | ||||
|     </style> | ||||
| 
 | ||||
|     <div style="display: flex; justify-content: space-between; margin-bottom: 8px;"> | ||||
| @ -97,8 +101,13 @@ const TPL = ` | ||||
|         </tr> | ||||
|         <tr class="attr-row-promoted" | ||||
|             title="${t("attribute_detail.promoted_title")}"> | ||||
|             <th>${t("attribute_detail.promoted")}</th> | ||||
|             <td><input type="checkbox" class="attr-input-promoted form-check" /></td> | ||||
|             <th></th> | ||||
|             <td> | ||||
|                 <label class="tn-checkbox"> | ||||
|                     <input type="checkbox" class="attr-input-promoted form-check" /> | ||||
|                     ${t("attribute_detail.promoted")} | ||||
|                 </label> | ||||
|             </td> | ||||
|         </tr> | ||||
|         <tr class="attr-row-promoted-alias"> | ||||
|             <th title="${t("attribute_detail.promoted_alias_title")}">${t("attribute_detail.promoted_alias")}</th> | ||||
| @ -149,8 +158,13 @@ const TPL = ` | ||||
|             </td> | ||||
|         </tr> | ||||
|         <tr title="${t("attribute_detail.inheritable_title")}"> | ||||
|             <th>${t("attribute_detail.inheritable")}</th> | ||||
|             <td><input type="checkbox" class="attr-input-inheritable form-check" /></td> | ||||
|             <th></th> | ||||
|             <td> | ||||
|                 <label class="tn-checkbox"> | ||||
|                     <input type="checkbox" class="attr-input-inheritable form-check" /> | ||||
|                     ${t("attribute_detail.inheritable")} | ||||
|                 </label> | ||||
|             </td> | ||||
|         </tr> | ||||
|     </table> | ||||
| 
 | ||||
|  | ||||
| @ -77,8 +77,8 @@ const TPL = ` | ||||
| 
 | ||||
|     <div class="attribute-list-editor" tabindex="200"></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="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div> | ||||
|     <div class="bx bx-save save-attributes-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.save_attributes"))}"></div> | ||||
|     <div class="bx bx-plus add-new-attribute-button tn-tool-button" title="${escapeQuotes(t("attribute_editor.add_a_new_attribute"))}"></div> | ||||
| 
 | ||||
|     <div class="attribute-errors" style="display: none;"></div> | ||||
| </div> | ||||
|  | ||||
| @ -25,16 +25,22 @@ const TPL = ` | ||||
|                     ${t("include_note.box_size_prompt")} | ||||
| 
 | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="include-note-box-size" value="small"> | ||||
|                         <label class="form-check-label">${t("include_note.box_size_small")}</label> | ||||
|                         <label class="form-check-label tn-radio"> | ||||
|                             <input class="form-check-input" type="radio" name="include-note-box-size" value="small"> | ||||
|                             ${t("include_note.box_size_small")} | ||||
|                         </label> | ||||
|                     </div> | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="include-note-box-size" value="medium" checked> | ||||
|                         <label class="form-check-label">${t("include_note.box_size_medium")}</label> | ||||
|                         <label class="form-check-label tn-radio"> | ||||
|                             <input class="form-check-input" type="radio" name="include-note-box-size" value="medium" checked> | ||||
|                             ${t("include_note.box_size_medium")} | ||||
|                         </label> | ||||
|                     </div> | ||||
|                     <div class="form-check"> | ||||
|                         <input class="form-check-input" type="radio" name="include-note-box-size" value="full"> | ||||
|                         <label class="form-check-label">${t("include_note.box_size_full")}</label> | ||||
|                         <label class="form-check-label tn-radio"> | ||||
|                             <input class="form-check-input" type="radio" name="include-note-box-size" value="full"> | ||||
|                             ${t("include_note.box_size_full")} | ||||
|                         </label> | ||||
|                     </div> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|  | ||||
| @ -27,7 +27,7 @@ const TPL = ` | ||||
|         <span class="editability-active-desc">${t("editability_select.auto")}</span> | ||||
|         <span class="caret"></span> | ||||
|     </button> | ||||
|     <div class="editability-dropdown dropdown-menu dropdown-menu-right"> | ||||
|     <div class="editability-dropdown dropdown-menu dropdown-menu-right tn-dropdown-list"> | ||||
|         <a class="dropdown-item" href="#" data-editability="auto"> | ||||
|             <span class="check">✓</span> | ||||
|             ${t("editability_select.auto")} | ||||
|  | ||||
| @ -33,11 +33,15 @@ const TPL = ` | ||||
|         } | ||||
| 
 | ||||
|         .find-widget-found-wrapper { | ||||
|             font-weight: bold; | ||||
|             justify-content: center; | ||||
|             min-width: 60px; | ||||
|             padding: 0 4px; | ||||
|             font-size: .85em; | ||||
|             text-align: center; | ||||
|         } | ||||
| 
 | ||||
|         .find-widget-search-term-input-group, .replace-widget-replacetext-input { | ||||
|             max-width: 300px; | ||||
|             max-width: 350px; | ||||
|         } | ||||
| 
 | ||||
|         .find-widget-spacer { | ||||
| @ -49,6 +53,13 @@ const TPL = ` | ||||
|         <div class="input-group find-widget-search-term-input-group"> | ||||
|             <input type="text" class="form-control find-widget-search-term-input" placeholder="${t("find.find_placeholder")}"> | ||||
|             <button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button> | ||||
|             <div class="find-widget-found-wrapper input-group-text"> | ||||
|                 <span> | ||||
|                     <span class="find-widget-current-found">0</span> | ||||
|                     / | ||||
|                     <span class="find-widget-total-found">0</span> | ||||
|                 <span> | ||||
|             </div> | ||||
|             <button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button> | ||||
|         </div> | ||||
| 
 | ||||
| @ -66,11 +77,7 @@ const TPL = ` | ||||
|             </label> | ||||
|         </div> | ||||
| 
 | ||||
|         <div class="find-widget-found-wrapper"> | ||||
|             <span class="find-widget-current-found">0</span> | ||||
|             / | ||||
|             <span class="find-widget-total-found">0</span> | ||||
|         </div> | ||||
| 
 | ||||
| 
 | ||||
|         <div class="find-widget-spacer"></div> | ||||
| 
 | ||||
|  | ||||
| @ -40,7 +40,7 @@ const TPL = ` | ||||
|         <span class="note-type-desc"></span> | ||||
|         <span class="caret"></span> | ||||
|     </button> | ||||
|     <div class="note-type-dropdown dropdown-menu dropdown-menu-left"></div> | ||||
|     <div class="note-type-dropdown dropdown-menu dropdown-menu-left tn-dropdown-list"></div> | ||||
| </div> | ||||
| `;
 | ||||
| 
 | ||||
|  | ||||
| @ -26,7 +26,7 @@ const TPL = ` | ||||
|         } | ||||
|     </style> | ||||
| 
 | ||||
|     <div class="links-wrapper"></div> | ||||
|     <div class="links-wrapper use-tn-links"></div> | ||||
| 
 | ||||
|     <div class="attachment-wrapper"></div> | ||||
| </div>`; | ||||
| @ -57,7 +57,7 @@ export default class AttachmentDetailTypeWidget extends TypeWidget { | ||||
| 
 | ||||
|         this.$linksWrapper.empty().append( | ||||
|             t("attachment_detail.owning_note"), | ||||
|             await linkService.createLink(this.noteId), | ||||
|             (await linkService.createLink(this.noteId)), | ||||
|             t("attachment_detail.you_can_also_open"), | ||||
|             await linkService.createLink(this.noteId, { | ||||
|                 title: t("attachment_detail.list_of_all_attachments"), | ||||
|  | ||||
| @ -17,7 +17,7 @@ const TPL = ` | ||||
|     <hr /> | ||||
| 
 | ||||
|     <div class="side-checkbox"> | ||||
|         <label class="form-check"> | ||||
|         <label class="form-check tn-checkbox"> | ||||
|             <input type="checkbox" class="native-title-bar form-check-input" /> | ||||
|             <strong>${t("electron_integration.native-title-bar")}</strong> | ||||
|             <p>${t("electron_integration.native-title-bar-description")}</p> | ||||
| @ -25,7 +25,7 @@ const TPL = ` | ||||
|     </div> | ||||
| 
 | ||||
|     <div class="side-checkbox"> | ||||
|         <label class="form-check"> | ||||
|         <label class="form-check tn-checkbox"> | ||||
|             <input type="checkbox" class="background-effects form-check-input" /> | ||||
|             <strong>${t("electron_integration.background-effects")}</strong> | ||||
|             <p>${t("electron_integration.background-effects-description")}</p> | ||||
|  | ||||
| @ -7,7 +7,7 @@ const TPL = ` | ||||
| <div class="options-section"> | ||||
|     <h4>${t("tray.title")}</h4> | ||||
| 
 | ||||
|     <label> | ||||
|     <label class="tn-checkbox"> | ||||
|         <input type="checkbox" class="tray-enabled"> | ||||
|         ${t("tray.enable_tray")} | ||||
|     </label> | ||||
|  | ||||
| @ -17,7 +17,7 @@ const TPL_ELECTRON = ` | ||||
| 
 | ||||
|     <p>${t("spellcheck.restart-required")}</p> | ||||
| 
 | ||||
|     <label> | ||||
|     <label class="tn-checkbox"> | ||||
|         <input type="checkbox" class="spell-check-enabled"> | ||||
|         ${t("spellcheck.enable")} | ||||
|     </label> | ||||
|  | ||||
| @ -9,6 +9,12 @@ const TPL = ` | ||||
|         width: 300px; | ||||
|         margin: 30px auto auto; | ||||
|     } | ||||
| 
 | ||||
|     .protected-session-password-component input, | ||||
|     .protected-session-password-component button { | ||||
|         margin-top: 12px; | ||||
|     } | ||||
| 
 | ||||
|     </style> | ||||
| 
 | ||||
|     <form class="protected-session-password-form"> | ||||
|  | ||||
| @ -1,6 +1,8 @@ | ||||
| @import url(./forms.css); | ||||
| @import url(./shell.css); | ||||
| @import url(./ribbon.css); | ||||
| @import url(./settings.css); | ||||
| @import url(./notes/empty.css); | ||||
| @import url(./notes/text.css); | ||||
| 
 | ||||
| @font-face { | ||||
| @ -64,3 +66,29 @@ | ||||
|     /* Theme capabilities */ | ||||
|     --tab-note-icons: true; | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Note search suggestions | ||||
|  */ | ||||
| 
 | ||||
| /* List body */ | ||||
| .jump-to-note-dialog .jump-to-note-results .aa-suggestions, | ||||
| .note-detail-empty .aa-suggestions { | ||||
|     padding: 0; | ||||
| } | ||||
| 
 | ||||
| /* List item */ | ||||
| .jump-to-note-dialog .aa-suggestions div, | ||||
| .note-detail-empty .aa-suggestions div { | ||||
|     border-radius: 6px; | ||||
|     padding: 6px 12px; | ||||
|     color: var(--menu-text-color); | ||||
|     cursor: default; | ||||
| } | ||||
| 
 | ||||
| /* Selected list item */ | ||||
| .jump-to-note-dialog .aa-suggestions div.aa-cursor, | ||||
| .note-detail-empty .aa-suggestions div.aa-cursor { | ||||
|     background: var(--hover-item-background-color); | ||||
|     color: var(--hover-item-text-color); | ||||
| } | ||||
| @ -78,10 +78,11 @@ button.btn.btn-success kbd { | ||||
|  * Icon buttons | ||||
|  */ | ||||
| 
 | ||||
| :root .icon-action:not(.global-menu-button) { | ||||
| :root .icon-action:not(.global-menu-button), | ||||
| :root .tn-tool-button { | ||||
|     width: var(--icon-button-size); | ||||
|     height: var(--icon-button-size); | ||||
|     border: unset; | ||||
|     border: unset !important; | ||||
|     border-radius: 8px; | ||||
|     padding: 0; | ||||
|     text-align: center; | ||||
| @ -89,7 +90,8 @@ button.btn.btn-success kbd { | ||||
| } | ||||
| 
 | ||||
| /* The "x" icon button */ | ||||
| :root .icon-action.bx-x { | ||||
| :root .icon-action.bx-x, | ||||
| :root .tn-tool-button.bx-x { | ||||
|     --icon-button-hover-background: var(--tab-close-button-hover-background); | ||||
|     --icon-button-hover-color: var(--tab-close-button-hover-color); | ||||
|     --icon-button-size: 24px; | ||||
| @ -99,23 +101,28 @@ button.btn.btn-success kbd { | ||||
| } | ||||
| 
 | ||||
| /* The icon */ | ||||
| :root .icon-action:not(.global-menu-button)::before { | ||||
| :root .icon-action:not(.global-menu-button)::before, | ||||
| :root .tn-tool-button::before { | ||||
|     display: block; | ||||
|     line-height: var(--icon-button-size); | ||||
|     font-size: calc(var(--icon-button-size) * var(--icon-button-icon-ratio)); | ||||
| } | ||||
| 
 | ||||
| :root .icon-action:not(.global-menu-button):hover, | ||||
| :root .icon-action:not(.global-menu-button).show { | ||||
| :root .icon-action:not(.global-menu-button).show, | ||||
| :root .tn-tool-button:hover, | ||||
| :root .tn-tool-button.show { | ||||
|     background: var(--icon-button-hover-background); | ||||
|     color: var(--icon-button-hover-color); | ||||
| } | ||||
| 
 | ||||
| :root .icon-action:not(.global-menu-button):active::before { | ||||
| :root .icon-action:not(.global-menu-button):active::before, | ||||
| :root .tn-tool-button:active::before { | ||||
|     transform: scale(.85); | ||||
| } | ||||
| 
 | ||||
| :root .icon-action:not(.global-menu-button):focus-visible { | ||||
| :root .icon-action:not(.global-menu-button):focus-visible, | ||||
| :root .tn-tool-button:focus-visible { | ||||
|     outline: 2px solid var(--input-focus-outline-color); | ||||
| } | ||||
| 
 | ||||
| @ -216,6 +223,7 @@ input::selection, | ||||
| .input-group button, | ||||
| .input-group a { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     --bs-border-width: 0; | ||||
|     --accented-background-color: transparent; | ||||
|     background: transparent; | ||||
| @ -228,19 +236,26 @@ input::selection, | ||||
| .input-group button:hover, | ||||
| .input-group a:hover { | ||||
|     --muted-text-color: var(--input-action-button-hover); | ||||
|     color: var(--input-action-button-hover); | ||||
| } | ||||
| 
 | ||||
| .input-group button:focus-visible, | ||||
| .input-group a:focus-visible { | ||||
|     box-shadow: unset; | ||||
|     outline: transparent; | ||||
|     border: transparent; | ||||
|     text-shadow: 0 0 3px var(--input-action-button-hover); | ||||
| } | ||||
| 
 | ||||
| .input-group button:active { | ||||
|     background: transparent !important; | ||||
| } | ||||
| 
 | ||||
| .input-group a.disabled { | ||||
|     opacity: .5; | ||||
|     /* Workaround to set the "background" property. */ | ||||
|     --button-disabled-background-color: transparent; | ||||
|     --button-disabled-text-color: var(--input-action-button-color); | ||||
| } | ||||
| 
 | ||||
| .input-group .input-clearer-button { | ||||
| @ -273,6 +288,7 @@ input::selection, | ||||
| 
 | ||||
| select, | ||||
| select.form-select, | ||||
| select.form-control, | ||||
| .select-button.dropdown-toggle.btn { | ||||
|     outline: 3px solid transparent; | ||||
|     outline-offset: 6px; | ||||
| @ -285,6 +301,7 @@ select.form-select, | ||||
| 
 | ||||
| select:hover, | ||||
| select.form-select:hover, | ||||
| select.form-control:hover, | ||||
| .select-button.dropdown-toggle.btn:hover { | ||||
|     background-color: var(--input-hover-background); | ||||
|     color: var(--input-hover-color); | ||||
| @ -297,6 +314,7 @@ button.select-button.dropdown-toggle.btn:active { | ||||
| 
 | ||||
| select:focus, | ||||
| select.form-select:focus, | ||||
| select.form-control:focus, | ||||
| .select-button.dropdown-toggle.btn:focus { | ||||
|     box-shadow: unset; | ||||
|     outline: 3px solid var(--input-focus-outline-color); | ||||
|  | ||||
							
								
								
									
										11
									
								
								src/public/stylesheets/theme-next/notes/empty.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/public/stylesheets/theme-next/notes/empty.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | ||||
| /* The container */ | ||||
| div.note-detail-empty { | ||||
|     max-width: 70%; | ||||
|     margin: 50px auto; | ||||
| } | ||||
| 
 | ||||
| /* The search results list */ | ||||
| .note-detail-empty span.aa-dropdown-menu { | ||||
|     margin-top: 1em; | ||||
|     border: unset; | ||||
| } | ||||
| @ -126,3 +126,41 @@ html .note-detail-editable-text :not(figure, .include-note, hr):first-child { | ||||
|     border: 0 !important; | ||||
|     border-top: 1px solid var(--main-border-color) !important; | ||||
| } | ||||
| 
 | ||||
| /* Table caption */ | ||||
| 
 | ||||
| .ck-content .table > figcaption { | ||||
|     background: var(--accented-background-color); | ||||
|     color: var(--main-text-color); | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Search in text panel | ||||
|  */ | ||||
| 
 | ||||
| .find-replace-widget > div { | ||||
|     padding: 8px; | ||||
| } | ||||
| 
 | ||||
| .find-replace-widget > div + div { | ||||
|     padding-top: 0; | ||||
| } | ||||
| 
 | ||||
| div.find-replace-widget div.find-widget-found-wrapper { | ||||
|     min-width: 50px; | ||||
|     font-style: normal; | ||||
|     font-weight: normal; | ||||
| } | ||||
| 
 | ||||
|  /* The up / down buttons of the "Find in text" input */ | ||||
| .find-replace-widget .input-group button { | ||||
|     font-size: 1.3em; | ||||
| } | ||||
| 
 | ||||
| .find-replace-widget .form-check { | ||||
|     padding-left: 0; | ||||
| } | ||||
| 
 | ||||
| .find-replace-widget .form-check .form-check-input { | ||||
|     margin-left: 0; | ||||
| } | ||||
							
								
								
									
										41
									
								
								src/public/stylesheets/theme-next/ribbon.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/public/stylesheets/theme-next/ribbon.css
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | ||||
| /* | ||||
|  * Owned attributes | ||||
|  */ | ||||
| 
 | ||||
| .attribute-list .add-new-attribute-button, | ||||
| .attribute-list .save-attributes-button  { | ||||
|     bottom: .3em; | ||||
| } | ||||
| 
 | ||||
| .attribute-list .save-attributes-button { | ||||
|     right: 30px; | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Similar notes | ||||
|  */ | ||||
| 
 | ||||
| :root .similar-notes-widget a { | ||||
|     color: var(--cmd-button-text-color); | ||||
|     background: var(--cmd-button-background-color); | ||||
| } | ||||
| 
 | ||||
| :root .similar-notes-widget a:hover { | ||||
|     color: var(--cmd-button-hover-text-color); | ||||
|     background: var(--cmd-button-hover-background-color); | ||||
| } | ||||
| 
 | ||||
| /*  | ||||
|  * Note info | ||||
|  */ | ||||
| 
 | ||||
| .note-info-widget-table th { | ||||
|     opacity: .65; | ||||
|     font-weight: 500; | ||||
| } | ||||
| 
 | ||||
| :root .note-info-widget-table button.calculate-button { | ||||
|     min-width: 0; | ||||
|     padding: 4px 10px !important; | ||||
|     font-size: .8em; | ||||
| } | ||||
| @ -227,6 +227,12 @@ body.layout-horizontal > .horizontal { | ||||
|     --hover-item-background-color: transparent; | ||||
| } | ||||
| 
 | ||||
| #launcher-pane.horizontal .global-menu-button .global-menu-button-update-available { | ||||
|     right: -23px; | ||||
|     bottom: -22px; | ||||
|     transform: scale(0.85); | ||||
| } | ||||
| 
 | ||||
| .tooltip .tooltip-arrow { | ||||
|     display: none; | ||||
| } | ||||
| @ -945,6 +951,9 @@ body.mobile .note-title { | ||||
| 
 | ||||
| /* | ||||
|  * Menus | ||||
|  * | ||||
|  * Note: apply the "tn-dropdown-list" class for scrollable dropdown menus. Submenus are not | ||||
|  * supported when this class is used. | ||||
|  */ | ||||
| 
 | ||||
| .dropdown-menu { | ||||
| @ -953,6 +962,10 @@ body.mobile .note-title { | ||||
|     font-size: 0.9rem !important; | ||||
| } | ||||
| 
 | ||||
| .dropdown-menu::-webkit-scrollbar-track { | ||||
|     background: var(--menu-background-color); | ||||
| } | ||||
| 
 | ||||
| body.mobile .dropdown-menu { | ||||
|     backdrop-filter: var(--dropdown-backdrop-filter); | ||||
|     border-radius: var(--dropdown-border-radius); | ||||
| @ -976,6 +989,14 @@ body.desktop .dropdown-menu::before { | ||||
|     z-index: -1; | ||||
| } | ||||
| 
 | ||||
| body.desktop .dropdown-menu.tn-dropdown-list { | ||||
|     backdrop-filter: var(--dropdown-backdrop-filter); | ||||
| } | ||||
| 
 | ||||
| body.desktop .dropdown-menu.tn-dropdown-list::before { | ||||
|     display: none; | ||||
| } | ||||
| 
 | ||||
| body.desktop .dropdown-submenu .dropdown-menu::before { | ||||
|     content: unset; | ||||
| } | ||||
| @ -1237,25 +1258,6 @@ body .calendar-dropdown-widget .calendar-body a:hover { | ||||
|     background: transparent !important; | ||||
| } | ||||
| 
 | ||||
| /* List body */ | ||||
| .jump-to-note-dialog .jump-to-note-results .aa-suggestions { | ||||
|     padding: 0; | ||||
| } | ||||
| 
 | ||||
| /* List item */ | ||||
| .jump-to-note-dialog .aa-suggestions div { | ||||
|     border-radius: 6px; | ||||
|     padding: 6px 12px; | ||||
|     color: var(--menu-text-color); | ||||
|     cursor: default; | ||||
| } | ||||
| 
 | ||||
| /* Selected list item */ | ||||
| .jump-to-note-dialog .aa-suggestions div.aa-cursor { | ||||
|     background: var(--hover-item-background-color); | ||||
|     color: var(--hover-item-text-color); | ||||
| } | ||||
| 
 | ||||
| /* | ||||
|  * Recent changes list | ||||
|  */ | ||||
| @ -1657,7 +1659,17 @@ body .calendar-dropdown-widget .calendar-body a:hover { | ||||
|     padding-left: 12px; | ||||
| } | ||||
| 
 | ||||
| .note-actions { | ||||
|     --menu-item-icon-vert-offset: -2.5px; | ||||
| } | ||||
| 
 | ||||
| /* Promoted attributes */ | ||||
| .promoted-attribute-cell div.input-group { | ||||
|     margin: 1px 0;     | ||||
| } | ||||
| 
 | ||||
| /* Delete notes preview dialog */ | ||||
| 
 | ||||
| .delete-notes-list .note-path { | ||||
|     padding-left: 8px; | ||||
| } | ||||
| @ -216,6 +216,7 @@ function deleteBranch(req: Request) { | ||||
| 
 | ||||
| function setPrefix(req: Request) { | ||||
|     const branchId = req.params.branchId; | ||||
|     //TriliumNextTODO: req.body arrives as string, so req.body.prefix will be undefined – did the code below ever even work?
 | ||||
|     const prefix = utils.isEmptyOrWhitespace(req.body.prefix) ? null : req.body.prefix; | ||||
| 
 | ||||
|     const branch = becca.getBranchOrThrow(branchId); | ||||
|  | ||||
| @ -6,7 +6,7 @@ import eventService from "./events.js"; | ||||
| import cls from "../services/cls.js"; | ||||
| import protectedSessionService from "../services/protected_session.js"; | ||||
| import log from "../services/log.js"; | ||||
| import { newEntityId, isString, unescapeHtml, quoteRegex, toMap } from "../services/utils.js"; | ||||
| import { newEntityId, unescapeHtml, quoteRegex, toMap } from "../services/utils.js"; | ||||
| import revisionService from "./revisions.js"; | ||||
| import request from "./request.js"; | ||||
| import path from "path"; | ||||
| @ -734,13 +734,13 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment | ||||
|     note.setContent(newContent, { forceFrontendReload }); | ||||
| 
 | ||||
|     if (attachments?.length > 0) { | ||||
|         const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title"); | ||||
|       const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title"); | ||||
| 
 | ||||
|         for (const { attachmentId, role, mime, title, position, content } of attachments) { | ||||
|             if (attachmentId || !(title in existingAttachmentsByTitle)) { | ||||
|             const existingAttachment = existingAttachmentsByTitle.get(title); | ||||
|             if (attachmentId || !existingAttachment) { | ||||
|                 note.saveAttachment({ attachmentId, role, mime, title, content, position }); | ||||
|             } else { | ||||
|                 const existingAttachment = existingAttachmentsByTitle[title]; | ||||
|                 existingAttachment.role = role; | ||||
|                 existingAttachment.mime = mime; | ||||
|                 existingAttachment.position = position; | ||||
| @ -887,7 +887,7 @@ async function asyncPostProcessContent(note: BNote, content: string | Buffer) { | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     if (note.hasStringContent() && !isString(content)) { | ||||
|     if (note.hasStringContent() && typeof content !== "string") { | ||||
|         content = content.toString(); | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -0,0 +1,13 @@ | ||||
| import { describe, it, expect } from "vitest"; | ||||
| import { processMindmapContent } from "./note_content_fulltext.js"; | ||||
| 
 | ||||
| describe("processMindmapContent", () => { | ||||
|     it("supports empty JSON", () => { | ||||
|         expect(processMindmapContent("{}")).toEqual(""); | ||||
|     }); | ||||
| 
 | ||||
|     it("supports blank text / invalid JSON", () => { | ||||
|         expect(processMindmapContent("")).toEqual(""); | ||||
|         expect(processMindmapContent(`{ "node": " }`)).toEqual(""); | ||||
|     }); | ||||
| }); | ||||
| @ -131,52 +131,7 @@ class NoteContentFulltextExp extends Expression { | ||||
| 
 | ||||
|             content = content.replace(/ /g, " "); | ||||
|         } else if (type === "mindMap" && mime === "application/json") { | ||||
|             let mindMapcontent = JSON.parse(content); | ||||
| 
 | ||||
|             // Define interfaces for the JSON structure
 | ||||
|             interface MindmapNode { | ||||
|                 id: string; | ||||
|                 topic: string; | ||||
|                 children: MindmapNode[]; // Recursive structure
 | ||||
|                 direction?: number; | ||||
|                 expanded?: boolean; | ||||
|             } | ||||
| 
 | ||||
|             interface MindmapData { | ||||
|                 nodedata: MindmapNode; | ||||
|                 arrows: any[]; // If you know the structure, replace `any` with the correct type
 | ||||
|                 summaries: any[]; | ||||
|                 direction: number; | ||||
|                 theme: { | ||||
|                     name: string; | ||||
|                     type: string; | ||||
|                     palette: string[]; | ||||
|                     cssvar: Record<string, string>; // Object with string keys and string values
 | ||||
|                 }; | ||||
|             } | ||||
| 
 | ||||
|             // Recursive function to collect all topics
 | ||||
|             function collectTopics(node: MindmapNode): string[] { | ||||
|                 // Collect the current node's topic
 | ||||
|                 let topics = [node.topic]; | ||||
| 
 | ||||
|                 // If the node has children, collect topics recursively
 | ||||
|                 if (node.children && node.children.length > 0) { | ||||
|                     for (const child of node.children) { | ||||
|                         topics = topics.concat(collectTopics(child)); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 return topics; | ||||
|             } | ||||
| 
 | ||||
|             // Start extracting from the root node
 | ||||
|             const topicsArray = collectTopics(mindMapcontent.nodedata); | ||||
| 
 | ||||
|             // Combine topics into a single string
 | ||||
|             const topicsString = topicsArray.join(", "); | ||||
| 
 | ||||
|             content = normalize(topicsString.toString()); | ||||
|             content = processMindmapContent(content); | ||||
|         } else if (type === "canvas" && mime === "application/json") { | ||||
|             interface Element { | ||||
|                 type: string; | ||||
| @ -215,4 +170,63 @@ class NoteContentFulltextExp extends Expression { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function processMindmapContent(content: string) { | ||||
|     let mindMapcontent; | ||||
| 
 | ||||
|     try { | ||||
|         mindMapcontent = JSON.parse(content); | ||||
|     } catch (e) { | ||||
|         return ""; | ||||
|     } | ||||
| 
 | ||||
|     // Define interfaces for the JSON structure
 | ||||
|     interface MindmapNode { | ||||
|         id: string; | ||||
|         topic: string; | ||||
|         children: MindmapNode[]; // Recursive structure
 | ||||
|         direction?: number; | ||||
|         expanded?: boolean; | ||||
|     } | ||||
| 
 | ||||
|     interface MindmapData { | ||||
|         nodedata: MindmapNode; | ||||
|         arrows: any[]; // If you know the structure, replace `any` with the correct type
 | ||||
|         summaries: any[]; | ||||
|         direction: number; | ||||
|         theme: { | ||||
|             name: string; | ||||
|             type: string; | ||||
|             palette: string[]; | ||||
|             cssvar: Record<string, string>; // Object with string keys and string values
 | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     // Recursive function to collect all topics
 | ||||
|     function collectTopics(node?: MindmapNode): string[] { | ||||
|         if (!node) { | ||||
|             return []; | ||||
|         } | ||||
| 
 | ||||
|         // Collect the current node's topic
 | ||||
|         let topics = [node.topic]; | ||||
| 
 | ||||
|         // If the node has children, collect topics recursively
 | ||||
|         if (node.children && node.children.length > 0) { | ||||
|             for (const child of node.children) { | ||||
|                 topics = topics.concat(collectTopics(child)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         return topics; | ||||
|     } | ||||
| 
 | ||||
|     // Start extracting from the root node
 | ||||
|     const topicsArray = collectTopics(mindMapcontent.nodedata); | ||||
| 
 | ||||
|     // Combine topics into a single string
 | ||||
|     const topicsString = topicsArray.join(", "); | ||||
| 
 | ||||
|     return normalize(topicsString.toString()); | ||||
| } | ||||
| 
 | ||||
| export default NoteContentFulltextExp; | ||||
|  | ||||
| @ -1,61 +0,0 @@ | ||||
| import { expect, describe, it } from "vitest"; | ||||
| import { formatDownloadTitle } from "./utils.js"; | ||||
| 
 | ||||
| const testCases: [fnValue: Parameters<typeof formatDownloadTitle>, expectedValue: ReturnType<typeof formatDownloadTitle>][] = [ | ||||
|     // empty fileName tests
 | ||||
|     [["", "text", ""], "untitled.html"], | ||||
| 
 | ||||
|     [["", "canvas", ""], "untitled.json"], | ||||
| 
 | ||||
|     [["", null, ""], "untitled"], | ||||
| 
 | ||||
|     // json extension from type tests
 | ||||
|     [["test_file", "canvas", ""], "test_file.json"], | ||||
| 
 | ||||
|     [["test_file", "relationMap", ""], "test_file.json"], | ||||
| 
 | ||||
|     [["test_file", "search", ""], "test_file.json"], | ||||
| 
 | ||||
|     // extension based on mime type
 | ||||
|     [["test_file", null, "text/csv"], "test_file.csv"], | ||||
| 
 | ||||
|     [["test_file_wo_ext", "image", "image/svg+xml"], "test_file_wo_ext.svg"], | ||||
| 
 | ||||
|     [["test_file_wo_ext", "file", "application/json"], "test_file_wo_ext.json"], | ||||
| 
 | ||||
|     [["test_file_w_fake_ext.ext", "image", "image/svg+xml"], "test_file_w_fake_ext.ext.svg"], | ||||
| 
 | ||||
|     [["test_file_w_correct_ext.svg", "image", "image/svg+xml"], "test_file_w_correct_ext.svg"], | ||||
| 
 | ||||
|     [["test_file_w_correct_ext.svgz", "image", "image/svg+xml"], "test_file_w_correct_ext.svgz"], | ||||
| 
 | ||||
|     [["test_file.zip", "file", "application/zip"], "test_file.zip"], | ||||
| 
 | ||||
|     [["test_file", "file", "application/zip"], "test_file.zip"], | ||||
| 
 | ||||
|     // application/octet-stream tests
 | ||||
|     [["test_file", "file", "application/octet-stream"], "test_file"], | ||||
| 
 | ||||
|     [["test_file.zip", "file", "application/octet-stream"], "test_file.zip"], | ||||
| 
 | ||||
|     [["test_file.unknown", null, "application/octet-stream"], "test_file.unknown"], | ||||
| 
 | ||||
|     // sanitized filename tests
 | ||||
|     [["test/file", null, "application/octet-stream"], "testfile"], | ||||
| 
 | ||||
|     [["test:file.zip", "file", "application/zip"], "testfile.zip"], | ||||
| 
 | ||||
|     [[":::", "file", "application/zip"], ".zip"], | ||||
| 
 | ||||
|     [[":::a", "file", "application/zip"], "a.zip"] | ||||
| ]; | ||||
| 
 | ||||
| describe("utils/formatDownloadTitle unit tests", () => { | ||||
|     testCases.forEach((testCase) => { | ||||
|         return it(`With args '${JSON.stringify(testCase[0])}' it should return '${testCase[1]}'`, () => { | ||||
|             const [value, expected] = testCase; | ||||
|             const actual = formatDownloadTitle(...value); | ||||
|             expect(actual).toStrictEqual(expected); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
							
								
								
									
										630
									
								
								src/services/utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										630
									
								
								src/services/utils.spec.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,630 @@ | ||||
| import { describe, it, expect } from "vitest"; | ||||
| import utils from "./utils.js"; | ||||
| 
 | ||||
| type TestCase<T extends (...args: any) => any> = [desc: string, fnParams: Parameters<T>, expected: ReturnType<T>]; | ||||
| 
 | ||||
| describe("#newEntityId", () => { | ||||
| 
 | ||||
|   it("should return a string with a length of 12", () => { | ||||
|     const result = utils.newEntityId(); | ||||
|     expect(result).toBeTypeOf("string"); | ||||
|     expect(result).toHaveLength(12); | ||||
|   }); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| describe("#randomString", () => { | ||||
| 
 | ||||
|   it("should return a string with a length as per argument", () => { | ||||
|     const stringLength = 5; | ||||
|     const result = utils.randomString(stringLength); | ||||
|     expect(result).toBeTypeOf("string"); | ||||
|     expect(result).toHaveLength(stringLength); | ||||
|   }); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| // TriliumNextTODO: should use mocks and assert that functions get called
 | ||||
| describe("#randomSecureToken", () => { | ||||
|     // base64 -> 4 * (bytes/3) length -> if padding and rounding up is ignored for simplicity
 | ||||
|     // https://stackoverflow.com/a/13378842
 | ||||
|     const byteToBase64Length = (bytes: number) => 4 * (bytes / 3); | ||||
| 
 | ||||
|     it("should return a string and use 32 bytes by default", () => { | ||||
|         const result = utils.randomSecureToken(); | ||||
|         expect(result).toBeTypeOf("string"); | ||||
|         expect(result.length).toBeGreaterThanOrEqual(byteToBase64Length(32)); | ||||
|     }); | ||||
| 
 | ||||
|     it("should return a string and use passed byte length", () => { | ||||
|         const bytes = 16; | ||||
|         const result = utils.randomSecureToken(bytes); | ||||
|         expect(result).toBeTypeOf("string"); | ||||
|         expect(result.length).toBeGreaterThanOrEqual(byteToBase64Length(bytes)); | ||||
|         expect(result.length).toBeLessThan(44); // default argument uses 32 bytes -> which translates to 44 base64 legal chars
 | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // TriliumNextTODO: should use mocks and assert that functions get called
 | ||||
| describe.todo("#md5", () => {}); | ||||
| 
 | ||||
| // TriliumNextTODO: should use mocks and assert that functions get called
 | ||||
| describe.todo("#hashedBlobId", () => {}); | ||||
| 
 | ||||
| // TriliumNextTODO: should use mocks and assert that functions get called
 | ||||
| describe.todo("#toBase64", () => {}); | ||||
| 
 | ||||
| // TriliumNextTODO: should use mocks and assert that functions get called
 | ||||
| describe.todo("#fromBase64", () => {}); | ||||
| 
 | ||||
| // TriliumNextTODO: should use mocks and assert that functions get called
 | ||||
| describe.todo("#hmac", () => {}); | ||||
| 
 | ||||
| // TriliumNextTODO: should use mocks and assert that functions get called
 | ||||
| describe.todo("#hash", () => {}); | ||||
| 
 | ||||
| describe("#isEmptyOrWhitespace", () => { | ||||
| 
 | ||||
|   const testCases: TestCase<typeof utils.isEmptyOrWhitespace>[] = [ | ||||
|     ["w/ 'null' it should return true", [null], true], | ||||
|     ["w/ 'null' it should return true", [null], true], | ||||
|     ["w/ undefined it should return true", [undefined], true], | ||||
|     ["w/ empty string '' it should return true", [""], true], | ||||
|     ["w/ single whitespace string ' ' it should return true", [" "], true], | ||||
|     ["w/ multiple whitespace string '   ' it should return true", ["  "], true], | ||||
|     ["w/ non-empty string ' t  ' it should return false", [" t  "], false], | ||||
|   ]; | ||||
| 
 | ||||
|   testCases.forEach(testCase => { | ||||
|     const [desc, fnParams, expected] = testCase; | ||||
|     it(desc, () => { | ||||
|       const result = utils.isEmptyOrWhitespace(...fnParams); | ||||
|       expect(result).toStrictEqual(expected); | ||||
|     }) | ||||
|   }) | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| describe("#sanitizeSqlIdentifier", () => { | ||||
| 
 | ||||
|   const testCases: TestCase<typeof utils.sanitizeSqlIdentifier>[] = [ | ||||
|     ["w/ 'test' it should not strip anything", ["test"], "test"], | ||||
|     ["w/ 'test123' it should not strip anything", ["test123"], "test123"], | ||||
|     ["w/ 'tEst_TeSt' it should not strip anything", ["tEst_TeSt"], "tEst_TeSt"], | ||||
|     ["w/ 'test_test' it should not strip '_'", ["test_test"], "test_test"], | ||||
|     ["w/ 'test-' it should strip the '-'", ["test-"], "test"], | ||||
|     ["w/ 'test-test' it should strip the '-'", ["test-test"], "testtest"], | ||||
|     ["w/ 'test; --test' it should strip the '; --'", ["test; --test"], "testtest"], | ||||
|     ["w/ 'test test' it should strip the ' '", ["test test"], "testtest"], | ||||
|   ]; | ||||
| 
 | ||||
|   testCases.forEach(testCase => { | ||||
|     const [desc, fnParams, expected] = testCase; | ||||
|     it(desc, () => { | ||||
|       const result = utils.sanitizeSqlIdentifier(...fnParams); | ||||
|       expect(result).toStrictEqual(expected); | ||||
|     }) | ||||
|   }); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| describe("#escapeHtml", () => { | ||||
|     it("should re-export 'escape-html' npm module as escapeHtml", () => { | ||||
|         expect(utils.escapeHtml).toBeTypeOf("function"); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#unescapeHtml", () => { | ||||
|     it("should re-export 'unescape' npm module as unescapeHtml", () => { | ||||
|         expect(utils.unescapeHtml).toBeTypeOf("function"); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#toObject", () => { | ||||
|     it("should return an object with keys and value being set from the supplied Function", () => { | ||||
|         type TestListEntry = { testPropA: string, testPropB: string }; | ||||
|         type TestListFn = (testListEntry: TestListEntry) => [string, string]; | ||||
|         const testList: [TestListEntry, TestListEntry] = [{ testPropA: "keyA", testPropB: "valueA" }, { testPropA: "keyB", testPropB: "valueB" }]; | ||||
|         const fn: TestListFn = (testListEntry: TestListEntry) => [testListEntry.testPropA + "_fn", testListEntry.testPropB + "_fn"]; | ||||
| 
 | ||||
|         const result = utils.toObject(testList, fn); | ||||
|         expect(result).toStrictEqual({ | ||||
|             "keyA_fn": "valueA_fn", | ||||
|             "keyB_fn": "valueB_fn" | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#stripTags", () => { | ||||
| 
 | ||||
|     //prettier-ignore
 | ||||
|     const htmlWithNewlines = | ||||
| `<p>abc
 | ||||
| def</p> | ||||
| <p>ghi</p>`;
 | ||||
| 
 | ||||
|     const testCases: TestCase<typeof utils.stripTags>[] = [ | ||||
|         ["should strip all tags and only return the content, leaving new lines and spaces in tact", [htmlWithNewlines], "abc\ndef\nghi"], | ||||
|         //TriliumNextTODO: should this actually insert a space between content to prevent concatenated text?
 | ||||
|         ["should strip all tags and only return the content", ["<h1>abc</h1><p>def</p>"], "abcdef"], | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|           const result = utils.stripTags(...fnParams); | ||||
|           expect(result).toStrictEqual(expected); | ||||
|         }) | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe.todo("#escapeRegExp", () => {}); | ||||
| 
 | ||||
| describe.todo("#crash", () => {}); | ||||
| 
 | ||||
| describe("#getContentDisposition", () => { | ||||
| 
 | ||||
|     const defaultFallBackDisposition = `file; filename="file"; filename*=UTF-8''file`; | ||||
|     const testCases: TestCase<typeof utils.getContentDisposition>[] = [ | ||||
|         [ | ||||
|             "when passed filename is empty, it should fallback to default value 'file'",  | ||||
|             [" "], | ||||
|             defaultFallBackDisposition | ||||
|         ], | ||||
|         [ | ||||
|             "when passed filename '..' would cause sanitized filename to be empty, it should fallback to default value 'file'",  | ||||
|             [".."], | ||||
|             defaultFallBackDisposition | ||||
|         ], | ||||
|         // COM1 is a Windows specific "illegal filename" that sanitize filename strips away
 | ||||
|         [ | ||||
|             "when passed filename 'COM1' would cause sanitized filename to be empty, it should fallback to default value 'file'", | ||||
|             ["COM1"], | ||||
|             defaultFallBackDisposition | ||||
|         ], | ||||
|         [ | ||||
|             "sanitized passed filename should be returned URIEncoded", | ||||
|             ["test file.csv"], | ||||
|             `file; filename="test%20file.csv"; filename*=UTF-8''test%20file.csv` | ||||
|         ] | ||||
|     ] | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|             const result = utils.getContentDisposition(...fnParams); | ||||
|             expect(result).toStrictEqual(expected); | ||||
|         }) | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#isStringNote", () => { | ||||
| 
 | ||||
|     const testCases: TestCase<typeof utils.isStringNote>[] = [ | ||||
|         [ | ||||
|             "w/ 'undefined' note type, but a string mime type, it should return true", | ||||
|             [undefined, "application/javascript"], | ||||
|             true | ||||
|         ], | ||||
|         [ | ||||
|             "w/ non-string note type, it should return false", | ||||
|             ["image", "image/jpeg"], | ||||
|             false | ||||
|         ], | ||||
|         [ | ||||
|             "w/ string note type (text), it should return true", | ||||
|             ["text", "text/html"], | ||||
|             true | ||||
|         ], | ||||
|         [ | ||||
|             "w/ string note type (code), it should return true", | ||||
|             ["code", "application/json"], | ||||
|             true | ||||
|         ], | ||||
|         [ | ||||
|             "w/ non-string note type (file), but string mime type, it should return true", | ||||
|             ["file", "application/json"], | ||||
|             true | ||||
|         ], | ||||
|         [ | ||||
|             "w/ non-string note type (file), but mime type starting with 'text/', it should return true", | ||||
|             ["file", "text/html"], | ||||
|             true | ||||
|         ], | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|             const result = utils.isStringNote(...fnParams); | ||||
|             expect(result).toStrictEqual(expected); | ||||
|           }); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe.todo("#quoteRegex", () => {}); | ||||
| 
 | ||||
| describe.todo("#replaceAll", () => {}); | ||||
| 
 | ||||
| describe("#removeTextFileExtension", () => { | ||||
|     const testCases: TestCase<typeof utils.removeTextFileExtension>[] = [ | ||||
|         ["w/ 'test.md' it should strip '.md'", ["test.md"], "test"], | ||||
|         ["w/ 'test.markdown' it should strip '.markdown'", ["test.markdown"], "test"], | ||||
|         ["w/ 'test.html' it should strip '.html'", ["test.html"], "test"], | ||||
|         ["w/ 'test.htm' it should strip '.htm'", ["test.htm"], "test"], | ||||
|         ["w/ 'test.zip' it should NOT strip '.zip'", ["test.zip"], "test.zip"], | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|             const result = utils.removeTextFileExtension(...fnParams); | ||||
|             expect(result).toStrictEqual(expected); | ||||
|           }); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| describe("#getNoteTitle", () => { | ||||
|     const testCases: TestCase<typeof utils.getNoteTitle>[] = [ | ||||
|         [ | ||||
|           "when file has no spaces, and no special file extension, it should return the filename unaltered", | ||||
|           ["test.json", true, undefined], | ||||
|           "test.json" | ||||
|         ], | ||||
|         [ | ||||
|           "when replaceUnderscoresWithSpaces is false, it should keep the underscores in the title", | ||||
|           ["test_file.json", false, undefined], | ||||
|           "test_file.json" | ||||
|         ], | ||||
|         [ | ||||
|           "when replaceUnderscoresWithSpaces is true, it should replace the underscores in the title", | ||||
|           ["test_file.json", true, undefined], | ||||
|           "test file.json" | ||||
|         ], | ||||
|         [ | ||||
|           "when filePath ends with one of the extra handled endings (.md), it should strip the file extension from the title", | ||||
|           ["test_file.md", false, undefined], | ||||
|           "test_file" | ||||
|         ], | ||||
|         [ | ||||
|           "when filePath ends with one of the extra handled endings (.md) and replaceUnderscoresWithSpaces is true, it should strip the file extension from the title and replace underscores", | ||||
|           ["test_file.md", true, undefined], | ||||
|           "test file" | ||||
|         ], | ||||
|         [ | ||||
|           "when filepath contains a full path, it should only return the basename of the file", | ||||
|           ["Trilium Demo/Scripting examples/Statistics/Most cloned notes/template.zip", true, undefined], | ||||
|           "template.zip" | ||||
|         ], | ||||
|         [ | ||||
|           "when filepath contains a full path and has extra handled ending (.html), it should only return the basename of the file and strip the file extension", | ||||
|           ["Trilium Demo/Scripting examples/Statistics/Most cloned notes/template.html", true, undefined], | ||||
|           "template" | ||||
|         ], | ||||
|         [ | ||||
|           "when a noteMeta object is passed, it should use the title from the noteMeta, if present", | ||||
|           //@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
 | ||||
|           ["test_file.md", true, { title: "some other title"}], | ||||
|           "some other title" | ||||
|         ], | ||||
|         [ | ||||
|           "when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed", | ||||
|           //@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
 | ||||
|           ["test_file.md", true, { title: ""}], | ||||
|           "test file" | ||||
|         ], | ||||
|         [ | ||||
|             "when a noteMeta object is passed, but the title prop is empty, it should try to handle the filename as if no noteMeta was passed", | ||||
|             //@ts-expect-error - passing in incomplete noteMeta - but we only care about the title prop here
 | ||||
|             ["test_file.json", false, { title: " "}], | ||||
|             "test_file.json" | ||||
|         ] | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|             const result = utils.getNoteTitle(...fnParams); | ||||
|             expect(result).toStrictEqual(expected); | ||||
|           }); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| describe("#timeLimit", () => { | ||||
| 
 | ||||
|     it("when promise execution does NOT exceed timeout, it should resolve with promises' value", async () => { | ||||
|         const resolvedValue = `resolved: ${new Date().toISOString()}`; | ||||
|         const testPromise = new Promise((res, rej) => { | ||||
|             setTimeout(() => { | ||||
|                 return res(resolvedValue); | ||||
|             }, 200); | ||||
|             //rej("rejected!");
 | ||||
|         }); | ||||
|         await expect(utils.timeLimit(testPromise, 1_000)).resolves.toBe(resolvedValue); | ||||
|     }); | ||||
| 
 | ||||
|     it("when promise execution rejects within timeout, it should return the original promises' rejected value, not the custom set one", async () => { | ||||
|         const rejectedValue = `rejected: ${new Date().toISOString()}`; | ||||
|         const testPromise = new Promise((res, rej) => { | ||||
|             setTimeout(() => { | ||||
|                 //return res("resolved");
 | ||||
|                 rej(rejectedValue); | ||||
|             }, 100); | ||||
|         }); | ||||
|         await expect(utils.timeLimit(testPromise, 200, "Custom Error")).rejects.toThrow(rejectedValue) | ||||
|     }); | ||||
| 
 | ||||
|     it("when promise execution exceeds the set timeout, and 'errorMessage' is NOT set, it should reject the promise and display default error message", async () => { | ||||
|         const testPromise = new Promise((res, rej) => { | ||||
|             setTimeout(() => { | ||||
|                 return res("resolved"); | ||||
|             }, 500); | ||||
|             //rej("rejected!");
 | ||||
|         }); | ||||
|         await expect(utils.timeLimit(testPromise, 200)).rejects.toThrow(`Process exceeded time limit 200`) | ||||
|     }); | ||||
| 
 | ||||
|     it("when promise execution exceeds the set timeout, and 'errorMessage' is set, it should reject the promise and display set error message", async () => { | ||||
|         const customErrorMsg = "Custom Error"; | ||||
|         const testPromise = new Promise((res, rej) => { | ||||
|             setTimeout(() => { | ||||
|                 return res("resolved"); | ||||
|             }, 500); | ||||
|             //rej("rejected!");
 | ||||
|         }); | ||||
|         await expect(utils.timeLimit(testPromise, 200, customErrorMsg)).rejects.toThrow(customErrorMsg) | ||||
|     }); | ||||
| 
 | ||||
|     // TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
 | ||||
|     it("when the passed promise is not a promise but 'undefined', it should return 'undefined'", async () => { | ||||
|         //@ts-expect-error - passing in illegal type 'undefined'
 | ||||
|         expect(utils.timeLimit(undefined, 200)).toBe(undefined) | ||||
|     }); | ||||
| 
 | ||||
|     // TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
 | ||||
|     it("when the passed promise is not a promise, it should return the passed value", async () => { | ||||
|         //@ts-expect-error - passing in illegal type 'object'
 | ||||
|         expect(utils.timeLimit({test: 1}, 200)).toStrictEqual({test: 1}) | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| describe("#deferred", () => { | ||||
|     it("should return a promise", () => { | ||||
|         const result = utils.deferred(); | ||||
|         expect(result).toBeInstanceOf(Promise) | ||||
|     }) | ||||
|     // TriliumNextTODO: Add further tests!
 | ||||
| }); | ||||
| 
 | ||||
| describe("#removeDiacritic", () => { | ||||
| 
 | ||||
|     const testCases: TestCase<typeof utils.removeDiacritic>[] = [ | ||||
|         ["w/ 'Äpfel' it should replace the 'Ä'", ["Äpfel"], "Apfel"], | ||||
|         ["w/ 'Été' it should replace the 'É' and 'é'", ["Été"], "Ete"], | ||||
|         ["w/ 'Fête' it should replace the 'ê'", ["Fête"], "Fete"], | ||||
|         ["w/ 'Αλφαβήτα' it should replace the 'ή'", ["Αλφαβήτα"], "Αλφαβητα"], | ||||
|         ["w/ '' (empty string) it should return empty string", [""], ""], | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|             const result = utils.removeDiacritic(...fnParams); | ||||
|             expect(result).toStrictEqual(expected); | ||||
|           }); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| describe("#normalize", () => { | ||||
| 
 | ||||
|     const testCases: TestCase<typeof utils.normalize>[] = [ | ||||
|         ["w/ 'Äpfel' it should replace the 'Ä' and return lowercased", ["Äpfel"], "apfel"], | ||||
|         ["w/ 'Été' it should replace the 'É' and 'é' and return lowercased", ["Été"], "ete"], | ||||
|         ["w/ 'FêTe' it should replace the 'ê' and return lowercased", ["FêTe"], "fete"], | ||||
|         ["w/ 'ΑλΦαβήΤα' it should replace the 'ή' and return lowercased", ["ΑλΦαβήΤα"], "αλφαβητα"], | ||||
|         ["w/ '' (empty string) it should return empty string", [""], ""], | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|             const result = utils.normalize(...fnParams); | ||||
|             expect(result).toStrictEqual(expected); | ||||
|           }); | ||||
|     }); | ||||
| 
 | ||||
| }); | ||||
| 
 | ||||
| describe("#toMap", () => { | ||||
|     it("should return an instace of Map, with the correct size and keys, when supplied with a list and existing keys", () => { | ||||
|         const testList = [{title: "test", propA: "text", propB: 123 }, {title: "test2", propA: "prop2", propB: 456 }]; | ||||
|         const result = utils.toMap(testList, "title"); | ||||
|         expect(result).toBeInstanceOf(Map); | ||||
|         expect(result.size).toBe(2); | ||||
|         expect(Array.from(result.keys())).toStrictEqual(["test", "test2"]); | ||||
|     }); | ||||
|     it("should return an instace of Map, with an empty size, when the supplied list does not contain the supplied key", () => { | ||||
|         const testList = [{title: "test", propA: "text", propB: 123 }, {title: "test2", propA: "prop2", propB: 456 }]; | ||||
|         //@ts-expect-error - key is non-existing on supplied list type
 | ||||
|         const result = utils.toMap(testList, "nonExistingKey"); | ||||
|         expect(result).toBeInstanceOf(Map); | ||||
|         expect(result.size).toBe(0); | ||||
|     }); | ||||
|     it.fails("should correctly handle duplicate keys? (currently it will overwrite the entry, so returned size will be 1 instead of 2)", () => { | ||||
|         const testList = [{title: "testDupeTitle", propA: "text", propB: 123 }, {title: "testDupeTitle", propA: "prop2", propB: 456 }]; | ||||
|         const result = utils.toMap(testList, "title"); | ||||
|         expect(result).toBeInstanceOf(Map); | ||||
|         expect(result.size).toBe(2); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#envToBoolean", () => { | ||||
|     const testCases: TestCase<typeof utils.envToBoolean>[] = [ | ||||
|         ["w/ 'true' it should return boolean 'true'", ["true"], true], | ||||
|         ["w/ 'True' it should return boolean 'true'", ["True"], true], | ||||
|         ["w/ 'TRUE' it should return boolean 'true'", ["TRUE"], true], | ||||
|         ["w/ 'true ' it should return boolean 'true'", ["true "], true], | ||||
|         ["w/ 'false' it should return boolean 'false'", ["false"], false], | ||||
|         ["w/ 'False' it should return boolean 'false'", ["False"], false], | ||||
|         ["w/ 'FALSE' it should return boolean 'false'", ["FALSE"], false], | ||||
|         ["w/ 'false ' it should return boolean 'false'", ["false "], false], | ||||
|         ["w/ 'whatever' (non-boolean string) it should return undefined", ["whatever"], undefined], | ||||
|         ["w/ '-' (non-boolean string) it should return undefined", ["-"], undefined], | ||||
|         ["w/ '' (empty string) it should return undefined", [""], undefined], | ||||
|         ["w/ ' ' (white space string) it should return undefined", [" "], undefined], | ||||
|         ["w/ undefined it should return undefined", [undefined], undefined], | ||||
|         //@ts-expect-error - pass wrong type as param
 | ||||
|         ["w/ number 1 it should return undefined", [1], undefined], | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach(testCase => { | ||||
|         const [desc, fnParams, expected] = testCase; | ||||
|         it(desc, () => { | ||||
|             const result = utils.envToBoolean(...fnParams); | ||||
|             expect(result).toStrictEqual(expected); | ||||
|           }); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe.todo("#getResourceDir", () => {}); | ||||
| 
 | ||||
| describe("#isElectron", () => { | ||||
|     it("should export a boolean", () => { | ||||
|         expect(utils.isElectron).toBeTypeOf("boolean"); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#isMac", () => { | ||||
|     it("should export a boolean", () => { | ||||
|         expect(utils.isMac).toBeTypeOf("boolean"); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#isWindows", () => { | ||||
|     it("should export a boolean", () => { | ||||
|         expect(utils.isWindows).toBeTypeOf("boolean"); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#isDev", () => { | ||||
|     it("should export a boolean", () => { | ||||
|         expect(utils.isDev).toBeTypeOf("boolean"); | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| describe("#formatDownloadTitle", () => { | ||||
| 
 | ||||
|     //prettier-ignore
 | ||||
|     const testCases: [fnValue: Parameters<typeof utils.formatDownloadTitle>, expectedValue: ReturnType<typeof utils.formatDownloadTitle>][] = [ | ||||
| 
 | ||||
|         // empty fileName tests
 | ||||
|         [ | ||||
|             ["", "text", ""], | ||||
|             "untitled.html" | ||||
|         ], | ||||
|         [ | ||||
|             ["", "canvas", ""], | ||||
|             "untitled.json" | ||||
|         ], | ||||
|         [ | ||||
|             ["", null, ""], | ||||
|             "untitled" | ||||
|         ], | ||||
| 
 | ||||
| 
 | ||||
|         // json extension from type tests
 | ||||
|         [ | ||||
|             ["test_file", "canvas", ""], | ||||
|             "test_file.json" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file", "relationMap", ""], | ||||
|             "test_file.json" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file", "search", ""], | ||||
|             "test_file.json" | ||||
|         ], | ||||
| 
 | ||||
| 
 | ||||
|         // extension based on mime type
 | ||||
|         [ | ||||
|             ["test_file", null, "text/csv"], | ||||
|             "test_file.csv" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file_wo_ext", "image", "image/svg+xml"], | ||||
|             "test_file_wo_ext.svg" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file_wo_ext", "file", "application/json"], | ||||
|             "test_file_wo_ext.json" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file_w_fake_ext.ext", "image", "image/svg+xml"], | ||||
|             "test_file_w_fake_ext.ext.svg" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file_w_correct_ext.svg", "image", "image/svg+xml"], | ||||
|             "test_file_w_correct_ext.svg" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file_w_correct_ext.svgz", "image", "image/svg+xml"], | ||||
|             "test_file_w_correct_ext.svgz" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file.zip", "file", "application/zip"], | ||||
|             "test_file.zip" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file", "file", "application/zip"], | ||||
|             "test_file.zip" | ||||
|         ], | ||||
| 
 | ||||
| 
 | ||||
|         // application/octet-stream tests
 | ||||
|         [ | ||||
|             ["test_file", "file", "application/octet-stream"], | ||||
|             "test_file" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file.zip", "file", "application/octet-stream"], | ||||
|             "test_file.zip" | ||||
|         ], | ||||
|         [ | ||||
|             ["test_file.unknown", null, "application/octet-stream"], | ||||
|             "test_file.unknown" | ||||
|         ], | ||||
| 
 | ||||
| 
 | ||||
|         // sanitized filename tests
 | ||||
|         [ | ||||
|             ["test/file", null, "application/octet-stream"], | ||||
|             "testfile" | ||||
|         ], | ||||
|         [ | ||||
|             ["test:file.zip", "file", "application/zip"], | ||||
|             "testfile.zip" | ||||
|         ], | ||||
|         [ | ||||
|             [":::", "file", "application/zip"], | ||||
|             ".zip" | ||||
|         ], | ||||
|         [ | ||||
|             [":::a", "file", "application/zip"], | ||||
|             "a.zip" | ||||
|         ] | ||||
|     ]; | ||||
| 
 | ||||
|     testCases.forEach((testCase) => { | ||||
|         const [fnParams, expected] = testCase; | ||||
|         return it(`With args '${JSON.stringify(fnParams)}', it should return '${expected}'`, () => { | ||||
|             const actual = utils.formatDownloadTitle(...fnParams); | ||||
|             expect(actual).toStrictEqual(expected); | ||||
|         }); | ||||
|     }); | ||||
| }); | ||||
| @ -9,6 +9,7 @@ import mimeTypes from "mime-types"; | ||||
| import path from "path"; | ||||
| import { fileURLToPath } from "url"; | ||||
| import { dirname, join } from "path"; | ||||
| import type NoteMeta from "./meta/note_meta.js"; | ||||
| 
 | ||||
| const randtoken = generator({ source: "crypto" }); | ||||
| 
 | ||||
| @ -71,21 +72,18 @@ export function hash(text: string) { | ||||
|     return crypto.createHash("sha1").update(text).digest("base64"); | ||||
| } | ||||
| 
 | ||||
| export function isEmptyOrWhitespace(str: string) { | ||||
|     return str === null || str.match(/^ *$/) !== null; | ||||
| export function isEmptyOrWhitespace(str: string | null | undefined) { | ||||
|     if (!str) return true; | ||||
|     return str.match(/^ *$/) !== null; | ||||
| } | ||||
| 
 | ||||
| export function sanitizeSqlIdentifier(str: string) { | ||||
|     return str.replace(/[^A-Za-z0-9_]/g, ""); | ||||
| } | ||||
| 
 | ||||
| export function escapeHtml(str: string) { | ||||
|     return escape(str); | ||||
| } | ||||
| export const escapeHtml = escape; | ||||
| 
 | ||||
| export function unescapeHtml(str: string) { | ||||
|     return unescape(str); | ||||
| } | ||||
| export const unescapeHtml = unescape; | ||||
| 
 | ||||
| export function toObject<T, K extends string | number | symbol, V>(array: T[], fn: (item: T) => [K, V]): Record<K, V> { | ||||
|     const obj: Record<K, V> = {} as Record<K, V>; // TODO: unsafe?
 | ||||
| @ -103,29 +101,6 @@ export function stripTags(text: string) { | ||||
|     return text.replace(/<(?:.|\n)*?>/gm, ""); | ||||
| } | ||||
| 
 | ||||
| export function union<T extends string | number | symbol>(a: T[], b: T[]): T[] { | ||||
|     const obj: Record<T, T> = {} as Record<T, T>; // TODO: unsafe?
 | ||||
| 
 | ||||
|     for (let i = a.length - 1; i >= 0; i--) { | ||||
|         obj[a[i]] = a[i]; | ||||
|     } | ||||
| 
 | ||||
|     for (let i = b.length - 1; i >= 0; i--) { | ||||
|         obj[b[i]] = b[i]; | ||||
|     } | ||||
| 
 | ||||
|     const res: T[] = []; | ||||
| 
 | ||||
|     for (const k in obj) { | ||||
|         if (obj.hasOwnProperty(k)) { | ||||
|             // <-- optional
 | ||||
|             res.push(obj[k]); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     return res; | ||||
| } | ||||
| 
 | ||||
| export function escapeRegExp(str: string) { | ||||
|     return str.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1"); | ||||
| } | ||||
| @ -138,27 +113,18 @@ export async function crash() { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function sanitizeFilenameForHeader(filename: string) { | ||||
|     let sanitizedFilename = sanitize(filename); | ||||
| 
 | ||||
|     if (sanitizedFilename.trim().length === 0) { | ||||
|         sanitizedFilename = "file"; | ||||
|     } | ||||
| 
 | ||||
|     return encodeURIComponent(sanitizedFilename); | ||||
| } | ||||
| 
 | ||||
| export function getContentDisposition(filename: string) { | ||||
|     const sanitizedFilename = sanitizeFilenameForHeader(filename); | ||||
| 
 | ||||
|     return `file; filename="${sanitizedFilename}"; filename*=UTF-8''${sanitizedFilename}`; | ||||
|     const sanitizedFilename = sanitize(filename).trim() || "file"; | ||||
|     const uriEncodedFilename = encodeURIComponent(sanitizedFilename); | ||||
|     return `file; filename="${uriEncodedFilename}"; filename*=UTF-8''${uriEncodedFilename}`; | ||||
| } | ||||
| 
 | ||||
| // render and book are string note in the sense that they are expected to contain empty string
 | ||||
| const STRING_NOTE_TYPES = new Set(["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"]); | ||||
| const STRING_MIME_TYPES = new Set(["application/javascript", "application/x-javascript", "application/json", "application/x-sql", "image/svg+xml"]); | ||||
| 
 | ||||
| export function isStringNote(type: string | undefined, mime: string) { | ||||
|     // render and book are string note in the sense that they are expected to contain empty string
 | ||||
|     return (type && ["text", "code", "relationMap", "search", "render", "book", "mermaid", "canvas"].includes(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime); | ||||
|     return (type && STRING_NOTE_TYPES.has(type)) || mime.startsWith("text/") || STRING_MIME_TYPES.has(mime); | ||||
| } | ||||
| 
 | ||||
| export function quoteRegex(url: string) { | ||||
| @ -211,26 +177,23 @@ export function removeTextFileExtension(filePath: string) { | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: { title?: string }) { | ||||
|     if (noteMeta?.title) { | ||||
|         return noteMeta.title; | ||||
|     } else { | ||||
|         const basename = path.basename(removeTextFileExtension(filePath)); | ||||
|         if (replaceUnderscoresWithSpaces) { | ||||
|             return basename.replace(/_/g, " ").trim(); | ||||
|         } | ||||
|         return basename; | ||||
|     } | ||||
| export function getNoteTitle(filePath: string, replaceUnderscoresWithSpaces: boolean, noteMeta?: NoteMeta) { | ||||
|     const trimmedNoteMeta = noteMeta?.title?.trim(); | ||||
|     if (trimmedNoteMeta) return trimmedNoteMeta; | ||||
| 
 | ||||
|     const basename = path.basename(removeTextFileExtension(filePath)); | ||||
|     return replaceUnderscoresWithSpaces ? basename.replace(/_/g, " ").trim() : basename; | ||||
| } | ||||
| 
 | ||||
| export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage?: string): Promise<T> { | ||||
|     // TriliumNextTODO: since TS avoids this from ever happening – do we need this check?
 | ||||
|     if (!promise || !promise.then) { | ||||
|         // it's not actually a promise
 | ||||
|         return promise; | ||||
|     } | ||||
| 
 | ||||
|     // better stack trace if created outside of promise
 | ||||
|     const error = new Error(errorMessage || `Process exceeded time limit ${limitMs}`); | ||||
|     const errorTimeLimit = new Error(errorMessage || `Process exceeded time limit ${limitMs}`); | ||||
| 
 | ||||
|     return new Promise((res, rej) => { | ||||
|         let resolved = false; | ||||
| @ -245,7 +208,7 @@ export function timeLimit<T>(promise: Promise<T>, limitMs: number, errorMessage? | ||||
| 
 | ||||
|         setTimeout(() => { | ||||
|             if (!resolved) { | ||||
|                 rej(error); | ||||
|                 rej(errorTimeLimit); | ||||
|             } | ||||
|         }, limitMs); | ||||
|     }); | ||||
| @ -284,20 +247,18 @@ export function normalize(str: string) { | ||||
|     return removeDiacritic(str).toLowerCase(); | ||||
| } | ||||
| 
 | ||||
| export function toMap<T extends Record<string, any>>(list: T[], key: keyof T): Record<string, T> { | ||||
|     const map: Record<string, T> = {}; | ||||
| 
 | ||||
| export function toMap<T extends Record<string, any>>(list: T[], key: keyof T) { | ||||
|     const map = new Map<string, T>(); | ||||
|     for (const el of list) { | ||||
|         map[el[key]] = el; | ||||
|         const keyForMap = el[key]; | ||||
|         if (!keyForMap) continue; | ||||
|         // TriliumNextTODO: do we need to handle the case when the same key is used?
 | ||||
|         // currently this will overwrite the existing entry in the map
 | ||||
|         map.set(keyForMap, el); | ||||
|     } | ||||
| 
 | ||||
|     return map; | ||||
| } | ||||
| 
 | ||||
| export function isString(x: any) { | ||||
|     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; | ||||
| @ -317,48 +278,87 @@ export function envToBoolean(val: string | undefined) { | ||||
|  * @returns the resource dir. | ||||
|  */ | ||||
| export function getResourceDir() { | ||||
|     if (isElectron && !isDev) { | ||||
|         return process.resourcesPath; | ||||
|     } else { | ||||
|         return join(dirname(fileURLToPath(import.meta.url)), "..", ".."); | ||||
|     if (isElectron && !isDev) return process.resourcesPath; | ||||
|     return join(dirname(fileURLToPath(import.meta.url)), "..", ".."); | ||||
| } | ||||
| 
 | ||||
| // TODO: Deduplicate with src/public/app/services/utils.ts
 | ||||
| /** | ||||
|  * Compares two semantic version strings. | ||||
|  * Returns: | ||||
|  *   1  if v1 is greater than v2 | ||||
|  *   0  if v1 is equal to v2 | ||||
|  *   -1 if v1 is less than v2 | ||||
|  * | ||||
|  * @param v1 First version string | ||||
|  * @param v2 Second version string | ||||
|  * @returns | ||||
|  */ | ||||
| function compareVersions(v1: string, v2: string): number { | ||||
|     // Remove 'v' prefix and everything after dash if present
 | ||||
|     v1 = v1.replace(/^v/, "").split("-")[0]; | ||||
|     v2 = v2.replace(/^v/, "").split("-")[0]; | ||||
| 
 | ||||
|     const v1parts = v1.split(".").map(Number); | ||||
|     const v2parts = v2.split(".").map(Number); | ||||
| 
 | ||||
|     // Pad shorter version with zeros
 | ||||
|     while (v1parts.length < 3) v1parts.push(0); | ||||
|     while (v2parts.length < 3) v2parts.push(0); | ||||
| 
 | ||||
|     // Compare major version
 | ||||
|     if (v1parts[0] !== v2parts[0]) { | ||||
|         return v1parts[0] > v2parts[0] ? 1 : -1; | ||||
|     } | ||||
| 
 | ||||
|     // Compare minor version
 | ||||
|     if (v1parts[1] !== v2parts[1]) { | ||||
|         return v1parts[1] > v2parts[1] ? 1 : -1; | ||||
|     } | ||||
| 
 | ||||
|     // Compare patch version
 | ||||
|     if (v1parts[2] !== v2parts[2]) { | ||||
|         return v1parts[2] > v2parts[2] ? 1 : -1; | ||||
|     } | ||||
| 
 | ||||
|     return 0; | ||||
| } | ||||
| 
 | ||||
| export default { | ||||
|     randomSecureToken, | ||||
|     randomString, | ||||
|     compareVersions, | ||||
|     crash, | ||||
|     deferred, | ||||
|     envToBoolean, | ||||
|     escapeHtml, | ||||
|     escapeRegExp, | ||||
|     formatDownloadTitle, | ||||
|     fromBase64, | ||||
|     getContentDisposition, | ||||
|     getNoteTitle, | ||||
|     getResourceDir, | ||||
|     hash, | ||||
|     hashedBlobId, | ||||
|     hmac, | ||||
|     isDev, | ||||
|     isElectron, | ||||
|     isEmptyOrWhitespace, | ||||
|     isMac, | ||||
|     isStringNote, | ||||
|     isWindows, | ||||
|     md5, | ||||
|     newEntityId, | ||||
|     toBase64, | ||||
|     fromBase64, | ||||
|     hmac, | ||||
|     isElectron, | ||||
|     hash, | ||||
|     isEmptyOrWhitespace, | ||||
|     sanitizeSqlIdentifier, | ||||
|     escapeHtml, | ||||
|     unescapeHtml, | ||||
|     toObject, | ||||
|     stripTags, | ||||
|     union, | ||||
|     escapeRegExp, | ||||
|     crash, | ||||
|     getContentDisposition, | ||||
|     isStringNote, | ||||
|     quoteRegex, | ||||
|     replaceAll, | ||||
|     getNoteTitle, | ||||
|     removeTextFileExtension, | ||||
|     formatDownloadTitle, | ||||
|     timeLimit, | ||||
|     deferred, | ||||
|     removeDiacritic, | ||||
|     normalize, | ||||
|     hashedBlobId, | ||||
|     quoteRegex, | ||||
|     randomSecureToken, | ||||
|     randomString, | ||||
|     removeDiacritic, | ||||
|     removeTextFileExtension, | ||||
|     replaceAll, | ||||
|     sanitizeSqlIdentifier, | ||||
|     stripTags, | ||||
|     timeLimit, | ||||
|     toBase64, | ||||
|     toMap, | ||||
|     isString, | ||||
|     getResourceDir, | ||||
|     isMac, | ||||
|     isWindows, | ||||
|     envToBoolean | ||||
|     toObject, | ||||
|     unescapeHtml | ||||
| }; | ||||
|  | ||||
| @ -56,18 +56,26 @@ | ||||
|         <div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;"> | ||||
|             <form data-bind="submit: selectSetupType"> | ||||
| 
 | ||||
|               <div class="radio" style="margin-bottom: 15px;"> | ||||
|                   <label><input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType""> | ||||
|                       <%= t("setup.new-document") %></label> | ||||
|               </div> | ||||
|               <div class="radio" style="margin-bottom: 15px;"> | ||||
|                   <label><input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType"> | ||||
|                       <%= t("setup.sync-from-desktop") %></label> | ||||
|               </div> | ||||
|               <div class="radio" style="margin-bottom: 15px;"> | ||||
|                   <label><input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType""> | ||||
|                       <%= t("setup.sync-from-server") %></label> | ||||
|               </div> | ||||
|             <div class="radio" style="margin-bottom: 15px;"> | ||||
|                 <label class="tn-radio"> | ||||
|                     <input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType"> | ||||
|                     <%= t("setup.new-document") %> | ||||
|                 </label> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="radio" style="margin-bottom: 15px;"> | ||||
|                 <label class="tn-radio"> | ||||
|                     <input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType"> | ||||
|                     <%= t("setup.sync-from-desktop") %> | ||||
|                 </label> | ||||
|             </div> | ||||
| 
 | ||||
|             <div class="radio" style="margin-bottom: 15px;"> | ||||
|                 <label class="tn-radio"> | ||||
|                     <input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType"> | ||||
|                     <%= t("setup.sync-from-server") %> | ||||
|                 </label> | ||||
|             </div> | ||||
| 
 | ||||
|               <button type="submit" data-bind="disable: !setupTypeSelected()" class="btn btn-primary"><%= t("setup.next") %></button> | ||||
|             </form> | ||||
|  | ||||
							
								
								
									
										11
									
								
								src/www.ts
									
									
									
									
									
								
							
							
						
						
									
										11
									
								
								src/www.ts
									
									
									
									
									
								
							| @ -12,7 +12,8 @@ import ws from "./services/ws.js"; | ||||
| import utils from "./services/utils.js"; | ||||
| import port from "./services/port.js"; | ||||
| import host from "./services/host.js"; | ||||
| import semver from "semver"; | ||||
| 
 | ||||
| const MINIMUM_NODE_VERSION = "20.0.0"; | ||||
| 
 | ||||
| // setup basic error handling even before requiring dependencies, since those can produce errors as well
 | ||||
| 
 | ||||
| @ -32,8 +33,12 @@ function exit() { | ||||
| process.on("SIGINT", exit); | ||||
| process.on("SIGTERM", exit); | ||||
| 
 | ||||
| if (!semver.satisfies(process.version, ">=10.5.0")) { | ||||
|     console.error("Trilium only supports node.js 10.5 and later"); | ||||
| if (utils.compareVersions(process.versions.node, MINIMUM_NODE_VERSION) < 0) { | ||||
|     console.error(); | ||||
|     console.error(`The Trilium server requires Node.js ${MINIMUM_NODE_VERSION} and later in order to start.\n`); | ||||
|     console.error(`\tCurrent version:\t${process.versions.node}`); | ||||
|     console.error(`\tExpected version:\t${MINIMUM_NODE_VERSION}`); | ||||
|     console.error(); | ||||
|     process.exit(1); | ||||
| } | ||||
| 
 | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Elian Doran
						Elian Doran