mirror of
https://github.com/zadam/trilium.git
synced 2025-12-26 18:19:57 +01:00
Compare commits
307 Commits
nightly
...
v0.90.11-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6734d765c9 | ||
|
|
470594b1c7 | ||
|
|
782d34566d | ||
|
|
1b2a772612 | ||
|
|
46218d6ab4 | ||
|
|
1d2366fa06 | ||
|
|
0acba0eac4 | ||
|
|
48d53e276e | ||
|
|
47baa02bca | ||
|
|
bc35c3c641 | ||
|
|
790b87f23f | ||
|
|
48ba15ad88 | ||
|
|
cda28cfd65 | ||
|
|
7ffe145481 | ||
|
|
ac2bca790b | ||
|
|
774966e640 | ||
|
|
81310d33b0 | ||
|
|
34e6430977 | ||
|
|
15b4eacdca | ||
|
|
7c342aed9e | ||
|
|
8c69d47aed | ||
|
|
f88d3220b5 | ||
|
|
70a98a3d33 | ||
|
|
6e0a10cf2c | ||
|
|
c421e75f55 | ||
|
|
89420eafa3 | ||
|
|
7a70fc14b3 | ||
|
|
d2008e7e5f | ||
|
|
745c9846a6 | ||
|
|
3972bb2ecf | ||
|
|
06262adf91 | ||
|
|
5771060b57 | ||
|
|
6a11f9c073 | ||
|
|
85ee7def84 | ||
|
|
b88f0e0109 | ||
|
|
787aa6f5a6 | ||
|
|
4f39188198 | ||
|
|
dd6e762dab | ||
|
|
48bc9204ac | ||
|
|
918f425e1f | ||
|
|
821af8dc11 | ||
|
|
44734435ea | ||
|
|
01c53b6d9f | ||
|
|
9a5de0d4c8 | ||
|
|
5116bddc5f | ||
|
|
92aa671ec7 | ||
|
|
1f4d09f6f0 | ||
|
|
29e83b97e6 | ||
|
|
18de0857b3 | ||
|
|
2048a30aa5 | ||
|
|
78017e4d36 | ||
|
|
35fe5845a3 | ||
|
|
1261bdbb29 | ||
|
|
91fa1a6cb1 | ||
|
|
1816fcd3ac | ||
|
|
d13044b972 | ||
|
|
f5205fdd30 | ||
|
|
930b8e0ce2 | ||
|
|
b5988ba7c2 | ||
|
|
75e2ceed5d | ||
|
|
d2ee3738a2 | ||
|
|
8a548f6589 | ||
|
|
0859a955b1 | ||
|
|
a02146df17 | ||
|
|
a6385557b5 | ||
|
|
00aebfcdf0 | ||
|
|
c6b3ace807 | ||
|
|
6799544950 | ||
|
|
da1cf4d6ed | ||
|
|
21cfb64f83 | ||
|
|
dd7c2084fa | ||
|
|
4f5d874028 | ||
|
|
80e6276d31 | ||
|
|
0192060ad2 | ||
|
|
e41ff54c0d | ||
|
|
bdece7216f | ||
|
|
611fb90a52 | ||
|
|
75e554d86b | ||
|
|
0db1a63cef | ||
|
|
4ffc6f716c | ||
|
|
fa3200ba8f | ||
|
|
bff9bedc44 | ||
|
|
f8777b0de1 | ||
|
|
48e6c1a33d | ||
|
|
4c43ac5bdd | ||
|
|
45ccc7562e | ||
|
|
e72eb5f27c | ||
|
|
d1404492a7 | ||
|
|
238c9c6f0d | ||
|
|
443f02a78e | ||
|
|
5fbd052138 | ||
|
|
bc84a71929 | ||
|
|
a514a51fff | ||
|
|
24022834e2 | ||
|
|
9fdc84d91f | ||
|
|
9c27672794 | ||
|
|
f37fa3723b | ||
|
|
b14065d442 | ||
|
|
1554e25283 | ||
|
|
4e945583a1 | ||
|
|
92c588dc98 | ||
|
|
5c66e3fd04 | ||
|
|
e508313f21 | ||
|
|
df3f51d1f3 | ||
|
|
0a6815e448 | ||
|
|
293db6962e | ||
|
|
eb05c5b919 | ||
|
|
bbaed45f6b | ||
|
|
aa7d7b3afd | ||
|
|
fc4797d04f | ||
|
|
c2baa4b752 | ||
|
|
faeefc75ba | ||
|
|
d0904c1051 | ||
|
|
11a82e62f1 | ||
|
|
7b24f7e332 | ||
|
|
7f17f93767 | ||
|
|
cdd5a17fce | ||
|
|
dbca50d9b0 | ||
|
|
57a86c75d8 | ||
|
|
9e3c1b46cd | ||
|
|
00209ec77a | ||
|
|
dfa4f3cd84 | ||
|
|
3af29a78dc | ||
|
|
4d783f1879 | ||
|
|
c3e10b2b76 | ||
|
|
f57ab4b9f0 | ||
|
|
a690155d7e | ||
|
|
cc0b3db424 | ||
|
|
ae60f8c842 | ||
|
|
90dffdc6ed | ||
|
|
ac13291744 | ||
|
|
bbc038f254 | ||
|
|
f8df3a6933 | ||
|
|
b10e2d9ec4 | ||
|
|
2387bbd17f | ||
|
|
f13d88c3c0 | ||
|
|
2459bbf341 | ||
|
|
60426ea487 | ||
|
|
b112cb609f | ||
|
|
b9ebc66122 | ||
|
|
2f4ed92346 | ||
|
|
d3d001d8ea | ||
|
|
70cee7dbf6 | ||
|
|
36fde2b03d | ||
|
|
88d8f57697 | ||
|
|
b7e254975f | ||
|
|
97b2ba2da1 | ||
|
|
bda8173932 | ||
|
|
48f9f072b4 | ||
|
|
9c55203ea0 | ||
|
|
dbb5e0e971 | ||
|
|
b8eb09b46b | ||
|
|
5682b2d819 | ||
|
|
5109c07e9c | ||
|
|
b8569ea243 | ||
|
|
52bc28def7 | ||
|
|
e65d4cdfbf | ||
|
|
96b9042559 | ||
|
|
e68d070320 | ||
|
|
ef5f2c680b | ||
|
|
6717b1b4ae | ||
|
|
41e3163595 | ||
|
|
514653fb50 | ||
|
|
e843f1adc1 | ||
|
|
83f5b47c99 | ||
|
|
2fdff29067 | ||
|
|
0d270cbeb6 | ||
|
|
f947a039b9 | ||
|
|
d2235a185b | ||
|
|
87bc142552 | ||
|
|
1a25f60264 | ||
|
|
fe4dbae079 | ||
|
|
e1ae014b74 | ||
|
|
7952a5a81e | ||
|
|
60b6f7df89 | ||
|
|
7354fb5b4a | ||
|
|
1fb0b74f76 | ||
|
|
9e3b915612 | ||
|
|
7505db220e | ||
|
|
a3932376f3 | ||
|
|
3a609d54ab | ||
|
|
c4bd4eb440 | ||
|
|
e931df721d | ||
|
|
1e9324c303 | ||
|
|
6c4513fb2e | ||
|
|
c7e1362105 | ||
|
|
acf37f9327 | ||
|
|
f80cf0aa02 | ||
|
|
6078620bf1 | ||
|
|
579b3f4ca0 | ||
|
|
bf28005f46 | ||
|
|
c81b847b61 | ||
|
|
88cd2ac25c | ||
|
|
e3e6f56a88 | ||
|
|
0768a2a0a3 | ||
|
|
84d216da54 | ||
|
|
391f518c01 | ||
|
|
2324c9a13b | ||
|
|
6799c44e22 | ||
|
|
f0052d56b7 | ||
|
|
53822fd47f | ||
|
|
b02c4b54e5 | ||
|
|
27f07ee604 | ||
|
|
03a23d15f9 | ||
|
|
70d55097ee | ||
|
|
560467bdba | ||
|
|
cb4fe4481f | ||
|
|
eee088316d | ||
|
|
81ca0a3776 | ||
|
|
48b0af1bba | ||
|
|
43ef452d44 | ||
|
|
70ebf1a08f | ||
|
|
9f6f0f5d60 | ||
|
|
af67362ad6 | ||
|
|
77550f3087 | ||
|
|
5813282248 | ||
|
|
e77b223508 | ||
|
|
7aafdce629 | ||
|
|
a2f0cb394a | ||
|
|
e8d1518965 | ||
|
|
8b333b32af | ||
|
|
cda369ed4d | ||
|
|
b5bc93d794 | ||
|
|
b96047e962 | ||
|
|
31ccbb0d23 | ||
|
|
cb9403535d | ||
|
|
b5ee90a1d2 | ||
|
|
9ed7eb977e | ||
|
|
8cc487da7c | ||
|
|
ae593ea363 | ||
|
|
26e4decaec | ||
|
|
28f6712a4f | ||
|
|
93efce4023 | ||
|
|
689b3a3079 | ||
|
|
4ad725842e | ||
|
|
d4956ad3a2 | ||
|
|
c7b7c68a05 | ||
|
|
cab1d7d353 | ||
|
|
7957c6d34e | ||
|
|
c18c972a57 | ||
|
|
29a700f731 | ||
|
|
815eab26f6 | ||
|
|
103da23b5a | ||
|
|
ba1d82bc0a | ||
|
|
21f8a29761 | ||
|
|
f38870b27d | ||
|
|
56a6d27240 | ||
|
|
38e5ef2c7d | ||
|
|
e29d600517 | ||
|
|
42605fbbad | ||
|
|
11ca427a28 | ||
|
|
28d8088763 | ||
|
|
664c4789c0 | ||
|
|
7c5667b457 | ||
|
|
0afd22e196 | ||
|
|
b3abee71b7 | ||
|
|
9bd5596b2a | ||
|
|
e0e3c15e6e | ||
|
|
31396264fa | ||
|
|
b1aada22b5 | ||
|
|
d7eaf72a6d | ||
|
|
59df442676 | ||
|
|
9770db7f3c | ||
|
|
8c36cea71b | ||
|
|
b03f40f1f9 | ||
|
|
00dba7bef4 | ||
|
|
4186f3d136 | ||
|
|
529502524d | ||
|
|
7c518e9512 | ||
|
|
5e2d1bc124 | ||
|
|
7dfe6f276e | ||
|
|
858db68d66 | ||
|
|
b72f46f108 | ||
|
|
83dbe0539e | ||
|
|
87e0cf55f1 | ||
|
|
8315d5c778 | ||
|
|
61bd7dca18 | ||
|
|
4faf27364f | ||
|
|
52a6d0b48a | ||
|
|
cddc9a7b6a | ||
|
|
75d019863f | ||
|
|
849a6a3aef | ||
|
|
e7378306a2 | ||
|
|
1277dfc5d5 | ||
|
|
ae680847dc | ||
|
|
a5fd57308a | ||
|
|
fa769df7b0 | ||
|
|
8136a2972e | ||
|
|
8c8c3974f3 | ||
|
|
e81bfa3693 | ||
|
|
a857f4816f | ||
|
|
ade34f9745 | ||
|
|
fd66cb930d | ||
|
|
182d9afac1 | ||
|
|
4e6ef0be95 | ||
|
|
88961ea93f | ||
|
|
a3f2946a17 | ||
|
|
8ae5f9ea9b | ||
|
|
9ec2508f09 | ||
|
|
bae63b08a2 | ||
|
|
729a188528 | ||
|
|
2c5a5acffa | ||
|
|
e45c5f429d | ||
|
|
5c44ac5ad8 | ||
|
|
b44c2f5ebf | ||
|
|
0a69189b9b | ||
|
|
d46963e496 |
32
.github/workflows/main-docker.yml
vendored
32
.github/workflows/main-docker.yml
vendored
@@ -129,6 +129,8 @@ jobs:
|
||||
type=ref,event=branch
|
||||
type=ref,event=tag
|
||||
type=sha
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
@@ -213,7 +215,9 @@ jobs:
|
||||
images: |
|
||||
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
flavor: |
|
||||
latest=false
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
@@ -242,6 +246,32 @@ jobs:
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${REF_NAME} \
|
||||
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
# If the ref is a tag, also tag the image as stable as this is part of a 'release'
|
||||
# and only go in the `if` if there is NOT a `-` in the tag's name, due to tagging of `-alpha`, `-beta`, etc...
|
||||
if [[ "${GITHUB_REF}" == refs/tags/* && ! "${REF_NAME}" =~ - ]]; then
|
||||
# First create stable tags
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
|
||||
$(printf '${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable \
|
||||
$(printf '${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@sha256:%s ' *)
|
||||
|
||||
# Small delay to ensure stable tag is fully propagated
|
||||
sleep 5
|
||||
|
||||
# Now update latest tags
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
|
||||
|
||||
docker buildx imagetools create \
|
||||
-t ${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest \
|
||||
${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:stable
|
||||
|
||||
fi
|
||||
|
||||
- name: Inspect image
|
||||
run: |
|
||||
|
||||
2
.github/workflows/main.yml
vendored
2
.github/workflows/main.yml
vendored
@@ -43,7 +43,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Update build info
|
||||
run: npm run 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)
|
||||
|
||||
25
.github/workflows/nightly.yml
vendored
25
.github/workflows/nightly.yml
vendored
@@ -5,10 +5,14 @@ on:
|
||||
- cron: '0 2 * * *' # run at 2 AM UTC
|
||||
# This can be used to allow manually triggering nightlies from the web interface
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
GITHUB_UPLOAD_URL: https://uploads.github.com/repos/TriliumNext/Notes/releases/179589950/assets{?name,label}
|
||||
GITHUB_RELEASE_ID: 179589950
|
||||
permissions:
|
||||
contents: write
|
||||
jobs:
|
||||
nightly-electron:
|
||||
name: Deploy nightly
|
||||
name: Deploy nightly
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -23,7 +27,7 @@ jobs:
|
||||
- name: windows
|
||||
image: windows-latest
|
||||
extension: exe
|
||||
runs-on: ${{ matrix.os.image }}
|
||||
runs-on: ${{ matrix.os.image }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set up node & dependencies
|
||||
@@ -37,6 +41,8 @@ jobs:
|
||||
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)
|
||||
@@ -71,16 +77,16 @@ jobs:
|
||||
- name: Deploy release
|
||||
uses: WebFreak001/deploy-nightly@v3.1.0
|
||||
with:
|
||||
# upload_url: # find out this value by opening https://api.github.com/repos/<owner>/<repo>/releases in your browser and copy the full "upload_url" value including the {?name,label} part
|
||||
# release_id: # same as above (id can just be taken out the upload_url, it's used to find old releases)
|
||||
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.1.0
|
||||
with:
|
||||
# upload_url: # find out this value by opening https://api.github.com/repos/<owner>/<repo>/releases in your browser and copy the full "upload_url" value including the {?name,label} part
|
||||
# release_id: # same as above (id can just be taken out the upload_url, it's used to find old releases)
|
||||
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
|
||||
@@ -99,6 +105,7 @@ jobs:
|
||||
- name: Run Linux server build (x86_64)
|
||||
run: |
|
||||
npm run update-build-info
|
||||
npm run ci-update-nightly-version
|
||||
./bin/build-server.sh
|
||||
- name: Prepare artifacts
|
||||
if: runner.os != 'windows'
|
||||
@@ -115,8 +122,8 @@ jobs:
|
||||
- name: Deploy release
|
||||
uses: WebFreak001/deploy-nightly@v3.1.0
|
||||
with:
|
||||
# upload_url: # find out this value by opening https://api.github.com/repos/<owner>/<repo>/releases in your browser and copy the full "upload_url" value including the {?name,label} part
|
||||
# release_id: # same as above (id can just be taken out the upload_url, it's used to find old releases)
|
||||
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
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ build/
|
||||
src/public/app-dist/
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
po-*/
|
||||
|
||||
*.db
|
||||
!integration-tests/db/document.db
|
||||
|
||||
13
.vscode/i18n-ally-reviews.yml
vendored
13
.vscode/i18n-ally-reviews.yml
vendored
@@ -5,3 +5,16 @@ reviews:
|
||||
description: >-
|
||||
Describes the shortcut which triggers a search within the current
|
||||
page/note only
|
||||
add_label.to_value:
|
||||
locales:
|
||||
fr:
|
||||
comments:
|
||||
- user:
|
||||
name: Potjoe-97
|
||||
email: giann@LAPTOPT490-GF
|
||||
id: QXec0JUoxfGmMlpch-B1S
|
||||
comment: ''
|
||||
suggestion: vers la valeur
|
||||
type: request_change
|
||||
time: '2024-10-15T16:57:06.188Z'
|
||||
resolved: true
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -18,4 +18,7 @@
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "vscode.typescript-language-features"
|
||||
},
|
||||
"github-actions.workflows.pinned.workflows": [
|
||||
".github/workflows/nightly.yml"
|
||||
],
|
||||
}
|
||||
|
||||
63
Dockerfile
63
Dockerfile
@@ -1,7 +1,7 @@
|
||||
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
|
||||
FROM node:20.15.1-bullseye-slim
|
||||
# Build stage
|
||||
FROM node:20.15.1-bullseye-slim AS builder
|
||||
|
||||
# Configure system dependencies
|
||||
# Configure build dependencies in a single layer
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
autoconf \
|
||||
automake \
|
||||
@@ -12,49 +12,52 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
nasm \
|
||||
libpng-dev \
|
||||
python3 \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Bundle app source
|
||||
# Copy only necessary files for build
|
||||
COPY . .
|
||||
COPY server-package.json package.json
|
||||
|
||||
# Copy TypeScript build artifacts into the original directory structure.
|
||||
# Copy the healthcheck
|
||||
# Build and cleanup in a single layer
|
||||
RUN cp -R build/src/* src/. && \
|
||||
cp build/docker_healthcheck.js . && \
|
||||
rm -r build && \
|
||||
rm docker_healthcheck.ts
|
||||
|
||||
# Install app dependencies
|
||||
RUN apt-get purge -y --auto-remove \
|
||||
autoconf \
|
||||
automake \
|
||||
g++ \
|
||||
gcc \
|
||||
libtool \
|
||||
make \
|
||||
nasm \
|
||||
libpng-dev \
|
||||
python3 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
RUN npm install && \
|
||||
rm docker_healthcheck.ts && \
|
||||
npm install && \
|
||||
npm run webpack && \
|
||||
npm prune --omit=dev
|
||||
RUN cp src/public/app/share.js src/public/app-dist/. && \
|
||||
npm prune --omit=dev && \
|
||||
npm cache clean --force && \
|
||||
cp src/public/app/share.js src/public/app-dist/. && \
|
||||
cp -r src/public/app/doc_notes src/public/app-dist/. && \
|
||||
rm -rf src/public/app && rm src/services/asset_path.ts
|
||||
rm -rf src/public/app && \
|
||||
rm src/services/asset_path.ts
|
||||
|
||||
# Some setup tools need to be kept
|
||||
# Runtime stage
|
||||
FROM node:20.15.1-bullseye-slim
|
||||
|
||||
# Install only runtime dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
gosu \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
&& rm -rf /var/lib/apt/lists/* && \
|
||||
rm -rf /var/cache/apt/*
|
||||
|
||||
# Start the application
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy only necessary files from builder
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=builder /usr/src/app/src ./src
|
||||
COPY --from=builder /usr/src/app/db ./db
|
||||
COPY --from=builder /usr/src/app/docker_healthcheck.js .
|
||||
COPY --from=builder /usr/src/app/start-docker.sh .
|
||||
COPY --from=builder /usr/src/app/package.json .
|
||||
COPY --from=builder /usr/src/app/config-sample.ini .
|
||||
COPY --from=builder /usr/src/app/images ./images
|
||||
COPY --from=builder /usr/src/app/translations ./translations
|
||||
COPY --from=builder /usr/src/app/libraries ./libraries
|
||||
|
||||
# Configure container
|
||||
EXPOSE 8080
|
||||
CMD [ "./start-docker.sh" ]
|
||||
|
||||
HEALTHCHECK --start-period=10s CMD exec gosu node node docker_healthcheck.js
|
||||
@@ -1,7 +1,7 @@
|
||||
# !!! Don't try to build this Dockerfile directly, run it through bin/build-docker.sh script !!!
|
||||
FROM node:20.15.1-alpine
|
||||
# Build stage
|
||||
FROM node:20.15.1-alpine AS builder
|
||||
|
||||
# Configure system dependencies
|
||||
# Configure build dependencies
|
||||
RUN apk add --no-cache --virtual .build-dependencies \
|
||||
autoconf \
|
||||
automake \
|
||||
@@ -11,43 +11,52 @@ RUN apk add --no-cache --virtual .build-dependencies \
|
||||
make \
|
||||
nasm \
|
||||
libpng-dev \
|
||||
python3
|
||||
python3
|
||||
|
||||
# Create app directory
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Bundle app source
|
||||
# Copy only necessary files for build
|
||||
COPY . .
|
||||
|
||||
COPY server-package.json package.json
|
||||
|
||||
# Copy TypeScript build artifacts into the original directory structure.
|
||||
# Copy the healthcheck
|
||||
# Build and cleanup in a single layer
|
||||
RUN cp -R build/src/* src/. && \
|
||||
cp build/docker_healthcheck.js . && \
|
||||
rm -r build && \
|
||||
rm docker_healthcheck.ts
|
||||
|
||||
# Install app dependencies
|
||||
RUN set -x && \
|
||||
rm docker_healthcheck.ts && \
|
||||
npm install && \
|
||||
apk del .build-dependencies && \
|
||||
npm run webpack && \
|
||||
npm prune --omit=dev && \
|
||||
npm cache clean --force && \
|
||||
cp src/public/app/share.js src/public/app-dist/. && \
|
||||
cp -r src/public/app/doc_notes src/public/app-dist/. && \
|
||||
rm -rf src/public/app && \
|
||||
rm src/services/asset_path.ts
|
||||
|
||||
# Runtime stage
|
||||
FROM node:20.15.1-alpine
|
||||
|
||||
# Some setup tools need to be kept
|
||||
# Install runtime dependencies
|
||||
RUN apk add --no-cache su-exec shadow
|
||||
|
||||
# Add application user and setup proper volume permissions
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Copy only necessary files from builder
|
||||
COPY --from=builder /usr/src/app/node_modules ./node_modules
|
||||
COPY --from=builder /usr/src/app/src ./src
|
||||
COPY --from=builder /usr/src/app/db ./db
|
||||
COPY --from=builder /usr/src/app/docker_healthcheck.js .
|
||||
COPY --from=builder /usr/src/app/start-docker.sh .
|
||||
COPY --from=builder /usr/src/app/package.json .
|
||||
COPY --from=builder /usr/src/app/config-sample.ini .
|
||||
COPY --from=builder /usr/src/app/images ./images
|
||||
COPY --from=builder /usr/src/app/translations ./translations
|
||||
COPY --from=builder /usr/src/app/libraries ./libraries
|
||||
|
||||
# Add application user
|
||||
RUN adduser -s /bin/false node; exit 0
|
||||
|
||||
# Start the application
|
||||
# Configure container
|
||||
EXPOSE 8080
|
||||
CMD [ "./start-docker.sh" ]
|
||||
|
||||
HEALTHCHECK --start-period=10s CMD exec su-exec node node docker_healthcheck.js
|
||||
HEALTHCHECK --start-period=10s CMD exec su-exec node node docker_healthcheck.js
|
||||
14
README.md
14
README.md
@@ -1,5 +1,7 @@
|
||||
# TriliumNext Notes
|
||||
|
||||
 
|
||||
|
||||
[English](./README.md) | [Chinese](./README-ZH_CN.md) | [Russian](./README.ru.md) | [Japanese](./README.ja.md) | [Italian](./README.it.md) | [Spanish](./README.es.md)
|
||||
|
||||
TriliumNext Notes is an open-source, cross-platform hierarchical note taking application with focus on building large personal knowledge bases.
|
||||
@@ -16,6 +18,8 @@ See [screenshots](https://triliumnext.github.io/Docs/Wiki/screenshot-tour) for q
|
||||
|
||||
There are no special migration steps to migrate from a zadam/Trilium instance to a TriliumNext/Notes instance. Just upgrade your Trilium instance to the latest version and [install TriliumNext/Notes as usual](#-installation)
|
||||
|
||||
Versions up to and including [v0.90.4](https://github.com/TriliumNext/Notes/releases/tag/v0.90.4) are compatible with the latest zadam/trilium version of [v0.63.7](https://github.com/zadam/trilium/releases/tag/v0.63.7). Any later versions of TriliumNext have their sync versions incremented.
|
||||
|
||||
## 💬 Discuss with us
|
||||
|
||||
Feel free to join our official conversations. We would love to hear what features, suggestions, or issues you may have!
|
||||
@@ -63,6 +67,16 @@ To use TriliumNext on your desktop machine (Linux, MacOS, and Windows) you have
|
||||
* Currently only the latest versions of Chrome & Firefox are supported (and tested).
|
||||
* (Coming Soon) TriliumNext will also be provided as a Flatpak
|
||||
|
||||
#### MacOS
|
||||
Currently when running TriliumNext/Notes on MacOS, you may get the following error:
|
||||
> Apple could not verify "TriliumNext Notes" is free of malware and may harm your Mac or compromise your privacy.
|
||||
|
||||
You will need to run the command on your shell to resolve the error (documented [here](https://github.com/TriliumNext/Notes/issues/329#issuecomment-2287164137)):
|
||||
|
||||
```bash
|
||||
xattr -c "/path/to/Trilium Next.app"
|
||||
```
|
||||
|
||||
### Mobile
|
||||
|
||||
To use TriliumNext on a mobile device:
|
||||
|
||||
@@ -24,11 +24,12 @@ rm -r $PKG_DIR/node/include/node
|
||||
rm -r $PKG_DIR/node_modules/electron*
|
||||
rm -r $PKG_DIR/electron*.js
|
||||
|
||||
printf "#!/bin/sh\n./node/bin/node src/www" > $PKG_DIR/trilium.sh
|
||||
printf "#!/bin/sh\n./node/bin/node src/main" > $PKG_DIR/trilium.sh
|
||||
chmod 755 $PKG_DIR/trilium.sh
|
||||
|
||||
cp bin/tpl/anonymize-database.sql $PKG_DIR/
|
||||
|
||||
cp -r translations $PKG_DIR/
|
||||
cp -r dump-db $PKG_DIR/
|
||||
rm -rf $PKG_DIR/dump-db/node_modules
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const copy = async () => {
|
||||
await fs.copy(file, path.join(DEST_DIR, file));
|
||||
}
|
||||
|
||||
const dirsToCopy = ["images", "libraries", "db"];
|
||||
const dirsToCopy = ["images", "libraries", "translations", "db"];
|
||||
for (const dir of dirsToCopy) {
|
||||
log(`Copying ${dir}`);
|
||||
await fs.copy(dir, path.join(DEST_DIR, dir));
|
||||
@@ -47,6 +47,15 @@ const copy = async () => {
|
||||
await fs.copy(dir, path.join(DEST_DIR_SRC, path.basename(dir)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Directories to be copied relative to the project root into <resource_dir>/src/public/app-dist.
|
||||
*/
|
||||
const publicDirsToCopy = [ "./src/public/app/doc_notes" ];
|
||||
const PUBLIC_DIR = path.join(DEST_DIR, "src", "public", "app-dist");
|
||||
for (const dir of publicDirsToCopy) {
|
||||
await fs.copy(dir, path.join(PUBLIC_DIR, path.basename(dir)));
|
||||
}
|
||||
|
||||
const nodeModulesFile = [
|
||||
"node_modules/react/umd/react.production.min.js",
|
||||
"node_modules/react/umd/react.development.js",
|
||||
@@ -55,6 +64,7 @@ const copy = async () => {
|
||||
"node_modules/katex/dist/katex.min.js",
|
||||
"node_modules/katex/dist/contrib/mhchem.min.js",
|
||||
"node_modules/katex/dist/contrib/auto-render.min.js",
|
||||
"node_modules/@highlightjs/cdn-assets/highlight.min.js"
|
||||
];
|
||||
|
||||
for (const file of nodeModulesFile) {
|
||||
@@ -89,7 +99,9 @@ const copy = async () => {
|
||||
"node_modules/codemirror/addon/",
|
||||
"node_modules/codemirror/mode/",
|
||||
"node_modules/codemirror/keymap/",
|
||||
"node_modules/mind-elixir/dist/"
|
||||
"node_modules/mind-elixir/dist/",
|
||||
"node_modules/@highlightjs/cdn-assets/languages",
|
||||
"node_modules/@highlightjs/cdn-assets/styles"
|
||||
];
|
||||
|
||||
for (const folder of nodeModulesFolder) {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"src": "dist/trilium-linux-x64",
|
||||
"dest": "dist/",
|
||||
"compression": "xz",
|
||||
"name": "trilium",
|
||||
"productName": "Trilium Notes",
|
||||
"genericName": "Note taker",
|
||||
"description": "Trilium Notes is a hierarchical note taking application with focus on building large personal knowledge bases.",
|
||||
"sections": "misc",
|
||||
"maintainer": "zadam.apps@gmail.com",
|
||||
"homepage": "https://github.com/zadam/trilium",
|
||||
"bin": "trilium",
|
||||
"icon": "dist/trilium-linux-x64/icon.png",
|
||||
"categories": [ "Office" ]
|
||||
}
|
||||
12
bin/electron-forge/desktop.ejs
Normal file
12
bin/electron-forge/desktop.ejs
Normal file
@@ -0,0 +1,12 @@
|
||||
[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(';') %>;
|
||||
<% } %>
|
||||
98
bin/translation.sh
Executable file
98
bin/translation.sh
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
#
|
||||
# Create PO files to make easier the labor of translation.
|
||||
#
|
||||
# Info:
|
||||
# https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
|
||||
# https://docs.translatehouse.org/projects/translate-toolkit/en/latest/commands/json2po.html
|
||||
#
|
||||
# Dependencies:
|
||||
# jq
|
||||
# translate-toolkit
|
||||
# python-wcwidth
|
||||
#
|
||||
# Created by @hasecilu
|
||||
#
|
||||
# --------------------------------------------------------------------------------------------------
|
||||
|
||||
stats() {
|
||||
# Print the number of existing strings on the JSON files for each locale
|
||||
s=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[0]}/en/server.json" | wc -l)
|
||||
c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/en/translation.json" | wc -l)
|
||||
echo "|locale |server strings |client strings |"
|
||||
echo "|-------|---------------|---------------|"
|
||||
echo "| en | ${s} | ${c} |"
|
||||
for locale in "${locales[@]}"; do
|
||||
s=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[0]}/${locale}/server.json" | wc -l)
|
||||
c=$(jq 'path(..) | select(length == 2) | .[1]' "${paths[1]}/${locale}/translation.json" | wc -l)
|
||||
echo "| ${locale} | ${s} | ${c} |"
|
||||
done
|
||||
}
|
||||
|
||||
help() {
|
||||
echo -e "\nDescription:"
|
||||
echo -e "\tCreate PO files to make easier the labor of translation"
|
||||
echo -e "\nUsage:"
|
||||
echo -e "\t./translation.sh [--stats] [--update <OPT_LOCALE>] [--update2 <OPT_LOCALE>]"
|
||||
echo -e "\nFlags:"
|
||||
echo -e " --clear\n\tClear all po-* directories"
|
||||
echo -e " --stats\n\tPrint the number of existing strings on the JSON files for each locale"
|
||||
echo -e " --update <LOCALE>\n\tUpdate PO files from English and localized JSON files as source"
|
||||
echo -e " --update2 <LOCALE>\n\tRecover translation from PO files to localized JSON files"
|
||||
}
|
||||
|
||||
# Main function ------------------------------------------------------------------------------------
|
||||
|
||||
# Get script directory to set file path relative to it
|
||||
file_path="$(
|
||||
cd -- "$(dirname "${0}")" >/dev/null 2>&1 || exit
|
||||
pwd -P
|
||||
)"
|
||||
paths=("${file_path}/../translations/" "${file_path}/../src/public/translations/")
|
||||
locales=(cn es fr ro)
|
||||
|
||||
if [ $# -eq 1 ]; then
|
||||
if [ "$1" == "--clear" ]; then
|
||||
for path in "${paths[@]}"; do
|
||||
for locale in "${locales[@]}"; do
|
||||
[ -d "${path}/po-${locale}" ] && rm -r "${path}/po-${locale}"
|
||||
done
|
||||
done
|
||||
elif [ "$1" == "--stats" ]; then
|
||||
stats
|
||||
elif [ "$1" == "--update" ]; then
|
||||
# Update PO files from English and localized JSON files as source
|
||||
for path in "${paths[@]}"; do
|
||||
for locale in "${locales[@]}"; do
|
||||
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
|
||||
done
|
||||
done
|
||||
elif [ "$1" == "--update2" ]; then
|
||||
# Recover translation from PO files to localized JSON files
|
||||
for path in "${paths[@]}"; do
|
||||
for locale in "${locales[@]}"; do
|
||||
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
|
||||
done
|
||||
done
|
||||
else
|
||||
help
|
||||
fi
|
||||
elif [ $# -eq 2 ]; then
|
||||
if [ "$1" == "--update" ]; then
|
||||
locale="$2"
|
||||
for path in "${paths[@]}"; do
|
||||
json2po -t "${path}/en" "${path}/${locale}" "${path}/po-${locale}"
|
||||
done
|
||||
elif [ "$1" == "--update2" ]; then
|
||||
locale="$2"
|
||||
for path in "${paths[@]}"; do
|
||||
po2json -t "${path}/en" "${path}/po-${locale}" "${path}/${locale}"
|
||||
done
|
||||
else
|
||||
help
|
||||
fi
|
||||
else
|
||||
help
|
||||
fi
|
||||
50
bin/update-nightly-version.ts
Normal file
50
bin/update-nightly-version.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* @module
|
||||
*
|
||||
* The nightly version works uses the version described in `package.json`, just like any release.
|
||||
* The problem with this approach is that production builds have a very aggressive cache, and
|
||||
* usually running the nightly with this cached version of the application will mean that the
|
||||
* user might run into module not found errors or styling errors caused by an old cache.
|
||||
*
|
||||
* This script is supposed to be run in the CI, which will update locally the version field of
|
||||
* `package.json` to contain the date. For example, `0.90.9-beta` will become `0.90.9-test-YYMMDD-HHMMSS`.
|
||||
*
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname, join } from "path";
|
||||
import fs from "fs";
|
||||
|
||||
function processVersion(version) {
|
||||
// Remove the beta suffix if any.
|
||||
version = version.replace("-beta", "");
|
||||
|
||||
// Add the nightly suffix, plus the date.
|
||||
const referenceDate = new Date()
|
||||
.toISOString()
|
||||
.substring(2, 19)
|
||||
.replace(/[-:]*/g, "")
|
||||
.replace("T", "-");
|
||||
version = `${version}-test-${referenceDate}`;
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const packageJsonPath = join(scriptDir, "..", "package.json");
|
||||
|
||||
// Read the version from package.json and process it.
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
||||
const currentVersion = packageJson.version;
|
||||
const adjustedVersion = processVersion(currentVersion);
|
||||
console.log("Current version is", currentVersion);
|
||||
console.log("Adjusted version is", adjustedVersion);
|
||||
|
||||
// Write the adjusted version back in.
|
||||
packageJson.version = adjustedVersion;
|
||||
const formattedJson = JSON.stringify(packageJson, null, 4);
|
||||
fs.writeFileSync(packageJsonPath, formattedJson);
|
||||
}
|
||||
|
||||
main();
|
||||
BIN
db/demo.zip
BIN
db/demo.zip
Binary file not shown.
@@ -10,17 +10,31 @@ module.exports = {
|
||||
overwrite: true,
|
||||
asar: true,
|
||||
icon: "./images/app-icons/icon",
|
||||
extraResource: getExtraResourcesForPlatform(),
|
||||
extraResource: [
|
||||
// Moved to root
|
||||
...getExtraResourcesForPlatform(),
|
||||
|
||||
// Moved to resources (TriliumNext Notes.app/Contents/Resources on macOS)
|
||||
"translations/",
|
||||
"node_modules/@highlightjs/cdn-assets/styles"
|
||||
],
|
||||
afterComplete: [(buildPath, _electronVersion, platform, _arch, callback) => {
|
||||
const extraResources = getExtraResourcesForPlatform();
|
||||
for (const resource of extraResources) {
|
||||
const baseName = path.basename(resource);
|
||||
let sourcePath;
|
||||
if (platform === 'darwin') {
|
||||
sourcePath = path.join(buildPath, `${APP_NAME}.app`, 'Contents', 'Resources', path.basename(resource));
|
||||
sourcePath = path.join(buildPath, `${APP_NAME}.app`, 'Contents', 'Resources', baseName);
|
||||
} else {
|
||||
sourcePath = path.join(buildPath, 'resources', path.basename(resource));
|
||||
sourcePath = path.join(buildPath, 'resources', baseName);
|
||||
}
|
||||
let destPath;
|
||||
|
||||
if (baseName !== "256x256.png") {
|
||||
destPath = path.join(buildPath, baseName);
|
||||
} else {
|
||||
destPath = path.join(buildPath, "icon.png");
|
||||
}
|
||||
const destPath = path.join(buildPath, path.basename(resource));
|
||||
|
||||
// Copy files from resources folder to root
|
||||
fs.move(sourcePath, destPath)
|
||||
@@ -38,6 +52,7 @@ module.exports = {
|
||||
config: {
|
||||
options: {
|
||||
icon: "./images/app-icons/png/128x128.png",
|
||||
desktopTemplate: path.resolve("./bin/electron-forge/desktop.ejs")
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -75,7 +90,10 @@ module.exports = {
|
||||
|
||||
|
||||
function getExtraResourcesForPlatform() {
|
||||
let resources = ['dump-db/', './bin/tpl/anonymize-database.sql']
|
||||
let resources = [
|
||||
'dump-db/',
|
||||
'./bin/tpl/anonymize-database.sql'
|
||||
];
|
||||
const scripts = ['trilium-portable', 'trilium-safe-mode', 'trilium-no-cert-check']
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
@@ -86,6 +104,7 @@ function getExtraResourcesForPlatform() {
|
||||
case 'darwin':
|
||||
break;
|
||||
case 'linux':
|
||||
resources.push("images/app-icons/png/256x256.png")
|
||||
for (const script of scripts) {
|
||||
resources.push(`./bin/tpl/${script}.sh`)
|
||||
}
|
||||
|
||||
@@ -526,16 +526,19 @@
|
||||
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
|
||||
.ck-content pre {
|
||||
padding: 1em;
|
||||
color: hsl(0, 0%, 20.8%);
|
||||
background: hsla(0, 0%, 78%, 0.3);
|
||||
border: 1px solid hsl(0, 0%, 77%);
|
||||
border-radius: 2px;
|
||||
text-align: left;
|
||||
direction: ltr;
|
||||
tab-size: 4;
|
||||
white-space: pre-wrap;
|
||||
font-style: normal;
|
||||
min-width: 200px;
|
||||
border: 0px;
|
||||
border-radius: 6px;
|
||||
box-shadow: 1px 1px 6px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.ck-content pre:not(.hljs) {
|
||||
color: hsl(0, 0%, 20.8%);
|
||||
background: hsla(0, 0%, 78%, 0.3);
|
||||
}
|
||||
/* @ckeditor/ckeditor5-code-block/theme/codeblock.css */
|
||||
.ck-content pre code {
|
||||
|
||||
49
libraries/ckeditor/ckeditor.d.ts
vendored
Normal file
49
libraries/ckeditor/ckeditor.d.ts
vendored
Normal file
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @license Copyright (c) 2003-2024, CKSource Holding sp. z o.o. All rights reserved.
|
||||
* For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
||||
*/
|
||||
import { DecoupledEditor as DecoupledEditorBase } from '@ckeditor/ckeditor5-editor-decoupled';
|
||||
import { Essentials } from '@ckeditor/ckeditor5-essentials';
|
||||
import { Alignment } from '@ckeditor/ckeditor5-alignment';
|
||||
import { FontSize, FontFamily, FontColor, FontBackgroundColor } from '@ckeditor/ckeditor5-font';
|
||||
import { CKFinderUploadAdapter } from '@ckeditor/ckeditor5-adapter-ckfinder';
|
||||
import { Autoformat } from '@ckeditor/ckeditor5-autoformat';
|
||||
import { Bold, Italic, Strikethrough, Underline } from '@ckeditor/ckeditor5-basic-styles';
|
||||
import { BlockQuote } from '@ckeditor/ckeditor5-block-quote';
|
||||
import { CKBox } from '@ckeditor/ckeditor5-ckbox';
|
||||
import { CKFinder } from '@ckeditor/ckeditor5-ckfinder';
|
||||
import { EasyImage } from '@ckeditor/ckeditor5-easy-image';
|
||||
import { Heading } from '@ckeditor/ckeditor5-heading';
|
||||
import { Image, ImageCaption, ImageResize, ImageStyle, ImageToolbar, ImageUpload, PictureEditing } from '@ckeditor/ckeditor5-image';
|
||||
import { Indent, IndentBlock } from '@ckeditor/ckeditor5-indent';
|
||||
import { Link } from '@ckeditor/ckeditor5-link';
|
||||
import { List, ListProperties } from '@ckeditor/ckeditor5-list';
|
||||
import { MediaEmbed } from '@ckeditor/ckeditor5-media-embed';
|
||||
import { Paragraph } from '@ckeditor/ckeditor5-paragraph';
|
||||
import { PasteFromOffice } from '@ckeditor/ckeditor5-paste-from-office';
|
||||
import { Table, TableToolbar } from '@ckeditor/ckeditor5-table';
|
||||
import { TextTransformation } from '@ckeditor/ckeditor5-typing';
|
||||
import { CloudServices } from '@ckeditor/ckeditor5-cloud-services';
|
||||
export default class DecoupledEditor extends DecoupledEditorBase {
|
||||
static builtinPlugins: (typeof TextTransformation | typeof Essentials | typeof Alignment | typeof FontBackgroundColor | typeof FontColor | typeof FontFamily | typeof FontSize | typeof CKFinderUploadAdapter | typeof Paragraph | typeof Heading | typeof Autoformat | typeof Bold | typeof Italic | typeof Strikethrough | typeof Underline | typeof BlockQuote | typeof Image | typeof ImageCaption | typeof ImageResize | typeof ImageStyle | typeof ImageToolbar | typeof ImageUpload | typeof CloudServices | typeof CKBox | typeof CKFinder | typeof EasyImage | typeof List | typeof ListProperties | typeof Indent | typeof IndentBlock | typeof Link | typeof MediaEmbed | typeof PasteFromOffice | typeof Table | typeof TableToolbar | typeof PictureEditing)[];
|
||||
static defaultConfig: {
|
||||
toolbar: {
|
||||
items: string[];
|
||||
};
|
||||
image: {
|
||||
resizeUnit: "px";
|
||||
toolbar: string[];
|
||||
};
|
||||
table: {
|
||||
contentToolbar: string[];
|
||||
};
|
||||
list: {
|
||||
properties: {
|
||||
styles: boolean;
|
||||
startIndex: boolean;
|
||||
reversed: boolean;
|
||||
};
|
||||
};
|
||||
language: string;
|
||||
};
|
||||
}
|
||||
2
libraries/ckeditor/ckeditor.js
vendored
2
libraries/ckeditor/ckeditor.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
1072
package-lock.json
generated
1072
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
64
package.json
64
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "TriliumNext Notes",
|
||||
"description": "Build your personal knowledge base with TriliumNext Notes",
|
||||
"version": "0.90.6-beta",
|
||||
"version": "0.90.11-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "./dist/electron-main.js",
|
||||
"author": {
|
||||
@@ -46,12 +46,14 @@
|
||||
"integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000"
|
||||
"generate-document": "cross-env nodemon src/tools/generate_document.ts 1000",
|
||||
"ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
"@electron/remote": "2.1.2",
|
||||
"@excalidraw/excalidraw": "0.17.6",
|
||||
"@highlightjs/cdn-assets": "11.10.0",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"autocomplete.js": "0.38.1",
|
||||
@@ -61,34 +63,34 @@
|
||||
"boxicons": "2.1.4",
|
||||
"chokidar": "3.6.0",
|
||||
"cls-hooked": "4.2.2",
|
||||
"codemirror": "5.65.17",
|
||||
"codemirror": "5.65.18",
|
||||
"compression": "1.7.4",
|
||||
"cookie-parser": "1.4.6",
|
||||
"cookie-parser": "1.4.7",
|
||||
"csurf": "1.11.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dayjs-plugin-utc": "0.1.2",
|
||||
"debounce": "2.1.1",
|
||||
"debounce": "2.2.0",
|
||||
"ejs": "3.1.10",
|
||||
"electron-debug": "4.0.1",
|
||||
"electron-dl": "4.0.0",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
"electron-window-state": "5.0.3",
|
||||
"escape-html": "1.0.3",
|
||||
"eslint": "9.10.0",
|
||||
"express": "4.21.0",
|
||||
"eslint": "9.14.0",
|
||||
"express": "4.21.1",
|
||||
"express-partial-content": "1.0.2",
|
||||
"express-rate-limit": "7.4.0",
|
||||
"express-session": "1.18.0",
|
||||
"force-graph": "1.43.5",
|
||||
"express-rate-limit": "7.4.1",
|
||||
"express-session": "1.18.1",
|
||||
"force-graph": "1.46.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"helmet": "7.1.0",
|
||||
"html": "1.0.0",
|
||||
"html2plaintext": "2.1.4",
|
||||
"http-proxy-agent": "7.0.2",
|
||||
"https-proxy-agent": "7.0.5",
|
||||
"i18next": "23.15.1",
|
||||
"i18next": "23.16.4",
|
||||
"i18next-fs-backend": "2.3.2",
|
||||
"i18next-http-backend": "2.6.1",
|
||||
"i18next-http-backend": "2.6.2",
|
||||
"image-type": "4.1.0",
|
||||
"ini": "5.0.0",
|
||||
"is-animated": "2.0.2",
|
||||
@@ -103,10 +105,10 @@
|
||||
"katex": "0.16.11",
|
||||
"knockout": "3.5.1",
|
||||
"mark.js": "8.11.1",
|
||||
"marked": "14.1.2",
|
||||
"mermaid": "11.2.0",
|
||||
"marked": "14.1.3",
|
||||
"mermaid": "11.4.0",
|
||||
"mime-types": "2.1.35",
|
||||
"mind-elixir": "4.1.1",
|
||||
"mind-elixir": "4.3.1",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-abi": "3.67.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
@@ -119,7 +121,7 @@
|
||||
"request": "2.88.2",
|
||||
"safe-compare": "1.1.4",
|
||||
"sanitize-filename": "1.6.3",
|
||||
"sanitize-html": "2.13.0",
|
||||
"sanitize-html": "2.13.1",
|
||||
"sax": "1.4.1",
|
||||
"semver": "7.6.3",
|
||||
"serve-favicon": "2.5.0",
|
||||
@@ -132,19 +134,19 @@
|
||||
"tree-kill": "1.2.2",
|
||||
"turndown": "7.2.0",
|
||||
"unescape": "1.0.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.2",
|
||||
"vanilla-js-wheel-zoom": "9.0.4",
|
||||
"ws": "8.18.0",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "7.4.0",
|
||||
"@electron-forge/maker-deb": "7.4.0",
|
||||
"@electron-forge/maker-dmg": "7.4.0",
|
||||
"@electron-forge/maker-squirrel": "7.4.0",
|
||||
"@electron-forge/maker-zip": "7.4.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.4.0",
|
||||
"@playwright/test": "1.47.1",
|
||||
"@electron-forge/cli": "7.5.0",
|
||||
"@electron-forge/maker-deb": "7.5.0",
|
||||
"@electron-forge/maker-dmg": "7.5.0",
|
||||
"@electron-forge/maker-squirrel": "7.5.0",
|
||||
"@electron-forge/maker-zip": "7.5.0",
|
||||
"@electron-forge/plugin-auto-unpack-natives": "7.5.0",
|
||||
"@playwright/test": "1.48.2",
|
||||
"@types/archiver": "6.0.2",
|
||||
"@types/better-sqlite3": "7.6.11",
|
||||
"@types/cls-hooked": "4.3.8",
|
||||
@@ -163,7 +165,7 @@
|
||||
"@types/jsdom": "21.1.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
"@types/node": "22.5.4",
|
||||
"@types/node": "22.7.8",
|
||||
"@types/safe-compare": "1.1.2",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
"@types/sax": "1.2.7",
|
||||
@@ -182,17 +184,17 @@
|
||||
"electron-rebuild": "3.2.9",
|
||||
"esm": "3.2.25",
|
||||
"iconsur": "1.7.0",
|
||||
"jasmine": "5.3.0",
|
||||
"jasmine": "5.4.0",
|
||||
"jsdoc": "4.0.3",
|
||||
"lorem-ipsum": "2.0.8",
|
||||
"nodemon": "3.1.4",
|
||||
"nodemon": "3.1.7",
|
||||
"rcedit": "4.0.1",
|
||||
"rimraf": "6.0.1",
|
||||
"ts-node": "10.9.2",
|
||||
"tslib": "2.7.0",
|
||||
"tsx": "4.19.1",
|
||||
"typescript": "5.6.2",
|
||||
"webpack": "5.94.0",
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.19.2",
|
||||
"typescript": "5.6.3",
|
||||
"webpack": "5.96.1",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ app.use(cookieParser());
|
||||
app.use(express.static(path.join(scriptDir, 'public/root')));
|
||||
app.use(`/manifest.webmanifest`, express.static(path.join(scriptDir, 'public/manifest.webmanifest')));
|
||||
app.use(`/robots.txt`, express.static(path.join(scriptDir, 'public/robots.txt')));
|
||||
app.use(`/icon.png`, express.static(path.join(scriptDir, 'public/icon.png')));
|
||||
app.use(sessionParser);
|
||||
app.use(favicon(`${scriptDir}/../images/app-icons/icon.ico`));
|
||||
|
||||
|
||||
@@ -38,9 +38,18 @@ export interface RecentNoteRow {
|
||||
utcDateCreated?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database representation of an option.
|
||||
*
|
||||
* Options are key-value pairs that are used to store information such as user preferences (for example
|
||||
* the current theme, sync server information), but also information about the state of the application).
|
||||
*/
|
||||
export interface OptionRow {
|
||||
/** The name of the option. */
|
||||
name: string;
|
||||
/** The value of the option. */
|
||||
value: string;
|
||||
/** `true` if the value should be synced across multiple instances (e.g. locale) or `false` if it should be local-only (e.g. theme). */
|
||||
isSynced: boolean;
|
||||
utcDateModified?: string;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import MobileScreenSwitcherExecutor from "./mobile_screen_switcher.js";
|
||||
import MainTreeExecutors from "./main_tree_executors.js";
|
||||
import toast from "../services/toast.js";
|
||||
import ShortcutComponent from "./shortcut_component.js";
|
||||
import { initLocale } from "../services/i18n.js";
|
||||
import { t, initLocale } from "../services/i18n.js";
|
||||
|
||||
class AppContext extends Component {
|
||||
constructor(isMainWindow) {
|
||||
@@ -33,11 +33,11 @@ class AppContext extends Component {
|
||||
await initLocale();
|
||||
}
|
||||
|
||||
setLayout(layout) {
|
||||
setLayout(layout) {
|
||||
this.layout = layout;
|
||||
}
|
||||
|
||||
async start() {
|
||||
async start() {
|
||||
this.initComponents();
|
||||
this.renderWidgets();
|
||||
|
||||
@@ -151,7 +151,7 @@ $(window).on('beforeunload', () => {
|
||||
if (!component.beforeUnloadEvent()) {
|
||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||
|
||||
toast.showMessage("Please wait for a couple of seconds for the save to finish, then you can try again.", 10000);
|
||||
toast.showMessage(t("app_context.please_wait_for_save"), 10000);
|
||||
|
||||
allSaved = false;
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import ws from "../services/ws.js";
|
||||
import bundleService from "../services/bundle.js";
|
||||
import froca from "../services/froca.js";
|
||||
import linkService from "../services/link.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
|
||||
export default class Entrypoints extends Component {
|
||||
constructor() {
|
||||
@@ -172,13 +173,13 @@ export default class Entrypoints extends Component {
|
||||
const resp = await server.post(`sql/execute/${note.noteId}`);
|
||||
|
||||
if (!resp.success) {
|
||||
toastService.showError(`Error occurred while executing SQL query: ${resp.error}`);
|
||||
toastService.showError(t("entrypoints.sql-error", { message: resp.error }));
|
||||
}
|
||||
|
||||
await appContext.triggerEvent('sqlQueryResults', {ntxId: ntxId, results: resp.results});
|
||||
}
|
||||
|
||||
toastService.showMessage("Note executed");
|
||||
toastService.showMessage(t("entrypoints.note-executed"));
|
||||
}
|
||||
|
||||
hideAllPopups() {
|
||||
@@ -200,6 +201,6 @@ export default class Entrypoints extends Component {
|
||||
|
||||
await server.post(`notes/${noteId}/revision`);
|
||||
|
||||
toastService.showMessage("Note revision has been created.");
|
||||
toastService.showMessage(t("entrypoints.note-revision-created"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -551,7 +551,7 @@ export default class TabManager extends Component {
|
||||
await this.removeNoteContext(ntxIdToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async closeOtherTabsCommand({ntxId}) {
|
||||
for (const ntxIdToRemove of this.mainNoteContexts.map(nc => nc.ntxId)) {
|
||||
if (ntxIdToRemove !== ntxId) {
|
||||
@@ -560,6 +560,18 @@ export default class TabManager extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
async closeRightTabsCommand({ntxId}) {
|
||||
const ntxIds = this.mainNoteContexts.map(nc => nc.ntxId);
|
||||
const index = ntxIds.indexOf(ntxId);
|
||||
|
||||
if (index !== -1) {
|
||||
const idsToRemove = ntxIds.slice(index + 1);
|
||||
for (const ntxIdToRemove of idsToRemove) {
|
||||
await this.removeNoteContext(ntxIdToRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async closeTabCommand({ntxId}) {
|
||||
await this.removeNoteContext(ntxId);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ class ZoomComponent extends Component {
|
||||
|
||||
window.addEventListener("wheel", event => {
|
||||
if (event.ctrlKey) {
|
||||
this.setZoomFactorAndSave(this.getCurrentZoom() + event.deltaY * 0.001);
|
||||
this.setZoomFactorAndSave(this.getCurrentZoom() - event.deltaY * 0.001);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -56,7 +56,7 @@ class ZoomComponent extends Component {
|
||||
zoomResetEvent() {
|
||||
this.setZoomFactorAndSave(1);
|
||||
}
|
||||
|
||||
|
||||
setZoomFactorAndSaveEvent({zoomFactor}) {
|
||||
this.setZoomFactorAndSave(zoomFactor);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import electronContextMenu from "./menus/electron_context_menu.js";
|
||||
import glob from "./services/glob.js";
|
||||
import { t } from "./services/i18n.js";
|
||||
|
||||
await appContext.earlyInit();
|
||||
|
||||
bundleService.getWidgetBundlesByParent().then(async widgetBundles => {
|
||||
await appContext.earlyInit();
|
||||
|
||||
// A dynamic import is required for layouts since they initialize components which require translations.
|
||||
const DesktopLayout = (await import("./layouts/desktop_layout.js")).default;
|
||||
|
||||
|
||||
@@ -82,6 +82,7 @@ import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import CopyImageReferenceButton from "../widgets/floating_buttons/copy_image_reference_button.js";
|
||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
|
||||
export default class DesktopLayout {
|
||||
constructor(customWidgets) {
|
||||
@@ -140,6 +141,7 @@ export default class DesktopLayout {
|
||||
// the order of the widgets matter. Some of these want to "activate" themselves
|
||||
// when visible. When this happens to multiple of them, the first one "wins".
|
||||
// promoted attributes should always win.
|
||||
.ribbon(new ClassicEditorToolbar())
|
||||
.ribbon(new PromotedAttributesWidget())
|
||||
.ribbon(new ScriptExecutorWidget())
|
||||
.ribbon(new SearchDefinitionWidget())
|
||||
|
||||
@@ -23,6 +23,7 @@ import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||
import RootContainer from "../widgets/containers/root_container.js";
|
||||
import SharedInfoWidget from "../widgets/shared_info.js";
|
||||
import PromotedAttributesWidget from "../widgets/ribbon_widgets/promoted_attributes.js";
|
||||
import ClassicEditorToolbar from "../widgets/ribbon_widgets/classic_editor_toolbar.js";
|
||||
|
||||
const MOBILE_CSS = `
|
||||
<style>
|
||||
@@ -167,6 +168,7 @@ export default class MobileLayout {
|
||||
.child(new NoteListWidget())
|
||||
.child(new FilePropertiesWidget().css('font-size','smaller'))
|
||||
)
|
||||
.child(new ClassicEditorToolbar())
|
||||
)
|
||||
.child(new ProtectedSessionPasswordDialog())
|
||||
.child(new ConfirmDialog())
|
||||
|
||||
@@ -3,6 +3,7 @@ import froca from "../services/froca.js";
|
||||
import contextMenu from "./context_menu.js";
|
||||
import dialogService from "../services/dialog.js";
|
||||
import server from "../services/server.js";
|
||||
import { t } from '../services/i18n.js';
|
||||
|
||||
export default class LauncherContextMenu {
|
||||
/**
|
||||
@@ -33,29 +34,27 @@ export default class LauncherContextMenu {
|
||||
const isAvailableItem = parentNoteId === '_lbAvailableLaunchers';
|
||||
const isItem = isVisibleItem || isAvailableItem;
|
||||
const canBeDeleted = !note.noteId.startsWith("_"); // fixed notes can't be deleted
|
||||
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();;
|
||||
const canBeReset = !canBeDeleted && note.isLaunchBarConfig();
|
||||
|
||||
return [
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a note launcher', command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a script launcher', command: 'addScriptLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: 'Add a custom widget', command: 'addWidgetLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: 'Add spacer', command: 'addSpacerLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-note-launcher"), command: 'addNoteLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-script-launcher"), command: 'addScriptLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-custom-widget"), command: 'addWidgetLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: t("launcher_context_menu.add-spacer"), command: 'addSpacerLauncher', uiIcon: "bx bx-plus" } : null,
|
||||
(isVisibleRoot || isAvailableRoot) ? { title: "----" } : null,
|
||||
{ title: 'Delete <kbd data-command="deleteNotes"></kbd>', command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted },
|
||||
{ title: 'Reset', command: "resetLauncher", uiIcon: "bx bx-empty", enabled: canBeReset},
|
||||
{ title: `${t("launcher_context_menu.delete")} <kbd data-command="deleteNotes"></kbd>`, command: "deleteNotes", uiIcon: "bx bx-trash", enabled: canBeDeleted },
|
||||
{ title: t("launcher_context_menu.reset"), command: "resetLauncher", uiIcon: "bx bx-empty", enabled: canBeReset},
|
||||
{ title: "----" },
|
||||
isAvailableItem ? { title: 'Move to visible launchers', command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
|
||||
isVisibleItem ? { title: 'Move to available launchers', command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
|
||||
{ title: `Duplicate launcher <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
|
||||
isAvailableItem ? { title: t("launcher_context_menu.move-to-visible-launchers"), command: "moveLauncherToVisible", uiIcon: "bx bx-show", enabled: true } : null,
|
||||
isVisibleItem ? { title: t("launcher_context_menu.move-to-available-launchers"), command: "moveLauncherToAvailable", uiIcon: "bx bx-hide", enabled: true } : null,
|
||||
{ title: `${t("launcher_context_menu.duplicate-launcher")} <kbd data-command="duplicateSubtree">`, command: "duplicateSubtree", uiIcon: "bx bx-empty",
|
||||
enabled: isItem }
|
||||
].filter(row => row !== null);
|
||||
}
|
||||
|
||||
async selectMenuItemHandler({command}) {
|
||||
if (command === 'resetLauncher') {
|
||||
const confirmed = await dialogService.confirm(`Do you really want to reset "${this.node.title}"?
|
||||
All data / settings in this note (and its children) will be lost
|
||||
and the launcher will be returned to its original location.`);
|
||||
const confirmed = await dialogService.confirm(t("launcher_context_menu.reset_launcher_confirm", { title: this.node.title }));
|
||||
|
||||
if (confirmed) {
|
||||
await server.post(`special-notes/launchers/${this.node.data.noteId}/reset`);
|
||||
|
||||
@@ -62,8 +62,8 @@ export default class TreeContextMenu {
|
||||
{ title: "----" },
|
||||
{ title: `${t("tree-context-menu.search-in-subtree")} <kbd data-command="searchInSubtree"></kbd>`, command: "searchInSubtree", uiIcon: "bx bx-search",
|
||||
enabled: notSearch && noSelectedNotes },
|
||||
isHoisted ? null : { title: 'Hoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
|
||||
!isHoisted || !isNotRoot ? null : { title: 'Unhoist note <kbd data-command="toggleNoteHoisting"></kbd>', command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
isHoisted ? null : { title: `${t("tree-context-menu.hoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-empty", enabled: noSelectedNotes && notSearch },
|
||||
!isHoisted || !isNotRoot ? null : { title: `${t("tree-context-menu.unhoist-note")} <kbd data-command="toggleNoteHoisting"></kbd>`, command: "toggleNoteHoisting", uiIcon: "bx bx-door-open" },
|
||||
{ title: `${t("tree-context-menu.edit-branch-prefix")} <kbd data-command="editBranchPrefix"></kbd>`, command: "editBranchPrefix", uiIcon: "bx bx-empty",
|
||||
enabled: isNotRoot && parentNotSearch && noSelectedNotes && notOptions },
|
||||
{ title: t("tree-context-menu.advanced"), uiIcon: "bx bx-empty", enabled: true, items: [
|
||||
@@ -136,7 +136,7 @@ export default class TreeContextMenu {
|
||||
this.treeWidget.triggerCommand("openNewNoteSplit", {ntxId, notePath});
|
||||
}
|
||||
else if (command === 'convertNoteToAttachment') {
|
||||
if (!await dialogService.confirm(`Are you sure you want to convert note selected notes into attachments of their parent notes?`)) {
|
||||
if (!await dialogService.confirm(t("tree-context-menu.convert-to-attachment-confirm"))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -154,7 +154,7 @@ export default class TreeContextMenu {
|
||||
}
|
||||
}
|
||||
|
||||
toastService.showMessage(`${converted} notes have been converted to attachments.`);
|
||||
toastService.showMessage(t("tree-context-menu.converted-to-attachments", { count: converted }));
|
||||
}
|
||||
else if (command === 'copyNotePathToClipboard') {
|
||||
navigator.clipboard.writeText('#' + notePath);
|
||||
|
||||
@@ -5,6 +5,7 @@ import froca from "./froca.js";
|
||||
import hoistedNoteService from "./hoisted_note.js";
|
||||
import ws from "./ws.js";
|
||||
import appContext from "../components/app_context.js";
|
||||
import { t } from './i18n.js';
|
||||
|
||||
async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
branchIdsToMove = filterRootNote(branchIdsToMove);
|
||||
@@ -13,7 +14,7 @@ async function moveBeforeBranch(branchIdsToMove, beforeBranchId) {
|
||||
const beforeBranch = froca.getBranch(beforeBranchId);
|
||||
|
||||
if (['root', '_lbRoot', '_lbAvailableLaunchers', '_lbVisibleLaunchers'].includes(beforeBranch.noteId)) {
|
||||
toastService.showError('Cannot move notes here.');
|
||||
toastService.showError(t("branches.cannot-move-notes-here"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ async function moveAfterBranch(branchIdsToMove, afterBranchId) {
|
||||
];
|
||||
|
||||
if (forbiddenNoteIds.includes(afterNote.noteId)) {
|
||||
toastService.showError('Cannot move notes here.');
|
||||
toastService.showError(t("branches.cannot-move-notes-here"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ async function moveToParentNote(branchIdsToMove, newParentBranchId) {
|
||||
const newParentBranch = froca.getBranch(newParentBranchId);
|
||||
|
||||
if (newParentBranch.noteId === '_lbRoot') {
|
||||
toastService.showError('Cannot move notes here.');
|
||||
toastService.showError(t("branches.cannot-move-notes-here"));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,7 +193,7 @@ function filterRootNote(branchIds) {
|
||||
function makeToast(id, message) {
|
||||
return {
|
||||
id: id,
|
||||
title: "Delete status",
|
||||
title: t("branches.delete-status"),
|
||||
message: message,
|
||||
icon: "trash"
|
||||
};
|
||||
@@ -207,9 +208,9 @@ ws.subscribeToMessages(async message => {
|
||||
toastService.closePersistent(message.taskId);
|
||||
toastService.showError(message.message);
|
||||
} else if (message.type === 'taskProgressCount') {
|
||||
toastService.showPersistent(makeToast(message.taskId, `Delete notes in progress: ${message.progressCount}`));
|
||||
toastService.showPersistent(makeToast(message.taskId, t("branches.delete-notes-in-progress", { count: message.progressCount })));
|
||||
} else if (message.type === 'taskSucceeded') {
|
||||
const toast = makeToast(message.taskId, "Delete finished successfully.");
|
||||
const toast = makeToast(message.taskId, t("branches.delete-finished-successfully"));
|
||||
toast.closeAfter = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
@@ -225,9 +226,9 @@ ws.subscribeToMessages(async message => {
|
||||
toastService.closePersistent(message.taskId);
|
||||
toastService.showError(message.message);
|
||||
} else if (message.type === 'taskProgressCount') {
|
||||
toastService.showPersistent(makeToast(message.taskId, `Undeleting notes in progress: ${message.progressCount}`));
|
||||
toastService.showPersistent(makeToast(message.taskId, t("branches.undeleting-notes-in-progress", { count: message.progressCount })));
|
||||
} else if (message.type === 'taskSucceeded') {
|
||||
const toast = makeToast(message.taskId, "Undeleting notes finished successfully.");
|
||||
const toast = makeToast(message.taskId, t("branches.undeleting-notes-finished-successfully"));
|
||||
toast.closeAfter = 5000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
|
||||
@@ -3,6 +3,7 @@ import server from "./server.js";
|
||||
import toastService from "./toast.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
async function getAndExecuteBundle(noteId, originEntity = null, script = null, params = null) {
|
||||
const bundle = await server.post(`script/bundle/${noteId}`, {
|
||||
@@ -75,9 +76,23 @@ async function getWidgetBundlesByParent() {
|
||||
|
||||
try {
|
||||
widget = await executeBundle(bundle);
|
||||
widgetsByParent.add(widget);
|
||||
}
|
||||
catch (e) {
|
||||
if (widget) {
|
||||
widget._noteId = bundle.noteId;
|
||||
widgetsByParent.add(widget);
|
||||
}
|
||||
} catch (e) {
|
||||
const noteId = bundle.noteId;
|
||||
const note = await froca.getNote(noteId);
|
||||
toastService.showPersistent({
|
||||
title: t("toast.bundle-error.title"),
|
||||
icon: "alert",
|
||||
message: t("toast.bundle-error.message", {
|
||||
id: noteId,
|
||||
title: note.title,
|
||||
message: e.message
|
||||
})
|
||||
});
|
||||
|
||||
logError("Widget initialization failed: ", e);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import toastService from "./toast.js";
|
||||
import froca from "./froca.js";
|
||||
import linkService from "./link.js";
|
||||
import utils from "./utils.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
let clipboardBranchIds = [];
|
||||
let clipboardMode = null;
|
||||
@@ -78,7 +79,7 @@ async function copy(branchIds) {
|
||||
clipboard.writeHTML(links.join(', '));
|
||||
}
|
||||
|
||||
toastService.showMessage("Note(s) have been copied into clipboard.");
|
||||
toastService.showMessage(t("clipboard.copied"));
|
||||
}
|
||||
|
||||
function cut(branchIds) {
|
||||
@@ -87,7 +88,7 @@ function cut(branchIds) {
|
||||
if (clipboardBranchIds.length > 0) {
|
||||
clipboardMode = 'cut';
|
||||
|
||||
toastService.showMessage("Note(s) have been cut into clipboard.");
|
||||
toastService.showMessage(t("clipboard.cut"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import treeService from "./tree.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import FAttachment from "../entities/fattachment.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
|
||||
import mime_types from "./mime_types.js";
|
||||
|
||||
let idCounter = 1;
|
||||
|
||||
@@ -105,16 +107,25 @@ async function renderText(note, $renderedContent) {
|
||||
for (const el of referenceLinks) {
|
||||
await linkService.loadReferenceLinkTitle($(el));
|
||||
}
|
||||
|
||||
await applySyntaxHighlight($renderedContent);
|
||||
} else {
|
||||
await renderChildrenList($renderedContent, note);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {FNote} note */
|
||||
/**
|
||||
* Renders a code note, by displaying its content and applying syntax highlighting based on the selected MIME type.
|
||||
*
|
||||
* @param {FNote} note
|
||||
*/
|
||||
async function renderCode(note, $renderedContent) {
|
||||
const blob = await note.getBlob();
|
||||
|
||||
$renderedContent.append($("<pre>").text(blob.content));
|
||||
const $codeBlock = $("<code>");
|
||||
$codeBlock.text(blob.content);
|
||||
$renderedContent.append($("<pre>").append($codeBlock));
|
||||
await applySingleBlockSyntaxHighlight($codeBlock, mime_types.normalizeMimeTypeForCKEditor(note.mime));
|
||||
}
|
||||
|
||||
function renderImage(entity, $renderedContent, options = {}) {
|
||||
|
||||
@@ -217,8 +217,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
*/
|
||||
this.runOnBackend = async (func, params = []) => {
|
||||
if (func?.constructor.name === "AsyncFunction" || func?.startsWith?.("async ")) {
|
||||
toastService.showError("You're passing an async function to api.runOnBackend() which will likely not work as you intended. "
|
||||
+ "Either make the function synchronous (by removing 'async' keyword), or use api.runAsyncOnBackendWithManualTransactionHandling()");
|
||||
toastService.showError(t("frontend_script_api.async_warning"));
|
||||
}
|
||||
|
||||
return await this.__runOnBackendInner(func, params, true);
|
||||
@@ -240,8 +239,7 @@ function FrontendScriptApi(startNote, currentNote, originEntity = null, $contain
|
||||
*/
|
||||
this.runAsyncOnBackendWithManualTransactionHandling = async (func, params = []) => {
|
||||
if (func?.constructor.name === "Function" || func?.startsWith?.("function")) {
|
||||
toastService.showError("You're passing a synchronous function to api.runAsyncOnBackendWithManualTransactionHandling(), " +
|
||||
"while you should likely use api.runOnBackend() instead.");
|
||||
toastService.showError(t("frontend_script_api.sync_warning"));
|
||||
}
|
||||
|
||||
return await this.__runOnBackendInner(func, params, false);
|
||||
|
||||
@@ -26,21 +26,6 @@ function setupGlobs() {
|
||||
// for CKEditor integration (button on block toolbar)
|
||||
window.glob.importMarkdownInline = async () => appContext.triggerCommand("importMarkdownInline");
|
||||
|
||||
window.glob.SEARCH_HELP_TEXT = `
|
||||
<strong>Search tips</strong> - also see <button class="btn btn-sm" type="button" data-help-page="search.html">complete help on search</button>
|
||||
<p>
|
||||
<ul>
|
||||
<li>Just enter any text for full text search</li>
|
||||
<li><code>#abc</code> - returns notes with label abc</li>
|
||||
<li><code>#year = 2019</code> - matches notes with label <code>year</code> having value <code>2019</code></li>
|
||||
<li><code>#rock #pop</code> - matches notes which have both <code>rock</code> and <code>pop</code> labels</li>
|
||||
<li><code>#rock or #pop</code> - only one of the labels must be present</li>
|
||||
<li><code>#year <= 2000</code> - numerical comparison (also >, >=, <).</li>
|
||||
<li><code>note.dateCreated >= MONTH-1</code> - notes created in the last month</li>
|
||||
<li><code>=handler</code> - will execute script defined in <code>handler</code> relation to get results</li>
|
||||
</ul>
|
||||
</p>`;
|
||||
|
||||
window.onerror = function (msg, url, lineNo, columnNo, error) {
|
||||
const string = msg.toLowerCase();
|
||||
|
||||
@@ -64,6 +49,28 @@ function setupGlobs() {
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener("unhandledrejection", (e) => {
|
||||
const string = e?.reason?.message?.toLowerCase();
|
||||
|
||||
let message = "Uncaught error: ";
|
||||
|
||||
if (string?.includes("script error")) {
|
||||
message += 'No details available';
|
||||
} else {
|
||||
message += [
|
||||
`Message: ${e.reason.message}`,
|
||||
`Line: ${e.reason.lineNumber}`,
|
||||
`Column: ${e.reason.columnNumber}`,
|
||||
`Error object: ${JSON.stringify(e.reason)}`,
|
||||
`Stack: ${e.reason && e.reason.stack}`
|
||||
].join(', ');
|
||||
}
|
||||
|
||||
ws.logError(message);
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
for (const appCssNoteId of glob.appCssNoteIds || []) {
|
||||
libraryLoader.requireCss(`api/notes/download/${appCssNoteId}`, false);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import appContext from "../components/app_context.js";
|
||||
import treeService from "./tree.js";
|
||||
import dialogService from "./dialog.js";
|
||||
import froca from "./froca.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
function getHoistedNoteId() {
|
||||
const activeNoteContext = appContext.tabManager.getActiveContext();
|
||||
@@ -53,7 +54,7 @@ async function checkNoteAccess(notePath, noteContext) {
|
||||
const hoistedNote = await froca.getNote(hoistedNoteId);
|
||||
|
||||
if ((!hoistedNote.hasAncestor('_hidden') || resolvedNotePath.includes('_lbBookmarks'))
|
||||
&& !await dialogService.confirm(`Requested note '${requestedNote.title}' is outside of hoisted note '${hoistedNote.title}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?`)) {
|
||||
&& !await dialogService.confirm(t("hoisted_note.confirm_unhoisting", { requestedNote: requestedNote.title, hoistedNote: hoistedNote.title }))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ function copyImageReferenceToClipboard($imageWrapper) {
|
||||
const success = document.execCommand('copy');
|
||||
|
||||
if (success) {
|
||||
toastService.showMessage("A reference to the image has been copied to clipboard. This can be pasted in any text note.");
|
||||
toastService.showMessage(t("image.copied-to-clipboard"));
|
||||
} else {
|
||||
toastService.showAndLogError("Could not copy the image reference to clipboard.");
|
||||
toastService.showAndLogError(t("image.cannot-copy"));
|
||||
}
|
||||
}
|
||||
finally {
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function uploadFiles(entityType, parentNoteId, files, options) {
|
||||
type: 'POST',
|
||||
timeout: 60 * 60 * 1000,
|
||||
error: function (xhr) {
|
||||
toastService.showError(`Import failed: ${xhr.responseText}`);
|
||||
toastService.showError(t("import.failed", { message: xhr.responseText }));
|
||||
},
|
||||
contentType: false, // NEEDED, DON'T REMOVE THIS
|
||||
processData: false, // NEEDED, DON'T REMOVE THIS
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import mimeTypesService from "./mime_types.js";
|
||||
import optionsService from "./options.js";
|
||||
import { getStylesheetUrl } from "./syntax_highlight.js";
|
||||
|
||||
const CKEDITOR = {"js": ["libraries/ckeditor/ckeditor.js"]};
|
||||
|
||||
const CODE_MIRROR = {
|
||||
js: [
|
||||
"libraries/codemirror/codemirror.js",
|
||||
// "node_modules/codemirror/lib/codemirror.js",
|
||||
"node_modules/codemirror/lib/codemirror.js",
|
||||
"node_modules/codemirror/addon/display/placeholder.js",
|
||||
"node_modules/codemirror/addon/edit/matchbrackets.js",
|
||||
"node_modules/codemirror/addon/edit/matchtags.js",
|
||||
@@ -85,18 +88,44 @@ const MIND_ELIXIR = {
|
||||
]
|
||||
};
|
||||
|
||||
const HIGHLIGHT_JS = {
|
||||
js: () => {
|
||||
const mimeTypes = mimeTypesService.getMimeTypes();
|
||||
const scriptsToLoad = new Set();
|
||||
scriptsToLoad.add("node_modules/@highlightjs/cdn-assets/highlight.min.js");
|
||||
for (const mimeType of mimeTypes) {
|
||||
if (mimeType.enabled && mimeType.highlightJs) {
|
||||
scriptsToLoad.add(`node_modules/@highlightjs/cdn-assets/languages/${mimeType.highlightJs}.min.js`);
|
||||
}
|
||||
}
|
||||
|
||||
const currentTheme = optionsService.get("codeBlockTheme");
|
||||
loadHighlightingTheme(currentTheme);
|
||||
|
||||
return Array.from(scriptsToLoad);
|
||||
}
|
||||
};
|
||||
|
||||
async function requireLibrary(library) {
|
||||
if (library.css) {
|
||||
library.css.map(cssUrl => requireCss(cssUrl));
|
||||
}
|
||||
|
||||
if (library.js) {
|
||||
for (const scriptUrl of library.js) {
|
||||
for (const scriptUrl of unwrapValue(library.js)) {
|
||||
await requireScript(scriptUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function unwrapValue(value) {
|
||||
if (typeof value === "function") {
|
||||
return value();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// we save the promises in case of the same script being required concurrently multiple times
|
||||
const loadedScriptPromises = {};
|
||||
|
||||
@@ -128,9 +157,36 @@ async function requireCss(url, prependAssetPath = true) {
|
||||
}
|
||||
}
|
||||
|
||||
let highlightingThemeEl = null;
|
||||
function loadHighlightingTheme(theme) {
|
||||
if (!theme) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (theme === "none") {
|
||||
// Deactivate the theme.
|
||||
if (highlightingThemeEl) {
|
||||
highlightingThemeEl.remove();
|
||||
highlightingThemeEl = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!highlightingThemeEl) {
|
||||
highlightingThemeEl = $(`<link rel="stylesheet" type="text/css" />`);
|
||||
$("head").append(highlightingThemeEl);
|
||||
}
|
||||
|
||||
const url = getStylesheetUrl(theme);
|
||||
if (url) {
|
||||
highlightingThemeEl.attr("href", url);
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
requireCss,
|
||||
requireLibrary,
|
||||
loadHighlightingTheme,
|
||||
CKEDITOR,
|
||||
CODE_MIRROR,
|
||||
ESLINT,
|
||||
@@ -144,5 +200,6 @@ export default {
|
||||
EXCALIDRAW,
|
||||
MARKJS,
|
||||
I18NEXT,
|
||||
MIND_ELIXIR
|
||||
MIND_ELIXIR,
|
||||
HIGHLIGHT_JS
|
||||
}
|
||||
|
||||
@@ -1,162 +1,167 @@
|
||||
import options from "./options.js";
|
||||
|
||||
/**
|
||||
* A pseudo-MIME type which is used in the editor to automatically determine the language used in code blocks via heuristics.
|
||||
*/
|
||||
const MIME_TYPE_AUTO = "text-x-trilium-auto";
|
||||
|
||||
const MIME_TYPES_DICT = [
|
||||
{ default: true, title: "Plain text", mime: "text/plain" },
|
||||
{ default: true, title: "Plain text", mime: "text/plain", highlightJs: "plaintext" },
|
||||
{ title: "APL", mime: "text/apl" },
|
||||
{ title: "ASN.1", mime: "text/x-ttcn-asn" },
|
||||
{ title: "ASP.NET", mime: "application/x-aspx" },
|
||||
{ title: "Asterisk", mime: "text/x-asterisk" },
|
||||
{ title: "Brainfuck", mime: "text/x-brainfuck" },
|
||||
{ default: true, title: "C", mime: "text/x-csrc" },
|
||||
{ default: true, title: "C#", mime: "text/x-csharp" },
|
||||
{ default: true, title: "C++", mime: "text/x-c++src" },
|
||||
{ title: "Clojure", mime: "text/x-clojure" },
|
||||
{ title: "Brainfuck", mime: "text/x-brainfuck", highlightJs: "brainfuck" },
|
||||
{ default: true, title: "C", mime: "text/x-csrc", highlightJs: "c" },
|
||||
{ default: true, title: "C#", mime: "text/x-csharp", highlightJs: "csharp" },
|
||||
{ default: true, title: "C++", mime: "text/x-c++src", highlightJs: "cpp" },
|
||||
{ title: "Clojure", mime: "text/x-clojure", highlightJs: "clojure" },
|
||||
{ title: "ClojureScript", mime: "text/x-clojurescript" },
|
||||
{ title: "Closure Stylesheets (GSS)", mime: "text/x-gss" },
|
||||
{ title: "CMake", mime: "text/x-cmake" },
|
||||
{ title: "CMake", mime: "text/x-cmake", highlightJs: "cmake" },
|
||||
{ title: "Cobol", mime: "text/x-cobol" },
|
||||
{ title: "CoffeeScript", mime: "text/coffeescript" },
|
||||
{ title: "Common Lisp", mime: "text/x-common-lisp" },
|
||||
{ title: "CoffeeScript", mime: "text/coffeescript", highlightJs: "coffeescript" },
|
||||
{ title: "Common Lisp", mime: "text/x-common-lisp", highlightJs: "lisp" },
|
||||
{ title: "CQL", mime: "text/x-cassandra" },
|
||||
{ title: "Crystal", mime: "text/x-crystal" },
|
||||
{ default: true, title: "CSS", mime: "text/css" },
|
||||
{ title: "Crystal", mime: "text/x-crystal", highlightJs: "crystal" },
|
||||
{ default: true, title: "CSS", mime: "text/css", highlightJs: "css" },
|
||||
{ title: "Cypher", mime: "application/x-cypher-query" },
|
||||
{ title: "Cython", mime: "text/x-cython" },
|
||||
{ title: "D", mime: "text/x-d" },
|
||||
{ title: "Dart", mime: "application/dart" },
|
||||
{ title: "diff", mime: "text/x-diff" },
|
||||
{ title: "Django", mime: "text/x-django" },
|
||||
{ title: "Dockerfile", mime: "text/x-dockerfile" },
|
||||
{ title: "D", mime: "text/x-d", highlightJs: "d" },
|
||||
{ title: "Dart", mime: "application/dart", highlightJs: "dart" },
|
||||
{ title: "diff", mime: "text/x-diff", highlightJs: "diff" },
|
||||
{ title: "Django", mime: "text/x-django", highlightJs: "django" },
|
||||
{ title: "Dockerfile", mime: "text/x-dockerfile", highlightJs: "dockerfile" },
|
||||
{ title: "DTD", mime: "application/xml-dtd" },
|
||||
{ title: "Dylan", mime: "text/x-dylan" },
|
||||
{ title: "EBNF", mime: "text/x-ebnf" },
|
||||
{ title: "EBNF", mime: "text/x-ebnf", highlightJs: "ebnf" },
|
||||
{ title: "ECL", mime: "text/x-ecl" },
|
||||
{ title: "edn", mime: "application/edn" },
|
||||
{ title: "Eiffel", mime: "text/x-eiffel" },
|
||||
{ title: "Elm", mime: "text/x-elm" },
|
||||
{ title: "Elm", mime: "text/x-elm", highlightJs: "elm" },
|
||||
{ title: "Embedded Javascript", mime: "application/x-ejs" },
|
||||
{ title: "Embedded Ruby", mime: "application/x-erb" },
|
||||
{ title: "Erlang", mime: "text/x-erlang" },
|
||||
{ title: "Embedded Ruby", mime: "application/x-erb", highlightJs: "erb" },
|
||||
{ title: "Erlang", mime: "text/x-erlang", highlightJs: "erlang" },
|
||||
{ title: "Esper", mime: "text/x-esper" },
|
||||
{ title: "F#", mime: "text/x-fsharp" },
|
||||
{ title: "F#", mime: "text/x-fsharp", highlightJs: "fsharp" },
|
||||
{ title: "Factor", mime: "text/x-factor" },
|
||||
{ title: "FCL", mime: "text/x-fcl" },
|
||||
{ title: "Forth", mime: "text/x-forth" },
|
||||
{ title: "Fortran", mime: "text/x-fortran" },
|
||||
{ title: "Fortran", mime: "text/x-fortran", highlightJs: "fortran" },
|
||||
{ title: "Gas", mime: "text/x-gas" },
|
||||
{ title: "Gherkin", mime: "text/x-feature" },
|
||||
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm" },
|
||||
{ default: true, title: "Go", mime: "text/x-go" },
|
||||
{ default: true, title: "Groovy", mime: "text/x-groovy" },
|
||||
{ title: "HAML", mime: "text/x-haml" },
|
||||
{ default: true, title: "Haskell", mime: "text/x-haskell" },
|
||||
{ title: "Gherkin", mime: "text/x-feature", highlightJs: "gherkin" },
|
||||
{ title: "GitHub Flavored Markdown", mime: "text/x-gfm", highlightJs: "markdown" },
|
||||
{ default: true, title: "Go", mime: "text/x-go", highlightJs: "go" },
|
||||
{ default: true, title: "Groovy", mime: "text/x-groovy", highlightJs: "groovy" },
|
||||
{ title: "HAML", mime: "text/x-haml", highlightJs: "haml" },
|
||||
{ default: true, title: "Haskell", mime: "text/x-haskell", highlightJs: "haskell" },
|
||||
{ title: "Haskell (Literate)", mime: "text/x-literate-haskell" },
|
||||
{ title: "Haxe", mime: "text/x-haxe" },
|
||||
{ default: true, title: "HTML", mime: "text/html" },
|
||||
{ default: true, title: "HTTP", mime: "message/http" },
|
||||
{ title: "Haxe", mime: "text/x-haxe", highlightJs: "haxe" },
|
||||
{ default: true, title: "HTML", mime: "text/html", highlightJs: "xml" },
|
||||
{ default: true, title: "HTTP", mime: "message/http", highlightJs: "http" },
|
||||
{ title: "HXML", mime: "text/x-hxml" },
|
||||
{ title: "IDL", mime: "text/x-idl" },
|
||||
{ default: true, title: "Java", mime: "text/x-java" },
|
||||
{ title: "Java Server Pages", mime: "application/x-jsp" },
|
||||
{ default: true, title: "Java", mime: "text/x-java", highlightJs: "java" },
|
||||
{ title: "Java Server Pages", mime: "application/x-jsp", highlightJs: "java" },
|
||||
{ title: "Jinja2", mime: "text/jinja2" },
|
||||
{ default: true, title: "JS backend", mime: "application/javascript;env=backend" },
|
||||
{ default: true, title: "JS frontend", mime: "application/javascript;env=frontend" },
|
||||
{ default: true, title: "JSON", mime: "application/json" },
|
||||
{ title: "JSON-LD", mime: "application/ld+json" },
|
||||
{ title: "JSX", mime: "text/jsx" },
|
||||
{ title: "Julia", mime: "text/x-julia" },
|
||||
{ default: true, title: "Kotlin", mime: "text/x-kotlin" },
|
||||
{ title: "LaTeX", mime: "text/x-latex" },
|
||||
{ title: "LESS", mime: "text/x-less" },
|
||||
{ title: "LiveScript", mime: "text/x-livescript" },
|
||||
{ title: "Lua", mime: "text/x-lua" },
|
||||
{ title: "MariaDB SQL", mime: "text/x-mariadb" },
|
||||
{ default: true, title: "Markdown", mime: "text/x-markdown" },
|
||||
{ title: "Mathematica", mime: "text/x-mathematica" },
|
||||
{ default: true, title: "JS backend", mime: "application/javascript;env=backend", highlightJs: "javascript" },
|
||||
{ default: true, title: "JS frontend", mime: "application/javascript;env=frontend", highlightJs: "javascript" },
|
||||
{ default: true, title: "JSON", mime: "application/json", highlightJs: "json" },
|
||||
{ title: "JSON-LD", mime: "application/ld+json", highlightJs: "json" },
|
||||
{ title: "JSX", mime: "text/jsx", highlightJs: "javascript" },
|
||||
{ title: "Julia", mime: "text/x-julia", highlightJs: "julia" },
|
||||
{ default: true, title: "Kotlin", mime: "text/x-kotlin", highlightJs: "kotlin" },
|
||||
{ title: "LaTeX", mime: "text/x-latex", highlightJs: "latex" },
|
||||
{ title: "LESS", mime: "text/x-less", highlightJs: "less" },
|
||||
{ title: "LiveScript", mime: "text/x-livescript", highlightJs: "livescript" },
|
||||
{ title: "Lua", mime: "text/x-lua", highlightJs: "lua" },
|
||||
{ title: "MariaDB SQL", mime: "text/x-mariadb", highlightJs: "sql" },
|
||||
{ default: true, title: "Markdown", mime: "text/x-markdown", highlightJs: "markdown" },
|
||||
{ title: "Mathematica", mime: "text/x-mathematica", highlightJs: "mathematica" },
|
||||
{ title: "mbox", mime: "application/mbox" },
|
||||
{ title: "mIRC", mime: "text/mirc" },
|
||||
{ title: "Modelica", mime: "text/x-modelica" },
|
||||
{ title: "MS SQL", mime: "text/x-mssql" },
|
||||
{ title: "MS SQL", mime: "text/x-mssql", highlightJs: "sql" },
|
||||
{ title: "mscgen", mime: "text/x-mscgen" },
|
||||
{ title: "msgenny", mime: "text/x-msgenny" },
|
||||
{ title: "MUMPS", mime: "text/x-mumps" },
|
||||
{ title: "MySQL", mime: "text/x-mysql" },
|
||||
{ title: "Nginx", mime: "text/x-nginx-conf" },
|
||||
{ title: "NSIS", mime: "text/x-nsis" },
|
||||
{ title: "MySQL", mime: "text/x-mysql", highlightJs: "sql" },
|
||||
{ title: "Nginx", mime: "text/x-nginx-conf", highlightJs: "nginx" },
|
||||
{ title: "NSIS", mime: "text/x-nsis", highlightJs: "nsis" },
|
||||
{ title: "NTriples", mime: "application/n-triples" },
|
||||
{ title: "Objective-C", mime: "text/x-objectivec" },
|
||||
{ title: "OCaml", mime: "text/x-ocaml" },
|
||||
{ title: "Objective-C", mime: "text/x-objectivec", highlightJs: "objectivec" },
|
||||
{ title: "OCaml", mime: "text/x-ocaml", highlightJs: "ocaml" },
|
||||
{ title: "Octave", mime: "text/x-octave" },
|
||||
{ title: "Oz", mime: "text/x-oz" },
|
||||
{ title: "Pascal", mime: "text/x-pascal" },
|
||||
{ title: "Pascal", mime: "text/x-pascal", highlightJs: "delphi" },
|
||||
{ title: "PEG.js", mime: "null" },
|
||||
{ default: true, title: "Perl", mime: "text/x-perl" },
|
||||
{ title: "PGP", mime: "application/pgp" },
|
||||
{ default: true, title: "PHP", mime: "text/x-php" },
|
||||
{ title: "Pig", mime: "text/x-pig" },
|
||||
{ title: "PLSQL", mime: "text/x-plsql" },
|
||||
{ title: "PostgreSQL", mime: "text/x-pgsql" },
|
||||
{ title: "PowerShell", mime: "application/x-powershell" },
|
||||
{ title: "Properties files", mime: "text/x-properties" },
|
||||
{ title: "ProtoBuf", mime: "text/x-protobuf" },
|
||||
{ title: "PLSQL", mime: "text/x-plsql", highlightJs: "sql" },
|
||||
{ title: "PostgreSQL", mime: "text/x-pgsql", highlightJs: "pgsql" },
|
||||
{ title: "PowerShell", mime: "application/x-powershell", highlightJs: "powershell" },
|
||||
{ title: "Properties files", mime: "text/x-properties", highlightJs: "properties" },
|
||||
{ title: "ProtoBuf", mime: "text/x-protobuf", highlightJs: "protobuf" },
|
||||
{ title: "Pug", mime: "text/x-pug" },
|
||||
{ title: "Puppet", mime: "text/x-puppet" },
|
||||
{ default: true, title: "Python", mime: "text/x-python" },
|
||||
{ title: "Q", mime: "text/x-q" },
|
||||
{ title: "R", mime: "text/x-rsrc" },
|
||||
{ title: "Puppet", mime: "text/x-puppet", highlightJs: "puppet" },
|
||||
{ default: true, title: "Python", mime: "text/x-python", highlightJs: "python" },
|
||||
{ title: "Q", mime: "text/x-q", highlightJs: "q" },
|
||||
{ title: "R", mime: "text/x-rsrc", highlightJs: "r" },
|
||||
{ title: "reStructuredText", mime: "text/x-rst" },
|
||||
{ title: "RPM Changes", mime: "text/x-rpm-changes" },
|
||||
{ title: "RPM Spec", mime: "text/x-rpm-spec" },
|
||||
{ default: true, title: "Ruby", mime: "text/x-ruby" },
|
||||
{ title: "Rust", mime: "text/x-rustsrc" },
|
||||
{ title: "SAS", mime: "text/x-sas" },
|
||||
{ default: true, title: "Ruby", mime: "text/x-ruby", highlightJs: "ruby" },
|
||||
{ title: "Rust", mime: "text/x-rustsrc", highlightJs: "rust" },
|
||||
{ title: "SAS", mime: "text/x-sas", highlightJs: "sas" },
|
||||
{ title: "Sass", mime: "text/x-sass" },
|
||||
{ title: "Scala", mime: "text/x-scala" },
|
||||
{ title: "Scheme", mime: "text/x-scheme" },
|
||||
{ title: "SCSS", mime: "text/x-scss" },
|
||||
{ default: true, title: "Shell (bash)", mime: "text/x-sh" },
|
||||
{ title: "SCSS", mime: "text/x-scss", highlightJs: "scss" },
|
||||
{ default: true, title: "Shell (bash)", mime: "text/x-sh", highlightJs: "shell" },
|
||||
{ title: "Sieve", mime: "application/sieve" },
|
||||
{ title: "Slim", mime: "text/x-slim" },
|
||||
{ title: "Smalltalk", mime: "text/x-stsrc" },
|
||||
{ title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" },
|
||||
{ title: "Smarty", mime: "text/x-smarty" },
|
||||
{ title: "SML", mime: "text/x-sml" },
|
||||
{ title: "SML", mime: "text/x-sml", highlightJs: "sml" },
|
||||
{ title: "Solr", mime: "text/x-solr" },
|
||||
{ title: "Soy", mime: "text/x-soy" },
|
||||
{ title: "SPARQL", mime: "application/sparql-query" },
|
||||
{ title: "Spreadsheet", mime: "text/x-spreadsheet" },
|
||||
{ default: true, title: "SQL", mime: "text/x-sql" },
|
||||
{ title: "SQLite", mime: "text/x-sqlite" },
|
||||
{ default: true, title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium" },
|
||||
{ default: true, title: "SQL", mime: "text/x-sql", highlightJs: "sql" },
|
||||
{ title: "SQLite", mime: "text/x-sqlite", highlightJs: "sql" },
|
||||
{ default: true, title: "SQLite (Trilium)", mime: "text/x-sqlite;schema=trilium", highlightJs: "sql" },
|
||||
{ title: "Squirrel", mime: "text/x-squirrel" },
|
||||
{ title: "sTeX", mime: "text/x-stex" },
|
||||
{ title: "Stylus", mime: "text/x-styl" },
|
||||
{ title: "Stylus", mime: "text/x-styl", highlightJs: "stylus" },
|
||||
{ default: true, title: "Swift", mime: "text/x-swift" },
|
||||
{ title: "SystemVerilog", mime: "text/x-systemverilog" },
|
||||
{ title: "Tcl", mime: "text/x-tcl" },
|
||||
{ title: "Tcl", mime: "text/x-tcl", highlightJs: "tcl" },
|
||||
{ title: "Textile", mime: "text/x-textile" },
|
||||
{ title: "TiddlyWiki ", mime: "text/x-tiddlywiki" },
|
||||
{ title: "Tiki wiki", mime: "text/tiki" },
|
||||
{ title: "TOML", mime: "text/x-toml" },
|
||||
{ title: "TOML", mime: "text/x-toml", highlightJs: "ini" },
|
||||
{ title: "Tornado", mime: "text/x-tornado" },
|
||||
{ title: "troff", mime: "text/troff" },
|
||||
{ title: "TTCN", mime: "text/x-ttcn" },
|
||||
{ title: "TTCN_CFG", mime: "text/x-ttcn-cfg" },
|
||||
{ title: "Turtle", mime: "text/turtle" },
|
||||
{ title: "Twig", mime: "text/x-twig" },
|
||||
{ title: "TypeScript", mime: "application/typescript" },
|
||||
{ title: "Twig", mime: "text/x-twig", highlightJs: "twig" },
|
||||
{ title: "TypeScript", mime: "application/typescript", highlightJs: "typescript" },
|
||||
{ title: "TypeScript-JSX", mime: "text/typescript-jsx" },
|
||||
{ title: "VB.NET", mime: "text/x-vb" },
|
||||
{ title: "VBScript", mime: "text/vbscript" },
|
||||
{ title: "VB.NET", mime: "text/x-vb", highlightJs: "vbnet" },
|
||||
{ title: "VBScript", mime: "text/vbscript", highlightJs: "vbscript" },
|
||||
{ title: "Velocity", mime: "text/velocity" },
|
||||
{ title: "Verilog", mime: "text/x-verilog" },
|
||||
{ title: "VHDL", mime: "text/x-vhdl" },
|
||||
{ title: "Verilog", mime: "text/x-verilog", highlightJs: "verilog" },
|
||||
{ title: "VHDL", mime: "text/x-vhdl", highlightJs: "vhdl" },
|
||||
{ title: "Vue.js Component", mime: "text/x-vue" },
|
||||
{ title: "Web IDL", mime: "text/x-webidl" },
|
||||
{ default: true, title: "XML", mime: "text/xml" },
|
||||
{ title: "XQuery", mime: "application/xquery" },
|
||||
{ default: true, title: "XML", mime: "text/xml", highlightJs: "xml" },
|
||||
{ title: "XQuery", mime: "application/xquery", highlightJs: "xquery" },
|
||||
{ title: "xu", mime: "text/x-xu" },
|
||||
{ title: "Yacas", mime: "text/x-yacas" },
|
||||
{ default: true, title: "YAML", mime: "text/x-yaml" },
|
||||
{ default: true, title: "YAML", mime: "text/x-yaml", highlightJs: "yaml" },
|
||||
{ title: "Z80", mime: "text/x-z80" }
|
||||
];
|
||||
|
||||
@@ -173,7 +178,7 @@ function loadMimeTypes() {
|
||||
}
|
||||
}
|
||||
|
||||
async function getMimeTypes() {
|
||||
function getMimeTypes() {
|
||||
if (mimeTypes === null) {
|
||||
loadMimeTypes();
|
||||
}
|
||||
@@ -181,7 +186,46 @@ async function getMimeTypes() {
|
||||
return mimeTypes;
|
||||
}
|
||||
|
||||
export default {
|
||||
getMimeTypes,
|
||||
loadMimeTypes
|
||||
let mimeToHighlightJsMapping = null;
|
||||
|
||||
/**
|
||||
* Obtains the corresponding language tag for highlight.js for a given MIME type.
|
||||
*
|
||||
* The mapping is built the first time this method is built and then the results are cached for better performance.
|
||||
*
|
||||
* @param {string} mimeType The MIME type of the code block, in the CKEditor-normalized format (e.g. `text-c-src` instead of `text/c-src`).
|
||||
* @returns the corresponding highlight.js tag, for example `c` for `text-c-src`.
|
||||
*/
|
||||
function getHighlightJsNameForMime(mimeType) {
|
||||
if (!mimeToHighlightJsMapping) {
|
||||
const mimeTypes = getMimeTypes();
|
||||
mimeToHighlightJsMapping = {};
|
||||
for (const mimeType of mimeTypes) {
|
||||
// The mime stored by CKEditor is text-x-csrc instead of text/x-csrc so we keep this format for faster lookup.
|
||||
const normalizedMime = normalizeMimeTypeForCKEditor(mimeType.mime);
|
||||
mimeToHighlightJsMapping[normalizedMime] = mimeType.highlightJs;
|
||||
}
|
||||
}
|
||||
|
||||
return mimeToHighlightJsMapping[mimeType];
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a MIME type in the usual format (e.g. `text/csrc`), it returns a MIME type that can be passed down to the CKEditor
|
||||
* code plugin.
|
||||
*
|
||||
* @param {string} mimeType The MIME type to normalize, in the usual format (e.g. `text/c-src`).
|
||||
* @returns the normalized MIME type (e.g. `text-c-src`).
|
||||
*/
|
||||
function normalizeMimeTypeForCKEditor(mimeType) {
|
||||
return mimeType.toLowerCase()
|
||||
.replace(/[\W_]+/g,"-");
|
||||
}
|
||||
|
||||
export default {
|
||||
MIME_TYPE_AUTO,
|
||||
getMimeTypes,
|
||||
loadMimeTypes,
|
||||
getHighlightJsNameForMime,
|
||||
normalizeMimeTypeForCKEditor
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import ws from "./ws.js";
|
||||
import froca from "./froca.js";
|
||||
import treeService from "./tree.js";
|
||||
import toastService from "./toast.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
async function createNote(parentNotePath, options = {}) {
|
||||
options = Object.assign({
|
||||
@@ -119,7 +120,7 @@ async function duplicateSubtree(noteId, parentNotePath) {
|
||||
activeNoteContext.setNote(`${parentNotePath}/${note.noteId}`);
|
||||
|
||||
const origNote = await froca.getNote(noteId);
|
||||
toastService.showMessage(`Note "${origNote.title}" has been duplicated`);
|
||||
toastService.showMessage(t("note_create.duplicated", { title: origNote.title }));
|
||||
}
|
||||
|
||||
export default {
|
||||
|
||||
@@ -366,12 +366,13 @@ class NoteListRenderer {
|
||||
separateWordSearch: false,
|
||||
caseSensitive: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$content.append($renderedContent);
|
||||
$content.addClass(`type-${type}`);
|
||||
} catch (e) {
|
||||
console.log(`Caught error while rendering note '${note.noteId}' of type '${note.type}': ${e.message}, stack: ${e.stack}`);
|
||||
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
|
||||
console.error(e);
|
||||
|
||||
$content.append("rendering error");
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import appContext from "../components/app_context.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import options from "./options.js";
|
||||
import { t } from './i18n.js';
|
||||
|
||||
let protectedSessionDeferred = null;
|
||||
|
||||
@@ -50,7 +51,7 @@ async function setupProtectedSession(password) {
|
||||
const response = await server.post('login/protected', { password: password });
|
||||
|
||||
if (!response.success) {
|
||||
toastService.showError("Wrong password.", 3000);
|
||||
toastService.showError(t("protected_session.wrong_password"), 3000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -72,7 +73,7 @@ ws.subscribeToMessages(async message => {
|
||||
protectedSessionDeferred = null;
|
||||
}
|
||||
|
||||
toastService.showMessage("Protected session has been started.");
|
||||
toastService.showMessage(t("protected_session.started"));
|
||||
}
|
||||
else if (message.type === 'protectedSessionLogout') {
|
||||
utils.reloadFrontendApp(`Protected session logout`);
|
||||
@@ -85,10 +86,10 @@ async function protectNote(noteId, protect, includingSubtree) {
|
||||
await server.put(`notes/${noteId}/protect/${protect ? 1 : 0}?subtree=${includingSubtree ? 1 : 0}`);
|
||||
}
|
||||
|
||||
function makeToast(message, protectingLabel, text) {
|
||||
function makeToast(message, title, text) {
|
||||
return {
|
||||
id: message.taskId,
|
||||
title: `${protectingLabel} status`,
|
||||
title,
|
||||
message: text,
|
||||
icon: message.data.protect ? "check-shield" : "shield"
|
||||
};
|
||||
@@ -99,15 +100,19 @@ ws.subscribeToMessages(async message => {
|
||||
return;
|
||||
}
|
||||
|
||||
const protectingLabel = message.data.protect ? "Protecting" : "Unprotecting";
|
||||
|
||||
const isProtecting = message.data.protect;
|
||||
const title = isProtecting ? t("protected_session.protecting-title") : t("protected_session.unprotecting-title");
|
||||
|
||||
if (message.type === 'taskError') {
|
||||
toastService.closePersistent(message.taskId);
|
||||
toastService.showError(message.message);
|
||||
} else if (message.type === 'taskProgressCount') {
|
||||
toastService.showPersistent(makeToast(message, protectingLabel,`${protectingLabel} in progress: ${message.progressCount}`));
|
||||
const count = message.progressCount;
|
||||
const text = ( isProtecting ? t("protected_session.protecting-in-progress", { count }) : t("protected_session.unprotecting-in-progress-count", { count }));
|
||||
toastService.showPersistent(makeToast(message, title, text));
|
||||
} else if (message.type === 'taskSucceeded') {
|
||||
const toast = makeToast(message, protectingLabel, `${protectingLabel} finished successfully.`);
|
||||
const text = (isProtecting ? t("protected_session.protecting-finished-successfully") : t("protected_session.unprotecting-finished-successfully"))
|
||||
const toast = makeToast(message, title, text);
|
||||
toast.closeAfter = 3000;
|
||||
|
||||
toastService.showPersistent(toast);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from './i18n.js';
|
||||
import server from './server.js';
|
||||
import toastService from "./toast.js";
|
||||
|
||||
@@ -5,7 +6,7 @@ async function syncNow(ignoreNotConfigured = false) {
|
||||
const result = await server.post('sync/now');
|
||||
|
||||
if (result.success) {
|
||||
toastService.showMessage("Sync finished successfully.");
|
||||
toastService.showMessage(t("sync.finished-successfully"));
|
||||
}
|
||||
else {
|
||||
if (result.message.length > 200) {
|
||||
@@ -13,7 +14,7 @@ async function syncNow(ignoreNotConfigured = false) {
|
||||
}
|
||||
|
||||
if (!ignoreNotConfigured || result.errorCode !== 'NOT_CONFIGURED') {
|
||||
toastService.showError(`Sync failed: ${result.message}`);
|
||||
toastService.showError(t("sync.failed", { message: result.message }));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
94
src/public/app/services/syntax_highlight.js
Normal file
94
src/public/app/services/syntax_highlight.js
Normal file
@@ -0,0 +1,94 @@
|
||||
import library_loader from "./library_loader.js";
|
||||
import mime_types from "./mime_types.js";
|
||||
import options from "./options.js";
|
||||
|
||||
export function getStylesheetUrl(theme) {
|
||||
if (!theme) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const defaultPrefix = "default:";
|
||||
if (theme.startsWith(defaultPrefix)) {
|
||||
return `${window.glob.assetPath}/node_modules/@highlightjs/cdn-assets/styles/${theme.substr(defaultPrefix.length)}.min.css`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies all the code blocks (as `pre code`) under the specified hierarchy and uses the highlight.js library to obtain the highlighted text which is then applied on to the code blocks.
|
||||
*
|
||||
* @param $container the container under which to look for code blocks and to apply syntax highlighting to them.
|
||||
*/
|
||||
export async function applySyntaxHighlight($container) {
|
||||
if (!isSyntaxHighlightEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const codeBlocks = $container.find("pre code");
|
||||
for (const codeBlock of codeBlocks) {
|
||||
const normalizedMimeType = extractLanguageFromClassList(codeBlock);
|
||||
if (!normalizedMimeType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
applySingleBlockSyntaxHighlight($(codeBlock, normalizedMimeType));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies syntax highlight to the given code block (assumed to be <pre><code>), using highlight.js.
|
||||
*
|
||||
* @param {*} $codeBlock
|
||||
* @param {*} normalizedMimeType
|
||||
*/
|
||||
export async function applySingleBlockSyntaxHighlight($codeBlock, normalizedMimeType) {
|
||||
$codeBlock.parent().toggleClass("hljs");
|
||||
const text = $codeBlock.text();
|
||||
|
||||
if (!window.hljs) {
|
||||
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
|
||||
}
|
||||
|
||||
let highlightedText = null;
|
||||
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
|
||||
highlightedText = hljs.highlightAuto(text);
|
||||
} else if (normalizedMimeType) {
|
||||
const language = mime_types.getHighlightJsNameForMime(normalizedMimeType);
|
||||
if (language) {
|
||||
highlightedText = hljs.highlight(text, { language });
|
||||
} else {
|
||||
console.warn(`Unknown mime type: ${normalizedMimeType}.`);
|
||||
}
|
||||
}
|
||||
|
||||
if (highlightedText) {
|
||||
$codeBlock.html(highlightedText.value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether syntax highlighting should be enabled for code blocks, by querying the value of the `codeblockTheme` option.
|
||||
* @returns whether syntax highlighting should be enabled for code blocks.
|
||||
*/
|
||||
export function isSyntaxHighlightEnabled() {
|
||||
const theme = options.get("codeBlockTheme");
|
||||
return theme && theme !== "none";
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a HTML element, tries to extract the `language-` class name out of it.
|
||||
*
|
||||
* @param {string} el the HTML element from which to extract the language tag.
|
||||
* @returns the normalized MIME type (e.g. `text-css` instead of `language-text-css`).
|
||||
*/
|
||||
function extractLanguageFromClassList(el) {
|
||||
const prefix = "language-";
|
||||
for (const className of el.classList) {
|
||||
if (className.startsWith(prefix)) {
|
||||
return className.substr(prefix.length);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -16,7 +16,7 @@ function toast(options) {
|
||||
);
|
||||
|
||||
$toast.find('.toast-title').text(options.title);
|
||||
$toast.find('.toast-body').text(options.message);
|
||||
$toast.find('.toast-body').html(options.message);
|
||||
|
||||
if (options.id) {
|
||||
$toast.attr("id", `toast-${options.id}`);
|
||||
|
||||
@@ -527,6 +527,58 @@ function downloadSvg(nameWithoutExtension, svgContent) {
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {string} v1 First version string
|
||||
* @param {string} v2 Second version string
|
||||
* @returns {number}
|
||||
*/
|
||||
function compareVersions(v1, v2) {
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings and returns `true` if the latest version is greater than the current version.
|
||||
* @param {string} latestVersion
|
||||
* @param {string} currentVersion
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isUpdateAvailable(latestVersion, currentVersion) {
|
||||
return compareVersions(latestVersion, currentVersion) > 0;
|
||||
}
|
||||
|
||||
export default {
|
||||
reloadFrontendApp,
|
||||
parseDate,
|
||||
@@ -567,5 +619,7 @@ export default {
|
||||
areObjectsEqual,
|
||||
copyHtmlToClipboard,
|
||||
createImageSrcUrl,
|
||||
downloadSvg
|
||||
downloadSvg,
|
||||
compareVersions,
|
||||
isUpdateAvailable
|
||||
};
|
||||
|
||||
@@ -114,10 +114,10 @@ async function handleMessage(event) {
|
||||
await executeFrontendUpdate(message.data.entityChanges);
|
||||
}
|
||||
else if (message.type === 'sync-hash-check-failed') {
|
||||
toastService.showError("Sync check failed!", 60000);
|
||||
toastService.showError(t("ws.sync-check-failed"), 60000);
|
||||
}
|
||||
else if (message.type === 'consistency-checks-failed') {
|
||||
toastService.showError("Consistency checks failed! See logs for details.", 50 * 60000);
|
||||
toastService.showError(t("ws.consistency-checks-failed"), 50 * 60000);
|
||||
}
|
||||
else if (message.type === 'api-log-messages') {
|
||||
appContext.triggerEvent("apiLogMessages", {noteId: message.noteId, messages: message.messages});
|
||||
@@ -189,7 +189,7 @@ async function consumeFrontendUpdateData() {
|
||||
else {
|
||||
console.log("nonProcessedEntityChanges causing the timeout", nonProcessedEntityChanges);
|
||||
|
||||
toastService.showError(`Encountered error "${e.message}", check out the console.`);
|
||||
toastService.showError(t("ws.encountered-error", { message: e.message }));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -347,8 +347,7 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
|
||||
this.$editor.on("click", e => this.handleEditorClick(e));
|
||||
|
||||
/** @property {BalloonEditor} */
|
||||
this.textEditor = await BalloonEditor.create(this.$editor[0], editorConfig);
|
||||
this.textEditor = await CKEditor.BalloonEditor.create(this.$editor[0], editorConfig);
|
||||
this.textEditor.model.document.on('change:data', () => this.dataChanged());
|
||||
this.textEditor.editing.view.document.on('enter', (event, data) => {
|
||||
// disable entering new line - see https://github.com/ckeditor/ckeditor5/issues/9422
|
||||
@@ -358,9 +357,6 @@ export default class AttributeEditorWidget extends NoteContextAwareWidget {
|
||||
|
||||
// disable spellcheck for attribute editor
|
||||
this.textEditor.editing.view.change(writer => writer.setAttribute('spellcheck', 'false', this.textEditor.editing.view.document.getRoot()));
|
||||
|
||||
//await import(/* webpackIgnore: true */'../../libraries/ckeditor/inspector');
|
||||
//CKEditorInspector.attach(this.textEditor);
|
||||
}
|
||||
|
||||
dataChanged() {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Component from "../components/component.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import toastService from "../services/toast.js";
|
||||
|
||||
@@ -84,15 +85,8 @@ class BasicWidget extends Component {
|
||||
render() {
|
||||
try {
|
||||
this.doRender();
|
||||
} catch (e) {
|
||||
toastService.showPersistent({
|
||||
title: t("toast.widget-error.title"),
|
||||
icon: "alert",
|
||||
message: t("toast.widget-error.message", {
|
||||
title: this.widgetTitle,
|
||||
message: e.message
|
||||
})
|
||||
});
|
||||
} catch (e) {
|
||||
this.logRenderingError(e);
|
||||
}
|
||||
|
||||
this.$widget.attr('data-component-id', this.componentId);
|
||||
@@ -131,6 +125,35 @@ class BasicWidget extends Component {
|
||||
return this.$widget;
|
||||
}
|
||||
|
||||
logRenderingError(e) {
|
||||
console.log("Got issue in widget ", this);
|
||||
console.error(e);
|
||||
|
||||
let noteId = this._noteId;
|
||||
if (this._noteId) {
|
||||
froca.getNote(noteId, true).then((note) => {
|
||||
toastService.showPersistent({
|
||||
title: t("toast.widget-error.title"),
|
||||
icon: "alert",
|
||||
message: t("toast.widget-error.message-custom", {
|
||||
id: noteId,
|
||||
title: note.title,
|
||||
message: e.message
|
||||
})
|
||||
});
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.showPersistent({
|
||||
title: t("toast.widget-error.title"),
|
||||
icon: "alert",
|
||||
message: t("toast.widget-error.message-unknown", {
|
||||
message: e.message
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if the widget is enabled. Widgets are enabled by default. Generally setting this to `false` will cause the widget not to be displayed, however it will still be available on the DOM but hidden.
|
||||
* @returns
|
||||
|
||||
@@ -333,7 +333,8 @@ export default class GlobalMenuWidget extends BasicWidget {
|
||||
|
||||
const latestVersion = await this.fetchLatestVersion();
|
||||
this.updateAvailableWidget.updateVersionStatus(latestVersion);
|
||||
this.$updateToLatestVersionButton.toggle(latestVersion > glob.triliumVersion);
|
||||
// Show "click to download" button in options menu if there's a new version available
|
||||
this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
||||
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -127,18 +127,18 @@ export default class NoteActionsWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async convertNoteIntoAttachmentCommand() {
|
||||
if (!await dialogService.confirm(`Are you sure you want to convert note '${this.note.title}' into an attachment of the parent note?`)) {
|
||||
if (!await dialogService.confirm(t("note_actions.convert_into_attachment_prompt", { title: this.note.title }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {attachment: newAttachment} = await server.post(`notes/${this.noteId}/convert-to-attachment`);
|
||||
|
||||
if (!newAttachment) {
|
||||
toastService.showMessage(`Converting note '${this.note.title}' failed.`);
|
||||
toastService.showMessage(t("note_actions.convert_into_attachment_failed", { title: this.note.title }));
|
||||
return;
|
||||
}
|
||||
|
||||
toastService.showMessage(`Note '${newAttachment.title}' has been converted to attachment.`);
|
||||
toastService.showMessage(t("note_actions.convert_into_attachment_successful", { title: newAttachment.title }));
|
||||
await ws.waitForMaxKnownEntityChangeId();
|
||||
await appContext.tabManager.getActiveContext().setNote(newAttachment.ownerId, {
|
||||
viewScope: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import utils from "../../services/utils.js";
|
||||
|
||||
const TPL = `
|
||||
<div style="display: none;">
|
||||
@@ -34,6 +35,6 @@ export default class UpdateAvailableWidget extends BasicWidget {
|
||||
}
|
||||
|
||||
updateVersionStatus(latestVersion) {
|
||||
this.$widget.toggle(latestVersion > glob.triliumVersion);
|
||||
this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ export default class Container extends BasicWidget {
|
||||
|
||||
renderChildren() {
|
||||
for (const widget of this.children) {
|
||||
this.$widget.append(widget.render());
|
||||
try {
|
||||
this.$widget.append(widget.render());
|
||||
} catch (e) {
|
||||
widget.logRenderingError(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,7 +216,7 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
this.$tabContainer.empty();
|
||||
|
||||
for (const ribbonWidget of this.ribbonWidgets) {
|
||||
const ret = ribbonWidget.getTitle(note);
|
||||
const ret = await ribbonWidget.getTitle(note);
|
||||
|
||||
if (!ret.show) {
|
||||
continue;
|
||||
@@ -351,6 +351,16 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
|
||||
*
|
||||
* <p>
|
||||
* We need to refresh the ribbon for cases such as the classic editor which relies on the read-only state.
|
||||
*/
|
||||
readOnlyTemporarilyDisabledEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
getActiveRibbonWidget() {
|
||||
return this.ribbonWidgets.find(ch => ch.componentId === this.lastActiveComponentId)
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const TPL = `
|
||||
</div>
|
||||
|
||||
<div class="delete-notes-list-wrapper">
|
||||
<h4>${t('delete_notes.notes_to_be_deleted')} (<span class="deleted-notes-count"></span>)</h4>
|
||||
<h4>${t('delete_notes.notes_to_be_deleted', { noteCount: '<span class="deleted-notes-count"></span>' })}</h4>
|
||||
|
||||
<ul class="delete-notes-list" style="max-height: 200px; overflow: auto;"></ul>
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@ const TPL = `
|
||||
|
||||
<div class="broken-relations-wrapper">
|
||||
<div class="alert alert-danger">
|
||||
<h4>${t('delete_notes.broken_relations_to_be_deleted')} (<span class="broke-relations-count"></span>)</h4>
|
||||
<h4>${t('delete_notes.broken_relations_to_be_deleted', { relationCount: '<span class="broke-relations-count"></span>'})}</h4>
|
||||
|
||||
<ul class="broken-relations-list" style="max-height: 200px; overflow: auto;"></ul>
|
||||
</div>
|
||||
@@ -126,11 +126,11 @@ export default class DeleteNotesDialog extends BasicWidget {
|
||||
|
||||
for (const attr of response.brokenRelations) {
|
||||
this.$brokenRelationsList.append(
|
||||
$("<li>")
|
||||
.append(`${t('delete_notes.note')} `)
|
||||
.append(await linkService.createLink(attr.value))
|
||||
.append(` ${t('delete_notes.to_be_deleted', {attrName: attr.name})} `)
|
||||
.append(await linkService.createLink(attr.noteId))
|
||||
$("<li>").html(t("delete_notes.deleted_relation_text", {
|
||||
note: (await linkService.createLink(attr.value)).html(),
|
||||
relation: `<code>${attr.name}</code>`,
|
||||
source: (await linkService.createLink(attr.noteId)).html()
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ class NoteContextAwareWidget extends BasicWidget {
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* <p>
|
||||
* If the widget is not enabled, it will not receive `refreshWithNote` updates.
|
||||
*
|
||||
* @returns {boolean} true when an active note exists
|
||||
*/
|
||||
isEnabled() {
|
||||
|
||||
@@ -30,6 +30,7 @@ import ContentWidgetTypeWidget from "./type_widgets/content_widget.js";
|
||||
import AttachmentListTypeWidget from "./type_widgets/attachment_list.js";
|
||||
import AttachmentDetailTypeWidget from "./type_widgets/attachment_detail.js";
|
||||
import MindMapWidget from "./type_widgets/mind_map.js";
|
||||
import { getStylesheetUrl, isSyntaxHighlightEnabled } from "../services/syntax_highlight.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail">
|
||||
@@ -255,6 +256,19 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
const {assetPath} = window.glob;
|
||||
const cssToLoad = [
|
||||
`${assetPath}/node_modules/codemirror/lib/codemirror.css`,
|
||||
`${assetPath}/libraries/ckeditor/ckeditor-content.css`,
|
||||
`${assetPath}/node_modules/bootstrap/dist/css/bootstrap.min.css`,
|
||||
`${assetPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${assetPath}/stylesheets/print.css`,
|
||||
`${assetPath}/stylesheets/relation_map.css`,
|
||||
`${assetPath}/stylesheets/ckeditor-theme.css`
|
||||
];
|
||||
|
||||
if (isSyntaxHighlightEnabled()) {
|
||||
cssToLoad.push(getStylesheetUrl("default:vs"));
|
||||
}
|
||||
|
||||
this.$widget.find('.note-detail-printable:visible').printThis({
|
||||
header: $("<div>")
|
||||
@@ -273,15 +287,7 @@ export default class NoteDetailWidget extends NoteContextAwareWidget {
|
||||
</script>
|
||||
`,
|
||||
importCSS: false,
|
||||
loadCSS: [
|
||||
`${assetPath}/node_modules/codemirror/lib/codemirror.css`,
|
||||
`${assetPath}/libraries/ckeditor/ckeditor-content.css`,
|
||||
`${assetPath}/node_modules/bootstrap/dist/css/bootstrap.min.css`,
|
||||
`${assetPath}/node_modules/katex/dist/katex.min.css`,
|
||||
`${assetPath}/stylesheets/print.css`,
|
||||
`${assetPath}/stylesheets/relation_map.css`,
|
||||
`${assetPath}/stylesheets/ckeditor-theme.css`
|
||||
],
|
||||
loadCSS: cssToLoad,
|
||||
debug: true
|
||||
});
|
||||
}
|
||||
|
||||
@@ -666,8 +666,9 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
const node = this.prepareNode(branch);
|
||||
|
||||
noteList.push(node);
|
||||
if (node) {
|
||||
noteList.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return noteList;
|
||||
@@ -709,7 +710,8 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
const note = branch.getNoteFromCache();
|
||||
|
||||
if (!note) {
|
||||
throw new Error(`Branch '${branch.branchId}' has no child note '${branch.noteId}'`);
|
||||
console.warn(`Branch '${branch.branchId}' has no child note '${branch.noteId}'`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = `${branch.prefix ? (`${branch.prefix} - `) : ""}${note.title}`;
|
||||
@@ -1031,7 +1033,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
activeNode.load(true);
|
||||
activeNode.setExpanded(true, {noAnimation: true});
|
||||
|
||||
toastService.showMessage("Saved search note refreshed.");
|
||||
toastService.showMessage(t("note_tree.saved-search-note-refreshed"));
|
||||
}
|
||||
|
||||
async batchUpdate(cb) {
|
||||
@@ -1075,7 +1077,7 @@ export default class NoteTreeWidget extends NoteContextAwareWidget {
|
||||
node.setExpanded(false);
|
||||
|
||||
if (noneCollapsedYet) {
|
||||
toastService.showMessage("Auto collapsing notes after inactivity...");
|
||||
toastService.showMessage(t("note_tree.auto-collapsing-notes-after-inactivity"));
|
||||
noneCollapsedYet = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
|
||||
this.$noteTypeDropdown.append($typeLink);
|
||||
}
|
||||
|
||||
for (const mimeType of await mimeTypesService.getMimeTypes()) {
|
||||
for (const mimeType of mimeTypesService.getMimeTypes()) {
|
||||
if (!mimeType.enabled) {
|
||||
continue;
|
||||
}
|
||||
@@ -128,7 +128,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
|
||||
|
||||
async findTypeTitle(type, mime) {
|
||||
if (type === 'code') {
|
||||
const mimeTypes = await mimeTypesService.getMimeTypes();
|
||||
const mimeTypes = mimeTypesService.getMimeTypes();
|
||||
const found = mimeTypes.find(mt => mt.mime === mime);
|
||||
|
||||
return found ? found.title : mime;
|
||||
@@ -159,7 +159,7 @@ export default class NoteTypeWidget extends NoteContextAwareWidget {
|
||||
return true;
|
||||
}
|
||||
|
||||
return await dialogService.confirm("It is not recommended to change note type when note content is not empty. Do you want to continue anyway?");
|
||||
return await dialogService.confirm(t("note_types.confirm-change"));
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }) {
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import options from "../../services/options.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
const TPL = `\
|
||||
<div class="classic-toolbar-widget"></div>
|
||||
|
||||
<style>
|
||||
.classic-toolbar-widget {
|
||||
--ck-color-toolbar-background: transparent;
|
||||
--ck-color-button-default-background: transparent;
|
||||
--ck-color-button-default-disabled-background: transparent;
|
||||
min-height: 39px;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget .ck.ck-toolbar {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.classic-toolbar-widget .ck.ck-button.ck-disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
body.mobile .classic-toolbar-widget {
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
body.mobile .classic-toolbar-widget .ck.ck-toolbar {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
/**
|
||||
* Handles the editing toolbar when the CKEditor is in decoupled mode.
|
||||
*
|
||||
* <p>
|
||||
* This toolbar is only enabled if the user has selected the classic CKEditor.
|
||||
*
|
||||
* <p>
|
||||
* The ribbon item is active by default for text notes, as long as they are not in read-only mode.
|
||||
*/
|
||||
export default class ClassicEditorToolbar extends NoteContextAwareWidget {
|
||||
get name() {
|
||||
return "classicEditor";
|
||||
}
|
||||
|
||||
get toggleCommand() {
|
||||
return "toggleRibbonTabClassicEditor";
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
return {
|
||||
show: await this.#shouldDisplay(),
|
||||
activate: true,
|
||||
title: t("classic_editor_toolbar.title"),
|
||||
icon: "bx bx-edit-alt"
|
||||
};
|
||||
}
|
||||
|
||||
async #shouldDisplay() {
|
||||
if (options.get("textNoteEditorType") !== "ckeditor-classic") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.note.type !== "text") {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await this.noteContext.isReadOnly()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,6 +42,10 @@ const TPL = `
|
||||
word-break:keep-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.promoted-attribute-cell input[type="checkbox"] {
|
||||
height: 1.5em;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<div class="promoted-attributes-container"></div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import toastService from "../services/toast.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
|
||||
const WIDGET_TPL = `
|
||||
<div class="card widget">
|
||||
@@ -54,7 +56,9 @@ class RightPanelWidget extends NoteContextAwareWidget {
|
||||
this.$buttons.append(buttonWidget.render());
|
||||
}
|
||||
|
||||
this.initialized = this.doRenderBody();
|
||||
this.initialized = this.doRenderBody().catch(e => {
|
||||
this.logRenderingError(e);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -61,9 +61,9 @@ export default class SearchString extends AbstractSearchOption {
|
||||
|
||||
await this.setAttribute('label', 'searchString', searchString);
|
||||
|
||||
if (this.note.title.startsWith('Search: ')) {
|
||||
if (this.note.title.startsWith(t("search_string.search_prefix"))) {
|
||||
await server.put(`notes/${this.note.noteId}/title`, {
|
||||
title: `Search: ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}`
|
||||
title: `${t("search_string.search_prefix")} ${searchString.length < 30 ? searchString : `${searchString.substr(0, 30)}…`}`
|
||||
});
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import utils from "../services/utils.js";
|
||||
@@ -37,11 +38,11 @@ const TAB_TPL = `
|
||||
<div class="note-tab-drag-handle"></div>
|
||||
<div class="note-tab-icon"></div>
|
||||
<div class="note-tab-title"></div>
|
||||
<div class="note-tab-close bx bx-x" title="Close tab" data-trigger-command="closeActiveTab"></div>
|
||||
<div class="note-tab-close bx bx-x" title="${t('tab_row.close_tab')}" data-trigger-command="closeActiveTab"></div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="Add new tab">+</div>`;
|
||||
const NEW_TAB_BUTTON_TPL = `<div class="note-new-tab" data-trigger-command="openNewTab" title="${t('tab_row.add_new_tab')}">+</div>`;
|
||||
const FILLER_TPL = `<div class="tab-row-filler"></div>`;
|
||||
|
||||
const TAB_ROW_TPL = `
|
||||
@@ -258,10 +259,12 @@ export default class TabRowWidget extends BasicWidget {
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{title: "Close", command: "closeTab", uiIcon: "bx bx-x"},
|
||||
{title: "Close other tabs", command: "closeOtherTabs", uiIcon: "bx bx-x"},
|
||||
{title: "Close all tabs", command: "closeAllTabs", uiIcon: "bx bx-x"},
|
||||
{title: "Move this tab to a new window", command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"}
|
||||
{title: t('tab_row.close'), command: "closeTab", uiIcon: "bx bx-x"},
|
||||
{title: t('tab_row.close_other_tabs'), command: "closeOtherTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.length !== 1},
|
||||
{title: t('tab_row.close_right_tabs'), command: "closeRightTabs", uiIcon: "bx bx-empty", enabled: appContext.tabManager.noteContexts.at(-1).ntxId !== ntxId},
|
||||
{title: t('tab_row.close_all_tabs'), command: "closeAllTabs", uiIcon: "bx bx-empty"},
|
||||
{ title: "----" },
|
||||
{title: t('tab_row.move_tab_to_new_window'), command: "moveTabToNewWindow", uiIcon: "bx bx-window-open"}
|
||||
],
|
||||
selectMenuItemHandler: ({command}) => {
|
||||
this.triggerCommand(command, {ntxId});
|
||||
@@ -387,7 +390,7 @@ export default class TabRowWidget extends BasicWidget {
|
||||
this.$newTab.before($tab);
|
||||
this.setVisibility();
|
||||
this.setTabCloseEvent($tab);
|
||||
this.updateTitle($tab, 'New tab');
|
||||
this.updateTitle($tab, t('tab_row.new_tab'));
|
||||
this.cleanUpPreviouslyDraggedTabs();
|
||||
this.layoutTabs();
|
||||
this.setupDraggabilly();
|
||||
@@ -672,7 +675,7 @@ export default class TabRowWidget extends BasicWidget {
|
||||
const {note} = noteContext;
|
||||
|
||||
if (!note) {
|
||||
this.updateTitle($tab, 'New tab');
|
||||
this.updateTitle($tab, t('tab_row.new_tab'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* to the wrong heading (although what "right" means in those cases is not
|
||||
* clear), but it won't crash.
|
||||
*/
|
||||
|
||||
import { t } from "../services/i18n.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import RightPanelWidget from "./right_panel_widget.js";
|
||||
import options from "../services/options.js";
|
||||
@@ -55,14 +55,14 @@ const TPL = `<div class="toc-widget">
|
||||
|
||||
export default class TocWidget extends RightPanelWidget {
|
||||
get widgetTitle() {
|
||||
return "Table of Contents";
|
||||
return t("toc.table_of_contents");
|
||||
}
|
||||
|
||||
get widgetButtons() {
|
||||
return [
|
||||
new OnClickButtonWidget()
|
||||
.icon("bx-cog")
|
||||
.title("Options")
|
||||
.title(t("toc.options"))
|
||||
.titlePlacement("left")
|
||||
.onClick(() => appContext.tabManager.openContextWithNote('_optionsTextNotes', {activate: true}))
|
||||
.class("icon-action"),
|
||||
@@ -125,18 +125,18 @@ export default class TocWidget extends RightPanelWidget {
|
||||
*
|
||||
* @param {string} html Note's html content
|
||||
* @returns {string} The HTML content with mathematical formulas rendered by KaTeX.
|
||||
*/
|
||||
*/
|
||||
async replaceMathTextWithKatax(html) {
|
||||
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
|
||||
var matches = [...html.matchAll(mathTextRegex)];
|
||||
let modifiedText = html;
|
||||
|
||||
|
||||
if (matches.length > 0) {
|
||||
// Process all matches asynchronously
|
||||
for (const match of matches) {
|
||||
let latexCode = match[1];
|
||||
let rendered;
|
||||
|
||||
|
||||
try {
|
||||
rendered = katex.renderToString(latexCode, {
|
||||
throwOnError: false
|
||||
@@ -158,7 +158,7 @@ export default class TocWidget extends RightPanelWidget {
|
||||
rendered = match[0]; // Fall back to original on error
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Replace the matched formula in the modified text
|
||||
modifiedText = modifiedText.replace(match[0], rendered);
|
||||
}
|
||||
|
||||
120
src/public/app/widgets/type_widgets/abstract_code_type_widget.js
Normal file
120
src/public/app/widgets/type_widgets/abstract_code_type_widget.js
Normal file
@@ -0,0 +1,120 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import options from "../../services/options.js";
|
||||
|
||||
/**
|
||||
* An abstract {@link TypeWidget} which implements the CodeMirror editor, meant to be used as a parent for
|
||||
* widgets requiring the editor.
|
||||
*
|
||||
* The widget handles the loading and initialization of the CodeMirror editor, as well as some common
|
||||
* actions.
|
||||
*
|
||||
* The derived class must:
|
||||
*
|
||||
* - Define `$editor` in the constructor.
|
||||
* - Call `super.doRender()` in the extended class.
|
||||
* - Call `this._update(note, content)` in `#doRefresh(note)`.
|
||||
*/
|
||||
export default class AbstractCodeTypeWidget extends TypeWidget {
|
||||
|
||||
doRender() {
|
||||
this.initialized = this.#initEditor();
|
||||
}
|
||||
|
||||
async #initEditor() {
|
||||
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
|
||||
|
||||
// these conflict with backward/forward navigation shortcuts
|
||||
delete CodeMirror.keyMap.default["Alt-Left"];
|
||||
delete CodeMirror.keyMap.default["Alt-Right"];
|
||||
|
||||
CodeMirror.modeURL = `${window.glob.assetPath}/node_modules/codemirror/mode/%N/%N.js`;
|
||||
CodeMirror.modeInfo.find(mode=>mode.name === "JavaScript").mimes.push(...["application/javascript;env=frontend", "application/javascript;env=backend"]);
|
||||
CodeMirror.modeInfo.find(mode=>mode.name === "SQLite").mimes=["text/x-sqlite", "text/x-sqlite;schema=trilium"];
|
||||
|
||||
this.codeEditor = CodeMirror(this.$editor[0], {
|
||||
value: "",
|
||||
viewportMargin: Infinity,
|
||||
indentUnit: 4,
|
||||
matchBrackets: true,
|
||||
matchTags: {bothTags: true},
|
||||
highlightSelectionMatches: {showToken: false, annotateScrollbar: false},
|
||||
lineNumbers: true,
|
||||
// we line wrap partly also because without it horizontal scrollbar displays only when you scroll
|
||||
// all the way to the bottom of the note. With line wrap, there's no horizontal scrollbar so no problem
|
||||
lineWrapping: options.is('codeLineWrapEnabled'),
|
||||
...this.getExtraOpts()
|
||||
});
|
||||
this.onEditorInitialized();
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be extended in derived classes to add extra options to the CodeMirror constructor. The options are appended
|
||||
* at the end, so it is possible to override the default values introduced by the abstract editor as well.
|
||||
*
|
||||
* @returns the extra options to be passed to the CodeMirror constructor.
|
||||
*/
|
||||
getExtraOpts() {
|
||||
return {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Called as soon as the CodeMirror library has been loaded and the editor was constructed. Can be extended in
|
||||
* derived classes to add additional functionality or to register event handlers.
|
||||
*
|
||||
* By default, it does nothing.
|
||||
*/
|
||||
onEditorInitialized() {
|
||||
// Do nothing by default.
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called by the derived classes in `#doRefresh(note)` in order to react to changes.
|
||||
*
|
||||
* @param {*} note the note that was changed.
|
||||
* @param {*} content the new content of the note.
|
||||
*/
|
||||
_update(note, content) {
|
||||
// CodeMirror breaks pretty badly on null, so even though it shouldn't happen (guarded by a consistency check)
|
||||
// we provide fallback
|
||||
this.codeEditor.setValue(content || "");
|
||||
this.codeEditor.clearHistory();
|
||||
|
||||
let info = CodeMirror.findModeByMIME(note.mime);
|
||||
if (!info) {
|
||||
// Switch back to plain text if CodeMirror does not have a mode for whatever MIME type we're editing.
|
||||
// To avoid inheriting a mode from a previously open code note.
|
||||
info = CodeMirror.findModeByMIME("text/plain");
|
||||
}
|
||||
|
||||
this.codeEditor.setOption("mode", info.mime);
|
||||
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
|
||||
};
|
||||
|
||||
show() {
|
||||
this.$widget.show();
|
||||
|
||||
if (this.codeEditor) { // show can be called before render
|
||||
this.codeEditor.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$editor.focus();
|
||||
this.codeEditor.focus();
|
||||
}
|
||||
|
||||
scrollToEnd() {
|
||||
this.codeEditor.setCursor(this.codeEditor.lineCount(), 0);
|
||||
this.codeEditor.focus();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.codeEditor) {
|
||||
this.spacedUpdate.allowUpdateWithoutChange(() => {
|
||||
this.codeEditor.setValue('');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -4,8 +4,15 @@ import froca from "../../services/froca.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import contentRenderer from "../../services/content_renderer.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import options from "../../services/options.js";
|
||||
|
||||
export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
this.refreshCodeBlockOptions();
|
||||
}
|
||||
|
||||
setupImageOpening(singleClickOpens) {
|
||||
this.$widget.on("dblclick", "img", e => this.openImageInCurrentTab($(e.target)));
|
||||
|
||||
@@ -25,7 +32,7 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
|
||||
async openImageInCurrentTab($img) {
|
||||
const { noteId, viewScope } = await this.parseFromImage($img);
|
||||
|
||||
|
||||
if (noteId) {
|
||||
appContext.tabManager.getActiveContext().setNote(noteId, { viewScope });
|
||||
} else {
|
||||
@@ -33,8 +40,8 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
}
|
||||
}
|
||||
|
||||
openImageInNewTab($img) {
|
||||
const { noteId, viewScope } = this.parseFromImage($img);
|
||||
async openImageInNewTab($img) {
|
||||
const { noteId, viewScope } = await this.parseFromImage($img);
|
||||
|
||||
if (noteId) {
|
||||
appContext.tabManager.openTabWithNoteWithHoisting(noteId, { viewScope });
|
||||
@@ -108,4 +115,16 @@ export default class AbstractTextTypeWidget extends TypeWidget {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refreshCodeBlockOptions() {
|
||||
const wordWrap = options.is("codeBlockWordWrap");
|
||||
this.$widget.toggleClass("word-wrap", wordWrap);
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
if (loadResults.isOptionReloaded("codeBlockWordWrap")) {
|
||||
this.refreshCodeBlockOptions();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
354
src/public/app/widgets/type_widgets/ckeditor/syntax_highlight.js
Normal file
354
src/public/app/widgets/type_widgets/ckeditor/syntax_highlight.js
Normal file
@@ -0,0 +1,354 @@
|
||||
/*
|
||||
* This code is an adaptation of https://github.com/antoniotejada/Trilium-SyntaxHighlightWidget with additional improvements, such as:
|
||||
*
|
||||
* - support for selecting the language manually;
|
||||
* - support for determining the language automatically, if a special language is selected ("Auto-detected");
|
||||
* - limit for highlighting.
|
||||
*
|
||||
* TODO: Generally this class can be done directly in the CKEditor repository.
|
||||
*/
|
||||
|
||||
import library_loader from "../../../services/library_loader.js";
|
||||
import mime_types from "../../../services/mime_types.js";
|
||||
import { isSyntaxHighlightEnabled } from "../../../services/syntax_highlight.js";
|
||||
|
||||
export async function initSyntaxHighlighting(editor) {
|
||||
if (!isSyntaxHighlightEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
|
||||
initTextEditor(editor);
|
||||
}
|
||||
|
||||
const HIGHLIGHT_MAX_BLOCK_COUNT = 500;
|
||||
|
||||
const tag = "SyntaxHighlightWidget";
|
||||
const debugLevels = ["error", "warn", "info", "log", "debug"];
|
||||
const debugLevel = "debug";
|
||||
|
||||
let warn = function() {};
|
||||
if (debugLevel >= debugLevels.indexOf("warn")) {
|
||||
warn = console.warn.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let info = function() {};
|
||||
if (debugLevel >= debugLevels.indexOf("info")) {
|
||||
info = console.info.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let log = function() {};
|
||||
if (debugLevel >= debugLevels.indexOf("log")) {
|
||||
log = console.log.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
let dbg = function() {};
|
||||
if (debugLevel >= debugLevels.indexOf("debug")) {
|
||||
dbg = console.debug.bind(console, tag + ": ");
|
||||
}
|
||||
|
||||
function assert(e, msg) {
|
||||
console.assert(e, tag + ": " + msg);
|
||||
}
|
||||
|
||||
// TODO: Should this be scoped to note?
|
||||
let markerCounter = 0;
|
||||
|
||||
function initTextEditor(textEditor) {
|
||||
log("initTextEditor");
|
||||
|
||||
let widget = this;
|
||||
const document = textEditor.model.document;
|
||||
|
||||
// Create a conversion from model to view that converts
|
||||
// hljs:hljsClassName:uniqueId into a span with hljsClassName
|
||||
// See the list of hljs class names at
|
||||
// https://github.com/highlightjs/highlight.js/blob/6b8c831f00c4e87ecd2189ebbd0bb3bbdde66c02/docs/css-classes-reference.rst
|
||||
|
||||
textEditor.conversion.for('editingDowncast').markerToHighlight( {
|
||||
model: "hljs",
|
||||
view: ( { markerName } ) => {
|
||||
dbg("markerName " + markerName);
|
||||
// markerName has the pattern addMarker:cssClassName:uniqueId
|
||||
const [ , cssClassName, id ] = markerName.split( ':' );
|
||||
|
||||
// The original code at
|
||||
// https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-find-and-replace/src/findandreplaceediting.js
|
||||
// has this comment
|
||||
// Marker removal from the view has a bug:
|
||||
// https://github.com/ckeditor/ckeditor5/issues/7499
|
||||
// A minimal option is to return a new object for each converted marker...
|
||||
return {
|
||||
name: 'span',
|
||||
classes: [ cssClassName ],
|
||||
attributes: {
|
||||
// ...however, adding a unique attribute should be future-proof..
|
||||
'data-syntax-result': id
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// XXX This is done at BalloonEditor.create time, so it assumes this
|
||||
// document is always attached to this textEditor, empirically that
|
||||
// seems to be the case even with two splits showing the same note,
|
||||
// it's not clear if CKEditor5 has apis to attach and detach
|
||||
// documents around
|
||||
document.registerPostFixer(function(writer) {
|
||||
log("postFixer");
|
||||
// Postfixers are a simpler way of tracking changes than onchange
|
||||
// See
|
||||
// https://github.com/ckeditor/ckeditor5/blob/b53d2a4b49679b072f4ae781ac094e7e831cfb14/packages/ckeditor5-block-quote/src/blockquoteediting.js#L54
|
||||
const changes = document.differ.getChanges();
|
||||
let dirtyCodeBlocks = new Set();
|
||||
|
||||
for (const change of changes) {
|
||||
dbg("change " + JSON.stringify(change));
|
||||
|
||||
if ((change.type == "insert") && (change.name == "codeBlock")) {
|
||||
// A new code block was inserted
|
||||
const codeBlock = change.position.nodeAfter;
|
||||
// Even if it's a new codeblock, it needs dirtying in case
|
||||
// it already has children, like when pasting one or more
|
||||
// full codeblocks, undoing a delete, changing the language,
|
||||
// etc (the postfixer won't get later changes for those).
|
||||
log("dirtying inserted codeBlock " + JSON.stringify(codeBlock.toJSON()));
|
||||
dirtyCodeBlocks.add(codeBlock);
|
||||
|
||||
} else if (change.type == "remove" && (change.name == "codeBlock")) {
|
||||
// An existing codeblock was removed, do nothing. Note the
|
||||
// node is no longer in the editor so the codeblock cannot
|
||||
// be inspected here. No need to dirty the codeblock since
|
||||
// it has been removed
|
||||
log("removing codeBlock at path " + JSON.stringify(change.position.toJSON()));
|
||||
|
||||
} else if (((change.type == "remove") || (change.type == "insert")) &&
|
||||
change.position.parent.is('element', 'codeBlock')) {
|
||||
// Text was added or removed from the codeblock, force a
|
||||
// highlight
|
||||
const codeBlock = change.position.parent;
|
||||
log("dirtying codeBlock " + JSON.stringify(codeBlock.toJSON()));
|
||||
dirtyCodeBlocks.add(codeBlock);
|
||||
}
|
||||
}
|
||||
for (let codeBlock of dirtyCodeBlocks) {
|
||||
highlightCodeBlock(codeBlock, writer);
|
||||
}
|
||||
// Adding markers doesn't modify the document data so no need for
|
||||
// postfixers to run again
|
||||
return false;
|
||||
});
|
||||
|
||||
// This assumes the document is empty and a explicit call to highlight
|
||||
// is not necessary here. Empty documents have a single children of type
|
||||
// paragraph with no text
|
||||
assert((document.getRoot().childCount == 1) &&
|
||||
(document.getRoot().getChild(0).name == "paragraph") &&
|
||||
document.getRoot().getChild(0).isEmpty);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This implements highlighting via ephemeral markers (not stored in the
|
||||
* document).
|
||||
*
|
||||
* XXX Another option would be to use formatting markers, which would have
|
||||
* the benefit of making it work for readonly notes. On the flip side,
|
||||
* the formatting would be stored with the note and it would need a
|
||||
* way to remove that formatting when editing back the note.
|
||||
*/
|
||||
function highlightCodeBlock(codeBlock, writer) {
|
||||
log("highlighting codeblock " + JSON.stringify(codeBlock.toJSON()));
|
||||
const model = codeBlock.root.document.model;
|
||||
|
||||
// Can't invoke addMarker with an already existing marker name,
|
||||
// clear all highlight markers first. Marker names follow the
|
||||
// pattern hljs:cssClassName:uniqueId, eg hljs:hljs-comment:1
|
||||
const codeBlockRange = model.createRangeIn(codeBlock);
|
||||
for (const marker of model.markers.getMarkersIntersectingRange(codeBlockRange)) {
|
||||
dbg("removing marker " + marker.name);
|
||||
writer.removeMarker(marker.name);
|
||||
}
|
||||
|
||||
// Don't highlight if plaintext (note this needs to remove the markers
|
||||
// above first, in case this was a switch from non plaintext to
|
||||
// plaintext)
|
||||
const mimeType = codeBlock.getAttribute("language");
|
||||
if (mimeType == "text-plain") {
|
||||
// XXX There's actually a plaintext language that could be used
|
||||
// if you wanted the non-highlight formatting of
|
||||
// highlight.js css applied, see
|
||||
// https://github.com/highlightjs/highlight.js/issues/700
|
||||
log("not highlighting plaintext codeblock");
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the corresponding language for the given mimetype.
|
||||
const highlightJsLanguage = mime_types.getHighlightJsNameForMime(mimeType);
|
||||
|
||||
if (mimeType !== mime_types.MIME_TYPE_AUTO && !highlightJsLanguage) {
|
||||
console.warn(`Unsupported highlight.js for mime type ${mimeType}.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't highlight if the code is too big, as the typing performance will be highly degraded.
|
||||
if (codeBlock.childCount >= HIGHLIGHT_MAX_BLOCK_COUNT) {
|
||||
return;
|
||||
}
|
||||
|
||||
// highlight.js needs the full text without HTML tags, eg for the
|
||||
// text
|
||||
// #include <stdio.h>
|
||||
// the highlighted html is
|
||||
// <span class="hljs-meta">#<span class="hljs-keyword">include</span> <span class="hljs-string"><stdio.h></span></span>
|
||||
// But CKEditor codeblocks have <br> instead of \n
|
||||
|
||||
// Do a two pass algorithm:
|
||||
// - First pass collect the codeblock children text, change <br> to
|
||||
// \n
|
||||
// - invoke highlight.js on the collected text generating html
|
||||
// - Second pass parse the highlighted html spans and match each
|
||||
// char to the CodeBlock text. Issue addMarker CKEditor calls for
|
||||
// each span
|
||||
|
||||
// XXX This is brittle and assumes how highlight.js generates html
|
||||
// (blanks, which characters escapes, etc), a better approach
|
||||
// would be to use highlight.js beta api TreeTokenizer?
|
||||
|
||||
// Collect all the text nodes to pass to the highlighter Text is
|
||||
// direct children of the codeBlock
|
||||
let text = "";
|
||||
for (let i = 0; i < codeBlock.childCount; ++i) {
|
||||
let child = codeBlock.getChild(i);
|
||||
|
||||
// We only expect text and br elements here
|
||||
if (child.is("$text")) {
|
||||
dbg("child text " + child.data);
|
||||
text += child.data;
|
||||
|
||||
} else if (child.is("element") &&
|
||||
(child.name == "softBreak")) {
|
||||
dbg("softBreak");
|
||||
text += "\n";
|
||||
|
||||
} else {
|
||||
warn("Unkown child " + JSON.stringify(child.toJSON()));
|
||||
}
|
||||
}
|
||||
|
||||
let highlightRes;
|
||||
if (mimeType === mime_types.MIME_TYPE_AUTO) {
|
||||
highlightRes = hljs.highlightAuto(text);
|
||||
} else {
|
||||
highlightRes = hljs.highlight(text, { language: highlightJsLanguage });
|
||||
}
|
||||
dbg("text\n" + text);
|
||||
dbg("html\n" + highlightRes.value);
|
||||
|
||||
let iHtml = 0;
|
||||
let html = highlightRes.value;
|
||||
let spanStack = [];
|
||||
let iChild = -1;
|
||||
let childText = "";
|
||||
let child = null;
|
||||
let iChildText = 0;
|
||||
|
||||
while (iHtml < html.length) {
|
||||
// Advance the text index and fetch a new child if necessary
|
||||
if (iChildText >= childText.length) {
|
||||
iChild++;
|
||||
if (iChild < codeBlock.childCount) {
|
||||
dbg("Fetching child " + iChild);
|
||||
child = codeBlock.getChild(iChild);
|
||||
if (child.is("$text")) {
|
||||
dbg("child text " + child.data);
|
||||
childText = child.data;
|
||||
iChildText = 0;
|
||||
} else if (child.is("element", "softBreak")) {
|
||||
dbg("softBreak");
|
||||
iChildText = 0;
|
||||
childText = "\n";
|
||||
} else {
|
||||
warn("child unknown!!!");
|
||||
}
|
||||
} else {
|
||||
// Don't bail if beyond the last children, since there's
|
||||
// still html text, it must be a closing span tag that
|
||||
// needs to be dealt with below
|
||||
childText = "";
|
||||
}
|
||||
}
|
||||
|
||||
// This parsing is made slightly simpler and faster by only
|
||||
// expecting <span> and </span> tags in the highlighted html
|
||||
if ((html[iHtml] == "<") && (html[iHtml+1] != "/")) {
|
||||
// new span, note they can be nested eg C preprocessor lines
|
||||
// are inside a hljs-meta span, hljs-title function names
|
||||
// inside a hljs-function span, etc
|
||||
let iStartQuot = html.indexOf("\"", iHtml+1);
|
||||
let iEndQuot = html.indexOf("\"", iStartQuot+1);
|
||||
let className = html.slice(iStartQuot+1, iEndQuot);
|
||||
// XXX highlight js uses scope for Python "title function_",
|
||||
// etc for now just use the first style only
|
||||
// See https://highlightjs.readthedocs.io/en/latest/css-classes-reference.html#a-note-on-scopes-with-sub-scopes
|
||||
let iBlank = className.indexOf(" ");
|
||||
if (iBlank > 0) {
|
||||
className = className.slice(0, iBlank);
|
||||
}
|
||||
dbg("Found span start " + className);
|
||||
|
||||
iHtml = html.indexOf(">", iHtml) + 1;
|
||||
|
||||
// push the span
|
||||
let posStart = writer.createPositionAt(codeBlock, child.startOffset + iChildText);
|
||||
spanStack.push({ "className" : className, "posStart": posStart});
|
||||
|
||||
} else if ((html[iHtml] == "<") && (html[iHtml+1] == "/")) {
|
||||
// Done with this span, pop the span and mark the range
|
||||
iHtml = html.indexOf(">", iHtml+1) + 1;
|
||||
|
||||
let stackTop = spanStack.pop();
|
||||
let posStart = stackTop.posStart;
|
||||
let className = stackTop.className;
|
||||
let posEnd = writer.createPositionAt(codeBlock, child.startOffset + iChildText);
|
||||
let range = writer.createRange(posStart, posEnd);
|
||||
let markerName = "hljs:" + className + ":" + markerCounter;
|
||||
// Use an incrementing number for the uniqueId, random of
|
||||
// 10000000 is known to cause collisions with a few
|
||||
// codeblocks of 10s of lines on real notes (each line is
|
||||
// one or more marker).
|
||||
// Wrap-around for good measure so all numbers are positive
|
||||
// XXX Another option is to catch the exception and retry or
|
||||
// go through the markers and get the largest + 1
|
||||
markerCounter = (markerCounter + 1) & 0xFFFFFF;
|
||||
dbg("Found span end " + className);
|
||||
dbg("Adding marker " + markerName + ": " + JSON.stringify(range.toJSON()));
|
||||
writer.addMarker(markerName, {"range": range, "usingOperation": false});
|
||||
|
||||
} else {
|
||||
// Text, we should also have text in the children
|
||||
assert(
|
||||
((iChild < codeBlock.childCount) && (iChildText < childText.length)),
|
||||
"Found text in html with no corresponding child text!!!!"
|
||||
);
|
||||
if (html[iHtml] == "&") {
|
||||
// highlight.js only encodes
|
||||
// .replace(/&/g, '&')
|
||||
// .replace(/</g, '<')
|
||||
// .replace(/>/g, '>')
|
||||
// .replace(/"/g, '"')
|
||||
// .replace(/'/g, ''');
|
||||
// see https://github.com/highlightjs/highlight.js/blob/7addd66c19036eccd7c602af61f1ed84d215c77d/src/lib/utils.js#L5
|
||||
let iAmpEnd = html.indexOf(";", iHtml);
|
||||
dbg(html.slice(iHtml, iAmpEnd));
|
||||
iHtml = iAmpEnd + 1;
|
||||
} else {
|
||||
// regular text
|
||||
dbg(html[iHtml]);
|
||||
iHtml++;
|
||||
}
|
||||
iChildText++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,8 @@ import BackendLogWidget from "./content/backend_log.js";
|
||||
import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_timeout.js";
|
||||
import RibbonOptions from "./options/appearance/ribbon.js";
|
||||
import LocalizationOptions from "./options/appearance/i18n.js";
|
||||
import CodeBlockOptions from "./options/appearance/code_block.js";
|
||||
import EditorOptions from "./options/text_notes/editor.js";
|
||||
|
||||
const TPL = `<div class="note-detail-content-widget note-detail-printable">
|
||||
<style>
|
||||
@@ -59,6 +61,7 @@ const CONTENT_WIDGETS = {
|
||||
LocalizationOptions,
|
||||
ThemeOptions,
|
||||
FontsOptions,
|
||||
CodeBlockOptions,
|
||||
ZoomFactorOptions,
|
||||
NativeTitleBarOptions,
|
||||
MaxContentWidthOptions,
|
||||
@@ -66,6 +69,7 @@ const CONTENT_WIDGETS = {
|
||||
],
|
||||
_optionsShortcuts: [ KeyboardShortcutsOptions ],
|
||||
_optionsTextNotes: [
|
||||
EditorOptions,
|
||||
HeadingStyleOptions,
|
||||
TableOfContentsOptions,
|
||||
HighlightsListOptions,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import keyboardActionService from "../../services/keyboard_actions.js";
|
||||
import options from "../../services/options.js";
|
||||
import AbstractCodeTypeWidget from "./abstract_code_type_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-code note-detail-printable">
|
||||
@@ -21,53 +20,31 @@ const TPL = `
|
||||
<div class="note-detail-code-editor"></div>
|
||||
</div>`;
|
||||
|
||||
export default class EditableCodeTypeWidget extends TypeWidget {
|
||||
export default class EditableCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||
static getType() { return "editableCode"; }
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
this.$editor = this.$widget.find('.note-detail-code-editor');
|
||||
|
||||
keyboardActionService.setupActionsForElement('code-detail', this.$widget, this);
|
||||
|
||||
super.doRender();
|
||||
|
||||
this.initialized = this.initEditor();
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
async initEditor() {
|
||||
await libraryLoader.requireLibrary(libraryLoader.CODE_MIRROR);
|
||||
|
||||
CodeMirror.keyMap.default["Shift-Tab"] = "indentLess";
|
||||
CodeMirror.keyMap.default["Tab"] = "indentMore";
|
||||
|
||||
// these conflict with backward/forward navigation shortcuts
|
||||
delete CodeMirror.keyMap.default["Alt-Left"];
|
||||
delete CodeMirror.keyMap.default["Alt-Right"];
|
||||
|
||||
CodeMirror.modeURL = `${window.glob.assetPath}/node_modules/codemirror/mode/%N/%N.js`;
|
||||
CodeMirror.modeInfo.find(mode=>mode.name === "JavaScript").mimes.push(...["application/javascript;env=frontend", "application/javascript;env=backend"]);
|
||||
CodeMirror.modeInfo.find(mode=>mode.name === "SQLite").mimes=["text/x-sqlite", "text/x-sqlite;schema=trilium"];
|
||||
|
||||
this.codeEditor = CodeMirror(this.$editor[0], {
|
||||
value: "",
|
||||
viewportMargin: Infinity,
|
||||
indentUnit: 4,
|
||||
matchBrackets: true,
|
||||
|
||||
getExtraOpts() {
|
||||
return {
|
||||
keyMap: options.is('vimKeymapEnabled') ? "vim": "default",
|
||||
matchTags: {bothTags: true},
|
||||
highlightSelectionMatches: {showToken: false, annotateScrollbar: false},
|
||||
lint: true,
|
||||
gutters: ["CodeMirror-lint-markers"],
|
||||
lineNumbers: true,
|
||||
tabindex: 300,
|
||||
// we line wrap partly also because without it horizontal scrollbar displays only when you scroll
|
||||
// all the way to the bottom of the note. With line wrap, there's no horizontal scrollbar so no problem
|
||||
lineWrapping: options.is('codeLineWrapEnabled'),
|
||||
dragDrop: false, // with true the editor inlines dropped files which is not what we expect
|
||||
placeholder: t('editable_code.placeholder'),
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
onEditorInitialized() {
|
||||
this.codeEditor.on('change', () => this.spacedUpdate.scheduleUpdate());
|
||||
}
|
||||
|
||||
@@ -75,57 +52,18 @@ export default class EditableCodeTypeWidget extends TypeWidget {
|
||||
const blob = await this.note.getBlob();
|
||||
|
||||
await this.spacedUpdate.allowUpdateWithoutChange(() => {
|
||||
// CodeMirror breaks pretty badly on null, so even though it shouldn't happen (guarded by a consistency check)
|
||||
// we provide fallback
|
||||
this.codeEditor.setValue(blob.content || "");
|
||||
this.codeEditor.clearHistory();
|
||||
|
||||
let info = CodeMirror.findModeByMIME(note.mime);
|
||||
if (!info) {
|
||||
// Switch back to plain text if CodeMirror does not have a mode for whatever MIME type we're editing.
|
||||
// To avoid inheriting a mode from a previously open code note.
|
||||
info = CodeMirror.findModeByMIME("text/plain");
|
||||
}
|
||||
|
||||
this.codeEditor.setOption("mode", info.mime);
|
||||
CodeMirror.autoLoadMode(this.codeEditor, info.mode);
|
||||
this._update(note, blob.content);
|
||||
});
|
||||
|
||||
this.show();
|
||||
}
|
||||
|
||||
show() {
|
||||
this.$widget.show();
|
||||
|
||||
if (this.codeEditor) { // show can be called before render
|
||||
this.codeEditor.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
getData() {
|
||||
return {
|
||||
content: this.codeEditor.getValue()
|
||||
};
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.$editor.focus();
|
||||
this.codeEditor.focus();
|
||||
}
|
||||
|
||||
scrollToEnd() {
|
||||
this.codeEditor.setCursor(this.codeEditor.lineCount(), 0);
|
||||
this.codeEditor.focus();
|
||||
}
|
||||
|
||||
cleanup() {
|
||||
if (this.codeEditor) {
|
||||
this.spacedUpdate.allowUpdateWithoutChange(() => {
|
||||
this.codeEditor.setValue('');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async executeWithCodeEditorEvent({resolve, ntxId}) {
|
||||
if (!this.isNoteContext(ntxId)) {
|
||||
return;
|
||||
|
||||
@@ -10,6 +10,8 @@ import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
||||
import link from "../../services/link.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import { initSyntaxHighlighting } from "./ckeditor/syntax_highlight.js";
|
||||
import options from "../../services/options.js";
|
||||
|
||||
const ENABLE_INSPECTOR = false;
|
||||
|
||||
@@ -87,6 +89,29 @@ const TPL = `
|
||||
</div>
|
||||
`;
|
||||
|
||||
function buildListOfLanguages() {
|
||||
const userLanguages = (mimeTypesService.getMimeTypes())
|
||||
.filter(mt => mt.enabled)
|
||||
.map(mt => ({
|
||||
language: mimeTypesService.normalizeMimeTypeForCKEditor(mt.mime),
|
||||
label: mt.title
|
||||
}));
|
||||
|
||||
return [
|
||||
{
|
||||
language: mimeTypesService.MIME_TYPE_AUTO,
|
||||
label: t("editable-text.auto-detect-language")
|
||||
},
|
||||
...userLanguages
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The editor can operate into two distinct modes:
|
||||
*
|
||||
* - Ballon block mode, in which there is a floating toolbar for the selected text, but another floating button for the entire block (i.e. paragraph).
|
||||
* - Decoupled mode, in which the editing toolbar is actually added on the client side (in {@link ClassicEditorToolbar}), see https://ckeditor.com/docs/ckeditor5/latest/examples/framework/bottom-toolbar-editor.html for an example on how the decoupled editor works.
|
||||
*/
|
||||
export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
static getType() { return "editableText"; }
|
||||
|
||||
@@ -105,21 +130,17 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
|
||||
async initEditor() {
|
||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||
const isClassicEditor = (options.get("textNoteEditorType") === "ckeditor-classic")
|
||||
const editorClass = (isClassicEditor ? CKEditor.DecoupledEditor : CKEditor.BalloonEditor);
|
||||
|
||||
const codeBlockLanguages =
|
||||
(await mimeTypesService.getMimeTypes())
|
||||
.filter(mt => mt.enabled)
|
||||
.map(mt => ({
|
||||
language: mt.mime.toLowerCase().replace(/[\W_]+/g,"-"),
|
||||
label: mt.title
|
||||
}));
|
||||
const codeBlockLanguages = buildListOfLanguages();
|
||||
|
||||
// CKEditor since version 12 needs the element to be visible before initialization. At the same time,
|
||||
// we want to avoid flicker - i.e., show editor only once everything is ready. That's why we have separate
|
||||
// display of $widget in both branches.
|
||||
this.$widget.show();
|
||||
|
||||
this.watchdog = new EditorWatchdog(BalloonEditor, {
|
||||
this.watchdog = new CKEditor.EditorWatchdog(editorClass, {
|
||||
// An average number of milliseconds between the last editor errors (defaults to 5000).
|
||||
// When the period of time between errors is lower than that and the crashNumberLimit
|
||||
// is also reached, the watchdog changes its state to crashedPermanently, and it stops
|
||||
@@ -155,7 +176,22 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
});
|
||||
|
||||
this.watchdog.setCreator(async (elementOrData, editorConfig) => {
|
||||
const editor = await BalloonEditor.create(elementOrData, editorConfig);
|
||||
const editor = await editorClass.create(elementOrData, editorConfig);
|
||||
|
||||
await initSyntaxHighlighting(editor);
|
||||
|
||||
if (isClassicEditor) {
|
||||
let $classicToolbarWidget;
|
||||
if (!utils.isMobile()) {
|
||||
const $parentSplit = this.$widget.parents(".note-split.type-text");
|
||||
$classicToolbarWidget = $parentSplit.find("> .ribbon-container .classic-toolbar-widget");
|
||||
} else {
|
||||
$classicToolbarWidget = $("body").find(".classic-toolbar-widget");
|
||||
}
|
||||
|
||||
$classicToolbarWidget.empty();
|
||||
$classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element);
|
||||
}
|
||||
|
||||
editor.model.document.on('change:data', () => this.spacedUpdate.scheduleUpdate());
|
||||
|
||||
|
||||
@@ -18,14 +18,28 @@ const TPL = `
|
||||
width: 130px;
|
||||
text-align: center;
|
||||
margin: 10px;
|
||||
padding; 10px;
|
||||
border: 1px transparent solid;
|
||||
}
|
||||
|
||||
|
||||
.workspace-notes .workspace-note:hover {
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--main-border-color);
|
||||
}
|
||||
|
||||
.note-detail-empty-results .aa-dropdown-menu {
|
||||
max-height: 50vh;
|
||||
overflow: scroll;
|
||||
border: var(--bs-border-width) solid var(--bs-border-color);
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.empty-tab-search .note-autocomplete-input {
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.empty-tab-search .input-clearer-button {
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.workspace-icon {
|
||||
text-align: center;
|
||||
@@ -33,14 +47,14 @@ const TPL = `
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="workspace-notes"></div>
|
||||
<div class="form-group empty-tab-search">
|
||||
<label>${t('empty.open_note_instruction')}</label>
|
||||
<div class="input-group">
|
||||
<div class="input-group mt-1">
|
||||
<input class="form-control note-autocomplete" placeholder="${t('empty.search_placeholder')}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="workspace-notes"></div>
|
||||
<div class="note-detail-empty-results"></div>
|
||||
</div>`;
|
||||
|
||||
export default class EmptyTypeWidget extends TypeWidget {
|
||||
@@ -51,10 +65,12 @@ export default class EmptyTypeWidget extends TypeWidget {
|
||||
|
||||
this.$widget = $(TPL);
|
||||
this.$autoComplete = this.$widget.find(".note-autocomplete");
|
||||
this.$results = this.$widget.find(".note-detail-empty-results");
|
||||
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true
|
||||
allowCreatingNotes: true,
|
||||
container: this.$results
|
||||
})
|
||||
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
|
||||
if (!suggestion.notePath) {
|
||||
@@ -66,6 +82,7 @@ export default class EmptyTypeWidget extends TypeWidget {
|
||||
|
||||
this.$workspaceNotes = this.$widget.find('.workspace-notes');
|
||||
|
||||
noteAutocompleteService.showRecentNotes(this.$autoComplete);
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import library_loader from "../../../../services/library_loader.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
|
||||
const SAMPLE_LANGUAGE = "javascript";
|
||||
const SAMPLE_CODE = `\
|
||||
const n = 10;
|
||||
greet(n); // Print "Hello World" for n times
|
||||
|
||||
/**
|
||||
* Displays a "Hello World!" message for a given amount of times, on the standard console. The "Hello World!" text will be displayed once per line.
|
||||
*
|
||||
* @param {number} times The number of times to print the \`Hello World!\` message.
|
||||
*/
|
||||
function greet(times) {
|
||||
for (let i = 0; i++; i < times) {
|
||||
console.log("Hello World!");
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("highlighting.title")}</h4>
|
||||
|
||||
<p>${t("highlighting.description")}</p>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label>${t("highlighting.color-scheme")}</label>
|
||||
<select class="theme-select form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6 side-checkbox">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="word-wrap form-check-input" />
|
||||
${t("code_block.word_wrapping")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<div class="note-detail-readonly-text-content ck-content code-sample-wrapper">
|
||||
<pre class="hljs"><code class="code-sample">${SAMPLE_CODE}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.code-sample-wrapper {
|
||||
margin-top: 1em;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
`;
|
||||
|
||||
/**
|
||||
* Contains appearance settings for code blocks within text notes, such as the theme for the syntax highlighter.
|
||||
*/
|
||||
export default class CodeBlockOptions extends OptionsWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$themeSelect = this.$widget.find(".theme-select");
|
||||
this.$themeSelect.on("change", async () => {
|
||||
const newTheme = this.$themeSelect.val();
|
||||
library_loader.loadHighlightingTheme(newTheme);
|
||||
await server.put(`options/codeBlockTheme/${newTheme}`);
|
||||
});
|
||||
|
||||
this.$wordWrap = this.$widget.find("input.word-wrap");
|
||||
this.$wordWrap.on("change", () => this.updateCheckboxOption("codeBlockWordWrap", this.$wordWrap));
|
||||
|
||||
// Set up preview
|
||||
this.$sampleEl = this.$widget.find(".code-sample");
|
||||
}
|
||||
|
||||
#setupPreview(shouldEnableSyntaxHighlight) {
|
||||
const text = SAMPLE_CODE;
|
||||
if (shouldEnableSyntaxHighlight) {
|
||||
library_loader
|
||||
.requireLibrary(library_loader.HIGHLIGHT_JS)
|
||||
.then(() => {
|
||||
const highlightedText = hljs.highlight(text, {
|
||||
language: SAMPLE_LANGUAGE
|
||||
});
|
||||
this.$sampleEl.html(highlightedText.value);
|
||||
});
|
||||
} else {
|
||||
this.$sampleEl.text(text);
|
||||
}
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
const themeGroups = await server.get("options/codeblock-themes");
|
||||
this.$themeSelect.empty();
|
||||
|
||||
for (const [ key, themes ] of Object.entries(themeGroups)) {
|
||||
const $group = (key ? $("<optgroup>").attr("label", key) : null);
|
||||
|
||||
for (const theme of themes) {
|
||||
const option = $("<option>")
|
||||
.attr("value", theme.val)
|
||||
.text(theme.title);
|
||||
|
||||
if ($group) {
|
||||
$group.append(option);
|
||||
} else {
|
||||
this.$themeSelect.append(option);
|
||||
}
|
||||
}
|
||||
this.$themeSelect.append($group);
|
||||
}
|
||||
this.$themeSelect.val(options.codeBlockTheme);
|
||||
this.setCheckboxState(this.$wordWrap, options.codeBlockWordWrap);
|
||||
this.$widget.closest(".note-detail-printable").toggleClass("word-wrap", options.codeBlockWordWrap === "true");
|
||||
|
||||
this.#setupPreview(options.codeBlockTheme !== "none");
|
||||
}
|
||||
}
|
||||
@@ -133,14 +133,17 @@ export default class FontsOptions extends OptionsWidget {
|
||||
this.$widget.find(".reload-frontend-button").on("click", () => utils.reloadFrontendApp("changes from appearance options"));
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this._isEnabled;
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
if (options.overrideThemeFonts !== 'true') {
|
||||
this.toggleInt(false);
|
||||
this._isEnabled = (options.overrideThemeFonts === 'true');
|
||||
this.toggleInt(this._isEnabled);
|
||||
if (!this._isEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.toggleInt(true);
|
||||
|
||||
this.$mainFontSize.val(options.mainFontSize);
|
||||
this.fillFontFamilyOptions(this.$mainFontFamily, options.mainFontFamily);
|
||||
|
||||
|
||||
@@ -13,11 +13,11 @@ const TPL = `
|
||||
<select class="theme-select form-select"></select>
|
||||
</div>
|
||||
|
||||
<div class="col-6">
|
||||
<label>${t("theme.override_theme_fonts_label")}</label>
|
||||
<div class="form-check">
|
||||
<div class="col-6 side-checkbox">
|
||||
<label class="form-check">
|
||||
<input type="checkbox" class="override-theme-fonts form-check-input">
|
||||
</div>
|
||||
${t("theme.override_theme_fonts_label")}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -19,9 +19,24 @@ export default class CodeMimeTypesOptions extends OptionsWidget {
|
||||
|
||||
async optionsLoaded(options) {
|
||||
this.$mimeTypes.empty();
|
||||
let index = -1;
|
||||
let prevInitial = "";
|
||||
|
||||
for (const mimeType of await mimeTypesService.getMimeTypes()) {
|
||||
for (const mimeType of mimeTypesService.getMimeTypes()) {
|
||||
const id = "code-mime-type-" + (idCtr++);
|
||||
index++;
|
||||
|
||||
// Append a heading to group items by the first letter, excepting for the
|
||||
// first item ("Plain Text"). Note: this code assumes the items are already
|
||||
// in alphabetical ordered.
|
||||
if (index > 0) {
|
||||
const initial = mimeType.title.charAt(0).toUpperCase();
|
||||
|
||||
if (initial !== prevInitial) {
|
||||
this.$mimeTypes.append($("<h5>").text(initial));
|
||||
prevInitial = initial;
|
||||
}
|
||||
}
|
||||
|
||||
this.$mimeTypes.append($("<li>")
|
||||
.append($('<input type="checkbox" class="form-check-input">')
|
||||
|
||||
@@ -44,6 +44,20 @@ export default class OptionsWidget extends NoteContextAwareWidget {
|
||||
|
||||
optionsLoaded(options) {}
|
||||
|
||||
async refresh() {
|
||||
this.toggleInt(this.isEnabled());
|
||||
try {
|
||||
await this.refreshWithNote(this.note);
|
||||
} catch (e) {
|
||||
// Ignore errors when user is refreshing or navigating away.
|
||||
if (e === "rejected by browser") {
|
||||
return;
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async refreshWithNote(note) {
|
||||
const options = await server.get('options');
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import utils from "../../../../services/utils.js";
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("editing.editor_type.label")}</h4>
|
||||
|
||||
<select class="editor-type-select form-select">
|
||||
<option value="ckeditor-balloon">${t("editing.editor_type.floating")}</option>
|
||||
<option value="ckeditor-classic">${t("editing.editor_type.fixed")}</option>
|
||||
</select>
|
||||
</div>`;
|
||||
|
||||
export default class EditorOptions extends OptionsWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$body = $("body");
|
||||
this.$editorType = this.$widget.find(".editor-type-select");
|
||||
this.$editorType.on('change', async () => {
|
||||
const newEditorType = this.$editorType.val();
|
||||
await this.updateOption('textNoteEditorType', newEditorType);
|
||||
utils.reloadFrontendApp("editor type change");
|
||||
});
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
this.$editorType.val(options.textNoteEditorType);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import TypeWidget from "./type_widget.js";
|
||||
import AbstractCodeTypeWidget from "./abstract_code_type_widget.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-readonly-code note-detail-printable">
|
||||
@@ -16,12 +16,13 @@ const TPL = `
|
||||
<pre class="note-detail-readonly-code-content"></pre>
|
||||
</div>`;
|
||||
|
||||
export default class ReadOnlyCodeTypeWidget extends TypeWidget {
|
||||
export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||
static getType() { return "readOnlyCode"; }
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$content = this.$widget.find('.note-detail-readonly-code-content');
|
||||
this.contentSized();
|
||||
this.$editor = this.$widget.find('.note-detail-readonly-code-content');
|
||||
|
||||
super.doRender();
|
||||
}
|
||||
@@ -33,7 +34,14 @@ export default class ReadOnlyCodeTypeWidget extends TypeWidget {
|
||||
content = this.format(content);
|
||||
}
|
||||
|
||||
this.$content.text(content);
|
||||
this._update(note, content);
|
||||
this.show();
|
||||
}
|
||||
|
||||
getExtraOpts() {
|
||||
return {
|
||||
readOnly: true
|
||||
};
|
||||
}
|
||||
|
||||
async executeWithContentElementEvent({resolve, ntxId}) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import AbstractTextTypeWidget from "./abstract_text_type_widget.js";
|
||||
import libraryLoader from "../../services/library_loader.js";
|
||||
import { applySyntaxHighlight } from "../../services/syntax_highlight.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="note-detail-readonly-text note-detail-printable">
|
||||
@@ -89,7 +90,7 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
||||
// we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
|
||||
// we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
|
||||
// (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
|
||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||
await libraryLoader.requireLibrary(libraryLoader.CKEDITOR);
|
||||
|
||||
const blob = await note.getBlob();
|
||||
|
||||
@@ -110,6 +111,8 @@ export default class ReadOnlyTextTypeWidget extends AbstractTextTypeWidget {
|
||||
|
||||
renderMathInElement(this.$content[0], {trust: true});
|
||||
}
|
||||
|
||||
await applySyntaxHighlight(this.$content);
|
||||
}
|
||||
|
||||
async refreshIncludedNoteEvent({noteId}) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import server from "../services/server.js";
|
||||
import fileWatcher from "../services/file_watcher.js";
|
||||
@@ -11,12 +12,12 @@ const TPL = `
|
||||
}
|
||||
</style>
|
||||
|
||||
<p>File <code class="file-path"></code> has been last modified on <span class="file-last-modified"></span>.</p>
|
||||
<p>${t("watched_file_update_status.file_last_modified", { count: '' })}</p>
|
||||
|
||||
<div style="display: flex; flex-direction: row; justify-content: space-evenly;">
|
||||
<button class="btn btn-sm file-upload-button">Upload modified file</button>
|
||||
<button class="btn btn-sm file-upload-button">${t("watched_file_update_status.upload_modified_file")}</button>
|
||||
|
||||
<button class="btn btn-sm ignore-this-change-button">Ignore this change</button>
|
||||
<button class="btn btn-sm ignore-this-change-button">${t("watched_file_update_status.ignore_this_change")}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
BIN
src/public/icon.png
Normal file
BIN
src/public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -9,9 +9,9 @@
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "180x180 512x512",
|
||||
"type": "image/x-icon"
|
||||
"src": "icon.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,4 +20,10 @@
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
pre {
|
||||
box-shadow: unset !important;
|
||||
border: .75pt solid gray !important;
|
||||
border-radius: 2pt !important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
src: url(../fonts/JetBrainsMono-Light.woff2) format('woff');
|
||||
}
|
||||
|
||||
.table {
|
||||
--bs-table-bg: transparent !important;
|
||||
}
|
||||
|
||||
html {
|
||||
/* this fixes FF filter vs. position fixed bug: https://github.com/zadam/trilium/issues/233 */
|
||||
height: 100%;
|
||||
@@ -32,6 +36,10 @@ body {
|
||||
font-size: var(--main-font-size);
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a, a:visited, a:hover {
|
||||
color: var(--link-color);
|
||||
}
|
||||
@@ -62,30 +70,10 @@ textarea,
|
||||
background: var(--input-background-color) url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='%23ffffff' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/></svg>") right .75rem center/15px 20px no-repeat;
|
||||
}
|
||||
|
||||
/* Hide number input arrows */
|
||||
input[type="number"]::-webkit-inner-spin-button,
|
||||
input[type="number"]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Firefox browser */
|
||||
input[type="number"] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
|
||||
/* Show number input arrows when focus or hover */
|
||||
input[type="number"]:focus::-webkit-inner-spin-button,
|
||||
input[type="number"]:focus::-webkit-outer-spin-button,
|
||||
input[type="number"]:hover::-webkit-inner-spin-button,
|
||||
input[type="number"]:hover::-webkit-outer-spin-button {
|
||||
-webkit-appearance: inner-spin-button;
|
||||
}
|
||||
|
||||
/* Restore default apperance */
|
||||
input[type="number"]:focus,
|
||||
input[type="number"]:hover {
|
||||
appearance: auto;
|
||||
input[type="number"],
|
||||
input[type="checkbox"] {
|
||||
appearance: auto !important;
|
||||
}
|
||||
|
||||
#left-pane input,
|
||||
@@ -394,7 +382,7 @@ button.btn, button.btn-sm {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
pre:not(.CodeMirror-line) {
|
||||
pre:not(.CodeMirror-line):not(.hljs) {
|
||||
color: var(--main-text-color) !important;
|
||||
white-space: pre-wrap;
|
||||
font-size: 100%;
|
||||
@@ -728,6 +716,10 @@ div[data-notify="container"] {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ck-content a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#options-dialog input[type=number] {
|
||||
text-align: right;
|
||||
}
|
||||
@@ -815,6 +807,65 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
|
||||
vertical-align: baseline !important;
|
||||
}
|
||||
|
||||
.ck-content pre {
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 4px 4px 8px rgba(0, 0, 0, 0.1), 0px 0px 2px rgba(0, 0, 0, 0.2);
|
||||
padding: 0 !important;
|
||||
margin-top: 2px !important;
|
||||
overflow: unset;
|
||||
}
|
||||
|
||||
html .note-detail-editable-text :not(figure, .include-note):first-child {
|
||||
/* Create some space for the top-side shadow */
|
||||
margin-top: 1px !important;
|
||||
}
|
||||
|
||||
.ck.ck-editor__editable pre[data-language]::after {
|
||||
--ck-color-code-block-label-background: rgba(128, 128, 128, .5);
|
||||
border-radius: 0 0 5px 5px;
|
||||
padding: 0px 10px;
|
||||
letter-spacing: .5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.ck-content pre code {
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ck-content pre code::-webkit-scrollbar {
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
.ck-content pre code::-webkit-scrollbar-thumb {
|
||||
height: 4px;
|
||||
border: none !important;
|
||||
background: gray !important;
|
||||
}
|
||||
|
||||
.ck-content pre code::-webkit-scrollbar-track, ::-webkit-scrollbar-thumb {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.note-detail-printable:not(.word-wrap) pre code {
|
||||
white-space: pre;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.code-sample-wrapper .hljs {
|
||||
transition: background-color linear 100ms;
|
||||
}
|
||||
|
||||
.side-checkbox {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
padding-top: .375rem;
|
||||
padding-bottom: .375rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.ck-content .todo-list .todo-list__label > input:before {
|
||||
border: 1px solid var(--muted-text-color) !important;
|
||||
}
|
||||
@@ -1148,6 +1199,10 @@ button.close:hover {
|
||||
flex-grow: 0 !important;
|
||||
}
|
||||
|
||||
.options-mime-types {
|
||||
column-width: 250px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
cursor: auto;
|
||||
}
|
||||
@@ -1182,3 +1237,4 @@ textarea {
|
||||
.jump-to-note-results .aa-suggestions {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -92,3 +92,7 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
|
||||
.btn-close {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.ck-content pre {
|
||||
box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.6) !important;
|
||||
}
|
||||
@@ -15,8 +15,7 @@
|
||||
"message": "发生了严重错误,导致客户端应用程序无法启动:\n\n{{message}}\n\n这很可能是由于脚本以意外的方式失败引起的。请尝试以安全模式启动应用程序并解决问题。"
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "小部件初始化失败",
|
||||
"message": "标题为 \"{{title}}\" 的小部件由于以下原因无法初始化:\n\n{{message}}"
|
||||
"title": "小部件初始化失败"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@@ -73,13 +72,12 @@
|
||||
"delete_all_clones_description": "同时删除所有克隆(可以在最近修改中撤消)",
|
||||
"erase_notes_description": "通常(软)删除仅标记笔记为已删除,可以在一段时间内通过最近修改对话框撤消。选中此选项将立即擦除笔记,不可撤销。",
|
||||
"erase_notes_warning": "永久擦除笔记(无法撤销),包括所有克隆。这将强制应用程序重新加载。",
|
||||
"notes_to_be_deleted": "将删除以下笔记(<span class=\"deleted-notes-count\"></span>)",
|
||||
"notes_to_be_deleted": "将删除以下笔记 ({{- noteCount}})",
|
||||
"no_note_to_delete": "没有笔记将被删除(仅克隆)。",
|
||||
"broken_relations_to_be_deleted": "将删除以下关系并断开连接(<span class=\"broke-relations-count\"></span>)",
|
||||
"broken_relations_to_be_deleted": "将删除以下关系并断开连接 ({{- relationCount}})",
|
||||
"cancel": "取消",
|
||||
"ok": "确定",
|
||||
"note": "笔记",
|
||||
"to_be_deleted": " (将被删除的笔记) 被以下关系 <code>{{attrName}}</code> 引用, 来自 "
|
||||
"deleted_relation_text": "笔记 {{- note}} (将被删除的笔记) 被以下关系 {{- relation}} 引用, 来自 {{- source}}。"
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "导出笔记",
|
||||
@@ -247,6 +245,9 @@
|
||||
"revisions_deleted": "笔记历史版本已删除。",
|
||||
"revision_restored": "笔记历史版本已恢复。",
|
||||
"revision_deleted": "笔记历史版本已删除。",
|
||||
"snapshot_interval": "笔记快照保存间隔: {{seconds}}秒。",
|
||||
"maximum_revisions": "当前笔记的最历史数量: {{number}}。",
|
||||
"settings": "笔记历史设置",
|
||||
"download_button": "下载",
|
||||
"mime": "MIME类型:",
|
||||
"file_size": "文件大小:",
|
||||
@@ -1090,6 +1091,13 @@
|
||||
"note_revisions_snapshot_description": "笔记修改快照时间间隔是指经过多少秒后会为笔记创建新的修改历史。更多信息请参见<a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a>。",
|
||||
"snapshot_time_interval_label": "笔记修改快照时间间隔(单位:秒)"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "笔记历史快照限制",
|
||||
"note_revisions_snapshot_limit_description": "笔记历史快照数限制指的是每个笔记可以保存的最大历史记录数量。其中 -1 表示没有限制,0 表示删除所有历史记录。你可以通过 #versioningLimit 标签设置单个笔记的最大历史记录数量。",
|
||||
"snapshot_number_limit_label": "笔记历史快照数量限制:",
|
||||
"erase_excess_revision_snapshots": "立即删除多余的历史快照",
|
||||
"erase_excess_revision_snapshots_prompt": "多余的历史快照已被删除。"
|
||||
},
|
||||
"search_engine": {
|
||||
"title": "搜索引擎",
|
||||
"custom_search_engine_info": "自定义搜索引擎需要设置名称和URL。如果这两者之一未设置,将默认使用DuckDuckGo作为搜索引擎。",
|
||||
@@ -1280,6 +1288,8 @@
|
||||
"insert-child-note": "插入子笔记",
|
||||
"delete": "删除",
|
||||
"search-in-subtree": "在子树中搜索",
|
||||
"hoist-note": "提升笔记",
|
||||
"unhoist-note": "取消提升笔记",
|
||||
"edit-branch-prefix": "编辑分支前缀",
|
||||
"advanced": "高级",
|
||||
"expand-subtree": "展开子树",
|
||||
@@ -1391,5 +1401,26 @@
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "表"
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "关闭标签页",
|
||||
"add_new_tab": "添加新标签页",
|
||||
"close": "关闭",
|
||||
"close_other_tabs": "关闭其他标签页",
|
||||
"close_all_tabs": "关闭所有标签页",
|
||||
"move_tab_to_new_window": "将此标签页移动到新窗口",
|
||||
"new_tab": "新标签页"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "目录",
|
||||
"options": "选项"
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "文件 <code class=\"file-path\"></code> 最后修改时间为 <span class=\"file-last-modified\"></span>。",
|
||||
"upload_modified_file": "上传修改的文件",
|
||||
"ignore_this_change": "忽略此更改"
|
||||
},
|
||||
"app_context": {
|
||||
"please_wait_for_save": "请等待几秒钟以完成保存,然后您可以尝试再操作一次。"
|
||||
}
|
||||
}
|
||||
|
||||
1243
src/public/translations/de/translation.json
Normal file
1243
src/public/translations/de/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,12 @@
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "Failed to initialize a widget",
|
||||
"message": "Widget with title \"{{title}}\" could not be initialized due to:\n\n{{message}}"
|
||||
"message-custom": "Custom widget from note with ID \"{{id}}\", titled \"{{title}}\" could not be initialized due to:\n\n{{message}}",
|
||||
"message-unknown": "Unknown widget could not be initialized due to:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Failed to load a custom script",
|
||||
"message": "Script from note with ID \"{{id}}\", titled \"{{title}}\" could not be executed due to:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@@ -70,16 +75,15 @@
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "Delete notes preview",
|
||||
"delete_all_clones_description": "delete also all clones (can be undone in recent changes)",
|
||||
"delete_all_clones_description": "Delete also all clones (can be undone in recent changes)",
|
||||
"erase_notes_description": "Normal (soft) deletion only marks the notes as deleted and they can be undeleted (in recent changes dialog) within a period of time. Checking this option will erase the notes immediately and it won't be possible to undelete the notes.",
|
||||
"erase_notes_warning": "erase notes permanently (can't be undone), including all clones. This will force application reload.",
|
||||
"notes_to_be_deleted": "Following notes will be deleted (<span class=\"deleted-notes-count\"></span>)",
|
||||
"erase_notes_warning": "Erase notes permanently (can't be undone), including all clones. This will force application reload.",
|
||||
"notes_to_be_deleted": "Following notes will be deleted ({{- noteCount}})",
|
||||
"no_note_to_delete": "No note will be deleted (only clones).",
|
||||
"broken_relations_to_be_deleted": "Following relations will be broken and deleted (<span class=\"broke-relations-count\"></span>)",
|
||||
"broken_relations_to_be_deleted": "Following relations will be broken and deleted ({{- relationCount}})",
|
||||
"cancel": "Cancel",
|
||||
"ok": "OK",
|
||||
"note": "Note",
|
||||
"to_be_deleted": " (to be deleted) is referenced by relation <code>{{attrName}}</code> originating from "
|
||||
"deleted_relation_text": "Note {{- note}} (to be deleted) is referenced by relation {{- relation}} originating from {{- source}}."
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "Export note",
|
||||
@@ -166,7 +170,8 @@
|
||||
"textImportedAsText": "Import HTML, Markdown and TXT as text notes if it's unclear from metadata",
|
||||
"codeImportedAsCode": "Import recognized code files (e.g. <code>.json</code>) as code notes if it's unclear from metadata",
|
||||
"replaceUnderscoresWithSpaces": "Replace underscores with spaces in imported note names",
|
||||
"import": "Import"
|
||||
"import": "Import",
|
||||
"failed": "Import failed: {{message}}."
|
||||
},
|
||||
"include_note": {
|
||||
"dialog_title": "Include note",
|
||||
@@ -247,9 +252,9 @@
|
||||
"revisions_deleted": "Note revisions has been deleted.",
|
||||
"revision_restored": "Note revision has been restored.",
|
||||
"revision_deleted": "Note revision has been deleted.",
|
||||
"snapshot_interval":"Note Revisions Snapshot Interval: {{seconds}}s.",
|
||||
"maximum_revisions":"Maximum revisions for current note: {{number}}.",
|
||||
"settings":"Settings for Note revisions.",
|
||||
"snapshot_interval": "Note Revisions Snapshot Interval: {{seconds}}s.",
|
||||
"maximum_revisions": "Maximum revisions for current note: {{number}}.",
|
||||
"settings": "Settings for Note revisions",
|
||||
"download_button": "Download",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "File size:",
|
||||
@@ -632,7 +637,10 @@
|
||||
"export_note": "Export note",
|
||||
"delete_note": "Delete note",
|
||||
"print_note": "Print note",
|
||||
"save_revision": "Save revision"
|
||||
"save_revision": "Save revision",
|
||||
"convert_into_attachment_failed": "Converting note '{{title}}' failed.",
|
||||
"convert_into_attachment_successful": "Note '{{title}}' has been converted to attachment.",
|
||||
"convert_into_attachment_prompt": "Are you sure you want to convert note '{{title}}' into an attachment of the parent note?"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "Button widget '{{componentId}}' has no defined click handler"
|
||||
@@ -887,7 +895,8 @@
|
||||
"label_rock_or_pop": "only one of the labels must be present",
|
||||
"label_year_comparison": "numerical comparison (also >, >=, <).",
|
||||
"label_date_created": "notes created in the last month",
|
||||
"error": "Search error: {{error}}"
|
||||
"error": "Search error: {{error}}",
|
||||
"search_prefix": "Search:"
|
||||
},
|
||||
"attachment_detail": {
|
||||
"open_help_page": "Open help page on attachments",
|
||||
@@ -921,7 +930,15 @@
|
||||
},
|
||||
"protected_session": {
|
||||
"enter_password_instruction": "Showing protected note requires entering your password:",
|
||||
"start_session_button": "Start protected session"
|
||||
"start_session_button": "Start protected session",
|
||||
"started": "Protected session has been started.",
|
||||
"wrong_password": "Wrong password.",
|
||||
"protecting-finished-successfully": "Protecting finished successfully.",
|
||||
"unprotecting-finished-successfully": "Unprotecting finished successfully.",
|
||||
"protecting-in-progress": "Protecting in progress: {{count}}",
|
||||
"unprotecting-in-progress-count": "Unprotecting in progress: {{count}}",
|
||||
"protecting-title": "Protecting status",
|
||||
"unprotecting-title": "Unprotecting status"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Open in new tab",
|
||||
@@ -976,7 +993,7 @@
|
||||
"error_creating_anonymized_database": "Could not create anonymized database, check backend logs for details",
|
||||
"successfully_created_fully_anonymized_database": "Created fully anonymized database in {{anonymizedFilePath}}",
|
||||
"successfully_created_lightly_anonymized_database": "Created lightly anonymized database in {{anonymizedFilePath}}",
|
||||
"no_anonymized_database_yet": "no anonymized database yet"
|
||||
"no_anonymized_database_yet": "No anonymized database yet."
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "Database Integrity Check",
|
||||
@@ -992,7 +1009,9 @@
|
||||
"fill_entity_changes_button": "Fill entity changes records",
|
||||
"full_sync_triggered": "Full sync triggered",
|
||||
"filling_entity_changes": "Filling entity changes rows...",
|
||||
"sync_rows_filled_successfully": "Sync rows filled successfully"
|
||||
"sync_rows_filled_successfully": "Sync rows filled successfully",
|
||||
"finished-successfully": "Sync finished successfully.",
|
||||
"failed": "Sync failed: {{message}}"
|
||||
},
|
||||
"vacuum_database": {
|
||||
"title": "Vacuum Database",
|
||||
@@ -1034,7 +1053,7 @@
|
||||
"edited_notes_message": "Edited Notes ribbon tab will automatically open on day notes"
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme",
|
||||
"title": "Application Theme",
|
||||
"theme_label": "Theme",
|
||||
"override_theme_fonts_label": "Override theme fonts",
|
||||
"light_theme": "Light",
|
||||
@@ -1199,7 +1218,7 @@
|
||||
"password": {
|
||||
"heading": "Password",
|
||||
"alert_message": "Please take care to remember your new password. Password is used for logging into the web interface and to encrypt protected notes. If you forget your password, then all your protected notes are forever lost.",
|
||||
"reset_link": "click here to reset it.",
|
||||
"reset_link": "Click here to reset it.",
|
||||
"old_password": "Old password",
|
||||
"new_password": "New password",
|
||||
"new_password_confirmation": "New password confirmation",
|
||||
@@ -1290,6 +1309,8 @@
|
||||
"insert-child-note": "Insert child note",
|
||||
"delete": "Delete",
|
||||
"search-in-subtree": "Search in subtree",
|
||||
"hoist-note": "Hoist note",
|
||||
"unhoist-note": "Unhoist note",
|
||||
"edit-branch-prefix": "Edit branch prefix",
|
||||
"advanced": "Advanced",
|
||||
"expand-subtree": "Expand subtree",
|
||||
@@ -1309,7 +1330,9 @@
|
||||
"duplicate-subtree": "Duplicate subtree",
|
||||
"export": "Export",
|
||||
"import-into-note": "Import into note",
|
||||
"apply-bulk-actions": "Apply bulk actions"
|
||||
"apply-bulk-actions": "Apply bulk actions",
|
||||
"converted-to-attachments": "{{count}} notes have been converted to attachments.",
|
||||
"convert-to-attachment-confirm": "Are you sure you want to convert note selected notes into attachments of their parent notes?"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "This note is shared publicly on",
|
||||
@@ -1327,12 +1350,13 @@
|
||||
"mermaid-diagram": "Mermaid Diagram",
|
||||
"canvas": "Canvas",
|
||||
"web-view": "Web View",
|
||||
"mind-map": "Mind Map",
|
||||
"mind-map": "Mind Map (Beta)",
|
||||
"file": "File",
|
||||
"image": "Image",
|
||||
"launcher": "Launcher",
|
||||
"doc": "Doc",
|
||||
"widget": "Widget"
|
||||
"widget": "Widget",
|
||||
"confirm-change": "It is not recommended to change note type when note content is not empty. Do you want to continue anyway?"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "Protect the note",
|
||||
@@ -1378,7 +1402,9 @@
|
||||
"hide-archived-notes": "Hide archived notes",
|
||||
"automatically-collapse-notes": "Automatically collapse notes",
|
||||
"automatically-collapse-notes-title": "Notes will be collapsed after period of inactivity to declutter the tree.",
|
||||
"save-changes": "Save & apply changes"
|
||||
"save-changes": "Save & apply changes",
|
||||
"auto-collapsing-notes-after-inactivity": "Auto collapsing notes after inactivity...",
|
||||
"saved-search-note-refreshed": "Saved search note refreshed."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Keep this window on top."
|
||||
@@ -1401,5 +1427,99 @@
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tables"
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "Close tab",
|
||||
"add_new_tab": "Add new tab",
|
||||
"close": "Close",
|
||||
"close_other_tabs": "Close other tabs",
|
||||
"close_right_tabs": "Close tabs to the right",
|
||||
"close_all_tabs": "Close all tabs",
|
||||
"move_tab_to_new_window": "Move this tab to a new window",
|
||||
"new_tab": "New tab"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Table of Contents",
|
||||
"options": "Options"
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "File <code class=\"file-path\"></code> has been last modified on <span class=\"file-last-modified\"></span>.",
|
||||
"upload_modified_file": "Upload modified file",
|
||||
"ignore_this_change": "Ignore this change"
|
||||
},
|
||||
"app_context": {
|
||||
"please_wait_for_save": "Please wait for a couple of seconds for the save to finish, then you can try again."
|
||||
},
|
||||
"note_create": {
|
||||
"duplicated": "Note \"{{title}}\" has been duplicated."
|
||||
},
|
||||
"image": {
|
||||
"copied-to-clipboard": "A reference to the image has been copied to clipboard. This can be pasted in any text note.",
|
||||
"cannot-copy": "Could not copy the image reference to clipboard."
|
||||
},
|
||||
"clipboard": {
|
||||
"cut": "Note(s) have been cut into clipboard.",
|
||||
"copied": "Note(s) have been copied into clipboard."
|
||||
},
|
||||
"entrypoints": {
|
||||
"note-revision-created": "Note revision has been created.",
|
||||
"note-executed": "Note executed.",
|
||||
"sql-error": "Error occurred while executing SQL query: {{message}}"
|
||||
},
|
||||
"branches": {
|
||||
"cannot-move-notes-here": "Cannot move notes here.",
|
||||
"delete-status": "Delete status",
|
||||
"delete-notes-in-progress": "Delete notes in progress: {{count}}",
|
||||
"delete-finished-successfully": "Delete finished successfully.",
|
||||
"undeleting-notes-in-progress": "Undeleting notes in progress: {{count}}",
|
||||
"undeleting-notes-finished-successfully": "Undeleting notes finished successfully."
|
||||
},
|
||||
"frontend_script_api": {
|
||||
"async_warning": "You're passing an async function to `api.runOnBackend()` which will likely not work as you intended.\\nEither make the function synchronous (by removing `async` keyword), or use `api.runAsyncOnBackendWithManualTransactionHandling()`.",
|
||||
"sync_warning": "You're passing a synchronous function to `api.runAsyncOnBackendWithManualTransactionHandling()`,\\nwhile you should likely use `api.runOnBackend()` instead."
|
||||
},
|
||||
"ws": {
|
||||
"sync-check-failed": "Sync check failed!",
|
||||
"consistency-checks-failed": "Consistency checks failed! See logs for details.",
|
||||
"encountered-error": "Encountered error \"{{message}}\", check out the console."
|
||||
},
|
||||
"hoisted_note": {
|
||||
"confirm_unhoisting": "Requested note '{{requestedNote}}' is outside of hoisted note '{{hoistedNote}}' subtree and you must unhoist to access the note. Do you want to proceed with unhoisting?"
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"reset_launcher_confirm": "Do you really want to reset \"{{title}}\"? All data / settings in this note (and its children) will be lost and the launcher will be returned to its original location.",
|
||||
"add-note-launcher": "Add a note launcher",
|
||||
"add-script-launcher": "Add a script launcher",
|
||||
"add-custom-widget": "Add a custom widget",
|
||||
"add-spacer": "Add spacer",
|
||||
"delete": "Delete",
|
||||
"reset": "Reset",
|
||||
"move-to-visible-launchers": "Move to visible launchers",
|
||||
"move-to-available-launchers": "Move to available launchers",
|
||||
"duplicate-launcher": "Duplicate launcher"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Auto-detected"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Code Syntax Highlighting for Text Notes",
|
||||
"description": "Controls the syntax highlighting for code blocks inside text notes, code notes will not be affected.",
|
||||
"color-scheme": "Color Scheme"
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "Word wrapping"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "Formatting"
|
||||
},
|
||||
"editor": {
|
||||
"title": "Editor"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "Formatting toolbar",
|
||||
"floating": "Floating (editing tools appear near the cursor)",
|
||||
"fixed": "Fixed (editing tools appear in the \"Formatting\" ribbon tab)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
},
|
||||
"widget-error": {
|
||||
"title": "No se pudo inicializar un widget",
|
||||
"message": "Widget con título \"{{title}}\" no pudo ser inicializado debido a:\n\n{{message}}"
|
||||
"message-custom": "El widget personalizado de la nota con ID \"{{id}}\", titulada \"{{title}}\" no pudo ser inicializado debido a:\n\n{{message}}",
|
||||
"message-unknown": "Un widget no pudo ser inicializado debido a:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Hubo un fallo al cargar un script personalizado",
|
||||
"message": "El script de la nota con ID \"{{id}}\", titulado \"{{title}}\" no pudo ser ejecutado debido a:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"add_link": {
|
||||
@@ -70,16 +75,15 @@
|
||||
},
|
||||
"delete_notes": {
|
||||
"delete_notes_preview": "Eliminar vista previa de notas",
|
||||
"delete_all_clones_description": "eliminar también todos los clones (se puede deshacer en cambios recientes)",
|
||||
"delete_all_clones_description": "Eliminar también todos los clones (se puede deshacer en cambios recientes)",
|
||||
"erase_notes_description": "La eliminación normal (suave) solo marca las notas como eliminadas y se pueden recuperar (en el cuadro de diálogo de cambios recientes) dentro de un periodo de tiempo. Al marcar esta opción se borrarán las notas inmediatamente y no será posible recuperarlas.",
|
||||
"erase_notes_warning": "eliminar notas permanentemente (no se puede deshacer), incluidos todos los clones. Esto forzará la recarga de la aplicación.",
|
||||
"notes_to_be_deleted": "Las siguientes notas serán eliminadas (<span class=\"deleted-notes-count\"></span>)",
|
||||
"erase_notes_warning": "Eliminar notas permanentemente (no se puede deshacer), incluidos todos los clones. Esto forzará la recarga de la aplicación.",
|
||||
"notes_to_be_deleted": "Las siguientes notas serán eliminadas ({{- noteCount}})",
|
||||
"no_note_to_delete": "No se eliminará ninguna nota (solo clones).",
|
||||
"broken_relations_to_be_deleted": "Las siguientes relaciones se romperán y serán eliminadas (<span class=\"broke-relations-count\"></span>)",
|
||||
"broken_relations_to_be_deleted": "Las siguientes relaciones se romperán y serán eliminadas ({{- relationCount}})",
|
||||
"cancel": "Cancelar",
|
||||
"ok": "Aceptar",
|
||||
"note": "Nota",
|
||||
"to_be_deleted": " (para ser eliminada) está referenciado por la relación <code>{{attrName}}</code> que se origina en "
|
||||
"deleted_relation_text": "Nota {{- note}} (para ser eliminada) está referenciado por la relación {{- relation}} que se origina en {{- source}}."
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "Exportar nota",
|
||||
@@ -166,7 +170,8 @@
|
||||
"textImportedAsText": "Importar HTML, Markdown y TXT como notas de texto si no está claro en los metadatos",
|
||||
"codeImportedAsCode": "Importar archivos de código reconocidos (por ejemplo, <code>.json</code>) como notas de código si no están claros en los metadatos",
|
||||
"replaceUnderscoresWithSpaces": "Reemplazar guiones bajos con espacios en nombres de notas importadas",
|
||||
"import": "Importar"
|
||||
"import": "Importar",
|
||||
"failed": "La importación falló: {{message}}."
|
||||
},
|
||||
"include_note": {
|
||||
"dialog_title": "Incluir nota",
|
||||
@@ -247,6 +252,9 @@
|
||||
"revisions_deleted": "Se han eliminado las revisiones de nota.",
|
||||
"revision_restored": "Se ha restaurado la revisión de nota.",
|
||||
"revision_deleted": "Se ha eliminado la revisión de la nota.",
|
||||
"snapshot_interval": "Intervalo de respaldo de revisiones de nota: {{seconds}}s.",
|
||||
"maximum_revisions": "Máximo de revisiones para la nota actual: {{number}}.",
|
||||
"settings": "Ajustes para revisiones de nota",
|
||||
"download_button": "Descargar",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "Tamaño del archivo:",
|
||||
@@ -327,9 +335,9 @@
|
||||
"run_on_instance": "Definir en qué instancia de Trilium se debe ejecutar esto. Predeterminado para todas las instancias.",
|
||||
"run_at_hour": "¿A qué hora debería funcionar? Debe usarse junto con <code>#run=hourly</code>. Se puede definir varias veces para varias ejecuciones durante el día.",
|
||||
"disable_inclusion": "los scripts con esta etiqueta no se incluirán en la ejecución del script principal.",
|
||||
"sorted": "mantiene las notas hijo ordenadas alfabéticamente por título",
|
||||
"sorted": "mantiene las subnotas ordenadas alfabéticamente por título",
|
||||
"sort_direction": "ASC (el valor predeterminado) o DESC",
|
||||
"sort_folders_first": "Las carpetas (notas con hijos) deben ordenarse en la parte superior",
|
||||
"sort_folders_first": "Las carpetas (notas con subnotas) deben ordenarse en la parte superior",
|
||||
"top": "mantener la nota dada en la parte superior de su padre (se aplica solo en padres ordenados)",
|
||||
"hide_promoted_attributes": "Ocultar atributos promovidos en esta nota",
|
||||
"read_only": "el editor está en modo de sólo lectura. Funciona sólo para notas de texto y código.",
|
||||
@@ -344,13 +352,13 @@
|
||||
"widget": "marca esta nota como un widget personalizado que se agregará al árbol de componentes de Trilium",
|
||||
"workspace": "marca esta nota como un espacio de trabajo que permite un fácil levantamiento",
|
||||
"workspace_icon_class": "define la clase CSS del ícono de cuadro que se usará en la pestaña cuando se levante a esta nota",
|
||||
"workspace_tab_background_color": "color CSS utilizado en la pestaña de nota cuando se eleva a esta nota",
|
||||
"workspace_tab_background_color": "color CSS utilizado en la pestaña de nota cuando se ancla a esta nota",
|
||||
"workspace_calendar_root": "Define la raíz del calendario por cada espacio de trabajo",
|
||||
"workspace_template": "Esta nota aparecerá en la selección de plantillas disponibles al crear una nueva nota, pero solo cuando se levante a un espacio de trabajo que contenga esta plantilla",
|
||||
"search_home": "se crearán nuevas notas de búsqueda como hijas de esta nota",
|
||||
"workspace_search_home": "se crearán nuevas notas de búsqueda como hijo de esta nota cuando se elevan a algún antecesor de esta nota del espacio de trabajo",
|
||||
"inbox": "ubicación predeterminada de la bandeja de entrada para nuevas notas - cuando crea una nota usando el botón \"nueva nota\" en la barra lateral, las notas serán creadas como notas hijo de la nota marcada con la etiqueta <code>#inbox</code>.",
|
||||
"workspace_inbox": "ubicación predeterminada de la bandeja de entrada para nuevas notas cuando se elevan a algún antecesor de esta nota del espacio de trabajo",
|
||||
"search_home": "se crearán nuevas notas de búsqueda como subnotas de esta nota",
|
||||
"workspace_search_home": "se crearán nuevas notas de búsqueda como subnotas de esta nota cuando se anclan a algún antecesor de esta nota del espacio de trabajo",
|
||||
"inbox": "ubicación predeterminada de la bandeja de entrada para nuevas notas - cuando crea una nota usando el botón \"nueva nota\" en la barra lateral, las notas serán creadas como subnotas de la nota marcada con la etiqueta <code>#inbox</code>.",
|
||||
"workspace_inbox": "ubicación predeterminada de la bandeja de entrada para nuevas notas cuando se anclan a algún antecesor de esta nota del espacio de trabajo",
|
||||
"sql_console_home": "ubicación predeterminada de las notas de la consola SQL",
|
||||
"bookmark_folder": "la nota con esta etiqueta aparecerá en los marcadores como carpeta (permitiendo el acceso a sus elementos hijos).",
|
||||
"share_hidden_from_tree": "esta nota está oculta en el árbol de navegación izquierdo, pero aún se puede acceder a ella con su URL",
|
||||
@@ -365,12 +373,12 @@
|
||||
"share_index": "tenga en cuenta que con esto esta etiqueta enumerará todas las raíces de las notas compartidas",
|
||||
"display_relations": "nombres de relaciones delimitados por comas que deben mostrarse. Todos los demás estarán ocultos.",
|
||||
"hide_relations": "nombres de relaciones delimitados por comas que deben ocultarse. Se mostrarán todos los demás.",
|
||||
"title_template": "título por defecto de notas creadas como hijo de esta nota. El valor es evaluado como una cadena de JavaScript \n y por lo tanto puede ser enriquecida con contenido dinámico vía las variables inyectadas <code>now</code> y <code>parentNote</code>. Ejemplos:\n \n <ul>\n <li><code>trabajos literarios de ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Registro para ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Consulte la <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki para obtener más detalles</a>, documentación de la API para <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> y <a href=\"https://day.js.org/docs/en/display/format\">now</a> para más detalles.",
|
||||
"title_template": "título por defecto de notas creadas como subnota de esta nota. El valor es evaluado como una cadena de JavaScript \n y por lo tanto puede ser enriquecida con contenido dinámico vía las variables inyectadas <code>now</code> y <code>parentNote</code>. Ejemplos:\n \n <ul>\n <li><code>trabajos literarios de ${parentNote.getLabelValue('authorName')}</code></li>\n <li><code>Registro para ${now.format('YYYY-MM-DD HH:mm:ss')}</code></li>\n </ul>\n \n Consulte la <a href=\"https://triliumnext.github.io/Docs/Wiki/default-note-title.html\">wiki para obtener más detalles</a>, documentación de la API para <a href=\"https://zadam.github.io/trilium/backend_api/Note.html\">parentNote</a> y <a href=\"https://day.js.org/docs/en/display/format\">now</a> para más detalles.",
|
||||
"template": "Esta nota aparecerá en la selección de plantillas disponibles al crear una nueva nota",
|
||||
"toc": "<code>#toc</code> o <code>#toc=show</code> forzará que se muestre la tabla de contenido, <code>#toc=hide</code> forzará a ocultarla. Si la etiqueta no existe, se observa la configuración global",
|
||||
"color": "define el color de la nota en el árbol de notas, enlaces, etc. Utilice cualquier valor de color CSS válido como 'red' o #a13d5f",
|
||||
"keyboard_shortcut": "Define un atajo de teclado que saltará inmediatamente a esta nota. Ejemplo: 'ctrl+alt+e'. Es necesario recargar la interfaz para que el cambio surta efecto.",
|
||||
"keep_current_hoisting": "Abrir este enlace no cambiará la elevación incluso si la nota no se puede mostrar en el subárbol elevado actual.",
|
||||
"keep_current_hoisting": "Abrir este enlace no cambiará el anclaje incluso si la nota no se puede mostrar en el subárbol anclado actualmente.",
|
||||
"execute_button": "Título del botón que ejecutará la nota de código actual",
|
||||
"execute_description": "Descripción más larga de la nota de código actual que se muestra junto con el botón de ejecución",
|
||||
"exclude_from_note_map": "Las notas con esta etiqueta se ocultarán del Mapa de notas",
|
||||
@@ -629,7 +637,10 @@
|
||||
"export_note": "Exportar nota",
|
||||
"delete_note": "Eliminar nota",
|
||||
"print_note": "Imprimir nota",
|
||||
"save_revision": "Guardar revisión"
|
||||
"save_revision": "Guardar revisión",
|
||||
"convert_into_attachment_failed": "La conversión de nota '{{title}}' falló.",
|
||||
"convert_into_attachment_successful": "La nota '{{title}}' ha sido convertida a un archivo adjunto.",
|
||||
"convert_into_attachment_prompt": "¿Está seguro que desea convertir la nota '{{title}}' en un archivo adjunto de la nota padre?"
|
||||
},
|
||||
"onclick_button": {
|
||||
"no_click_handler": "El widget de botón '{{componentId}}' no tiene un controlador de clics definido"
|
||||
@@ -667,7 +678,7 @@
|
||||
"button_title": "Exportar diagrama como SVG"
|
||||
},
|
||||
"relation_map_buttons": {
|
||||
"create_child_note_title": "Crear una nueva nota hijo y agregarla a este mapa de relaciones",
|
||||
"create_child_note_title": "Crear una nueva subnota y agregarla a este mapa de relaciones",
|
||||
"reset_pan_zoom_title": "Restablecer la panorámica y el zoom a las coordenadas y ampliación iniciales",
|
||||
"zoom_in_title": "Acercar",
|
||||
"zoom_out_title": "Alejar"
|
||||
@@ -678,7 +689,7 @@
|
||||
"relation": "relación"
|
||||
},
|
||||
"mobile_detail_menu": {
|
||||
"insert_child_note": "Insertar nota hijo",
|
||||
"insert_child_note": "Insertar subnota",
|
||||
"delete_this_note": "Eliminar esta nota",
|
||||
"error_cannot_get_branch_id": "No se puede obtener el branchID del notePath '{{notePath}}'",
|
||||
"error_unrecognized_command": "Comando no reconocido {{command}}"
|
||||
@@ -699,7 +710,7 @@
|
||||
"grid": "Cuadrícula",
|
||||
"list": "Lista",
|
||||
"collapse_all_notes": "Contraer todas las notas",
|
||||
"expand_all_children": "Ampliar todos los hijos",
|
||||
"expand_all_children": "Ampliar todas las subnotas",
|
||||
"collapse": "Colapsar",
|
||||
"expand": "Expandir",
|
||||
"book_properties": "Propiedades del libro",
|
||||
@@ -759,7 +770,7 @@
|
||||
"clone_button": "Clonar nota a nueva ubicación...",
|
||||
"intro_placed": "Esta nota está colocada en las siguientes rutas:",
|
||||
"intro_not_placed": "Esta nota aún no se ha colocado en el árbol de notas.",
|
||||
"outside_hoisted": "Esta ruta está fuera de la nota elevada y habría que bajarla.",
|
||||
"outside_hoisted": "Esta ruta está fuera de la nota anclada y habría que bajarla.",
|
||||
"archived": "Archivado",
|
||||
"search": "Buscar"
|
||||
},
|
||||
@@ -853,7 +864,7 @@
|
||||
"content_and_attachments_size": "Tenga en cuenta el tamaño del contenido, incluidos los archivos adjuntos",
|
||||
"content_and_attachments_and_revisions_size": "Tenga en cuenta el tamaño del contenido, incluidos los archivos adjuntos y las revisiones",
|
||||
"revision_count": "Número de revisiones",
|
||||
"children_count": "Notas sobre el número de hijos",
|
||||
"children_count": "Número de subnotas",
|
||||
"parent_count": "Número de clones",
|
||||
"owned_label_count": "Número de etiquetas",
|
||||
"owned_relation_count": "Número de relaciones",
|
||||
@@ -884,12 +895,13 @@
|
||||
"label_rock_or_pop": "sólo una de las etiquetas debe estar presente",
|
||||
"label_year_comparison": "comparación numérica (también >, >=, <).",
|
||||
"label_date_created": "notas creadas en el último mes",
|
||||
"error": "Error de búsqueda: {{error}}"
|
||||
"error": "Error de búsqueda: {{error}}",
|
||||
"search_prefix": "Buscar:"
|
||||
},
|
||||
"attachment_detail": {
|
||||
"open_help_page": "Abrir página de ayuda en archivos adjuntos",
|
||||
"owning_note": "Nota dueña: ",
|
||||
"you_can_also_open": ", también puedes abri el ",
|
||||
"you_can_also_open": ", también puede abrir el ",
|
||||
"list_of_all_attachments": "Lista de todos los archivos adjuntos",
|
||||
"attachment_deleted": "Este archivo adjunto ha sido eliminado."
|
||||
},
|
||||
@@ -900,7 +912,7 @@
|
||||
"no_attachments": "Esta nota no tiene archivos adjuntos."
|
||||
},
|
||||
"book": {
|
||||
"no_children_help": "Esta nota de tipo libro no tieneninguna nota hijo así que no hay nada que mostrar. Véa la <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para más detalles."
|
||||
"no_children_help": "Esta nota de tipo libro no tieneninguna subnota así que no hay nada que mostrar. Véa la <a href=\"https://triliumnext.github.io/Docs/Wiki/book-note.html\">wiki</a> para más detalles."
|
||||
},
|
||||
"editable_code": {
|
||||
"placeholder": "Escriba el contenido de su nota de código aquí..."
|
||||
@@ -918,7 +930,15 @@
|
||||
},
|
||||
"protected_session": {
|
||||
"enter_password_instruction": "Para mostrar una nota protegida es necesario ingresar su contraseña:",
|
||||
"start_session_button": "Iniciar sesión protegida"
|
||||
"start_session_button": "Iniciar sesión protegida",
|
||||
"started": "La sesión protegida ha iniciado.",
|
||||
"wrong_password": "Contraseña incorrecta.",
|
||||
"protecting-finished-successfully": "La protección finalizó exitosamente.",
|
||||
"unprotecting-finished-successfully": "La desprotección finalizó exitosamente.",
|
||||
"protecting-in-progress": "Protección en progreso: {{count}}",
|
||||
"unprotecting-in-progress-count": "Desprotección en progreso: {{count}}",
|
||||
"protecting-title": "Estado de protección",
|
||||
"unprotecting-title": "Estado de desprotección"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Abrir en nueva pestaña",
|
||||
@@ -973,7 +993,7 @@
|
||||
"error_creating_anonymized_database": "No se pudo crear una base de datos anónima; consulte los registros de backend para obtener más detalles",
|
||||
"successfully_created_fully_anonymized_database": "Se creó una base de datos completamente anónima en {{anonymizedFilePath}}",
|
||||
"successfully_created_lightly_anonymized_database": "Se creó una base de datos ligeramente anónima en {{anonymizedFilePath}}",
|
||||
"no_anonymized_database_yet": "aún no hay base de datos anónima"
|
||||
"no_anonymized_database_yet": "Aún no hay base de datos anónima."
|
||||
},
|
||||
"database_integrity_check": {
|
||||
"title": "Verificación de integridad de la base de datos",
|
||||
@@ -989,7 +1009,9 @@
|
||||
"fill_entity_changes_button": "Llenar registros de cambios de entidad",
|
||||
"full_sync_triggered": "Sincronización completa activada",
|
||||
"filling_entity_changes": "Rellenar filas de cambios de entidad...",
|
||||
"sync_rows_filled_successfully": "Sincronizar filas completadas correctamente"
|
||||
"sync_rows_filled_successfully": "Sincronizar filas completadas correctamente",
|
||||
"finished-successfully": "La sincronización finalizó exitosamente.",
|
||||
"failed": "La sincronización falló: {{message}}"
|
||||
},
|
||||
"vacuum_database": {
|
||||
"title": "Limpiar base de datos",
|
||||
@@ -1090,6 +1112,13 @@
|
||||
"note_revisions_snapshot_description": "El intervalo de tiempo de la instantánea de revisión de nota es el tiempo en segundos después de lo cual se creará una nueva revisión para la nota. Ver <a href=\"https://triliumnext.github.io/docs/wiki/note-revisions.html\" class=\"external\"> wiki </a> para obtener más información.",
|
||||
"snapshot_time_interval_label": "Intervalo de tiempo de la instantánea de revisión de notas (en segundos)"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "Límite de respaldos de revisiones de nota",
|
||||
"note_revisions_snapshot_limit_description": "El límite de número de respaldos de revisiones de notas se refiere al número máximo de revisiones que pueden guardarse para cada nota. Donde -1 significa sin límite, 0 significa borrar todas las revisiones. Puede establecer el máximo de revisiones para una sola nota a través de la etiqueta #versioningLimit.",
|
||||
"snapshot_number_limit_label": "Número límite de respaldos de revisiones de nota:",
|
||||
"erase_excess_revision_snapshots": "Eliminar el exceso de respaldos de revisiones ahora",
|
||||
"erase_excess_revision_snapshots_prompt": "El exceso de respaldos de revisiones han sido eliminadas."
|
||||
},
|
||||
"search_engine": {
|
||||
"title": "Motor de búsqueda",
|
||||
"custom_search_engine_info": "El motor de búsqueda personalizado requiere que se establezcan un nombre y una URL. Si alguno de estos no está configurado, DuckDuckGo se utilizará como motor de búsqueda predeterminado.",
|
||||
@@ -1128,7 +1157,7 @@
|
||||
},
|
||||
"table_of_contents": {
|
||||
"title": "Tabla de contenido",
|
||||
"description": "La tabla de contenido aparecerá en las notas de texto cuando la nota tenga más de un número definido de títulos. Puedes personalizar este número:",
|
||||
"description": "La tabla de contenido aparecerá en las notas de texto cuando la nota tenga más de un número definido de títulos. Puede personalizar este número:",
|
||||
"disable_info": "También puede utilizar esta opción para desactivar la TDC (TOC) de forma efectiva estableciendo un número muy alto.",
|
||||
"shortcut_info": "Puede configurar un atajo de teclado para alternar rápidamente el panel derecho (incluido el TDC) en Opciones -> Atajos (nombre 'toggleRightPane')."
|
||||
},
|
||||
@@ -1189,7 +1218,7 @@
|
||||
"password": {
|
||||
"heading": "Contraseña",
|
||||
"alert_message": "Tenga cuidado de recordar su nueva contraseña. La contraseña se utiliza para iniciar sesión en la interfaz web y cifrar las notas protegidas. Si olvida su contraseña, todas sus notas protegidas se perderán para siempre.",
|
||||
"reset_link": "haga clic aquí para restablecerla.",
|
||||
"reset_link": "Dé clic aquí para restablecerla.",
|
||||
"old_password": "Contraseña anterior",
|
||||
"new_password": "Nueva contraseña",
|
||||
"new_password_confirmation": "Confirmación de nueva contraseña",
|
||||
@@ -1277,9 +1306,11 @@
|
||||
"open-in-a-new-tab": "Abrir en nueva pestaña",
|
||||
"open-in-a-new-split": "Abrir en nueva división",
|
||||
"insert-note-after": "Insertar nota después de",
|
||||
"insert-child-note": "Insertar nota hijo",
|
||||
"insert-child-note": "Insertar subnota",
|
||||
"delete": "Eliminar",
|
||||
"search-in-subtree": "Buscar en subárbol",
|
||||
"hoist-note": "Anclar nota",
|
||||
"unhoist-note": "Desanclar nota",
|
||||
"edit-branch-prefix": "Editar prefijo de rama",
|
||||
"advanced": "Avanzado",
|
||||
"expand-subtree": "Expandir subárbol",
|
||||
@@ -1299,7 +1330,9 @@
|
||||
"duplicate-subtree": "Duplicar subárbol",
|
||||
"export": "Exportar",
|
||||
"import-into-note": "Importar a nota",
|
||||
"apply-bulk-actions": "Aplicar acciones en lote"
|
||||
"apply-bulk-actions": "Aplicar acciones en lote",
|
||||
"converted-to-attachments": "{{count}} notas han sido convertidas en archivos adjuntos.",
|
||||
"convert-to-attachment-confirm": "¿Está seguro que desea convertir las notas seleccionadas en archivos adjuntos de sus notas padres?"
|
||||
},
|
||||
"shared_info": {
|
||||
"shared_publicly": "Esta nota está compartida públicamente en",
|
||||
@@ -1317,12 +1350,13 @@
|
||||
"mermaid-diagram": "Diagrama Mermaid",
|
||||
"canvas": "Lienzo",
|
||||
"web-view": "Vista Web",
|
||||
"mind-map": "Mapa Mental",
|
||||
"mind-map": "Mapa Mental (beta)",
|
||||
"file": "Archivo",
|
||||
"image": "Imagen",
|
||||
"launcher": "Lanzador",
|
||||
"doc": "Doc",
|
||||
"widget": "Widget"
|
||||
"widget": "Widget",
|
||||
"confirm-change": "No es recomendado cambiar el tipo de nota cuando el contenido de la nota no está vacío. ¿Desea continuar de cualquier manera?"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-on": "Proteger la nota",
|
||||
@@ -1368,7 +1402,9 @@
|
||||
"hide-archived-notes": "Ocultar notas archivadas",
|
||||
"automatically-collapse-notes": "Colapsar notas automaticamente",
|
||||
"automatically-collapse-notes-title": "Las notas serán colapsadas después de un periodo de inactividad para despejar el árbol.",
|
||||
"save-changes": "Guardar y aplicar cambios"
|
||||
"save-changes": "Guardar y aplicar cambios",
|
||||
"auto-collapsing-notes-after-inactivity": "Colapsando notas automáticamente después de inactividad...",
|
||||
"saved-search-note-refreshed": "La nota de búsqueda guardada fue recargada."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Mantener esta ventana en la parte superior."
|
||||
@@ -1391,5 +1427,99 @@
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tablas"
|
||||
},
|
||||
"tab_row": {
|
||||
"close_tab": "Cerrar pestaña",
|
||||
"add_new_tab": "Agregar nueva pestaña",
|
||||
"close": "Cerrar",
|
||||
"close_other_tabs": "Cerrar otras pestañas",
|
||||
"close_right_tabs": "Cerrar pestañas a la derecha",
|
||||
"close_all_tabs": "Cerras todas las pestañas",
|
||||
"move_tab_to_new_window": "Mover esta pestaña a una nueva ventana",
|
||||
"new_tab": "Nueva pestaña"
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Tabla de contenido",
|
||||
"options": "Opciones"
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "Archivo <code class=\"file-path\"></code> ha sido modificado por última vez en<span class=\"file-last-modified\"></span>.",
|
||||
"upload_modified_file": "Subir archivo modificado",
|
||||
"ignore_this_change": "Ignorar este cambio"
|
||||
},
|
||||
"app_context": {
|
||||
"please_wait_for_save": "Por favor espere algunos segundos a que se termine de guardar, después intente de nuevo."
|
||||
},
|
||||
"note_create": {
|
||||
"duplicated": "La nota \"{{title}}\" ha sido duplicada."
|
||||
},
|
||||
"image": {
|
||||
"copied-to-clipboard": "Una referencia a la imagen ha sido copiada al portapapeles. Esta puede ser pegada en cualquier nota de texto.",
|
||||
"cannot-copy": "No se pudo copiar la referencia de imagen al portapapeles."
|
||||
},
|
||||
"clipboard": {
|
||||
"cut": "La(s) notas(s) han sido cortadas al portapapeles.",
|
||||
"copied": "La(s) notas(s) han sido copiadas al portapapeles."
|
||||
},
|
||||
"entrypoints": {
|
||||
"note-revision-created": "Una revisión de nota ha sido creada.",
|
||||
"note-executed": "Nota ejecutada.",
|
||||
"sql-error": "Ocurrió un error al ejecutar la consulta SQL: {{message}}"
|
||||
},
|
||||
"branches": {
|
||||
"cannot-move-notes-here": "No se pueden mover notas aquí.",
|
||||
"delete-status": "Estado de eliminación",
|
||||
"delete-notes-in-progress": "Eliminación de notas en progreso: {{count}}",
|
||||
"delete-finished-successfully": "La eliminación finalizó exitosamente.",
|
||||
"undeleting-notes-in-progress": "Recuperación de notas en progreso: {{count}}",
|
||||
"undeleting-notes-finished-successfully": "La recuperación de notas finalizó exitosamente."
|
||||
},
|
||||
"frontend_script_api": {
|
||||
"async_warning": "Está pasando una función asíncrona a `api.runOnBackend ()` que probablemente no funcionará como pretendía.",
|
||||
"sync_warning": "Estás pasando una función sincrónica a `api.runasynconbackendwithmanualTransactionHandling ()`, \\ n while debería usar `api.runonbackend ()` en su lugar."
|
||||
},
|
||||
"ws": {
|
||||
"sync-check-failed": "¡La comprobación de sincronización falló!",
|
||||
"consistency-checks-failed": "¡Las comprobaciones de consistencia fallaron! Vea los registros para más detalles.",
|
||||
"encountered-error": "Error encontrado \"{{message}}\", compruebe la consola."
|
||||
},
|
||||
"hoisted_note": {
|
||||
"confirm_unhoisting": "La nota requerida '{{requestedNote}}' está fuera del subárbol de la nota anclada '{{hoistedNote}}' y debe desanclarla para acceder a la nota. ¿Desea proceder con el desanclaje?"
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"reset_launcher_confirm": "¿Realmente desea restaurar \"{{title}}\"? Todos los datos / ajustes en esta nota (y sus subnotas) se van a perder y el lanzador regresará a su ubicación original.",
|
||||
"add-note-launcher": "Agregar un lanzador de nota",
|
||||
"add-script-launcher": "Agregar un lanzador de script",
|
||||
"add-custom-widget": "Agregar un widget personalizado",
|
||||
"add-spacer": "Agregar espaciador",
|
||||
"delete": "Eliminar",
|
||||
"reset": "Restaurar",
|
||||
"move-to-visible-launchers": "Mover a lanzadores visibles",
|
||||
"move-to-available-launchers": "Mover a lanzadores disponibles",
|
||||
"duplicate-launcher": "Duplicar lanzador"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Detectado automaticamente"
|
||||
},
|
||||
"highlighting": {
|
||||
"title": "Resaltado de sintaxis de de código para Notas de Texto",
|
||||
"description": "Controla el resaltado de sintaxis para bloques de código dentro de las notas de texto, las notas de código no serán afectadas.",
|
||||
"color-scheme": "Esquema de color"
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "Ajuste de palabras"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "Formato"
|
||||
},
|
||||
"editor": {
|
||||
"title": "Editor"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "Barra de herramientas de formato",
|
||||
"floating": "Flotante (las herramientas de edición aparecen cerca del cursor)",
|
||||
"fixed": "Fijo (las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1500
src/public/translations/fr/translation.json
Normal file
1500
src/public/translations/fr/translation.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -381,7 +381,7 @@
|
||||
"full_anonymization_description": "Această acțiune va crea o nouă copie a bazei de date și o va anonimiza (se șterge conținutul tuturor notițelor și se menține doar structura și câteva metainformații neconfidențiale) pentru a putea fi partajate online cu scopul de a depana anumite probleme fără a risca expunerea datelor personale.",
|
||||
"light_anonymization": "Anonimizare parțială",
|
||||
"light_anonymization_description": "Această acțiune va crea o copie a bazei de date și o va anonimiza parțial - mai exact se va șterge conținutul tuturor notițelor, dar titlurile și atributele vor rămâne. De asemenea, script-urile de front-end sau back-end și widget-urile personalizate vor rămâne și ele. Acest lucru oferă mai mult context pentru a depana probleme.",
|
||||
"no_anonymized_database_yet": "încă nu există nicio bază de date anonimizată",
|
||||
"no_anonymized_database_yet": "Încă nu există nicio bază de date anonimizată.",
|
||||
"save_fully_anonymized_database": "Salvează bază de date complet anonimizată",
|
||||
"save_lightly_anonymized_database": "Salvează bază de date parțial anonimizată",
|
||||
"successfully_created_fully_anonymized_database": "S-a creat cu succes o bază de date complet anonimizată în {{anonymizedFilePath}}",
|
||||
@@ -414,17 +414,16 @@
|
||||
"undelete_notes_instruction": "După ștergere, se pot recupera din ecranul Schimbări recente."
|
||||
},
|
||||
"delete_notes": {
|
||||
"broken_relations_to_be_deleted": "Următoarele relații vor fi întrerupte și șterse (<span class=\"broke-relations-count\"></span>)",
|
||||
"broken_relations_to_be_deleted": "Următoarele relații vor fi întrerupte și șterse ({{- relationCount}})",
|
||||
"cancel": "Anulează",
|
||||
"delete_all_clones_description": "Șterge și toate clonele (se pot recupera în ecranul Schimbări recente)",
|
||||
"delete_notes_preview": "Șterge previzualizările notițelor",
|
||||
"delete_notes_preview": "Previzualizare ștergerea notițelor",
|
||||
"erase_notes_description": "Ștergerea obișnuită doar marchează notițele ca fiind șterse și pot fi recuperate (în ecranul Schimbări recente) pentru o perioadă de timp. Dacă se bifează această opțiune, notițele vor fi șterse imediat fără posibilitatea de a le recupera.",
|
||||
"erase_notes_warning": "șterge notițele permanent (nu se mai pot recupera), incluzând toate clonele. Va forța reîncărcarea aplicației.",
|
||||
"erase_notes_warning": "Șterge notițele permanent (nu se mai pot recupera), incluzând toate clonele. Va forța reîncărcarea aplicației.",
|
||||
"no_note_to_delete": "Nicio notiță nu va fi ștearsă (doar clonele).",
|
||||
"note": "Notiță",
|
||||
"notes_to_be_deleted": "Următoarele notițe vor fi șterse (<span class=\"deleted-notes-count\"></span>)",
|
||||
"notes_to_be_deleted": "Următoarele notițe vor fi șterse ({{- noteCount}})",
|
||||
"ok": "OK",
|
||||
"to_be_deleted": " (pentru ștergere) este referențiat(ă) de relația <code>{{attrName}}</code> originând de la "
|
||||
"deleted_relation_text": "Notița {{- note}} ce va fi ștearsă este referențiată de relația {{- relation}}, originând din {{- source}}."
|
||||
},
|
||||
"delete_relation": {
|
||||
"allowed_characters": "Se permit caractere alfanumerice, underline și două puncte.",
|
||||
@@ -554,7 +553,7 @@
|
||||
"open_sql_console": "Deschide consola SQL",
|
||||
"open_sql_console_history": "Deschide istoricul consolei SQL",
|
||||
"options": "Opțiuni",
|
||||
"reload_frontend": "Reîncarcă interfață",
|
||||
"reload_frontend": "Reîncarcă interfața",
|
||||
"reload_hint": "Reîncărcarea poate ajuta atunci când există ceva probleme vizuale fără a trebui repornită întreaga aplicație.",
|
||||
"reset_zoom_level": "Resetează nivelul de zoom",
|
||||
"show_backend_log": "Afișează log-ul din backend",
|
||||
@@ -685,7 +684,8 @@
|
||||
"safeImportTooltip": "Fișierele de Trilium exportate în format <code>.zip</code> pot conține scripturi executabile ce pot avea un comportament malițios. Importarea sigură va dezactiva execuția automată a tuturor scripturilor importate. Debifați „Importare sigură” dacă arhiva importată conține scripturi executabile dorite și aveți încredere deplină în conținutul acestora.",
|
||||
"shrinkImages": "Micșorare imagini",
|
||||
"shrinkImagesTooltip": "<p>Dacă bifați această opțiune, Trilium va încerca să micșoreze imaginea importată prin scalarea și importarea ei, aspect ce poate afecta calitatea aparentă a imaginii. Dacă nu este bifat, imaginile vor fi importate fără nicio modificare.</p><p>Acest lucru nu se aplică la importuri de tip <code>.zip</code> cu metainformații deoarece se asumă că aceste fișiere sunt deja optimizate.</p>",
|
||||
"textImportedAsText": "Importă HTML, Markdown și TXT ca notițe de tip text dacă este neclar din metainformații"
|
||||
"textImportedAsText": "Importă HTML, Markdown și TXT ca notițe de tip text dacă este neclar din metainformații",
|
||||
"failed": "Eroare la importare: {{message}}."
|
||||
},
|
||||
"include_archived_notes": {
|
||||
"include_archived_notes": "Include notițele arhivate"
|
||||
@@ -785,7 +785,10 @@
|
||||
"print_note": "Imprimare notiță",
|
||||
"re_render_note": "Reinterpretare notiță",
|
||||
"save_revision": "Salvează o nouă revizie",
|
||||
"search_in_note": "Caută în notiță"
|
||||
"search_in_note": "Caută în notiță",
|
||||
"convert_into_attachment_failed": "Nu s-a putut converti notița „{{title}}”.",
|
||||
"convert_into_attachment_successful": "Notița „{{title}}” a fost convertită în atașament.",
|
||||
"convert_into_attachment_prompt": "Doriți convertirea notiței „{{title}}” într-un atașament al notiței părinte?"
|
||||
},
|
||||
"note_erasure_timeout": {
|
||||
"deleted_notes_erased": "Notițele șterse au fost eliminate permanent.",
|
||||
@@ -903,7 +906,15 @@
|
||||
},
|
||||
"protected_session": {
|
||||
"enter_password_instruction": "Afișarea notițelor protejate necesită introducerea parolei:",
|
||||
"start_session_button": "Deschide sesiunea protejată"
|
||||
"start_session_button": "Deschide sesiunea protejată",
|
||||
"started": "Sesiunea protejată este activă.",
|
||||
"wrong_password": "Parolă greșită.",
|
||||
"protecting-finished-successfully": "Protejarea a avut succes.",
|
||||
"protecting-in-progress": "Protejare în curs: {{count}}",
|
||||
"protecting-title": "Stare protejare",
|
||||
"unprotecting-title": "Stare deprotejare",
|
||||
"unprotecting-finished-successfully": "Deprotejarea a avut succes.",
|
||||
"unprotecting-in-progress-count": "Deprotejare în curs: {{count}}"
|
||||
},
|
||||
"protected_session_password": {
|
||||
"close_label": "Închide",
|
||||
@@ -998,7 +1009,10 @@
|
||||
"revision_deleted": "Revizia notiței a fost ștearsă.",
|
||||
"revision_last_edited": "Revizia a fost ultima oară modificată pe {{date}}",
|
||||
"revision_restored": "Revizia notiței a fost restaurată.",
|
||||
"revisions_deleted": "Notița reviziei a fost ștearsă."
|
||||
"revisions_deleted": "Notița reviziei a fost ștearsă.",
|
||||
"maximum_revisions": "Numărul maxim de revizii pentru notița curentă: {{number}}.",
|
||||
"settings": "Setări revizii ale notițelor",
|
||||
"snapshot_interval": "Intervalul de creare a reviziilor pentru notițe: {{seconds}}s."
|
||||
},
|
||||
"revisions_button": {
|
||||
"note_revisions": "Revizii ale notiței"
|
||||
@@ -1079,7 +1093,8 @@
|
||||
"label_year_comparison": "comparații numerice (de asemenea >, >=, <).",
|
||||
"placeholder": "cuvinte cheie pentru căutarea în conținut, #etichetă = valoare...",
|
||||
"search_syntax": "Sintaxa de căutare",
|
||||
"title_column": "Textul de căutat:"
|
||||
"title_column": "Textul de căutat:",
|
||||
"search_prefix": "Căutare:"
|
||||
},
|
||||
"shortcuts": {
|
||||
"action_name": "Denumirea acțiunii",
|
||||
@@ -1131,7 +1146,9 @@
|
||||
"force_full_sync_button": "Forțează sincronizare completă",
|
||||
"full_sync_triggered": "S-a activat o sincronizare completă",
|
||||
"sync_rows_filled_successfully": "Rândurile de sincronizare s-au completat cu succes",
|
||||
"title": "Sincronizare"
|
||||
"title": "Sincronizare",
|
||||
"failed": "Eroare la sincronizare: {{message}}",
|
||||
"finished-successfully": "Sincronizarea a avut succes."
|
||||
},
|
||||
"sync_2": {
|
||||
"config_title": "Configurația sincronizării",
|
||||
@@ -1164,7 +1181,7 @@
|
||||
"light_theme": "Temă luminoasă",
|
||||
"override_theme_fonts_label": "Suprascrie fonturile temei",
|
||||
"theme_label": "Temă",
|
||||
"title": "Temă"
|
||||
"title": "Tema aplicației"
|
||||
},
|
||||
"toast": {
|
||||
"critical-error": {
|
||||
@@ -1172,8 +1189,13 @@
|
||||
"title": "Eroare critică"
|
||||
},
|
||||
"widget-error": {
|
||||
"message": "Widget-ul intitulat „{{title}}” nu a putut fi inițializat din cauza:\n\n{{message}}",
|
||||
"title": "Eroare la inițializarea unui widget"
|
||||
"title": "Eroare la inițializarea unui widget",
|
||||
"message-custom": "Widget-ul personalizat din notița cu ID-ul „{{id}}”, întitulată ”{{title}}” nu a putut fi inițializată din cauza:\n\n{{message}}",
|
||||
"message-unknown": "Un widget necunoscut nu a putut fi inițializat din cauza:\n\n{{message}}"
|
||||
},
|
||||
"bundle-error": {
|
||||
"title": "Eroare la încărcarea unui script personalizat",
|
||||
"message": "Scriptul din notița cu ID-ul „{{id}}”, întitulată „{{title}}” nu a putut fi executată din cauza:\n\n{{message}}"
|
||||
}
|
||||
},
|
||||
"tray": {
|
||||
@@ -1276,7 +1298,11 @@
|
||||
"recent-changes-in-subtree": "Schimbări recente în ierarhie",
|
||||
"search-in-subtree": "Caută în ierarhie",
|
||||
"sort-by": "Ordonare după...",
|
||||
"unprotect-subtree": "Deprotejează ierarhia"
|
||||
"unprotect-subtree": "Deprotejează ierarhia",
|
||||
"hoist-note": "Focalizează notița",
|
||||
"unhoist-note": "Defocalizează notița",
|
||||
"converted-to-attachments": "{{count}} notițe au fost convertite în atașamente.",
|
||||
"convert-to-attachment-confirm": "Doriți convertirea notițelor selectate în atașamente ale notiței părinte?"
|
||||
},
|
||||
"shared_info": {
|
||||
"help_link": "Pentru informații vizitați <a href=\"https://triliumnext.github.io/Docs/Wiki/sharing.html\">wiki-ul</a>.",
|
||||
@@ -1288,7 +1314,7 @@
|
||||
"canvas": "Schiță",
|
||||
"code": "Cod sursă",
|
||||
"mermaid-diagram": "Diagramă Mermaid",
|
||||
"mind-map": "Hartă mentală",
|
||||
"mind-map": "Hartă mentală (beta)",
|
||||
"note-map": "Hartă notițe",
|
||||
"relation-map": "Hartă relații",
|
||||
"render-note": "Randare notiță",
|
||||
@@ -1299,7 +1325,8 @@
|
||||
"file": "Fișier",
|
||||
"image": "Imagine",
|
||||
"launcher": "Scurtătură",
|
||||
"widget": "Widget"
|
||||
"widget": "Widget",
|
||||
"confirm-change": "Nu se recomandă schimbarea tipului notiței atunci când ea are un conținut. Procedați oricum?"
|
||||
},
|
||||
"protect_note": {
|
||||
"toggle-off": "Deprotejează notița",
|
||||
@@ -1365,9 +1392,134 @@
|
||||
"hide-archived-notes": "Ascunde notițele arhivate",
|
||||
"save-changes": "Salvează și aplică modificările",
|
||||
"scroll-active-title": "Mergi la notița activă",
|
||||
"tree-settings-title": "Setări ale ierarhiei notițelor"
|
||||
"tree-settings-title": "Setări ale ierarhiei notițelor",
|
||||
"auto-collapsing-notes-after-inactivity": "Se minimizează notițele după inactivitate...",
|
||||
"saved-search-note-refreshed": "Notița de căutare salvată a fost reîmprospătată."
|
||||
},
|
||||
"title_bar_buttons": {
|
||||
"window-on-top": "Menține fereastra mereu vizibilă"
|
||||
},
|
||||
"note_detail": {
|
||||
"could_not_find_typewidget": "Nu s-a putut găsi widget-ul corespunzător tipului „{{type}}”"
|
||||
},
|
||||
"note_title": {
|
||||
"placeholder": "introduceți titlul notiței aici..."
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"erase_excess_revision_snapshots": "Șterge acum reviziile excesive",
|
||||
"erase_excess_revision_snapshots_prompt": "Reviziile excesive au fost șterse.",
|
||||
"note_revisions_snapshot_limit_description": "Limita numărului de revizii se referă la numărul maxim de revizii pentru fiecare notiță. -1 reprezintă nicio limită, 0 înseamnă ștergerea tuturor reviziilor. Se poate seta valoarea individual pentru o notiță prin eticheta #versioningLimit.",
|
||||
"note_revisions_snapshot_limit_title": "Limita de revizii a notițelor",
|
||||
"snapshot_number_limit_label": "Numărul maxim de revizii pentru notițe:"
|
||||
},
|
||||
"search_result": {
|
||||
"no_notes_found": "Nu au fost găsite notițe pentru parametrii de căutare dați.",
|
||||
"search_not_executed": "Căutarea n-a fost rulată încă. Clic pe butonul „Căutare” de deasupra pentru a vedea rezultatele."
|
||||
},
|
||||
"show_floating_buttons_button": {
|
||||
"button_title": "Afișează butoanele"
|
||||
},
|
||||
"spacer": {
|
||||
"configure_launchbar": "Configurează bara de lansare"
|
||||
},
|
||||
"sql_result": {
|
||||
"no_rows": "Nu s-a găsit niciun rând pentru această interogare"
|
||||
},
|
||||
"sql_table_schemas": {
|
||||
"tables": "Tabele"
|
||||
},
|
||||
"app_context": {
|
||||
"please_wait_for_save": "Așteptați câteva secunde până se salvează toate datele și apoi reîncercați."
|
||||
},
|
||||
"tab_row": {
|
||||
"add_new_tab": "Adaugă tab nou",
|
||||
"close": "Închide",
|
||||
"close_all_tabs": "Închide toate taburile",
|
||||
"close_other_tabs": "Închide celelalte taburi",
|
||||
"close_tab": "Închide tab",
|
||||
"move_tab_to_new_window": "Mută acest tab în altă fereastră",
|
||||
"new_tab": "Tab nou",
|
||||
"close_right_tabs": "Închide taburile din dreapta"
|
||||
},
|
||||
"toc": {
|
||||
"options": "Setări",
|
||||
"table_of_contents": "Cuprins"
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "Fișierul <code class=\"file-path\"></code> a fost ultima oară modificat la data de <span class=\"file-last-modified\"></span>.",
|
||||
"ignore_this_change": "Ignoră această schimbare",
|
||||
"upload_modified_file": "Încarcă fișier modificat"
|
||||
},
|
||||
"clipboard": {
|
||||
"copied": "Notițele au fost copiate în clipboard.",
|
||||
"cut": "Notițele au fost decupate în clipboard."
|
||||
},
|
||||
"entrypoints": {
|
||||
"note-executed": "Notița a fost executată.",
|
||||
"note-revision-created": "S-a creat o revizie a notiței.",
|
||||
"sql-error": "A apărut o eroare la executarea interogării SQL: {{message}}"
|
||||
},
|
||||
"image": {
|
||||
"cannot-copy": "Nu s-a putut copia în clipboard referința către imagine.",
|
||||
"copied-to-clipboard": "S-a copiat o referință către imagine în clipboard. Aceasta se poate lipi în orice notiță text."
|
||||
},
|
||||
"note_create": {
|
||||
"duplicated": "Notița „{{title}}” a fost dublificată."
|
||||
},
|
||||
"branches": {
|
||||
"cannot-move-notes-here": "Nu se pot muta notițe aici.",
|
||||
"delete-finished-successfully": "Ștergerea a avut succes.",
|
||||
"delete-notes-in-progress": "Ștergere în curs: {{count}}",
|
||||
"delete-status": "Starea ștergerii",
|
||||
"undeleting-notes-finished-successfully": "Restaurarea notițelor a avut succes.",
|
||||
"undeleting-notes-in-progress": "Restaurare notițe în curs: {{count}}"
|
||||
},
|
||||
"frontend_script_api": {
|
||||
"async_warning": "Ați trimis o funcție asincronă metodei `api.runOnBackend()` și este posibil să nu se comporte așa cum vă așteptați.\\nFie faceți metoda sincronă (prin ștergerea cuvântului-cheie `async`), sau folosiți `api.runAsyncOnBackendWithManualTransactionHandling()`.",
|
||||
"sync_warning": "Ați trimis o funcție sincronă funcției `api.runAsyncOnBackendWithManualTransactionHandling()`,\\ndar cel mai probabil trebuie folosit `api.runOnBackend()` în schimb."
|
||||
},
|
||||
"ws": {
|
||||
"consistency-checks-failed": "Au fost identificate erori de consistență! Vedeți mai multe detalii în loguri.",
|
||||
"encountered-error": "A fost întâmpinată o eroare: „{{message}}”. Vedeți în loguri pentru mai multe detalii.",
|
||||
"sync-check-failed": "Verificările de sincronizare au eșuat!"
|
||||
},
|
||||
"hoisted_note": {
|
||||
"confirm_unhoisting": "Notița dorită „{{requestedNote}}” este în afara ierarhiei notiței focalizate „{{hoistedNote}}”. Doriți defocalizarea pentru a accesa notița?"
|
||||
},
|
||||
"launcher_context_menu": {
|
||||
"reset_launcher_confirm": "Doriți resetarea lansatorului „{{title}}”? Toate datele și setările din această notiță (și subnotițele ei) vor fi pierdute, iar lansatorul va fi resetat în poziția lui originală.",
|
||||
"add-custom-widget": "Adaugă un widget personalizat",
|
||||
"add-note-launcher": "Adaugă un lansator de notiță",
|
||||
"add-script-launcher": "Adaugă un lansator de script",
|
||||
"add-spacer": "Adaugă un separator",
|
||||
"delete": "Șterge",
|
||||
"duplicate-launcher": "Dublifică lansatorul",
|
||||
"move-to-available-launchers": "Mută în Lansatoare disponibile",
|
||||
"move-to-visible-launchers": "Mută în Lansatoare vizibile",
|
||||
"reset": "Resetează"
|
||||
},
|
||||
"editable-text": {
|
||||
"auto-detect-language": "Automat"
|
||||
},
|
||||
"highlighting": {
|
||||
"color-scheme": "Temă de culori",
|
||||
"title": "Evidențiere de sintaxă pentru notițele de tip text",
|
||||
"description": "Controlează evidențierea de sintaxă pentru blocurile de cod în interiorul notițelor text, notițele de tip cod nu vor fi afectate de aceste setări."
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "Încadrare text"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "Formatare"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"fixed": "Editor cu bară fixă (uneltele de editare vor apărea în tab-ul „Formatare” din panglică)",
|
||||
"floating": "Editor cu bară flotantă (uneltele de editare vor apărea lângă cursor)",
|
||||
"label": "Bară de formatare"
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"title": "Editor"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import log from "../../services/log.js";
|
||||
import searchService from "../../services/search/services/search.js";
|
||||
import ValidationError from "../../errors/validation_error.js";
|
||||
import { Request } from 'express';
|
||||
import { changeLanguage } from "../../services/i18n.js";
|
||||
import { listSyntaxHighlightingThemes } from "../../services/code_block_theme.js";
|
||||
|
||||
// options allowed to be updated directly in the Options dialog
|
||||
const ALLOWED_OPTIONS = new Set([
|
||||
@@ -14,6 +16,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'revisionSnapshotNumberLimit',
|
||||
'zoomFactor',
|
||||
'theme',
|
||||
'codeBlockTheme',
|
||||
"codeBlockWordWrap",
|
||||
'syncServerHost',
|
||||
'syncServerTimeout',
|
||||
'syncProxy',
|
||||
@@ -61,7 +65,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'promotedAttributesOpenInRibbon',
|
||||
'editedNotesOpenInRibbon',
|
||||
'locale',
|
||||
'firstDayOfWeek'
|
||||
'firstDayOfWeek',
|
||||
'textNoteEditorType'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
@@ -108,6 +113,11 @@ function update(name: string, value: string) {
|
||||
|
||||
optionService.setOption(name, value);
|
||||
|
||||
if (name === "locale") {
|
||||
// This runs asynchronously, so it's not perfect, but it does the trick for now.
|
||||
changeLanguage(value);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -132,6 +142,10 @@ function getUserThemes() {
|
||||
return ret;
|
||||
}
|
||||
|
||||
function getSyntaxHighlightingThemes() {
|
||||
return listSyntaxHighlightingThemes();
|
||||
}
|
||||
|
||||
function getSupportedLocales() {
|
||||
// TODO: Currently hardcoded, needs to read the list of available languages.
|
||||
return [
|
||||
@@ -139,10 +153,18 @@ function getSupportedLocales() {
|
||||
"id": "en",
|
||||
"name": "English"
|
||||
},
|
||||
{
|
||||
"id": "de",
|
||||
"name": "Deutsch"
|
||||
},
|
||||
{
|
||||
"id": "es",
|
||||
"name": "Español"
|
||||
},
|
||||
{
|
||||
"id": "fr",
|
||||
"name": "Français"
|
||||
},
|
||||
{
|
||||
"id": "cn",
|
||||
"name": "简体中文"
|
||||
@@ -166,5 +188,6 @@ export default {
|
||||
updateOption,
|
||||
updateOptions,
|
||||
getUserThemes,
|
||||
getSyntaxHighlightingThemes,
|
||||
getSupportedLocales
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user