mirror of
https://github.com/zadam/trilium.git
synced 2025-10-26 07:46:30 +01:00
Merge remote-tracking branch 'origin/develop' into develop
; Conflicts: ; src/public/translations/de/translation.json
This commit is contained in:
25
.github/workflows/renovate.yaml
vendored
Normal file
25
.github/workflows/renovate.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Renovate
|
||||
on:
|
||||
schedule:
|
||||
# Run every day at 1 AM UTC (before the nightly build at 2 AM UTC)
|
||||
- cron: '0 1 * * *'
|
||||
# Allow manual triggering
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
renovate:
|
||||
name: Run Renovate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Self-hosted Renovate
|
||||
uses: renovatebot/github-action@v41.0.3
|
||||
with:
|
||||
configurationFile: renovate.json
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
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
|
||||
12
README.md
12
README.md
@@ -18,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!
|
||||
@@ -65,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:
|
||||
|
||||
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
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "trilium",
|
||||
"version": "0.90.10-beta",
|
||||
"version": "0.90.11-beta",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "trilium",
|
||||
"version": "0.90.10-beta",
|
||||
"version": "0.90.11-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@braintree/sanitize-url": "7.1.0",
|
||||
@@ -40,7 +40,7 @@
|
||||
"express-partial-content": "1.0.2",
|
||||
"express-rate-limit": "7.4.1",
|
||||
"express-session": "1.18.1",
|
||||
"force-graph": "1.45.0",
|
||||
"force-graph": "1.46.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"helmet": "7.1.0",
|
||||
"html": "1.0.0",
|
||||
@@ -67,7 +67,7 @@
|
||||
"marked": "14.1.3",
|
||||
"mermaid": "11.4.0",
|
||||
"mime-types": "2.1.35",
|
||||
"mind-elixir": "4.3.0",
|
||||
"mind-elixir": "4.3.1",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-abi": "3.67.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
@@ -9144,9 +9144,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/force-graph": {
|
||||
"version": "1.45.0",
|
||||
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.45.0.tgz",
|
||||
"integrity": "sha512-QM/J72Vji5D3ug+TDu8wH+qne0zEKE9Cn7m9ocH/1RtaVY0BBqZQ4Mn6MiwNRyxwl28lsUd0F54kDpINnagvOA==",
|
||||
"version": "1.46.0",
|
||||
"resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.46.0.tgz",
|
||||
"integrity": "sha512-RR4XIsMgKMquEmN6me2MoDeqMr85Cv1cpXDFha6gwEczaaC3RWDH4YmXQXnI8/egRiIKFMq4HKjBjWXZwyy/9Q==",
|
||||
"dependencies": {
|
||||
"@tweenjs/tween.js": "18 - 25",
|
||||
"accessor-fn": "1",
|
||||
@@ -12330,9 +12330,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mind-elixir": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/mind-elixir/-/mind-elixir-4.3.0.tgz",
|
||||
"integrity": "sha512-RWpIIIGBoQPWUfZ2bcWPMK1xaQfCDYAwfFsfr279P7d5O8gYlhbXgN1dt/Rxi6JXJqDaVN5q0czAqNjPIti0EQ=="
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mind-elixir/-/mind-elixir-4.3.1.tgz",
|
||||
"integrity": "sha512-9dHqiNRlAFUlGUKHwPwLC+Dka2cEaNunzHbZkOw+mafz8pqeZbmmm7Xxlk2S2zbKPGxeayxTYrDDg2tmNAXe3Q=="
|
||||
},
|
||||
"node_modules/minimalistic-assert": {
|
||||
"version": "1.0.1",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "trilium",
|
||||
"productName": "TriliumNext Notes",
|
||||
"description": "Build your personal knowledge base with TriliumNext Notes",
|
||||
"version": "0.90.10-beta",
|
||||
"version": "0.90.11-beta",
|
||||
"license": "AGPL-3.0-only",
|
||||
"main": "./dist/electron-main.js",
|
||||
"author": {
|
||||
@@ -81,7 +81,7 @@
|
||||
"express-partial-content": "1.0.2",
|
||||
"express-rate-limit": "7.4.1",
|
||||
"express-session": "1.18.1",
|
||||
"force-graph": "1.45.0",
|
||||
"force-graph": "1.46.0",
|
||||
"fs-extra": "11.2.0",
|
||||
"helmet": "7.1.0",
|
||||
"html": "1.0.0",
|
||||
@@ -108,7 +108,7 @@
|
||||
"marked": "14.1.3",
|
||||
"mermaid": "11.4.0",
|
||||
"mime-types": "2.1.35",
|
||||
"mind-elixir": "4.3.0",
|
||||
"mind-elixir": "4.3.1",
|
||||
"multer": "1.4.5-lts.1",
|
||||
"node-abi": "3.67.0",
|
||||
"normalize-strings": "1.1.1",
|
||||
|
||||
12
renovate.json
Normal file
12
renovate.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"repositories": ["TriliumNext/Notes"],
|
||||
"schedule": ["before 3am"],
|
||||
"labels": ["dependencies", "renovate"],
|
||||
"prHourlyLimit": 0,
|
||||
"prConcurrentLimit": 0,
|
||||
"branchConcurrentLimit": 0
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -13,22 +13,23 @@ import ExecuteScriptBulkAction from "../widgets/bulk_actions/execute_script.js";
|
||||
import AddLabelBulkAction from "../widgets/bulk_actions/label/add_label.js";
|
||||
import AddRelationBulkAction from "../widgets/bulk_actions/relation/add_relation.js";
|
||||
import RenameNoteBulkAction from "../widgets/bulk_actions/note/rename_note.js";
|
||||
import { t } from "./i18n.js";
|
||||
|
||||
const ACTION_GROUPS = [
|
||||
{
|
||||
title: 'Labels',
|
||||
title: t("bulk_actions.labels"),
|
||||
actions: [AddLabelBulkAction, UpdateLabelValueBulkAction, RenameLabelBulkAction, DeleteLabelBulkAction]
|
||||
},
|
||||
{
|
||||
title: 'Relations',
|
||||
title: t("bulk_actions.relations"),
|
||||
actions: [AddRelationBulkAction, UpdateRelationTargetBulkAction, RenameRelationBulkAction, DeleteRelationBulkAction]
|
||||
},
|
||||
{
|
||||
title: 'Notes',
|
||||
title: t("bulk_actions.notes"),
|
||||
actions: [RenameNoteBulkAction, MoveNoteBulkAction, DeleteNoteBulkAction, DeleteRevisionsBulkAction],
|
||||
},
|
||||
{
|
||||
title: 'Other',
|
||||
title: t("bulk_actions.other"),
|
||||
actions: [ExecuteScriptBulkAction]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -10,7 +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 { applySyntaxHighlight } from "./syntax_highlight.js";
|
||||
import { applySingleBlockSyntaxHighlight, applySyntaxHighlight } from "./syntax_highlight.js";
|
||||
import mime_types from "./mime_types.js";
|
||||
|
||||
let idCounter = 1;
|
||||
|
||||
@@ -113,11 +114,18 @@ async function renderText(note, $renderedContent) {
|
||||
}
|
||||
}
|
||||
|
||||
/** @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 = {}) {
|
||||
|
||||
@@ -254,8 +254,15 @@ function goToLinkExt(evt, hrefLink, $link) {
|
||||
window.open(hrefLink, '_blank');
|
||||
} else if (hrefLink.toLowerCase().startsWith('file:') && utils.isElectron()) {
|
||||
const electron = utils.dynamicRequire('electron');
|
||||
|
||||
electron.shell.openPath(hrefLink);
|
||||
} else {
|
||||
// Enable protocols supported by CKEditor 5 to be clickable.
|
||||
// Refer to `allowedProtocols` in https://github.com/TriliumNext/trilium-ckeditor5/blob/main/packages/ckeditor5-build-balloon-block/src/ckeditor.ts.
|
||||
// Adding `:` to these links might be safer.
|
||||
const otherAllowedProtocols = ['mailto:', 'tel:', 'sms:', 'sftp:', 'smb:', 'slack:', 'zotero:'];
|
||||
if (otherAllowedProtocols.some(protocol => hrefLink.toLowerCase().startsWith(protocol))){
|
||||
window.open(hrefLink, '_blank');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,10 @@ import options from "./options.js";
|
||||
*/
|
||||
const MIME_TYPE_AUTO = "text-x-trilium-auto";
|
||||
|
||||
/**
|
||||
* For highlight.js-supported languages, see https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md.
|
||||
*/
|
||||
|
||||
const MIME_TYPES_DICT = [
|
||||
{ default: true, title: "Plain text", mime: "text/plain", highlightJs: "plaintext" },
|
||||
{ title: "APL", mime: "text/apl" },
|
||||
@@ -119,7 +123,7 @@ const MIME_TYPES_DICT = [
|
||||
{ title: "Scala", mime: "text/x-scala" },
|
||||
{ title: "Scheme", mime: "text/x-scheme" },
|
||||
{ title: "SCSS", mime: "text/x-scss", highlightJs: "scss" },
|
||||
{ default: true, title: "Shell (bash)", mime: "text/x-sh", highlightJs: "shell" },
|
||||
{ default: true, title: "Shell (bash)", mime: "text/x-sh", highlightJs: "bash" },
|
||||
{ title: "Sieve", mime: "application/sieve" },
|
||||
{ title: "Slim", mime: "text/x-slim" },
|
||||
{ title: "Smalltalk", mime: "text/x-stsrc", highlightJs: "smalltalk" },
|
||||
|
||||
@@ -45,6 +45,16 @@ async function autocompleteSource(term, cb, options = {}) {
|
||||
].concat(results);
|
||||
}
|
||||
|
||||
if (term.trim().length >= 1 && options.allowSearchNotes) {
|
||||
results = results.concat([
|
||||
{
|
||||
action: 'search-notes',
|
||||
noteTitle: term,
|
||||
highlightedNotePathTitle: `Search for "${utils.escapeHtml(term)}" <kbd style='color: var(--muted-text-color); background-color: transparent; float: right;'>Ctrl+Enter</kbd>`
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
if (term.match(/^[a-z]+:\/\/.+/i) && options.allowExternalLinks) {
|
||||
results = [
|
||||
{
|
||||
@@ -138,6 +148,17 @@ function initNoteAutocomplete($el, options) {
|
||||
autocompleteOptions.debug = true; // don't close on blur
|
||||
}
|
||||
|
||||
if (options.allowSearchNotes) {
|
||||
$el.on('keydown', (event) => {
|
||||
if (event.ctrlKey && event.key === 'Enter') {
|
||||
// Prevent Ctrl + Enter from triggering autoComplete.
|
||||
event.stopImmediatePropagation();
|
||||
event.preventDefault();
|
||||
$el.trigger('autocomplete:selected', { action: 'search-notes', noteTitle: $el.autocomplete("val")});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$el.autocomplete({
|
||||
...autocompleteOptions,
|
||||
appendTo: document.querySelector('body'),
|
||||
@@ -192,6 +213,12 @@ function initNoteAutocomplete($el, options) {
|
||||
suggestion.notePath = note.getBestNotePathString(hoistedNoteId);
|
||||
}
|
||||
|
||||
if (suggestion.action === 'search-notes') {
|
||||
const searchString = suggestion.noteTitle;
|
||||
appContext.triggerCommand('searchNotes', { searchString });
|
||||
return;
|
||||
}
|
||||
|
||||
$el.setSelectedNotePath(suggestion.notePath);
|
||||
$el.setSelectedExternalLink(null);
|
||||
|
||||
|
||||
@@ -371,7 +371,8 @@ class NoteListRenderer {
|
||||
$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");
|
||||
}
|
||||
|
||||
@@ -23,33 +23,48 @@ export function getStylesheetUrl(theme) {
|
||||
export async function applySyntaxHighlight($container) {
|
||||
if (!isSyntaxHighlightEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await library_loader.requireLibrary(library_loader.HIGHLIGHT_JS);
|
||||
}
|
||||
|
||||
const codeBlocks = $container.find("pre code");
|
||||
for (const codeBlock of codeBlocks) {
|
||||
$(codeBlock).parent().toggleClass("hljs");
|
||||
|
||||
const text = codeBlock.innerText;
|
||||
|
||||
const normalizedMimeType = extractLanguageFromClassList(codeBlock);
|
||||
if (!normalizedMimeType) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let highlightedText = null;
|
||||
if (normalizedMimeType === mime_types.MIME_TYPE_AUTO) {
|
||||
highlightedText = hljs.highlightAuto(text);
|
||||
} else if (normalizedMimeType) {
|
||||
const language = mime_types.getHighlightJsNameForMime(normalizedMimeType);
|
||||
highlightedText = hljs.highlight(text, { language });
|
||||
}
|
||||
|
||||
if (highlightedText) {
|
||||
codeBlock.innerHTML = highlightedText.value;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -20,6 +20,13 @@ const TPL = `
|
||||
width: 20em;
|
||||
}
|
||||
|
||||
.attachment-actions .dropdown-item .bx {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.attachment-actions .dropdown-item[disabled], .attachment-actions .dropdown-item[disabled]:hover {
|
||||
color: var(--muted-text-color) !important;
|
||||
background-color: transparent !important;
|
||||
@@ -32,16 +39,22 @@ const TPL = `
|
||||
style="position: relative; top: 3px;"></button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a data-trigger-command="openAttachment" class="dropdown-item"
|
||||
title="${t('attachments_actions.open_externally_title')}">${t('attachments_actions.open_externally')}</a>
|
||||
<a data-trigger-command="openAttachmentCustom" class="dropdown-item"
|
||||
title="${t('attachments_actions.open_custom_title')}">${t('attachments_actions.open_custom')}</a>
|
||||
<a data-trigger-command="downloadAttachment" class="dropdown-item">${t('attachments_actions.download')}</a>
|
||||
<a data-trigger-command="renameAttachment" class="dropdown-item">${t('attachments_actions.rename_attachment')}</a>
|
||||
<a data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item">${t('attachments_actions.upload_new_revision')}</a>
|
||||
<a data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item">${t('attachments_actions.copy_link_to_clipboard')}</a>
|
||||
<a data-trigger-command="convertAttachmentIntoNote" class="dropdown-item">${t('attachments_actions.convert_attachment_into_note')}</a>
|
||||
<a data-trigger-command="deleteAttachment" class="dropdown-item">${t('attachments_actions.delete_attachment')}</a>
|
||||
<li data-trigger-command="openAttachment" class="dropdown-item"
|
||||
title="${t('attachments_actions.open_externally_title')}"><span class="bx bx-link-external"></span> ${t('attachments_actions.open_externally')}</li>
|
||||
<li data-trigger-command="openAttachmentCustom" class="dropdown-item"
|
||||
title="${t('attachments_actions.open_custom_title')}"><span class="bx bx-customize"></span> ${t('attachments_actions.open_custom')}</li>
|
||||
<li data-trigger-command="renameAttachment" class="dropdown-item">
|
||||
<span class="bx bx-rename"></span> ${t('attachments_actions.rename_attachment')}</li>
|
||||
<li data-trigger-command="copyAttachmentLinkToClipboard" class="dropdown-item"><span class="bx bx-copy">
|
||||
</span> ${t('attachments_actions.copy_link_to_clipboard')}</li>
|
||||
<li data-trigger-command="downloadAttachment" class="dropdown-item">
|
||||
<span class="bx bx-download"></span> ${t('attachments_actions.download')}</li>
|
||||
<li data-trigger-command="uploadNewAttachmentRevision" class="dropdown-item"><span class="bx bx-upload">
|
||||
</span> ${t('attachments_actions.upload_new_revision')}</li>
|
||||
<li data-trigger-command="convertAttachmentIntoNote" class="dropdown-item"><span class="bx bx-note">
|
||||
</span> ${t('attachments_actions.convert_attachment_into_note')}</li>
|
||||
<li data-trigger-command="deleteAttachment" class="dropdown-item">
|
||||
<span class="bx bx-trash"></span> ${t('attachments_actions.delete_attachment')}</li>
|
||||
</div>
|
||||
|
||||
<input type="file" class="attachment-upload-new-revision-input" style="display: none">
|
||||
|
||||
@@ -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.`);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,42 +11,63 @@ import { t } from "../../services/i18n.js";
|
||||
const TPL = `
|
||||
<div class="dropdown note-actions">
|
||||
<style>
|
||||
.note-actions {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-menu {
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
|
||||
color: var(--muted-text-color) !important;
|
||||
background-color: transparent !important;
|
||||
pointer-events: none; /* makes it unclickable */
|
||||
}
|
||||
.note-actions {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-menu {
|
||||
min-width: 15em;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-item .bx {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
font-size: 120%;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.note-actions .dropdown-item[disabled], .note-actions .dropdown-item[disabled]:hover {
|
||||
color: var(--muted-text-color) !important;
|
||||
background-color: transparent !important;
|
||||
pointer-events: none; /* makes it unclickable */
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
|
||||
aria-expanded="false" class="icon-action bx bx-dots-vertical-rounded"></button>
|
||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
|
||||
class="icon-action bx bx-dots-vertical-rounded"></button>
|
||||
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
<a data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">${t('note_actions.convert_into_attachment')}</a>
|
||||
<a data-trigger-command="renderActiveNote" class="dropdown-item render-note-button"><kbd data-command="renderActiveNote"></kbd> ${t('note_actions.re_render_note')}</a>
|
||||
<a data-trigger-command="findInText" class="dropdown-item find-in-text-button">${t('note_actions.search_in_note')} <kbd data-command="findInText"></kbd></a>
|
||||
<a data-trigger-command="showNoteSource" class="dropdown-item show-source-button"><kbd data-command="showNoteSource"></kbd> ${t('note_actions.note_source')}</a>
|
||||
<a data-trigger-command="showAttachments" class="dropdown-item show-attachments-button"><kbd data-command="showAttachments"></kbd> ${t('note_actions.note_attachments')}</a>
|
||||
<a data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button"
|
||||
title="${t('note_actions.open_note_externally_title')}">
|
||||
<kbd data-command="openNoteExternally"></kbd>
|
||||
${t('note_actions.open_note_externally')}
|
||||
</a>
|
||||
<a data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button"><kbd data-command="openNoteCustom"></kbd> ${t('note_actions.open_note_custom')}</a>
|
||||
<a class="dropdown-item import-files-button">${t('note_actions.import_files')}</a>
|
||||
<a class="dropdown-item export-note-button">${t('note_actions.export_note')}</a>
|
||||
<a class="dropdown-item delete-note-button">${t('note_actions.delete_note')}</a>
|
||||
<a data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button"><kbd data-command="printActiveNote"></kbd> ${t('note_actions.print_note')}</a>
|
||||
<a data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button"><kbd data-command="forceSaveRevision"></kbd> ${t('note_actions.save_revision')}</a>
|
||||
<li data-trigger-command="convertNoteIntoAttachment" class="dropdown-item">
|
||||
<span class="bx bx-paperclip"></span> ${t('note_actions.convert_into_attachment')}
|
||||
</li>
|
||||
<li data-trigger-command="renderActiveNote" class="dropdown-item render-note-button">
|
||||
<span class="bx bx-extension"></span> ${t('note_actions.re_render_note')}<kbd data-command="renderActiveNote"></kbd>
|
||||
</li>
|
||||
<li data-trigger-command="findInText" class="dropdown-item find-in-text-button">
|
||||
<span class='bx bx-search'></span> ${t('note_actions.search_in_note')}<kbd data-command="findInText"></kbd>
|
||||
</li>
|
||||
<li data-trigger-command="showNoteSource" class="dropdown-item show-source-button">
|
||||
<span class="bx bx-code"></span> ${t('note_actions.note_source')}<kbd data-command="showNoteSource"></kbd>
|
||||
</li>
|
||||
<li data-trigger-command="showAttachments" class="dropdown-item show-attachments-button">
|
||||
<span class="bx bx-paperclip"></span> ${t('note_actions.note_attachments')}<kbd data-command="showAttachments"></kbd>
|
||||
</li>
|
||||
<li data-trigger-command="openNoteExternally" class="dropdown-item open-note-externally-button" title="${t('note_actions.open_note_externally_title')}">
|
||||
<span class="bx bx-link-external"></span> ${t('note_actions.open_note_externally')}<kbd data-command="openNoteExternally"></kbd>
|
||||
</li>
|
||||
<li data-trigger-command="openNoteCustom" class="dropdown-item open-note-custom-button">
|
||||
<span class="bx bx-customize"></span> ${t('note_actions.open_note_custom')}<kbd data-command="openNoteCustom"></kbd>
|
||||
</li>
|
||||
<li class="dropdown-item import-files-button"><span class="bx bx-import"></span> ${t('note_actions.import_files')}</li>
|
||||
<li class="dropdown-item export-note-button"><span class="bx bx-export"></span> ${t('note_actions.export_note')}</li>
|
||||
<li class="dropdown-item delete-note-button"><span class="bx bx-trash"></span> ${t('note_actions.delete_note')}</li>
|
||||
<li data-trigger-command="printActiveNote" class="dropdown-item print-active-note-button">
|
||||
<span class="bx bx-printer"></span> ${t('note_actions.print_note')}<kbd data-command="printActiveNote"></kbd></li>
|
||||
<li data-trigger-command="forceSaveRevision" class="dropdown-item save-revision-button">
|
||||
<span class="bx bx-save"></span> ${t('note_actions.save_revision')}<kbd data-command="forceSaveRevision"></kbd>
|
||||
</li>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,21 @@ export default class RibbonContainer extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
noteTypeMimeChangedEvent() {
|
||||
// We are ignoring the event which triggers a refresh since it is usually already done by a different
|
||||
// event and causing a race condition in which the items appear twice.
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed as soon as the user presses the "Edit" floating button in a read-only text note.
|
||||
*
|
||||
* <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)
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ export default class JumpToNoteDialog extends BasicWidget {
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
||||
allowCreatingNotes: true,
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowSearchNotes: true,
|
||||
container: this.$results
|
||||
})
|
||||
// clear any event listener added in previous invocation of this function
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { t } from "../services/i18n.js";
|
||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import FindInText from "./find_in_text.js";
|
||||
import FindInCode from "./find_in_code.js";
|
||||
import FindInHtml from "./find_in_html.js";
|
||||
@@ -16,27 +17,26 @@ const waitForEnter = (findWidgetDelayMillis < 0);
|
||||
// the focusout handler is called with relatedTarget equal to the label instead
|
||||
// of undefined. It's -1 instead of > 0, so they don't tabstop
|
||||
const TPL = `
|
||||
<div style="contain: none;">
|
||||
<div class='find-replace-widget' style="contain: none; border-top: 1px solid var(--main-border-color);">
|
||||
<style>
|
||||
.find-widget-box {
|
||||
padding: 10px;
|
||||
border-top: 1px solid var(--main-border-color);
|
||||
.find-widget-box, .replace-widget-box {
|
||||
padding: 2px 10px 2px 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.find-widget-box > * {
|
||||
.find-widget-box > *, .replace-widget-box > *{
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.find-widget-box {
|
||||
.find-widget-box, .replace-widget-box {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
|
||||
.find-widget-found-wrapper {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.find-widget-search-term-input-group {
|
||||
.find-widget-search-term-input-group, .replace-widget-replacetext-input {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
@@ -47,19 +47,23 @@ const TPL = `
|
||||
|
||||
<div class="find-widget-box">
|
||||
<div class="input-group find-widget-search-term-input-group">
|
||||
<input type="text" class="form-control find-widget-search-term-input">
|
||||
<input type="text" class="form-control find-widget-search-term-input" placeholder="${t('find.find_placeholder')}">
|
||||
<button class="btn btn-outline-secondary bx bxs-chevron-up find-widget-previous-button" type="button"></button>
|
||||
<button class="btn btn-outline-secondary bx bxs-chevron-down find-widget-next-button" type="button"></button>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">
|
||||
<label tabIndex="-1" class="form-check-label">${t('find.case_sensitive')}</label>
|
||||
<label tabIndex="-1" class="form-check-label">
|
||||
<input type="checkbox" class="form-check-input find-widget-case-sensitive-checkbox">
|
||||
${t('find.case_sensitive')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
|
||||
<label tabIndex="-1" class="form-check-label">${t('find.match_words')}</label>
|
||||
<label tabIndex="-1" class="form-check-label">
|
||||
<input type="checkbox" class="form-check-input find-widget-match-words-checkbox">
|
||||
${t('find.match_words')}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="find-widget-found-wrapper">
|
||||
@@ -72,6 +76,12 @@ const TPL = `
|
||||
|
||||
<div class="find-widget-close-button"><button class="btn icon-action bx bx-x"></button></div>
|
||||
</div>
|
||||
|
||||
<div class="replace-widget-box" style='display: none'>
|
||||
<input type="text" class="form-control replace-widget-replacetext-input" placeholder="${t('find.replace_placeholder')}">
|
||||
<button class="btn btn-sm replace-widget-replaceall-button" type="button">${t('find.replace_all')}</button>
|
||||
<button class="btn btn-sm replace-widget-replace-button" type="button">${t('find.replace')}</button>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
export default class FindWidget extends NoteContextAwareWidget {
|
||||
@@ -93,8 +103,7 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$findBox = this.$widget.find('.find-widget-box');
|
||||
this.$findBox.hide();
|
||||
this.$widget.hide();
|
||||
this.$input = this.$widget.find('.find-widget-search-term-input');
|
||||
this.$currentFound = this.$widget.find('.find-widget-current-found');
|
||||
this.$totalFound = this.$widget.find('.find-widget-total-found');
|
||||
@@ -109,6 +118,13 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
this.$closeButton = this.$widget.find(".find-widget-close-button");
|
||||
this.$closeButton.on("click", () => this.closeSearch());
|
||||
|
||||
this.$replaceWidgetBox = this.$widget.find(".replace-widget-box");
|
||||
this.$replaceTextInput = this.$widget.find(".replace-widget-replacetext-input");
|
||||
this.$replaceAllButton = this.$widget.find(".replace-widget-replaceall-button");
|
||||
this.$replaceAllButton.on("click", () => this.replaceAll());
|
||||
this.$replaceButton = this.$widget.find(".replace-widget-replace-button");
|
||||
this.$replaceButton.on("click", () => this.replace());
|
||||
|
||||
this.$input.keydown(async e => {
|
||||
if ((e.metaKey || e.ctrlKey) && (e.key === 'F' || e.key === 'f')) {
|
||||
// If ctrl+f is pressed when the findbox is shown, select the
|
||||
@@ -121,7 +137,7 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
});
|
||||
|
||||
this.$findBox.keydown(async e => {
|
||||
this.$widget.keydown(async e => {
|
||||
if (e.key === 'Escape') {
|
||||
await this.closeSearch();
|
||||
}
|
||||
@@ -142,13 +158,25 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
this.handler = await this.getHandler();
|
||||
|
||||
const isReadOnly = await this.noteContext.isReadOnly();
|
||||
|
||||
const selectedText = window.getSelection().toString() || "";
|
||||
|
||||
this.$findBox.show();
|
||||
let selectedText = '';
|
||||
if (this.note.type === 'code' && !isReadOnly){
|
||||
const codeEditor = await this.noteContext.getCodeEditor();
|
||||
selectedText = codeEditor.getSelection();
|
||||
}else{
|
||||
selectedText = window.getSelection().toString() || "";
|
||||
}
|
||||
this.$widget.show();
|
||||
this.$input.focus();
|
||||
if (['text', 'code'].includes(this.note.type) && !isReadOnly) {
|
||||
this.$replaceWidgetBox.show();
|
||||
}else{
|
||||
this.$replaceWidgetBox.hide();
|
||||
}
|
||||
|
||||
const isAlreadyVisible = this.$findBox.is(":visible");
|
||||
const isAlreadyVisible = this.$widget.is(":visible");
|
||||
|
||||
if (isAlreadyVisible) {
|
||||
if (selectedText) {
|
||||
@@ -254,8 +282,8 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
|
||||
async closeSearch() {
|
||||
if (this.$findBox.is(":visible")) {
|
||||
this.$findBox.hide();
|
||||
if (this.$widget.is(":visible")) {
|
||||
this.$widget.hide();
|
||||
|
||||
// Restore any state, if there's a current occurrence clear markers
|
||||
// and scroll to and select the last occurrence
|
||||
@@ -268,13 +296,27 @@ export default class FindWidget extends NoteContextAwareWidget {
|
||||
}
|
||||
}
|
||||
|
||||
async replace() {
|
||||
const replaceText = this.$replaceTextInput.val();
|
||||
await this.handler.replace(replaceText);
|
||||
}
|
||||
|
||||
async replaceAll() {
|
||||
const replaceText = this.$replaceTextInput.val();
|
||||
await this.handler.replaceAll(replaceText);
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return super.isEnabled() && ['text', 'code', 'render'].includes(this.note.type);
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({loadResults}) {
|
||||
async entitiesReloadedEvent({ loadResults }) {
|
||||
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
||||
this.$totalFound.text("?")
|
||||
} else if (loadResults.getAttributeRows().find(attr => attr.type === 'label'
|
||||
&& (attr.name.toLowerCase().includes('readonly'))
|
||||
&& attributeService.isAffecting(attr, this.note))) {
|
||||
this.closeSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,4 +170,55 @@ export default class FindInCode {
|
||||
|
||||
codeEditor.focus();
|
||||
}
|
||||
async replace(replaceText) {
|
||||
// this.findResult may be undefined and null
|
||||
if (!this.findResult || this.findResult.length===0){
|
||||
return;
|
||||
}
|
||||
let currentFound = -1;
|
||||
this.findResult.forEach((marker, index) => {
|
||||
const pos = marker.find();
|
||||
if (pos) {
|
||||
if (marker.className === FIND_RESULT_SELECTED_CSS_CLASSNAME) {
|
||||
currentFound = index;
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (currentFound >= 0) {
|
||||
let marker = this.findResult[currentFound];
|
||||
let pos = marker.find();
|
||||
const codeEditor = await this.getCodeEditor();
|
||||
const doc = codeEditor.doc;
|
||||
doc.replaceRange(replaceText, pos.from, pos.to);
|
||||
marker.clear();
|
||||
|
||||
let nextFound;
|
||||
if (currentFound === this.findResult.length - 1) {
|
||||
nextFound = 0;
|
||||
} else {
|
||||
nextFound = currentFound;
|
||||
}
|
||||
this.findResult.splice(currentFound, 1);
|
||||
if (this.findResult.length > 0) {
|
||||
this.findNext(0, nextFound, nextFound);
|
||||
}
|
||||
}
|
||||
}
|
||||
async replaceAll(replaceText) {
|
||||
if (!this.findResult || this.findResult.length===0){
|
||||
return;
|
||||
}
|
||||
const codeEditor = await this.getCodeEditor();
|
||||
const doc = codeEditor.doc;
|
||||
codeEditor.operation(() => {
|
||||
for (let currentFound = 0; currentFound < this.findResult.length; currentFound++) {
|
||||
let marker = this.findResult[currentFound];
|
||||
let pos = marker.find();
|
||||
doc.replaceRange(replaceText, pos.from, pos.to);
|
||||
marker.clear();
|
||||
}
|
||||
});
|
||||
this.findResult = [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export default class FindInText {
|
||||
const findAndReplaceEditing = textEditor.plugins.get('FindAndReplaceEditing');
|
||||
findAndReplaceEditing.state.clear(model);
|
||||
findAndReplaceEditing.stop();
|
||||
this.editingState = findAndReplaceEditing.state;
|
||||
if (searchTerm !== "") {
|
||||
// Parameters are callback/text, options.matchCase=false, options.wholeWords=false
|
||||
// See https://github.com/ckeditor/ckeditor5/blob/b95e2faf817262ac0e1e21993d9c0bde3f1be594/packages/ckeditor5-find-and-replace/src/findcommand.js#L44
|
||||
@@ -29,7 +30,7 @@ export default class FindInText {
|
||||
// let re = new RegExp(searchTerm, 'gi');
|
||||
// let m = text.match(re);
|
||||
// totalFound = m ? m.length : 0;
|
||||
const options = { "matchCase" : matchCase, "wholeWords" : wholeWord };
|
||||
const options = { "matchCase": matchCase, "wholeWords": wholeWord };
|
||||
findResult = textEditor.execute('find', searchTerm, options);
|
||||
totalFound = findResult.results.length;
|
||||
// Find the result beyond the cursor
|
||||
@@ -102,4 +103,18 @@ export default class FindInText {
|
||||
|
||||
textEditor.focus();
|
||||
}
|
||||
|
||||
async replace(replaceText) {
|
||||
if (this.editingState !== undefined && this.editingState.highlightedResult !== null) {
|
||||
const textEditor = await this.getTextEditor();
|
||||
textEditor.execute('replace', replaceText, this.editingState.highlightedResult);
|
||||
}
|
||||
}
|
||||
|
||||
async replaceAll(replaceText) {
|
||||
if (this.editingState !== undefined && this.editingState.results.length > 0) {
|
||||
const textEditor = await this.getTextEditor();
|
||||
textEditor.execute('replaceAll', replaceText, this.editingState.results);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5171,7 +5171,7 @@ const icons = [
|
||||
"type_of_icon": "REGULAR"
|
||||
},
|
||||
{
|
||||
"name": '_share',
|
||||
"name": "share",
|
||||
"slug": "share-regular",
|
||||
"category_id": 101,
|
||||
"type_of_icon": "REGULAR"
|
||||
@@ -6826,7 +6826,7 @@ const icons = [
|
||||
"type_of_icon": "SOLID"
|
||||
},
|
||||
{
|
||||
"name": '_share',
|
||||
"name": "share",
|
||||
"slug": "share-solid",
|
||||
"category_id": 101,
|
||||
"type_of_icon": "SOLID"
|
||||
|
||||
@@ -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-text"
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -32,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 {
|
||||
@@ -40,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 });
|
||||
|
||||
@@ -35,6 +35,7 @@ import AttachmentErasureTimeoutOptions from "./options/other/attachment_erasure_
|
||||
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>
|
||||
@@ -68,6 +69,7 @@ const CONTENT_WIDGETS = {
|
||||
],
|
||||
_optionsShortcuts: [ KeyboardShortcutsOptions ],
|
||||
_optionsTextNotes: [
|
||||
EditorOptions,
|
||||
HeadingStyleOptions,
|
||||
TableOfContentsOptions,
|
||||
HighlightsListOptions,
|
||||
|
||||
@@ -12,7 +12,6 @@ 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";
|
||||
import { isSyntaxHighlightEnabled } from "../../services/syntax_highlight.js";
|
||||
|
||||
const ENABLE_INSPECTOR = false;
|
||||
|
||||
@@ -107,6 +106,12 @@ function buildListOfLanguages() {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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"; }
|
||||
|
||||
@@ -125,6 +130,8 @@ 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 = buildListOfLanguages();
|
||||
|
||||
@@ -133,7 +140,7 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
// 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
|
||||
@@ -169,10 +176,23 @@ 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());
|
||||
|
||||
if (glob.isDev && ENABLE_INSPECTOR) {
|
||||
|
||||
@@ -70,6 +70,7 @@ export default class EmptyTypeWidget extends TypeWidget {
|
||||
noteAutocompleteService.initNoteAutocomplete(this.$autoComplete, {
|
||||
hideGoToSelectedNoteButton: true,
|
||||
allowCreatingNotes: true,
|
||||
allowSearchNotes: true,
|
||||
container: this.$results
|
||||
})
|
||||
.on('autocomplete:noteselected', function(event, suggestion, dataset) {
|
||||
|
||||
@@ -2,6 +2,8 @@ import OptionsWidget from "../options_widget.js";
|
||||
import utils from "../../../../services/utils.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
|
||||
const MIN_VALUE = 640;
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("max_content_width.title")}</h4>
|
||||
@@ -11,7 +13,7 @@ const TPL = `
|
||||
<div class="form-group row">
|
||||
<div class="col-6">
|
||||
<label>${t("max_content_width.max_width_label")}</label>
|
||||
<input type="number" min="200" step="10" class="max-content-width form-control options-number-input">
|
||||
<input type="number" min="${MIN_VALUE}" step="10" class="max-content-width form-control options-number-input">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +36,6 @@ export default class MaxContentWidthOptions extends OptionsWidget {
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
this.$maxContentWidth.val(options.maxContentWidth);
|
||||
this.$maxContentWidth.val(Math.max(MIN_VALUE, options.maxContentWidth));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,21 @@ const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t('backup.existing_backups')}</h4>
|
||||
|
||||
<ul class="existing-backup-list"></ul>
|
||||
<table class="table table-stripped">
|
||||
<colgroup>
|
||||
<col width="33%" />
|
||||
<col />
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>${t("backup.date-and-time")}</th>
|
||||
<th>${t("backup.path")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="existing-backup-list-items">
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -73,7 +87,7 @@ export default class BackupOptions extends OptionsWidget {
|
||||
this.$monthlyBackupEnabled.on('change', () =>
|
||||
this.updateCheckboxOption('monthlyBackupEnabled', this.$monthlyBackupEnabled));
|
||||
|
||||
this.$existingBackupList = this.$widget.find(".existing-backup-list");
|
||||
this.$existingBackupList = this.$widget.find(".existing-backup-list-items");
|
||||
}
|
||||
|
||||
optionsLoaded(options) {
|
||||
@@ -85,11 +99,34 @@ export default class BackupOptions extends OptionsWidget {
|
||||
this.$existingBackupList.empty();
|
||||
|
||||
if (!backupFiles.length) {
|
||||
backupFiles = [{filePath: t('backup.no_backup_yet'), mtime: ''}];
|
||||
this.$existingBackupList.append($(`
|
||||
<tr>
|
||||
<td class="empty-table-placeholder" colspan="2">${t('backup.no_backup_yet')}</td>
|
||||
</tr>
|
||||
`));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Sort the backup files by modification date & time in a desceding order
|
||||
backupFiles.sort((a, b) => {
|
||||
if (a.mtime < b.mtime) return 1;
|
||||
if (a.mtime > b.mtime) return -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
const dateTimeFormatter = new Intl.DateTimeFormat(navigator.language, {
|
||||
dateStyle: "medium",
|
||||
timeStyle: "medium"
|
||||
});
|
||||
|
||||
for (const {filePath, mtime} of backupFiles) {
|
||||
this.$existingBackupList.append($("<li>").text(`${filePath} ${mtime ? ` - ${mtime}` : ''}`));
|
||||
this.$existingBackupList.append($(`
|
||||
<tr>
|
||||
<td>${(mtime) ? dateTimeFormatter.format(new Date(mtime)) : "-"}</td>
|
||||
<td>${filePath}</td>
|
||||
</tr>
|
||||
`));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -95,9 +95,9 @@ export default class EtapiOptions extends OptionsWidget {
|
||||
.append($("<td>").text(token.name))
|
||||
.append($("<td>").text(token.utcDateCreated))
|
||||
.append($("<td>").append(
|
||||
$('<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>')
|
||||
$(`<span class="bx bx-pen token-table-button" title="${t("etapi.rename_token")}"></span>`)
|
||||
.on("click", () => this.renameToken(token.etapiTokenId, token.name)),
|
||||
$('<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>')
|
||||
$(`<span class="bx bx-trash token-table-button" title="${t("etapi.delete_token")}"></span>`)
|
||||
.on("click", () => this.deleteToken(token.etapiTokenId, token.name))
|
||||
))
|
||||
);
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
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>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio" name="editor-type" value="ckeditor-balloon" />
|
||||
<strong>${t("editing.editor_type.floating.title")}</strong>
|
||||
- ${t("editing.editor_type.floating.description")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label>
|
||||
<input type="radio" name="editor-type" value="ckeditor-classic" />
|
||||
<strong>${t("editing.editor_type.fixed.title")}</strong>
|
||||
- ${t("editing.editor_type.fixed.description")}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>`;
|
||||
|
||||
export default class EditorOptions extends OptionsWidget {
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$body = $("body");
|
||||
this.$widget.find(`input[name="editor-type"]`).on('change', async () => {
|
||||
const newEditorType = this.$widget.find(`input[name="editor-type"]:checked`).val();
|
||||
await this.updateOption('textNoteEditorType', newEditorType);
|
||||
utils.reloadFrontendApp("editor type change");
|
||||
});
|
||||
}
|
||||
|
||||
async optionsLoaded(options) {
|
||||
this.$widget.find(`input[name="editor-type"][value="${options.textNoteEditorType}"]`)
|
||||
.prop("checked", "true");
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
|
||||
|
||||
await this.initialized;
|
||||
|
||||
resolve(this.$content);
|
||||
resolve(this.$editor);
|
||||
}
|
||||
|
||||
format(html) {
|
||||
|
||||
@@ -1238,3 +1238,7 @@ textarea {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.empty-table-placeholder {
|
||||
text-align: center;
|
||||
color: var(--muted-text-color);
|
||||
}
|
||||
@@ -82,8 +82,7 @@
|
||||
"no_note_to_delete": "Es werden keine Notizen gelöscht (nur Klone).",
|
||||
"broken_relations_to_be_deleted": "Folgende Beziehungen werden gelöst und gelöscht (<span class=\"broke-relations-count\"></span>)",
|
||||
"cancel": "Abbrechen",
|
||||
"ok": "OK",
|
||||
"to_be_deleted": "(zu löschen) wird durch die Beziehung <code>{{attrName}}</code> referenziert, die von folgendem stammt "
|
||||
"ok": "OK"
|
||||
},
|
||||
"export": {
|
||||
"export_note_title": "Notiz exportieren",
|
||||
@@ -234,8 +233,8 @@
|
||||
"erase_notes_button": "Jetzt gelöschte Notizen löschen",
|
||||
"deleted_notes_message": "Gelöschte Notizen wurden gelöscht.",
|
||||
"no_changes_message": "Noch keine Änderungen...",
|
||||
"Wiederherstellen_link": "Wiederherstellen",
|
||||
"confirm_undelete": "Möchtest du diese Notiz und ihre Unternotizen wiederherstellen?"
|
||||
"undelete_link": "Wiederherstellen",
|
||||
"confirm_undelete": "Möchten Sie diese Notiz und ihre Unternotizen wiederherstellen?"
|
||||
},
|
||||
"revisions": {
|
||||
"note_revisions": "Notizrevisionen",
|
||||
@@ -264,12 +263,12 @@
|
||||
"sort_child_notes": {
|
||||
"sort_children_by": "Unternotizen sortieren nach...",
|
||||
"sorting_criteria": "Sortierkriterien",
|
||||
"Titel": "Titel",
|
||||
"title": "Titel",
|
||||
"date_created": "Erstellungsdatum",
|
||||
"date_modified": "Änderungsdatum",
|
||||
"sorting_direction": "Sortierrichtung",
|
||||
"aufsteigend": "aufsteigend",
|
||||
"absteigend": "absteigend",
|
||||
"ascending": "aufsteigend",
|
||||
"descending": "absteigend",
|
||||
"folders": "Ordner",
|
||||
"sort_folders_at_top": "Ordne die Ordner oben",
|
||||
"natural_sort": "Natürliche Sortierung",
|
||||
@@ -756,7 +755,7 @@
|
||||
"type": "Typ",
|
||||
"note_size": "Notengröße",
|
||||
"note_size_info": "Die Notizgröße bietet eine grobe Schätzung des Speicherbedarfs für diese Notiz. Es berücksichtigt den Inhalt der Notiz und den Inhalt ihrer Notizrevisionen.",
|
||||
"berechnen": "berechnen",
|
||||
"calculate": "berechnen",
|
||||
"subtree_size": "(Teilbaumgröße: {{size}} in {{count}} Notizen)",
|
||||
"title": "Hinweisinfo"
|
||||
},
|
||||
@@ -800,19 +799,19 @@
|
||||
"add_search_option": "Suchoption hinzufügen:",
|
||||
"search_string": "Suchzeichenfolge",
|
||||
"search_script": "Suchskript",
|
||||
"Vorfahr": "Vorfahr",
|
||||
"ancestor": "Vorfahr",
|
||||
"fast_search": "schnelle Suche",
|
||||
"fast_search_description": "Die Option „Schnellsuche“ deaktiviert die Volltextsuche von Notizinhalten, was die Suche in großen Datenbanken beschleunigen könnte.",
|
||||
"include_archived": "archiviert einschließen",
|
||||
"include_archived_notes_description": "Archivierte Notizen sind standardmäßig von den Suchergebnissen ausgeschlossen, mit dieser Option werden sie einbezogen.",
|
||||
"order_by": "Bestellen nach",
|
||||
"Limit": "Limit",
|
||||
"limit": "Limit",
|
||||
"limit_description": "Begrenze die Anzahl der Ergebnisse",
|
||||
"debuggen": "debuggen",
|
||||
"debug": "debuggen",
|
||||
"debug_description": "Debug gibt zusätzliche Debuginformationen in die Konsole aus, um das Debuggen komplexer Abfragen zu erleichtern",
|
||||
"Aktion": "Aktion",
|
||||
"action": "Aktion",
|
||||
"search": "Suchen",
|
||||
"eingeben": "eingeben",
|
||||
"enter": "eingeben",
|
||||
"search_execute": "Aktionen suchen und ausführen",
|
||||
"save_to_note": "Als Notiz speichern",
|
||||
"search_parameters": "Suchparameter",
|
||||
@@ -831,7 +830,7 @@
|
||||
"ancestor": {
|
||||
"label": "Vorfahre",
|
||||
"placeholder": "Suche nach einer Notiz anhand ihres Namens",
|
||||
"Tiefe_label": "Tiefe",
|
||||
"depth_label": "Tiefe",
|
||||
"depth_doesnt_matter": "spielt keine Rolle",
|
||||
"depth_eq": "ist genau {{count}}",
|
||||
"direct_children": "direkte Kinder",
|
||||
@@ -1044,8 +1043,8 @@
|
||||
},
|
||||
"native_title_bar": {
|
||||
"title": "Native Titelleiste (App-Neustart erforderlich)",
|
||||
"ermöglicht": "ermöglicht",
|
||||
"deaktiviert": "deaktiviert"
|
||||
"enabled": "ermöglicht",
|
||||
"disabled": "deaktiviert"
|
||||
},
|
||||
"ribbon": {
|
||||
"widgets": "Multifunktionsleisten-Widgets",
|
||||
@@ -1187,7 +1186,7 @@
|
||||
"no_backup_yet": "noch kein Backup"
|
||||
},
|
||||
"etapi": {
|
||||
"title": "BÜHNE",
|
||||
"title": "ETAPI",
|
||||
"description": "ETAPI ist eine REST-API, die für den programmgesteuerten Zugriff auf die Trilium-Instanz ohne Benutzeroberfläche verwendet wird.",
|
||||
"see_more": "Weitere Details findest du unter",
|
||||
"wiki": "Woche",
|
||||
|
||||
@@ -51,7 +51,11 @@
|
||||
"chosen_actions": "Chosen actions",
|
||||
"execute_bulk_actions": "Execute bulk actions",
|
||||
"bulk_actions_executed": "Bulk actions have been executed successfully.",
|
||||
"none_yet": "None yet... add an action by clicking one of the available ones above."
|
||||
"none_yet": "None yet... add an action by clicking one of the available ones above.",
|
||||
"labels": "Labels",
|
||||
"relations": "Relations",
|
||||
"notes": "Notes",
|
||||
"other": "Other"
|
||||
},
|
||||
"clone_to": {
|
||||
"clone_notes_to": "Clone notes to...",
|
||||
@@ -238,23 +242,23 @@
|
||||
"confirm_undelete": "Do you want to undelete this note and its sub-notes?"
|
||||
},
|
||||
"revisions": {
|
||||
"note_revisions": "Note revisions",
|
||||
"note_revisions": "Note Revisions",
|
||||
"delete_all_revisions": "Delete all revisions of this note",
|
||||
"delete_all_button": "Delete all revisions",
|
||||
"help_title": "Help on Note revisions",
|
||||
"help_title": "Help on Note Revisions",
|
||||
"revision_last_edited": "This revision was last edited on {{date}}",
|
||||
"confirm_delete_all": "Do you want to delete all revisions of this note? This action will erase revision title and content, but still preserve revision metadata.",
|
||||
"confirm_delete_all": "Do you want to delete all revisions of this note? This action will erase the revision title and content, but still preserve the revision metadata.",
|
||||
"no_revisions": "No revisions for this note yet...",
|
||||
"restore_button": "Restore this revision",
|
||||
"confirm_restore": "Do you want to restore this revision? This will overwrite current title and content of the note with this revision.",
|
||||
"confirm_restore": "Do you want to restore this revision? This will overwrite the current title and content of the note with this revision.",
|
||||
"delete_button": "Delete this revision",
|
||||
"confirm_delete": "Do you want to delete this revision? This action will delete revision title and content, but still preserve revision metadata.",
|
||||
"revisions_deleted": "Note revisions has been deleted.",
|
||||
"confirm_delete": "Do you want to delete this revision? This action will delete the revision title and content, but still preserve the revision metadata.",
|
||||
"revisions_deleted": "Note revisions have been deleted.",
|
||||
"revision_restored": "Note revision has been restored.",
|
||||
"revision_deleted": "Note revision has been deleted.",
|
||||
"snapshot_interval": "Note Revisions Snapshot Interval: {{seconds}}s.",
|
||||
"maximum_revisions": "Maximum revisions for current note: {{number}}.",
|
||||
"settings": "Settings for Note revisions",
|
||||
"snapshot_interval": "Note Revision Snapshot Interval: {{seconds}}s.",
|
||||
"maximum_revisions": "Note Revision Snapshot Limit: {{number}}.",
|
||||
"settings": "Note Revision Settings",
|
||||
"download_button": "Download",
|
||||
"mime": "MIME: ",
|
||||
"file_size": "File size:",
|
||||
@@ -1108,12 +1112,12 @@
|
||||
"deleted_notes_erased": "Deleted notes have been erased."
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Note Revisions Snapshot Interval",
|
||||
"note_revisions_snapshot_description": "Note revision snapshot time interval is time in seconds after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
|
||||
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time in seconds after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
|
||||
"snapshot_time_interval_label": "Note revision snapshot time interval (in seconds):"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "Note Revision Snapshots Limit",
|
||||
"note_revisions_snapshot_limit_title": "Note Revision Snapshot Limit",
|
||||
"note_revisions_snapshot_limit_description": "The note revision snapshot number limit refers to the maximum number of revisions that can be saved for each note. Where -1 means no limit, 0 means delete all revisions. You can set the maximum revisions for a single note through the #versioningLimit label.",
|
||||
"snapshot_number_limit_label": "Note revision snapshot number limit:",
|
||||
"erase_excess_revision_snapshots": "Erase excess revision snapshots now",
|
||||
@@ -1183,6 +1187,8 @@
|
||||
"backup_now": "Backup now",
|
||||
"backup_database_now": "Backup database now",
|
||||
"existing_backups": "Existing backups",
|
||||
"date-and-time": "Date & time",
|
||||
"path": "Path",
|
||||
"database_backed_up_to": "Database has been backed up to",
|
||||
"no_backup_yet": "no backup yet"
|
||||
},
|
||||
@@ -1378,8 +1384,12 @@
|
||||
},
|
||||
"open-help-page": "Open help page",
|
||||
"find": {
|
||||
"case_sensitive": "case sensitive",
|
||||
"match_words": "match words"
|
||||
"case_sensitive": "Case sensitive",
|
||||
"match_words": "Match words",
|
||||
"find_placeholder": "Find in text...",
|
||||
"replace_placeholder": "Replace with...",
|
||||
"replace": "Replace",
|
||||
"replace_all": "Replace all"
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Highlights List",
|
||||
@@ -1508,5 +1518,24 @@
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "Word wrapping"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "Formatting"
|
||||
},
|
||||
"editor": {
|
||||
"title": "Editor"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "Formatting toolbar",
|
||||
"floating": {
|
||||
"title": "Floating",
|
||||
"description": "editing tools appear near the cursor;"
|
||||
},
|
||||
"fixed": {
|
||||
"title": "Fixed",
|
||||
"description": "editing tools appear in the \"Formatting\" ribbon tab."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1378,8 +1378,12 @@
|
||||
},
|
||||
"open-help-page": "Abrir página de ayuda",
|
||||
"find": {
|
||||
"case_sensitive": "distingue entre mayúsculas y minúsculas",
|
||||
"match_words": "coincidir palabras"
|
||||
"case_sensitive": "Distingue entre mayúsculas y minúsculas",
|
||||
"match_words": "Coincidir palabras",
|
||||
"find_placeholder": "Encontrar en texto...",
|
||||
"replace_placeholder": "Reemplazar con...",
|
||||
"replace": "Reemplazar",
|
||||
"replace_all": "Reemplazar todo"
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Lista de destacados",
|
||||
@@ -1508,5 +1512,24 @@
|
||||
},
|
||||
"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": {
|
||||
"title": "Flotante",
|
||||
"description": "las herramientas de edición aparecen cerca del cursor;"
|
||||
},
|
||||
"fixed": {
|
||||
"title": "Fijo",
|
||||
"description": "las herramientas de edición aparecen en la pestaña de la cinta \"Formato\")."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,6 +254,8 @@
|
||||
"enable_monthly_backup": "Activează copia de siguranță lunară",
|
||||
"enable_weekly_backup": "Activează copia de siguranță săptămânală",
|
||||
"existing_backups": "Copii de siguranță existente",
|
||||
"date-and-time": "Data și ora",
|
||||
"path": "Calea fișierului",
|
||||
"no_backup_yet": "nu există încă nicio copie de siguranță"
|
||||
},
|
||||
"basic_properties": {
|
||||
@@ -297,7 +299,11 @@
|
||||
"close": "Închide",
|
||||
"execute_bulk_actions": "Execută acțiunile în masă",
|
||||
"include_descendants": "Include descendenții notiței selectate",
|
||||
"none_yet": "Nicio acțiune... adaugați una printr-un click pe cele disponibile mai jos."
|
||||
"none_yet": "Nicio acțiune... adăugați una printr-un click pe cele disponibile mai jos.",
|
||||
"labels": "Etichete",
|
||||
"notes": "Notițe",
|
||||
"other": "Altele",
|
||||
"relations": "Relații"
|
||||
},
|
||||
"calendar": {
|
||||
"april": "Aprilie",
|
||||
@@ -1349,7 +1355,11 @@
|
||||
"open-help-page": "Deschide pagina de informații",
|
||||
"find": {
|
||||
"match_words": "doar cuvinte întregi",
|
||||
"case_sensitive": "ține cont de majuscule"
|
||||
"case_sensitive": "ține cont de majuscule",
|
||||
"replace_all": "Înlocuiește totul",
|
||||
"replace_placeholder": "Înlocuiește cu...",
|
||||
"replace": "Înlocuiește",
|
||||
"find_placeholder": "Căutați în text..."
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"options": "Setări",
|
||||
@@ -1508,5 +1518,24 @@
|
||||
},
|
||||
"code_block": {
|
||||
"word_wrapping": "Încadrare text"
|
||||
},
|
||||
"classic_editor_toolbar": {
|
||||
"title": "Formatare"
|
||||
},
|
||||
"editing": {
|
||||
"editor_type": {
|
||||
"label": "Bară de formatare",
|
||||
"floating": {
|
||||
"title": "Editor cu bară flotantă",
|
||||
"description": "uneltele de editare vor apărea lângă cursor."
|
||||
},
|
||||
"fixed": {
|
||||
"title": "Editor cu bară fixă",
|
||||
"description": "uneltele de editare vor apărea în tab-ul „Formatare” din panglică;"
|
||||
}
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"title": "Editor"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,8 @@ const ALLOWED_OPTIONS = new Set([
|
||||
'promotedAttributesOpenInRibbon',
|
||||
'editedNotesOpenInRibbon',
|
||||
'locale',
|
||||
'firstDayOfWeek'
|
||||
'firstDayOfWeek',
|
||||
'textNoteEditorType'
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
||||
@@ -42,7 +42,7 @@ function index(req: Request, res: Response) {
|
||||
isDev: env.isDev(),
|
||||
isMainWindow: !req.query.extraWindow,
|
||||
isProtectedSessionAvailable: protectedSessionService.isProtectedSessionAvailable(),
|
||||
maxContentWidth: parseInt(options.maxContentWidth),
|
||||
maxContentWidth: Math.max(640, parseInt(options.maxContentWidth)),
|
||||
triliumVersion: packageJson.version,
|
||||
assetPath: assetPath,
|
||||
appPath: appPath
|
||||
|
||||
@@ -9,6 +9,7 @@ import themeNames from "./code_block_theme_names.json" with { type: "json" }
|
||||
import { t } from "i18next";
|
||||
import { join } from "path";
|
||||
import utils from "./utils.js";
|
||||
import env from "./env.js";
|
||||
|
||||
/**
|
||||
* Represents a color scheme for the code block syntax highlight.
|
||||
@@ -30,8 +31,7 @@ interface ColorTheme {
|
||||
* @returns the supported themes, grouped.
|
||||
*/
|
||||
export function listSyntaxHighlightingThemes() {
|
||||
const stylesDir = (!utils.isElectron() ? "node_modules/@highlightjs/cdn-assets/styles" : "styles");
|
||||
const path = join(utils.getResourceDir(), stylesDir);
|
||||
const path = join(utils.getResourceDir(), getStylesDirectory());
|
||||
const systemThemes = readThemesFromFileSystem(path);
|
||||
|
||||
return {
|
||||
@@ -45,6 +45,14 @@ export function listSyntaxHighlightingThemes() {
|
||||
}
|
||||
}
|
||||
|
||||
function getStylesDirectory() {
|
||||
if (utils.isElectron() && !env.isDev()) {
|
||||
return "styles";
|
||||
}
|
||||
|
||||
return "node_modules/@highlightjs/cdn-assets/styles";
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads all the predefined themes by listing all minified CSSes from a given directory.
|
||||
*
|
||||
|
||||
@@ -420,6 +420,12 @@ function getDefaultKeyboardActions() {
|
||||
separator: t("keyboard_actions.ribbon-tabs")
|
||||
},
|
||||
|
||||
{
|
||||
actionName: "toggleRibbonTabClassicEditor",
|
||||
defaultShortcuts: [],
|
||||
description: t("keyboard_actions.toggle-classic-editor-toolbar"),
|
||||
scope: "window"
|
||||
},
|
||||
{
|
||||
actionName: "toggleRibbonTabBasicProperties",
|
||||
defaultShortcuts: [],
|
||||
|
||||
@@ -131,7 +131,10 @@ const defaultOptions: DefaultOption[] = [
|
||||
return "default:stackoverflow-dark";
|
||||
}
|
||||
}, isSynced: false },
|
||||
{ name: "codeBlockWordWrap", value: "false", isSynced: true }
|
||||
{ name: "codeBlockWordWrap", value: "false", isSynced: true },
|
||||
|
||||
// Text note configuration
|
||||
{ name: "textNoteEditorType", value: "ckeditor-balloon", isSynced: true }
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -111,13 +111,9 @@ async function createMainWindow(app: App) {
|
||||
}
|
||||
|
||||
function configureWebContents(webContents: WebContents, spellcheckEnabled: boolean) {
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
remoteMain.enable(webContents);
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
webContents.setWindowOpenHandler((details) => {
|
||||
async function openExternal() {
|
||||
(await import('electron')).shell.openExternal(details.url);
|
||||
}
|
||||
|
||||
@@ -89,7 +89,8 @@
|
||||
"copy-without-formatting": "Copy selected text without formatting",
|
||||
"force-save-revision": "Force creating / saving new note revision of the active note",
|
||||
"show-help": "Shows built-in Help / cheatsheet",
|
||||
"toggle-book-properties": "Toggle Book Properties"
|
||||
"toggle-book-properties": "Toggle Book Properties",
|
||||
"toggle-classic-editor-toolbar": "Toggle the Formatting tab for the editor with fixed toolbar"
|
||||
},
|
||||
"login": {
|
||||
"title": "Login",
|
||||
|
||||
@@ -89,7 +89,8 @@
|
||||
"copy-without-formatting": "Copiar el texto seleccionado sin formatear",
|
||||
"force-save-revision": "Forzar la creación/guardado de una nueva revisión de nota de la nota activa",
|
||||
"show-help": "Muestra ayuda/hoja de referencia integrada",
|
||||
"toggle-book-properties": "Alternar propiedades del libro"
|
||||
"toggle-book-properties": "Alternar propiedades del libro",
|
||||
"toggle-classic-editor-toolbar": "Alternar la pestaña de formato por el editor con barra de herramientas fija"
|
||||
},
|
||||
"login": {
|
||||
"title": "Iniciar sesión",
|
||||
|
||||
@@ -89,7 +89,8 @@
|
||||
"toggle-tray": "Afișează/ascunde aplicația din tray-ul de sistem",
|
||||
"unhoist": "Defocalizează complet",
|
||||
"zoom-in": "Mărește zoom-ul",
|
||||
"zoom-out": "Micșorează zoom-ul"
|
||||
"zoom-out": "Micșorează zoom-ul",
|
||||
"toggle-classic-editor-toolbar": "Comută tab-ul „Formatare” pentru editorul cu bară fixă"
|
||||
},
|
||||
"login": {
|
||||
"button": "Autentifică",
|
||||
|
||||
196
translations/tw/server.json
Normal file
196
translations/tw/server.json
Normal file
@@ -0,0 +1,196 @@
|
||||
{
|
||||
"keyboard_actions":{
|
||||
"open-jump-to-note-dialog":"打開「跳轉到筆記」對話框",
|
||||
"search-in-subtree":"在當前筆記的子樹中搜索筆記",
|
||||
"expand-subtree":"展開當前筆記的子樹",
|
||||
"collapse-tree":"折疊完整的筆記樹",
|
||||
"collapse-subtree":"折疊當前筆記的子樹",
|
||||
"sort-child-notes":"排序子筆記",
|
||||
"creating-and-moving-notes":"新增和移動筆記",
|
||||
"create-note-into-inbox":"在收件匣(如果有定義的話)或日記中新增筆記",
|
||||
"delete-note":"刪除筆記",
|
||||
"move-note-up":"上移筆記",
|
||||
"move-note-down":"下移筆記",
|
||||
"move-note-up-in-hierarchy":"上移筆記層級",
|
||||
"move-note-down-in-hierarchy":"下移筆記層級",
|
||||
"edit-note-title":"從筆記樹跳轉到筆記詳情並編輯標題",
|
||||
"edit-branch-prefix":"顯示編輯分支前綴對話框",
|
||||
"note-clipboard":"筆記剪貼簿",
|
||||
"copy-notes-to-clipboard":"複製選定的筆記到剪貼簿",
|
||||
"paste-notes-from-clipboard":"從剪貼簿粘貼筆記到活動筆記中",
|
||||
"cut-notes-to-clipboard":"剪下選定的筆記到剪貼簿",
|
||||
"select-all-notes-in-parent":"選擇當前筆記級別的所有筆記",
|
||||
"add-note-above-to-the-selection":"將上方筆記添加到選擇中",
|
||||
"add-note-below-to-selection":"將下方筆記添加到選擇中",
|
||||
"duplicate-subtree":"複製子樹",
|
||||
"tabs-and-windows":"標籤和窗口",
|
||||
"open-new-tab":"打開新標籤",
|
||||
"close-active-tab":"關閉活動標籤",
|
||||
"reopen-last-tab":"重新打開最後關閉的標籤",
|
||||
"activate-next-tab":"激活右側標籤",
|
||||
"activate-previous-tab":"激活左側標籤",
|
||||
"open-new-window":"打開新空白窗口",
|
||||
"toggle-tray":"顯示/隱藏應用程式的系統托盤",
|
||||
"first-tab":"激活列表中的第一個標籤",
|
||||
"second-tab":"激活列表中的第二個標籤",
|
||||
"third-tab":"激活列表中的第三個標籤",
|
||||
"fourth-tab":"激活列表中的第四個標籤",
|
||||
"fifth-tab":"激活列表中的第五個標籤",
|
||||
"sixth-tab":"激活列表中的第六個標籤",
|
||||
"seventh-tab":"激活列表中的第七個標籤",
|
||||
"eight-tab":"激活列表中的第八個標籤",
|
||||
"ninth-tab":"激活列表中的第九個標籤",
|
||||
"last-tab":"激活列表中的最後一個標籤",
|
||||
"dialogs":"對話框",
|
||||
"show-note-source":"顯示筆記源對話框",
|
||||
"show-options":"顯示選項對話框",
|
||||
"show-revisions":"顯示筆記歷史對話框",
|
||||
"show-recent-changes":"顯示最近更改對話框",
|
||||
"show-sql-console":"顯示SQL控制台對話框",
|
||||
"show-backend-log":"顯示後端日誌對話框",
|
||||
"text-note-operations":"文本筆記操作",
|
||||
"add-link-to-text":"打開對話框以將鏈接添加到文本",
|
||||
"follow-link-under-cursor":"跟隨遊標下的鏈接",
|
||||
"insert-date-and-time-to-text":"將當前日期和時間插入文本",
|
||||
"paste-markdown-into-text":"將剪貼簿中的Markdown粘貼到文本筆記中",
|
||||
"cut-into-note":"從當前筆記中剪下選擇並新增包含選定文本的子筆記",
|
||||
"add-include-note-to-text":"打開對話框以包含筆記",
|
||||
"edit-readonly-note":"編輯唯讀筆記",
|
||||
"attributes-labels-and-relations":"屬性(標籤和關係)",
|
||||
"add-new-label":"新增新標籤",
|
||||
"create-new-relation":"新增新關係",
|
||||
"ribbon-tabs":"功能區標籤",
|
||||
"toggle-basic-properties":"切換基本屬性",
|
||||
"toggle-file-properties":"切換文件屬性",
|
||||
"toggle-image-properties":"切換圖像屬性",
|
||||
"toggle-owned-attributes":"切換擁有的屬性",
|
||||
"toggle-inherited-attributes":"切換繼承的屬性",
|
||||
"toggle-promoted-attributes":"切換提升的屬性",
|
||||
"toggle-link-map":"切換鏈接地圖",
|
||||
"toggle-note-info":"切換筆記資訊",
|
||||
"toggle-note-paths":"切換筆記路徑",
|
||||
"toggle-similar-notes":"切換相似筆記",
|
||||
"other":"其他",
|
||||
"toggle-right-pane":"切換右側面板的顯示,包括目錄和高亮",
|
||||
"print-active-note":"打印活動筆記",
|
||||
"open-note-externally":"以預設應用程式打開筆記文件",
|
||||
"render-active-note":"渲染(重新渲染)活動筆記",
|
||||
"run-active-note":"運行主動的JavaScript(前端/後端)代碼筆記",
|
||||
"toggle-note-hoisting":"切換活動筆記的提升",
|
||||
"unhoist":"從任何地方取消提升",
|
||||
"reload-frontend-app":"重新加載前端應用",
|
||||
"open-dev-tools":"打開開發工具",
|
||||
"toggle-left-note-tree-panel":"切換左側(筆記樹)面板",
|
||||
"toggle-full-screen":"切換全熒幕",
|
||||
"zoom-out":"縮小",
|
||||
"zoom-in":"放大",
|
||||
"note-navigation":"筆記導航",
|
||||
"reset-zoom-level":"重置縮放級別",
|
||||
"copy-without-formatting":"複製不帶格式的選定文本",
|
||||
"force-save-revision":"強制新增/保存當前筆記的歷史版本",
|
||||
"show-help":"顯示內置說明/備忘單",
|
||||
"toggle-book-properties":"切換書籍屬性"
|
||||
},
|
||||
"login":{
|
||||
"title":"登入",
|
||||
"heading":"Trilium登入",
|
||||
"incorrect-password":"密碼不正確。請再試一次。",
|
||||
"password":"密碼",
|
||||
"remember-me":"記住我",
|
||||
"button":"登入"
|
||||
},
|
||||
"set_password":{
|
||||
"heading":"設定密碼",
|
||||
"description":"在您可以從Web開始使用Trilium之前,您需要先設定一個密碼。然後您將使用此密碼登錄。",
|
||||
"password":"密碼",
|
||||
"password-confirmation":"密碼確認",
|
||||
"button":"設定密碼"
|
||||
},
|
||||
"javascript-required":"Trilium需要啓用JavaScript。",
|
||||
"setup":{
|
||||
"heading":"TriliumNext筆記設定",
|
||||
"new-document":"我是新用戶,我想為我的筆記新增一個新的Trilium檔案",
|
||||
"sync-from-desktop":"我已經有一個桌面實例,我想設定與它的同步",
|
||||
"sync-from-server":"我已經有一個伺服器實例,我想設定與它的同步",
|
||||
"next":"下一步",
|
||||
"init-in-progress":"檔案初始化進行中",
|
||||
"redirecting":"您將很快被重定向到應用程式。",
|
||||
"title":"設定"
|
||||
},
|
||||
"setup_sync-from-desktop":{
|
||||
"heading":"從桌面同步",
|
||||
"description":"此設定需要從桌面實例啓動:",
|
||||
"step1":"打開您的TriliumNext筆記桌面實例。",
|
||||
"step2":"從Trilium菜單中,點擊選項。",
|
||||
"step3":"點擊同步。",
|
||||
"step4":"將伺服器實例地址更改為:{{- host}}並點擊保存。",
|
||||
"step5":"點擊「測試同步」按鈕以驗證連接是否成功。",
|
||||
"step6":"完成這些步驟後,點擊{{- link}}。",
|
||||
"step6-here":"這裡"
|
||||
},
|
||||
"setup_sync-from-server":{
|
||||
"heading":"從伺服器同步",
|
||||
"instructions":"請在下面輸入Trilium伺服器地址和密碼。這將從伺服器下載整個Trilium數據庫檔案並設定同步。因應數據庫大小和您的連接速度,這可能需要一段時間。",
|
||||
"server-host":"Trilium伺服器地址",
|
||||
"server-host-placeholder":"https://<主機名稱>:<端口>",
|
||||
"proxy-server":"代理伺服器(可選)",
|
||||
"proxy-server-placeholder":"https://<主機名稱>:<端口>",
|
||||
"note":"注意:",
|
||||
"proxy-instruction":"如果您將代理設定留空,將使用系統代理(僅適用於桌面程式)",
|
||||
"password":"密碼",
|
||||
"password-placeholder":"密碼",
|
||||
"back":"返回",
|
||||
"finish-setup":"完成設定"
|
||||
},
|
||||
"setup_sync-in-progress":{
|
||||
"heading":"同步中",
|
||||
"successful":"同步已正確設定。初始同步完成可能需要一些時間。完成後,您將被重定向到登入頁面。",
|
||||
"outstanding-items":"未完成的同步項目:",
|
||||
"outstanding-items-default":"無"
|
||||
},
|
||||
"share_404":{
|
||||
"title":"未找到",
|
||||
"heading":"未找到"
|
||||
},
|
||||
"share_page":{
|
||||
"parent":"上級目錄:",
|
||||
"clipped-from":"此筆記最初剪下自 {{- url}}",
|
||||
"child-notes":"子筆記:",
|
||||
"no-content":"此筆記沒有內容。"
|
||||
},
|
||||
"weekdays":{
|
||||
"monday":"週一",
|
||||
"tuesday":"週二",
|
||||
"wednesday":"週三",
|
||||
"thursday":"週四",
|
||||
"friday":"週五",
|
||||
"saturday":"週六",
|
||||
"sunday":"週日"
|
||||
},
|
||||
"months":{
|
||||
"january":"一月",
|
||||
"february":"二月",
|
||||
"march":"三月",
|
||||
"april":"四月",
|
||||
"may":"五月",
|
||||
"june":"六月",
|
||||
"july":"七月",
|
||||
"august":"八月",
|
||||
"september":"九月",
|
||||
"october":"十月",
|
||||
"november":"十一月",
|
||||
"december":"十二月"
|
||||
},
|
||||
"special_notes":{
|
||||
"search_prefix":"搜尋:"
|
||||
},
|
||||
"code_block":{
|
||||
"theme_none":"無格式高亮",
|
||||
"theme_group_light":"淺色主題",
|
||||
"theme_group_dark":"深色主題"
|
||||
},
|
||||
"test_sync":{
|
||||
"not-configured":"並未設定同步伺服器主機,請先設定同步",
|
||||
"successful":"成功與同步伺服器握手,現在開始同步"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user