mirror of
https://github.com/zadam/trilium.git
synced 2025-06-06 18:08:33 +02:00
Merge branch 'beta'
# Conflicts: # src/public/app/layouts/desktop_layout.js
This commit is contained in:
commit
494b240015
BIN
db/demo.zip
BIN
db/demo.zip
Binary file not shown.
@ -5,8 +5,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* CKEditor 5 (v39.0.2) content styles.
|
* CKEditor 5 (v40.0.0) content styles.
|
||||||
* Generated on Wed, 06 Sep 2023 07:32:15 GMT.
|
* Generated on Thu, 19 Oct 2023 13:45:23 GMT.
|
||||||
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
|
* For more information, check out https://ckeditor.com/docs/ckeditor5/latest/installation/advanced/content-styles.html
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@ -42,6 +42,18 @@
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
|
||||||
|
.ck-content .table > figcaption {
|
||||||
|
display: table-caption;
|
||||||
|
caption-side: top;
|
||||||
|
word-break: break-word;
|
||||||
|
text-align: center;
|
||||||
|
color: var(--ck-color-selector-caption-text);
|
||||||
|
background-color: var(--ck-color-selector-caption-background);
|
||||||
|
padding: .6em;
|
||||||
|
font-size: .75em;
|
||||||
|
outline-offset: -1px;
|
||||||
|
}
|
||||||
/* @ckeditor/ckeditor5-table/theme/table.css */
|
/* @ckeditor/ckeditor5-table/theme/table.css */
|
||||||
.ck-content .table {
|
.ck-content .table {
|
||||||
margin: 0.9em auto;
|
margin: 0.9em auto;
|
||||||
@ -75,18 +87,6 @@
|
|||||||
.ck-content[dir="ltr"] .table th {
|
.ck-content[dir="ltr"] .table th {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-table/theme/tablecaption.css */
|
|
||||||
.ck-content .table > figcaption {
|
|
||||||
display: table-caption;
|
|
||||||
caption-side: top;
|
|
||||||
word-break: break-word;
|
|
||||||
text-align: center;
|
|
||||||
color: var(--ck-color-selector-caption-text);
|
|
||||||
background-color: var(--ck-color-selector-caption-background);
|
|
||||||
padding: .6em;
|
|
||||||
font-size: .75em;
|
|
||||||
outline-offset: -1px;
|
|
||||||
}
|
|
||||||
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
|
/* @ckeditor/ckeditor5-page-break/theme/pagebreak.css */
|
||||||
.ck-content .page-break {
|
.ck-content .page-break {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -136,6 +136,7 @@
|
|||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
.ck-content .todo-list li {
|
.ck-content .todo-list li {
|
||||||
|
position: relative;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
@ -157,6 +158,13 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-content[dir=rtl] .todo-list .todo-list__label > input {
|
||||||
|
left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
right: -25px;
|
||||||
|
margin-left: -15px;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
.ck-content .todo-list .todo-list__label > input::before {
|
.ck-content .todo-list .todo-list__label > input::before {
|
||||||
display: block;
|
display: block;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -166,7 +174,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
border: 1px solid hsl(0, 0%, 20%);
|
border: 1px solid hsl(0, 0%, 20%);
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
transition: 250ms ease-in-out box-shadow, 250ms ease-in-out background, 250ms ease-in-out border;
|
transition: 250ms ease-in-out box-shadow;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
.ck-content .todo-list .todo-list__label > input::after {
|
.ck-content .todo-list .todo-list__label > input::after {
|
||||||
@ -197,19 +205,80 @@
|
|||||||
.ck-content .todo-list .todo-list__label .todo-list__label__description {
|
.ck-content .todo-list .todo-list__label .todo-list__label__description {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
.ck-content .image.image_resized {
|
.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
|
||||||
max-width: 100%;
|
position: absolute;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > input,
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > input:hover::before, .ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input:hover::before {
|
||||||
|
box-shadow: 0 0 0 5px hsla(0, 0%, 0%, 0.1);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
width: var(--ck-todo-list-checkmark-size);
|
||||||
|
height: var(--ck-todo-list-checkmark-size);
|
||||||
|
vertical-align: middle;
|
||||||
|
border: 0;
|
||||||
|
left: -25px;
|
||||||
|
margin-right: -15px;
|
||||||
|
right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content[dir=rtl] .todo-list .todo-list__label > span[contenteditable=false] > input {
|
||||||
|
left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
right: -25px;
|
||||||
|
margin-left: -15px;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::before {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: absolute;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
content: '';
|
||||||
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
|
|
||||||
.ck-content .image.image_resized img {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid hsl(0, 0%, 20%);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: 250ms ease-in-out box-shadow;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
.ck-content .image.image_resized > figcaption {
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input::after {
|
||||||
display: block;
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: content-box;
|
||||||
|
pointer-events: none;
|
||||||
|
content: '';
|
||||||
|
left: calc( var(--ck-todo-list-checkmark-size) / 3 );
|
||||||
|
top: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
|
||||||
|
width: calc( var(--ck-todo-list-checkmark-size) / 5.3 );
|
||||||
|
height: calc( var(--ck-todo-list-checkmark-size) / 2.6 );
|
||||||
|
border-style: solid;
|
||||||
|
border-color: transparent;
|
||||||
|
border-width: 0 calc( var(--ck-todo-list-checkmark-size) / 8 ) calc( var(--ck-todo-list-checkmark-size) / 8 ) 0;
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::before {
|
||||||
|
background: hsl(126, 64%, 41%);
|
||||||
|
border-color: hsl(126, 64%, 41%);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label > span[contenteditable=false] > input[checked]::after {
|
||||||
|
border-color: hsl(0, 0%, 100%);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-list/theme/todolist.css */
|
||||||
|
.ck-editor__editable.ck-content .todo-list .todo-list__label.todo-list__label_without-description input[type=checkbox] {
|
||||||
|
position: absolute;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-image/theme/image.css */
|
/* @ckeditor/ckeditor5-image/theme/image.css */
|
||||||
.ck-content .image {
|
.ck-content .image {
|
||||||
@ -225,6 +294,7 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-image/theme/image.css */
|
/* @ckeditor/ckeditor5-image/theme/image.css */
|
||||||
.ck-content .image-inline {
|
.ck-content .image-inline {
|
||||||
@ -259,6 +329,50 @@
|
|||||||
font-size: .75em;
|
font-size: .75em;
|
||||||
outline-offset: -1px;
|
outline-offset: -1px;
|
||||||
}
|
}
|
||||||
|
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
|
||||||
|
.ck-content img.image_resized {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
|
||||||
|
.ck-content .image.image_resized {
|
||||||
|
max-width: 100%;
|
||||||
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
|
||||||
|
.ck-content .image.image_resized img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-image/theme/imageresize.css */
|
||||||
|
.ck-content .image.image_resized > figcaption {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-yellow {
|
||||||
|
background-color: var(--ck-highlight-marker-yellow);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-green {
|
||||||
|
background-color: var(--ck-highlight-marker-green);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-pink {
|
||||||
|
background-color: var(--ck-highlight-marker-pink);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .marker-blue {
|
||||||
|
background-color: var(--ck-highlight-marker-blue);
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .pen-red {
|
||||||
|
color: var(--ck-highlight-pen-red);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
||||||
|
.ck-content .pen-green {
|
||||||
|
color: var(--ck-highlight-pen-green);
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
/* @ckeditor/ckeditor5-list/theme/list.css */
|
/* @ckeditor/ckeditor5-list/theme/list.css */
|
||||||
.ck-content ol {
|
.ck-content ol {
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
@ -295,32 +409,6 @@
|
|||||||
.ck-content ul ul ul ul {
|
.ck-content ul ul ul ul {
|
||||||
list-style-type: square;
|
list-style-type: square;
|
||||||
}
|
}
|
||||||
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
|
||||||
.ck-content .marker-yellow {
|
|
||||||
background-color: var(--ck-highlight-marker-yellow);
|
|
||||||
}
|
|
||||||
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
|
||||||
.ck-content .marker-green {
|
|
||||||
background-color: var(--ck-highlight-marker-green);
|
|
||||||
}
|
|
||||||
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
|
||||||
.ck-content .marker-pink {
|
|
||||||
background-color: var(--ck-highlight-marker-pink);
|
|
||||||
}
|
|
||||||
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
|
||||||
.ck-content .marker-blue {
|
|
||||||
background-color: var(--ck-highlight-marker-blue);
|
|
||||||
}
|
|
||||||
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
|
||||||
.ck-content .pen-red {
|
|
||||||
color: var(--ck-highlight-pen-red);
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
/* @ckeditor/ckeditor5-highlight/theme/highlight.css */
|
|
||||||
.ck-content .pen-green {
|
|
||||||
color: var(--ck-highlight-pen-green);
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
|
/* @ckeditor/ckeditor5-image/theme/imagestyle.css */
|
||||||
.ck-content .image-style-block-align-left,
|
.ck-content .image-style-block-align-left,
|
||||||
.ck-content .image-style-block-align-right {
|
.ck-content .image-style-block-align-right {
|
||||||
|
4
libraries/ckeditor/ckeditor.js
vendored
4
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
395
libraries/mermaid.min.js
vendored
395
libraries/mermaid.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1653,24 +1653,32 @@ class BNote extends AbstractBeccaEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @param {string} matchBy - choose by which property we detect if to update an existing attachment.
|
||||||
|
* Supported values are either 'attachmentId' (default) or 'title'
|
||||||
* @returns {BAttachment}
|
* @returns {BAttachment}
|
||||||
*/
|
*/
|
||||||
saveAttachment({attachmentId, role, mime, title, content, position}) {
|
saveAttachment({attachmentId, role, mime, title, content, position}, matchBy = 'attachmentId') {
|
||||||
|
if (!['attachmentId', 'title'].includes(matchBy)) {
|
||||||
|
throw new Error(`Unsupported value '${matchBy}' for matchBy param, has to be either 'attachmentId' or 'title'.`);
|
||||||
|
}
|
||||||
|
|
||||||
let attachment;
|
let attachment;
|
||||||
|
|
||||||
if (attachmentId) {
|
if (matchBy === 'title') {
|
||||||
|
attachment = this.getAttachmentByTitle(title);
|
||||||
|
} else if (matchBy === 'attachmentId' && attachmentId) {
|
||||||
attachment = this.becca.getAttachmentOrThrow(attachmentId);
|
attachment = this.becca.getAttachmentOrThrow(attachmentId);
|
||||||
} else {
|
|
||||||
attachment = new BAttachment({
|
|
||||||
ownerId: this.noteId,
|
|
||||||
title,
|
|
||||||
role,
|
|
||||||
mime,
|
|
||||||
isProtected: this.isProtected,
|
|
||||||
position
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
attachment = attachment || new BAttachment({
|
||||||
|
ownerId: this.noteId,
|
||||||
|
title,
|
||||||
|
role,
|
||||||
|
mime,
|
||||||
|
isProtected: this.isProtected,
|
||||||
|
position
|
||||||
|
});
|
||||||
|
|
||||||
content = content || "";
|
content = content || "";
|
||||||
attachment.setContent(content, {forceSave: true});
|
attachment.setContent(content, {forceSave: true});
|
||||||
|
|
||||||
|
@ -79,6 +79,7 @@ import HideFloatingButtonsButton from "../widgets/floating_buttons/hide_floating
|
|||||||
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
|
import ScriptExecutorWidget from "../widgets/ribbon_widgets/script_executor.js";
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.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 ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
@ -146,7 +147,6 @@ export default class DesktopLayout {
|
|||||||
.ribbon(new NotePropertiesWidget())
|
.ribbon(new NotePropertiesWidget())
|
||||||
.ribbon(new FilePropertiesWidget())
|
.ribbon(new FilePropertiesWidget())
|
||||||
.ribbon(new ImagePropertiesWidget())
|
.ribbon(new ImagePropertiesWidget())
|
||||||
.ribbon(new CanvasPropertiesWidget())
|
|
||||||
.ribbon(new BasicPropertiesWidget())
|
.ribbon(new BasicPropertiesWidget())
|
||||||
.ribbon(new OwnedAttributeListWidget())
|
.ribbon(new OwnedAttributeListWidget())
|
||||||
.ribbon(new InheritedAttributesWidget())
|
.ribbon(new InheritedAttributesWidget())
|
||||||
@ -163,6 +163,7 @@ export default class DesktopLayout {
|
|||||||
.child(new EditButton())
|
.child(new EditButton())
|
||||||
.child(new CodeButtonsWidget())
|
.child(new CodeButtonsWidget())
|
||||||
.child(new RelationMapButtons())
|
.child(new RelationMapButtons())
|
||||||
|
.child(new CopyImageReferenceButton())
|
||||||
.child(new MermaidExportButton())
|
.child(new MermaidExportButton())
|
||||||
.child(new BacklinksWidget())
|
.child(new BacklinksWidget())
|
||||||
.child(new HideFloatingButtonsButton())
|
.child(new HideFloatingButtonsButton())
|
||||||
|
@ -242,7 +242,7 @@ export default class RevisionsDialog extends BasicWidget {
|
|||||||
|
|
||||||
renderMathInElement(this.$content[0], {trust: true});
|
renderMathInElement(this.$content[0], {trust: true});
|
||||||
}
|
}
|
||||||
} else if (revisionItem.type === 'code' || revisionItem.type === 'mermaid') {
|
} else if (revisionItem.type === 'code') {
|
||||||
this.$content.html($("<pre>").text(fullRevision.content));
|
this.$content.html($("<pre>").text(fullRevision.content));
|
||||||
} else if (revisionItem.type === 'image') {
|
} else if (revisionItem.type === 'image') {
|
||||||
this.$content.html($("<img>")
|
this.$content.html($("<img>")
|
||||||
@ -279,6 +279,14 @@ export default class RevisionsDialog extends BasicWidget {
|
|||||||
this.$content.html($("<img>")
|
this.$content.html($("<img>")
|
||||||
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`)
|
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`)
|
||||||
.css("max-width", "100%"));
|
.css("max-width", "100%"));
|
||||||
|
} else if (revisionItem.type === 'mermaid') {
|
||||||
|
const sanitizedTitle = revisionItem.title.replace(/[^a-z0-9-.]/gi, "");
|
||||||
|
|
||||||
|
this.$content.html($("<img>")
|
||||||
|
.attr("src", `api/revisions/${revisionItem.revisionId}/image/${sanitizedTitle}?${Math.random()}`)
|
||||||
|
.css("max-width", "100%"));
|
||||||
|
|
||||||
|
this.$content.append($("<pre>").text(fullRevision.content));
|
||||||
} else {
|
} else {
|
||||||
this.$content.text("Preview isn't available for this note type.");
|
this.$content.text("Preview isn't available for this note type.");
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||||
|
import utils from "../../services/utils.js";
|
||||||
|
import imageService from "../../services/image.js";
|
||||||
|
|
||||||
|
const TPL = `
|
||||||
|
<button type="button"
|
||||||
|
class="copy-image-reference-button"
|
||||||
|
title="Copy image reference to the clipboard, can be pasted into a text note.">
|
||||||
|
<span class="bx bx-copy"></span>
|
||||||
|
|
||||||
|
<div class="hidden-image-copy"></div>
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
export default class CopyImageReferenceButton extends NoteContextAwareWidget {
|
||||||
|
isEnabled() {
|
||||||
|
return super.isEnabled()
|
||||||
|
&& ['mermaid', 'canvas'].includes(this.note?.type)
|
||||||
|
&& this.note.isContentAvailable()
|
||||||
|
&& this.noteContext?.viewScope.viewMode === 'default';
|
||||||
|
}
|
||||||
|
|
||||||
|
doRender() {
|
||||||
|
super.doRender();
|
||||||
|
|
||||||
|
this.$widget = $(TPL);
|
||||||
|
this.$hiddenImageCopy = this.$widget.find(".hidden-image-copy");
|
||||||
|
|
||||||
|
this.$widget.on('click', () => {
|
||||||
|
this.$hiddenImageCopy.empty().append(
|
||||||
|
$("<img>")
|
||||||
|
.attr("src", utils.createImageSrcUrl(this.note))
|
||||||
|
);
|
||||||
|
|
||||||
|
imageService.copyImageReferenceToClipboard(this.$hiddenImageCopy);
|
||||||
|
|
||||||
|
this.$hiddenImageCopy.empty();
|
||||||
|
});
|
||||||
|
this.contentSized();
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,6 @@
|
|||||||
import libraryLoader from "../services/library_loader.js";
|
import libraryLoader from "../services/library_loader.js";
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
||||||
|
import server from "../services/server.js";
|
||||||
|
|
||||||
const TPL = `<div class="mermaid-widget">
|
const TPL = `<div class="mermaid-widget">
|
||||||
<style>
|
<style>
|
||||||
@ -18,6 +19,10 @@ const TPL = `<div class="mermaid-widget">
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mermaid-render svg {
|
||||||
|
width: 95%; /* https://github.com/zadam/trilium/issues/4340 */
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div class="mermaid-error alert alert-warning">
|
<div class="mermaid-error alert alert-warning">
|
||||||
@ -77,6 +82,20 @@ export default class MermaidWidget extends NoteContextAwareWidget {
|
|||||||
try {
|
try {
|
||||||
const svg = await this.renderSvg();
|
const svg = await this.renderSvg();
|
||||||
|
|
||||||
|
if (this.dirtyAttachment) {
|
||||||
|
const payload = {
|
||||||
|
role: 'image',
|
||||||
|
title: 'mermaid-export.svg',
|
||||||
|
mime: 'image/svg+xml',
|
||||||
|
content: svg,
|
||||||
|
position: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
server.post(`notes/${this.noteId}/attachments?matchBy=title`, payload).then(() => {
|
||||||
|
this.dirtyAttachment = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.$display.html(svg);
|
this.$display.html(svg);
|
||||||
|
|
||||||
await wheelZoomLoaded;
|
await wheelZoomLoaded;
|
||||||
@ -85,8 +104,8 @@ export default class MermaidWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
WZoom.create(`#mermaid-render-${idCounter}`, {
|
WZoom.create(`#mermaid-render-${idCounter}`, {
|
||||||
type: 'html',
|
type: 'html',
|
||||||
maxScale: 10,
|
maxScale: 50,
|
||||||
speed: 20,
|
speed: 1.3,
|
||||||
zoomOnClick: false
|
zoomOnClick: false
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -107,6 +126,8 @@ export default class MermaidWidget extends NoteContextAwareWidget {
|
|||||||
|
|
||||||
async entitiesReloadedEvent({loadResults}) {
|
async entitiesReloadedEvent({loadResults}) {
|
||||||
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
if (loadResults.isNoteContentReloaded(this.noteId)) {
|
||||||
|
this.dirtyAttachment = true;
|
||||||
|
|
||||||
await this.refresh();
|
await this.refresh();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import imageService from "../../services/image.js";
|
|
||||||
|
|
||||||
const TPL = `
|
|
||||||
<div class="image-properties">
|
|
||||||
<div style="display: flex; justify-content: space-evenly; margin: 10px;">
|
|
||||||
<button class="canvas-copy-reference-to-clipboard btn btn-sm btn-primary"
|
|
||||||
title="Pasting this reference into a text note will insert the canvas note as image"
|
|
||||||
type="button">Copy reference to clipboard</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hidden-canvas-copy"></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default class CanvasPropertiesWidget extends NoteContextAwareWidget {
|
|
||||||
get name() {
|
|
||||||
return "canvasProperties";
|
|
||||||
}
|
|
||||||
|
|
||||||
get toggleCommand() {
|
|
||||||
return "toggleRibbonTabCanvasProperties";
|
|
||||||
}
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return this.note && this.note.type === 'canvas';
|
|
||||||
}
|
|
||||||
|
|
||||||
getTitle() {
|
|
||||||
return {
|
|
||||||
show: this.isEnabled(),
|
|
||||||
activate: false,
|
|
||||||
title: 'Canvas',
|
|
||||||
icon: 'bx bx-pen'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.contentSized();
|
|
||||||
|
|
||||||
this.$hiddenCanvasCopy = this.$widget.find('.hidden-canvas-copy');
|
|
||||||
|
|
||||||
this.$copyReferenceToClipboardButton = this.$widget.find(".canvas-copy-reference-to-clipboard");
|
|
||||||
this.$copyReferenceToClipboardButton.on('click', () => {
|
|
||||||
this.$hiddenCanvasCopy.empty().append(
|
|
||||||
$("<img>")
|
|
||||||
.attr("src", utils.createImageSrcUrl(this.note))
|
|
||||||
);
|
|
||||||
|
|
||||||
imageService.copyImageReferenceToClipboard(this.$hiddenCanvasCopy);
|
|
||||||
|
|
||||||
this.$hiddenCanvasCopy.empty();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -23,6 +23,7 @@ const TPL = `
|
|||||||
<option value="eq7">is exactly 7</option>
|
<option value="eq7">is exactly 7</option>
|
||||||
<option value="eq8">is exactly 8</option>
|
<option value="eq8">is exactly 8</option>
|
||||||
<option value="eq9">is exactly 9</option>
|
<option value="eq9">is exactly 9</option>
|
||||||
|
<option value="gt0">is greater than 0</option>
|
||||||
<option value="gt1">is greater than 1</option>
|
<option value="gt1">is greater than 1</option>
|
||||||
<option value="gt2">is greater than 2</option>
|
<option value="gt2">is greater than 2</option>
|
||||||
<option value="gt3">is greater than 3</option>
|
<option value="gt3">is greater than 3</option>
|
||||||
@ -32,6 +33,7 @@ const TPL = `
|
|||||||
<option value="gt7">is greater than 7</option>
|
<option value="gt7">is greater than 7</option>
|
||||||
<option value="gt8">is greater than 8</option>
|
<option value="gt8">is greater than 8</option>
|
||||||
<option value="gt9">is greater than 9</option>
|
<option value="gt9">is greater than 9</option>
|
||||||
|
<option value="lt2">is less than 2</option>
|
||||||
<option value="lt3">is less than 3</option>
|
<option value="lt3">is less than 3</option>
|
||||||
<option value="lt4">is less than 4</option>
|
<option value="lt4">is less than 4</option>
|
||||||
<option value="lt5">is less than 5</option>
|
<option value="lt5">is less than 5</option>
|
||||||
|
@ -49,8 +49,8 @@ class ImageTypeWidget extends TypeWidget {
|
|||||||
|
|
||||||
libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM).then(() => {
|
libraryLoader.requireLibrary(libraryLoader.WHEEL_ZOOM).then(() => {
|
||||||
WZoom.create(`#${this.$imageView.attr("id")}`, {
|
WZoom.create(`#${this.$imageView.attr("id")}`, {
|
||||||
maxScale: 10,
|
maxScale: 50,
|
||||||
speed: 20,
|
speed: 1.3,
|
||||||
zoomOnClick: false
|
zoomOnClick: false
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -32,9 +32,10 @@ function getAllAttachments(req) {
|
|||||||
function saveAttachment(req) {
|
function saveAttachment(req) {
|
||||||
const {noteId} = req.params;
|
const {noteId} = req.params;
|
||||||
const {attachmentId, role, mime, title, content} = req.body;
|
const {attachmentId, role, mime, title, content} = req.body;
|
||||||
|
const {matchBy} = req.query;
|
||||||
|
|
||||||
const note = becca.getNoteOrThrow(noteId);
|
const note = becca.getNoteOrThrow(noteId);
|
||||||
note.saveAttachment({attachmentId, role, mime, title, content});
|
note.saveAttachment({attachmentId, role, mime, title, content}, matchBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
function uploadAttachment(req) {
|
function uploadAttachment(req) {
|
||||||
|
@ -25,33 +25,14 @@ function returnImageInt(image, res) {
|
|||||||
if (!image) {
|
if (!image) {
|
||||||
res.set('Content-Type', 'image/png');
|
res.set('Content-Type', 'image/png');
|
||||||
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
|
return res.send(fs.readFileSync(`${RESOURCE_DIR}/db/image-deleted.png`));
|
||||||
} else if (!["image", "canvas"].includes(image.type)) {
|
} else if (!["image", "canvas", "mermaid"].includes(image.type)) {
|
||||||
return res.sendStatus(400);
|
return res.sendStatus(400);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* special "image" type. the canvas is actually type application/json
|
|
||||||
* to avoid bitrot and enable usage as referenced image the svg is included.
|
|
||||||
*/
|
|
||||||
if (image.type === 'canvas') {
|
if (image.type === 'canvas') {
|
||||||
let svgString = '<svg/>'
|
renderSvgAttachment(image, res, 'canvas-export.svg');
|
||||||
const attachment = image.getAttachmentByTitle('canvas-export.svg');
|
} else if (image.type === 'mermaid') {
|
||||||
|
renderSvgAttachment(image, res, 'mermaid-export.svg');
|
||||||
if (attachment) {
|
|
||||||
svgString = attachment.getContent();
|
|
||||||
} else {
|
|
||||||
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
|
|
||||||
const contentSvg = image.getJsonContentSafely()?.svg;
|
|
||||||
|
|
||||||
if (contentSvg) {
|
|
||||||
svgString = contentSvg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = svgString
|
|
||||||
res.set('Content-Type', "image/svg+xml");
|
|
||||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
||||||
res.send(svg);
|
|
||||||
} else {
|
} else {
|
||||||
res.set('Content-Type', image.mime);
|
res.set('Content-Type', image.mime);
|
||||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
@ -59,6 +40,28 @@ function returnImageInt(image, res) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSvgAttachment(image, res, attachmentName) {
|
||||||
|
let svgString = '<svg/>'
|
||||||
|
const attachment = image.getAttachmentByTitle(attachmentName);
|
||||||
|
|
||||||
|
if (attachment) {
|
||||||
|
svgString = attachment.getContent();
|
||||||
|
} else {
|
||||||
|
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
|
||||||
|
const contentSvg = image.getJsonContentSafely()?.svg;
|
||||||
|
|
||||||
|
if (contentSvg) {
|
||||||
|
svgString = contentSvg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = svgString
|
||||||
|
res.set('Content-Type', "image/svg+xml");
|
||||||
|
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
res.send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function returnAttachedImage(req, res) {
|
function returnAttachedImage(req, res) {
|
||||||
const attachment = becca.getAttachment(req.params.attachmentId);
|
const attachment = becca.getAttachment(req.params.attachmentId);
|
||||||
|
|
||||||
|
@ -28,22 +28,14 @@ function sanitize(dirtyHtml) {
|
|||||||
allowedTags: [
|
allowedTags: [
|
||||||
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'p', 'a', 'ul', 'ol',
|
||||||
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
|
'li', 'b', 'i', 'strong', 'em', 'strike', 's', 'del', 'abbr', 'code', 'hr', 'br', 'div',
|
||||||
'table', 'thead', 'caption', 'tbody', 'tr', 'th', 'td', 'pre', 'section', 'img',
|
'table', 'thead', 'caption', 'tbody', 'tfoot', 'tr', 'th', 'td', 'pre', 'section', 'img',
|
||||||
'figure', 'figcaption', 'span', 'label', 'input',
|
'figure', 'figcaption', 'span', 'label', 'input', 'details', 'summary', 'address', 'aside', 'footer',
|
||||||
|
'header', 'hgroup', 'main', 'nav', 'dl', 'dt', 'menu', 'bdi', 'bdo', 'dfn', 'kbd', 'mark', 'q', 'time',
|
||||||
|
'var', 'wbr', 'area', 'map', 'track', 'video', 'audio', 'picture', 'del', 'ins',
|
||||||
'en-media' // for ENEX import
|
'en-media' // for ENEX import
|
||||||
],
|
],
|
||||||
allowedAttributes: {
|
allowedAttributes: {
|
||||||
'a': [ 'href', 'class' ],
|
'*': [ 'class', 'style', 'title', 'src', 'href', 'hash', 'disabled', 'align', 'alt', 'center', 'data-*' ]
|
||||||
'img': [ 'src' ],
|
|
||||||
'section': [ 'class', 'data-note-id' ],
|
|
||||||
'figure': [ 'class' ],
|
|
||||||
'span': [ 'class', 'style' ],
|
|
||||||
'label': [ 'class' ],
|
|
||||||
'input': [ 'class', 'type', 'disabled' ],
|
|
||||||
'code': [ 'class' ],
|
|
||||||
'ul': [ 'class' ],
|
|
||||||
'table': [ 'class' ],
|
|
||||||
'en-media': [ 'hash' ]
|
|
||||||
},
|
},
|
||||||
allowedSchemes: [
|
allowedSchemes: [
|
||||||
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'irc', 'gemini', 'git',
|
'http', 'https', 'ftp', 'ftps', 'mailto', 'data', 'evernote', 'file', 'facetime', 'irc', 'gemini', 'git',
|
||||||
|
@ -121,7 +121,11 @@ function importMarkdown(taskContext, file, parentNote) {
|
|||||||
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
|
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
|
||||||
|
|
||||||
const markdownContent = file.buffer.toString("utf-8");
|
const markdownContent = file.buffer.toString("utf-8");
|
||||||
const htmlContent = markdownService.renderToHtml(markdownContent, title);
|
let htmlContent = markdownService.renderToHtml(markdownContent, title);
|
||||||
|
|
||||||
|
if (taskContext.data.safeImport) {
|
||||||
|
htmlContent = htmlSanitizer.sanitize(htmlContent);
|
||||||
|
}
|
||||||
|
|
||||||
const {note} = noteService.createNewNote({
|
const {note} = noteService.createNewNote({
|
||||||
parentNoteId: parentNote.noteId,
|
parentNoteId: parentNote.noteId,
|
||||||
@ -141,7 +145,10 @@ function importHtml(taskContext, file, parentNote) {
|
|||||||
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
|
const title = utils.getNoteTitle(file.originalname, taskContext.data.replaceUnderscoresWithSpaces);
|
||||||
let content = file.buffer.toString("utf-8");
|
let content = file.buffer.toString("utf-8");
|
||||||
|
|
||||||
content = htmlSanitizer.sanitize(content);
|
if (taskContext.data.safeImport) {
|
||||||
|
content = htmlSanitizer.sanitize(content);
|
||||||
|
}
|
||||||
|
|
||||||
content = importUtils.handleH1(content, title);
|
content = importUtils.handleH1(content, title);
|
||||||
|
|
||||||
const {note} = noteService.createNewNote({
|
const {note} = noteService.createNewNote({
|
||||||
|
@ -321,7 +321,9 @@ async function importZip(taskContext, fileBuffer, importRootNote) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
content = htmlSanitizer.sanitize(content);
|
if (taskContext.data.safeImport) {
|
||||||
|
content = htmlSanitizer.sanitize(content);
|
||||||
|
}
|
||||||
|
|
||||||
content = content.replace(/<html.*<body[^>]*>/gis, "");
|
content = content.replace(/<html.*<body[^>]*>/gis, "");
|
||||||
content = content.replace(/<\/body>.*<\/html>/gis, "");
|
content = content.replace(/<\/body>.*<\/html>/gis, "");
|
||||||
|
@ -24,7 +24,7 @@ function getContent(note) {
|
|||||||
} else if (note.type === 'code') {
|
} else if (note.type === 'code') {
|
||||||
renderCode(result);
|
renderCode(result);
|
||||||
} else if (note.type === 'mermaid') {
|
} else if (note.type === 'mermaid') {
|
||||||
renderMermaid(result);
|
renderMermaid(result, note);
|
||||||
} else if (note.type === 'image' || note.type === 'canvas') {
|
} else if (note.type === 'image' || note.type === 'canvas') {
|
||||||
renderImage(result, note);
|
renderImage(result, note);
|
||||||
} else if (note.type === 'file') {
|
} else if (note.type === 'file') {
|
||||||
@ -135,15 +135,14 @@ function renderCode(result) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMermaid(result) {
|
function renderMermaid(result, note) {
|
||||||
result.content = `
|
result.content = `
|
||||||
<div class="mermaid">${escapeHtml(result.content)}</div>
|
<img src="api/images/${note.noteId}/${note.escapedTitle}?${note.utcDateModified}">
|
||||||
<hr>
|
<hr>
|
||||||
<details>
|
<details>
|
||||||
<summary>Chart source</summary>
|
<summary>Chart source</summary>
|
||||||
<pre>${escapeHtml(result.content)}</pre>
|
<pre>${escapeHtml(result.content)}</pre>
|
||||||
</details>`
|
</details>`
|
||||||
result.header += `<script src="../../${assetPath}/libraries/mermaid.min.js"></script>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImage(result, note) {
|
function renderImage(result, note) {
|
||||||
|
@ -109,6 +109,27 @@ function checkNoteAccess(noteId, req, res) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderImageAttachment(image, res, attachmentName) {
|
||||||
|
let svgString = '<svg/>'
|
||||||
|
const attachment = image.getAttachmentByTitle(attachmentName);
|
||||||
|
|
||||||
|
if (attachment) {
|
||||||
|
svgString = attachment.getContent();
|
||||||
|
} else {
|
||||||
|
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
|
||||||
|
const contentSvg = image.getJsonContentSafely()?.svg;
|
||||||
|
|
||||||
|
if (contentSvg) {
|
||||||
|
svgString = contentSvg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = svgString
|
||||||
|
res.set('Content-Type', "image/svg+xml");
|
||||||
|
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
res.send(svg);
|
||||||
|
}
|
||||||
|
|
||||||
function register(router) {
|
function register(router) {
|
||||||
function renderNote(note, req, res) {
|
function renderNote(note, req, res) {
|
||||||
if (!note) {
|
if (!note) {
|
||||||
@ -237,37 +258,18 @@ function register(router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!["image", "canvas"].includes(image.type)) {
|
if (image.type === 'image') {
|
||||||
return res.status(400)
|
|
||||||
.json({ message: "Requested note is not a shareable image" });
|
|
||||||
} else if (image.type === "canvas") {
|
|
||||||
/**
|
|
||||||
* special "image" type. the canvas is actually type application/json
|
|
||||||
* to avoid bitrot and enable usage as referenced image the svg is included.
|
|
||||||
*/
|
|
||||||
let svgString = '<svg/>'
|
|
||||||
const attachment = image.getAttachmentByTitle('canvas-export.svg');
|
|
||||||
|
|
||||||
if (attachment) {
|
|
||||||
svgString = attachment.getContent();
|
|
||||||
} else {
|
|
||||||
// backwards compatibility, before attachments, the SVG was stored in the main note content as a separate key
|
|
||||||
const contentSvg = image.getJsonContentSafely()?.svg;
|
|
||||||
|
|
||||||
if (contentSvg) {
|
|
||||||
svgString = contentSvg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const svg = svgString
|
|
||||||
res.set('Content-Type', "image/svg+xml");
|
|
||||||
res.set("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
||||||
res.send(svg);
|
|
||||||
} else {
|
|
||||||
// normal image
|
// normal image
|
||||||
res.set('Content-Type', image.mime);
|
res.set('Content-Type', image.mime);
|
||||||
addNoIndexHeader(image, res);
|
addNoIndexHeader(image, res);
|
||||||
res.send(image.getContent());
|
res.send(image.getContent());
|
||||||
|
} else if (image.type === "canvas") {
|
||||||
|
renderImageAttachment(image, res, 'canvas-export.svg');
|
||||||
|
} else if (image.type === 'mermaid') {
|
||||||
|
renderImageAttachment(image, res, 'mermaid-export.svg');
|
||||||
|
} else {
|
||||||
|
return res.status(400)
|
||||||
|
.json({ message: "Requested note is not a shareable image" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user