Merge remote-tracking branch 'origin/develop' into develop

; Conflicts:
;	src/public/translations/de/translation.json
This commit is contained in:
Elian Doran
2024-11-20 19:10:04 +02:00
57 changed files with 1105 additions and 234 deletions

25
.github/workflows/renovate.yaml vendored Normal file
View 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 }}

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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;
};
}

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
View File

@@ -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",

View File

@@ -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
View 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
}

View File

@@ -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);
}

View File

@@ -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())

View File

@@ -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())

View File

@@ -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]
}
];

View File

@@ -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 = {}) {

View File

@@ -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');
}
}
}
}

View File

@@ -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" },

View File

@@ -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);

View File

@@ -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");
}

View File

@@ -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);
}
}
/**

View File

@@ -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
};

View File

@@ -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() {

View File

@@ -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">

View File

@@ -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.`);
}

View File

@@ -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>`;

View File

@@ -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));
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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();
}
}
}

View File

@@ -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 = [];
}
}

View File

@@ -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);
}
}
}

View File

@@ -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"

View File

@@ -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;
}
}

View File

@@ -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 });

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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));
}
}

View File

@@ -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>
`));
}
});
}

View File

@@ -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))
))
);

View File

@@ -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");
}
}

View File

@@ -51,7 +51,7 @@ export default class ReadOnlyCodeTypeWidget extends AbstractCodeTypeWidget {
await this.initialized;
resolve(this.$content);
resolve(this.$editor);
}
format(html) {

View File

@@ -1238,3 +1238,7 @@ textarea {
padding: 1rem;
}
.empty-table-placeholder {
text-align: center;
color: var(--muted-text-color);
}

View File

@@ -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",

View File

@@ -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."
}
}
}
}

View File

@@ -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\")."
}
}
}
}

View File

@@ -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"
}
}

View File

@@ -65,7 +65,8 @@ const ALLOWED_OPTIONS = new Set([
'promotedAttributesOpenInRibbon',
'editedNotesOpenInRibbon',
'locale',
'firstDayOfWeek'
'firstDayOfWeek',
'textNoteEditorType'
]);
function getOptions() {

View File

@@ -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

View File

@@ -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.
*

View File

@@ -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: [],

View File

@@ -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 }
];
/**

View File

@@ -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);
}

View File

@@ -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",

View File

@@ -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",

View File

@@ -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
View 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":"成功與同步伺服器握手,現在開始同步"
}
}