mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 15:04:24 +01:00
Compare commits
15 Commits
7938e38d7f
...
ca03c1300b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca03c1300b | ||
|
|
732494dfc5 | ||
|
|
b8748b856a | ||
|
|
cc71f15700 | ||
|
|
124ef640b1 | ||
|
|
f5e3df0cd2 | ||
|
|
c8431181c8 | ||
|
|
07fb5ab017 | ||
|
|
6735b257b4 | ||
|
|
cef242a9ce | ||
|
|
2923d917e5 | ||
|
|
9a76a9069c | ||
|
|
8e1d796870 | ||
|
|
8b0d4e5c3b | ||
|
|
5d5fd2079a |
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,6 +8,7 @@ out-tsc
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
/.idea
|
||||||
@ -18,6 +19,7 @@ node_modules
|
|||||||
*.launch
|
*.launch
|
||||||
.settings/
|
.settings/
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
.devcontainer
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
|
|||||||
19
README.md
19
README.md
@ -146,6 +146,21 @@ Here's the language coverage we have so far:
|
|||||||
|
|
||||||
### Code
|
### Code
|
||||||
|
|
||||||
|
General (OS / docker / podman, etc.) dependencies:
|
||||||
|
|
||||||
|
Debian
|
||||||
|
```
|
||||||
|
apt update
|
||||||
|
apt install -y build-essential python3 make g++ libsqlite3-dev
|
||||||
|
corepack enable
|
||||||
|
```
|
||||||
|
|
||||||
|
Alpine
|
||||||
|
```
|
||||||
|
apk add --no-cache build-base python3 python3-dev sqlite-dev
|
||||||
|
corepack enable
|
||||||
|
```
|
||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/TriliumNext/Trilium.git
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
@ -154,6 +169,10 @@ pnpm install
|
|||||||
pnpm run server:start
|
pnpm run server:start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> If you faced with some problems, try to delete all `node_modules` and `.pnpm-store` folders, not only from the root, from every directory, like `apps/{app_name}/node_modules`and `/packages/{package_name}/node_modules` and then reinstall it by the `pnpm install`.
|
||||||
|
|
||||||
|
Share styles not compiling by default, if you see share page without styles, make `pnpm run server:build` and then run development server.
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
||||||
|
|||||||
@ -592,11 +592,6 @@ button.btn-sm {
|
|||||||
color: var(--left-pane-text-color);
|
color: var(--left-pane-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.active:not(.btn-primary) {
|
|
||||||
background-color: var(--button-disabled-background-color) !important;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ck.ck-block-toolbar-button {
|
.ck.ck-block-toolbar-button {
|
||||||
transform: translateX(7px);
|
transform: translateX(7px);
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
|
|||||||
@ -41,6 +41,9 @@
|
|||||||
--cmd-button-keyboard-shortcut-color: white;
|
--cmd-button-keyboard-shortcut-color: white;
|
||||||
--cmd-button-disabled-opacity: 0.5;
|
--cmd-button-disabled-opacity: 0.5;
|
||||||
|
|
||||||
|
--button-group-active-button-background: #ffffff4e;
|
||||||
|
--button-group-active-button-text-color: white;
|
||||||
|
|
||||||
--icon-button-color: currentColor;
|
--icon-button-color: currentColor;
|
||||||
--icon-button-hover-background: var(--hover-item-background-color);
|
--icon-button-hover-background: var(--hover-item-background-color);
|
||||||
--icon-button-hover-color: var(--hover-item-text-color);
|
--icon-button-hover-color: var(--hover-item-text-color);
|
||||||
|
|||||||
@ -41,6 +41,9 @@
|
|||||||
--cmd-button-keyboard-shortcut-color: black;
|
--cmd-button-keyboard-shortcut-color: black;
|
||||||
--cmd-button-disabled-opacity: 0.5;
|
--cmd-button-disabled-opacity: 0.5;
|
||||||
|
|
||||||
|
--button-group-active-button-background: #00000026;
|
||||||
|
--button-group-active-button-text-color: black;
|
||||||
|
|
||||||
--icon-button-color: currentColor;
|
--icon-button-color: currentColor;
|
||||||
--icon-button-hover-background: var(--hover-item-background-color);
|
--icon-button-hover-background: var(--hover-item-background-color);
|
||||||
--icon-button-hover-color: var(--hover-item-text-color);
|
--icon-button-hover-color: var(--hover-item-text-color);
|
||||||
|
|||||||
@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
.modal .modal-header .btn-close,
|
.modal .modal-header .btn-close,
|
||||||
.modal .modal-header .help-button,
|
.modal .modal-header .help-button,
|
||||||
|
.modal .modal-header .custom-title-bar-button,
|
||||||
#toast-container .toast .toast-header .btn-close {
|
#toast-container .toast .toast-header .btn-close {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -55,15 +56,17 @@
|
|||||||
font-family: boxicons;
|
font-family: boxicons;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .modal-header .help-button {
|
.modal .modal-header .help-button,
|
||||||
|
.modal .modal-header .custom-title-bar-button {
|
||||||
margin-inline-end: 0;
|
margin-inline-end: 0;
|
||||||
font-size: calc(var(--modal-control-button-size) * .75);
|
font-size: calc(var(--modal-control-button-size) * .70);
|
||||||
font-family: unset;
|
font-family: unset;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal .modal-header .btn-close:hover,
|
.modal .modal-header .btn-close:hover,
|
||||||
.modal .modal-header .help-button:hover,
|
.modal .modal-header .help-button:hover,
|
||||||
|
.modal .modal-header .custom-title-bar-button:hover,
|
||||||
#toast-container .toast .toast-header .btn-close:hover {
|
#toast-container .toast .toast-header .btn-close:hover {
|
||||||
background: var(--modal-control-button-hover-background);
|
background: var(--modal-control-button-hover-background);
|
||||||
color: var(--modal-control-button-hover-color);
|
color: var(--modal-control-button-hover-color);
|
||||||
@ -71,6 +74,7 @@
|
|||||||
|
|
||||||
.modal .modal-header .btn-close:active,
|
.modal .modal-header .btn-close:active,
|
||||||
.modal .modal-header .help-button:active,
|
.modal .modal-header .help-button:active,
|
||||||
|
.modal .modal-header .custom-title-bar-button:active,
|
||||||
#toast-container .toast .toast-header .btn-close:active {
|
#toast-container .toast .toast-header .btn-close:active {
|
||||||
transform: scale(.85);
|
transform: scale(.85);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -146,6 +146,14 @@ button.btn.btn-success kbd {
|
|||||||
outline: 2px solid var(--input-focus-outline-color);
|
outline: 2px solid var(--input-focus-outline-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Button groups */
|
||||||
|
|
||||||
|
/* Active button */
|
||||||
|
:root .btn-group button.btn.active {
|
||||||
|
background-color: var(--button-group-active-button-background);
|
||||||
|
color: var(--button-group-active-button-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Input boxes
|
* Input boxes
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -1448,6 +1448,14 @@ div.promoted-attribute-cell .multiplicity:has(span) span {
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div.promoted-attribute-cell.promoted-attribute-label-color {
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.promoted-attribute-cell.promoted-attribute-label-color .input-group {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Floating buttons
|
* Floating buttons
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -2098,7 +2098,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "当前正在查看一个只读笔记。",
|
"read-only-note": "当前正在查看一个只读笔记。",
|
||||||
"auto-read-only-note": "这条笔记以只读模式显示便于快速加载。",
|
"auto-read-only-note": "这条笔记以只读模式显示便于快速加载。",
|
||||||
"auto-read-only-learn-more": "了解更多",
|
|
||||||
"edit-note": "编辑笔记"
|
"edit-note": "编辑笔记"
|
||||||
},
|
},
|
||||||
"note-color": {
|
"note-color": {
|
||||||
|
|||||||
@ -2097,7 +2097,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Aktuelle Notiz wird im Lese-Modus angezeigt.",
|
"read-only-note": "Aktuelle Notiz wird im Lese-Modus angezeigt.",
|
||||||
"auto-read-only-note": "Diese Notiz wird im Nur-Lesen-Modus angezeigt, um ein schnelleres Laden zu ermöglichen.",
|
"auto-read-only-note": "Diese Notiz wird im Nur-Lesen-Modus angezeigt, um ein schnelleres Laden zu ermöglichen.",
|
||||||
"auto-read-only-learn-more": "Mehr erfahren",
|
|
||||||
"edit-note": "Notiz bearbeiten"
|
"edit-note": "Notiz bearbeiten"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -112,6 +112,7 @@
|
|||||||
},
|
},
|
||||||
"help": {
|
"help": {
|
||||||
"title": "Cheatsheet",
|
"title": "Cheatsheet",
|
||||||
|
"editShortcuts": "Edit keyboard shortcuts",
|
||||||
"noteNavigation": "Note navigation",
|
"noteNavigation": "Note navigation",
|
||||||
"goUpDown": "go up/down in the list of notes",
|
"goUpDown": "go up/down in the list of notes",
|
||||||
"collapseExpand": "collapse/expand node",
|
"collapseExpand": "collapse/expand node",
|
||||||
|
|||||||
@ -2096,7 +2096,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Actualmente, está viendo una nota de solo lectura.",
|
"read-only-note": "Actualmente, está viendo una nota de solo lectura.",
|
||||||
"auto-read-only-note": "Esta nota se muestra en modo de solo lectura para una carga más rápida.",
|
"auto-read-only-note": "Esta nota se muestra en modo de solo lectura para una carga más rápida.",
|
||||||
"auto-read-only-learn-more": "Para saber más",
|
|
||||||
"edit-note": "Editar nota"
|
"edit-note": "Editar nota"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -2092,7 +2092,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Stai visualizzando una nota di sola lettura.",
|
"read-only-note": "Stai visualizzando una nota di sola lettura.",
|
||||||
"auto-read-only-note": "Questa nota viene visualizzata in modalità di sola lettura per un caricamento più rapido.",
|
"auto-read-only-note": "Questa nota viene visualizzata in modalità di sola lettura per un caricamento più rapido.",
|
||||||
"auto-read-only-learn-more": "Per saperne di più",
|
|
||||||
"edit-note": "Modifica nota"
|
"edit-note": "Modifica nota"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -2098,7 +2098,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "現在、読み取り専用のノートを表示しています。",
|
"read-only-note": "現在、読み取り専用のノートを表示しています。",
|
||||||
"auto-read-only-note": "このノートは読み込みを高速化するために読み取り専用モードで表示されています。",
|
"auto-read-only-note": "このノートは読み込みを高速化するために読み取り専用モードで表示されています。",
|
||||||
"auto-read-only-learn-more": "さらに詳しく",
|
|
||||||
"edit-note": "ノートを編集"
|
"edit-note": "ノートを編集"
|
||||||
},
|
},
|
||||||
"note-color": {
|
"note-color": {
|
||||||
|
|||||||
@ -2097,7 +2097,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "Vizualizați o notiță în modul doar în citire.",
|
"read-only-note": "Vizualizați o notiță în modul doar în citire.",
|
||||||
"auto-read-only-note": "Această notiță este afișată în modul doar în citire din motive de performanță.",
|
"auto-read-only-note": "Această notiță este afișată în modul doar în citire din motive de performanță.",
|
||||||
"auto-read-only-learn-more": "Mai multe detalii",
|
|
||||||
"edit-note": "Editează notița"
|
"edit-note": "Editează notița"
|
||||||
},
|
},
|
||||||
"calendar_view": {
|
"calendar_view": {
|
||||||
|
|||||||
@ -2098,7 +2098,6 @@
|
|||||||
"read-only-info": {
|
"read-only-info": {
|
||||||
"read-only-note": "目前正在檢視唯讀筆記。",
|
"read-only-note": "目前正在檢視唯讀筆記。",
|
||||||
"auto-read-only-note": "此筆記以唯讀模式顯示以加快載入速度。",
|
"auto-read-only-note": "此筆記以唯讀模式顯示以加快載入速度。",
|
||||||
"auto-read-only-learn-more": "了解更多",
|
|
||||||
"edit-note": "編輯筆記"
|
"edit-note": "編輯筆記"
|
||||||
},
|
},
|
||||||
"note-color": {
|
"note-color": {
|
||||||
|
|||||||
@ -33,8 +33,8 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.popup-editor-dialog .modal-header .title-row > * {
|
.modal.popup-editor-dialog .modal-header .note-title-widget {
|
||||||
margin: 5px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal.popup-editor-dialog .modal-body {
|
.modal.popup-editor-dialog .modal-body {
|
||||||
@ -66,12 +66,17 @@ body.mobile .modal.popup-editor-dialog .modal-dialog {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal.popup-editor-dialog div.promoted-attributes-container {
|
||||||
|
margin-block: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.modal.popup-editor-dialog .classic-toolbar-widget {
|
.modal.popup-editor-dialog .classic-toolbar-widget {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
|
margin-inline: 8px;
|
||||||
top: 0;
|
top: 0;
|
||||||
inset-inline-start: 0;
|
inset-inline-start: 0;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
background: transparent;
|
background: var(--modal-background-color);
|
||||||
z-index: 998;
|
z-index: 998;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -60,6 +60,11 @@ export default function PopupEditor() {
|
|||||||
<DialogWrapper>
|
<DialogWrapper>
|
||||||
<Modal
|
<Modal
|
||||||
title={<TitleRow />}
|
title={<TitleRow />}
|
||||||
|
customTitleBarButtons={[{
|
||||||
|
iconClassName: "bx-expand-alt",
|
||||||
|
title: "Switch to full editor",
|
||||||
|
onClick: () => {/* TO DO */}
|
||||||
|
}]}
|
||||||
className="popup-editor-dialog"
|
className="popup-editor-dialog"
|
||||||
size="lg"
|
size="lg"
|
||||||
show={shown}
|
show={shown}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import Modal from "../react/Modal.jsx";
|
import Modal from "../react/Modal.jsx";
|
||||||
import { t } from "../../services/i18n.js";
|
import { t } from "../../services/i18n.js";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import { CommandNames } from "../../components/app_context.js";
|
import appContext, { CommandNames } from "../../components/app_context.js";
|
||||||
import RawHtml from "../react/RawHtml.jsx";
|
import RawHtml from "../react/RawHtml.jsx";
|
||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import keyboard_actions from "../../services/keyboard_actions.js";
|
import keyboard_actions from "../../services/keyboard_actions.js";
|
||||||
@ -14,6 +14,7 @@ export default function HelpDialog() {
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
title={t("help.title")} className="help-dialog use-tn-links" minWidth="90%" size="lg" scrollable
|
title={t("help.title")} className="help-dialog use-tn-links" minWidth="90%" size="lg" scrollable
|
||||||
|
customTitleBarButtons={[{title: t("help.editShortcuts"), iconClassName: "bxs-pencil", onClick: editShortcuts}]}
|
||||||
onHidden={() => setShown(false)}
|
onHidden={() => setShown(false)}
|
||||||
show={shown}
|
show={shown}
|
||||||
>
|
>
|
||||||
@ -160,3 +161,7 @@ function Card({ title, children }: { title: string, children: ComponentChildren
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function editShortcuts() {
|
||||||
|
appContext.tabManager.openContextWithNote("_optionsShortcuts", { activate: true });
|
||||||
|
}
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
import clsx from "clsx";
|
||||||
import { useEffect, useRef, useMemo } from "preact/hooks";
|
import { useEffect, useRef, useMemo } from "preact/hooks";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
@ -7,9 +8,16 @@ import { Modal as BootstrapModal } from "bootstrap";
|
|||||||
import { memo } from "preact/compat";
|
import { memo } from "preact/compat";
|
||||||
import { useSyncedRef } from "./hooks";
|
import { useSyncedRef } from "./hooks";
|
||||||
|
|
||||||
|
interface CustomTitleBarButton {
|
||||||
|
title: string;
|
||||||
|
iconClassName: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface ModalProps {
|
interface ModalProps {
|
||||||
className: string;
|
className: string;
|
||||||
title: string | ComponentChildren;
|
title: string | ComponentChildren;
|
||||||
|
customTitleBarButtons?: (CustomTitleBarButton | null)[];
|
||||||
size: "xl" | "lg" | "md" | "sm";
|
size: "xl" | "lg" | "md" | "sm";
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
/**
|
/**
|
||||||
@ -72,7 +80,7 @@ interface ModalProps {
|
|||||||
noFocus?: boolean;
|
noFocus?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Modal({ children, className, size, title, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
export default function Modal({ children, className, size, title, customTitleBarButtons: titleBarButtons, header, footer, footerStyle, footerAlignment, onShown, onSubmit, helpPageId, minWidth, maxWidth, zIndex, scrollable, onHidden: onHidden, modalRef: externalModalRef, formRef, bodyStyle, show, stackable, keepInDom, noFocus }: ModalProps) {
|
||||||
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
const modalRef = useSyncedRef<HTMLDivElement>(externalModalRef);
|
||||||
const modalInstanceRef = useRef<BootstrapModal>();
|
const modalInstanceRef = useRef<BootstrapModal>();
|
||||||
const elementToFocus = useRef<Element | null>();
|
const elementToFocus = useRef<Element | null>();
|
||||||
@ -148,7 +156,17 @@ export default function Modal({ children, className, size, title, header, footer
|
|||||||
{helpPageId && (
|
{helpPageId && (
|
||||||
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
|
<button className="help-button" type="button" data-in-app-help={helpPageId} title={t("modal.help_title")}>?</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{titleBarButtons?.filter((b) => b !== null).map((titleBarButton) => (
|
||||||
|
<button type="button"
|
||||||
|
className={clsx("custom-title-bar-button bx", titleBarButton.iconClassName)}
|
||||||
|
title={titleBarButton.title}
|
||||||
|
onClick={titleBarButton.onClick}>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
|
||||||
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label={t("modal.close")}></button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{onSubmit ? (
|
{onSubmit ? (
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
import { ConvertToAttachmentResponse } from "@triliumnext/commons";
|
||||||
import { FormDropdownDivider, FormListItem } from "../react/FormList";
|
import { FormDropdownDivider, FormListHeader, FormListItem } from "../react/FormList";
|
||||||
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
import { isElectron as getIsElectron, isMac as getIsMac } from "../../services/utils";
|
||||||
import { ParentComponent } from "../react/react_utils";
|
import { ParentComponent } from "../react/react_utils";
|
||||||
import { t } from "../../services/i18n"
|
import { t } from "../../services/i18n"
|
||||||
@ -113,8 +113,7 @@ function NoteContextMenu({ note, noteContext }: { note: FNote, noteContext?: Not
|
|||||||
function DevelopmentActions({ note }: { note: FNote }) {
|
function DevelopmentActions({ note }: { note: FNote }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormDropdownDivider />
|
<FormListHeader text="Development-only Actions" />
|
||||||
<FormListItem disabled>Development-only Actions</FormListItem>
|
|
||||||
<FormListItem
|
<FormListItem
|
||||||
icon="bx bx-printer"
|
icon="bx bx-printer"
|
||||||
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
|
onClick={() => window.open(`/?print=#root/${note.noteId}`, "_blank")}
|
||||||
|
|||||||
@ -40,6 +40,13 @@ interface Subroot {
|
|||||||
|
|
||||||
type GetNoteFunction = (id: string) => SNote | BNote | null;
|
type GetNoteFunction = (id: string) => SNote | BNote | null;
|
||||||
|
|
||||||
|
function addContentAccessQuery(note: SNote | BNote, secondEl?:boolean) {
|
||||||
|
if (!(note instanceof BNote) && note.contentAccessor && note.contentAccessor?.type === "query") {
|
||||||
|
return secondEl ? `&cat=${note.contentAccessor.getToken()}` : `?cat=${note.contentAccessor.getToken()}`;
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot {
|
function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot {
|
||||||
if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||||
// share root itself is not shared
|
// share root itself is not shared
|
||||||
@ -111,7 +118,7 @@ export function renderNoteContent(note: SNote) {
|
|||||||
cssToLoad.push(`assets/scripts.css`);
|
cssToLoad.push(`assets/scripts.css`);
|
||||||
}
|
}
|
||||||
for (const cssRelation of note.getRelations("shareCss")) {
|
for (const cssRelation of note.getRelations("shareCss")) {
|
||||||
cssToLoad.push(`api/notes/${cssRelation.value}/download`);
|
cssToLoad.push(`api/notes/${cssRelation.value}/download${addContentAccessQuery(note)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine JS to load.
|
// Determine JS to load.
|
||||||
@ -119,11 +126,11 @@ export function renderNoteContent(note: SNote) {
|
|||||||
"assets/scripts.js"
|
"assets/scripts.js"
|
||||||
];
|
];
|
||||||
for (const jsRelation of note.getRelations("shareJs")) {
|
for (const jsRelation of note.getRelations("shareJs")) {
|
||||||
jsToLoad.push(`api/notes/${jsRelation.value}/download`);
|
jsToLoad.push(`api/notes/${jsRelation.value}/download${addContentAccessQuery(note)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customLogoId = note.getRelation("shareLogo")?.value;
|
const customLogoId = note.getRelation("shareLogo")?.value;
|
||||||
const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`;
|
const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png${addContentAccessQuery(note)}` : `../${assetUrlFragment}/images/icon-color.svg`;
|
||||||
|
|
||||||
return renderNoteContentInternal(note, {
|
return renderNoteContentInternal(note, {
|
||||||
subRoot,
|
subRoot,
|
||||||
@ -133,7 +140,7 @@ export function renderNoteContent(note: SNote) {
|
|||||||
logoUrl,
|
logoUrl,
|
||||||
ancestors,
|
ancestors,
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download` : `../favicon.ico`
|
faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download${addContentAccessQuery(note)}` : `../favicon.ico`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +165,7 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs)
|
|||||||
isEmpty,
|
isEmpty,
|
||||||
assetPath: shareAdjustedAssetPath,
|
assetPath: shareAdjustedAssetPath,
|
||||||
assetUrlFragment,
|
assetUrlFragment,
|
||||||
|
addContentAccessQuery: (second: boolean | undefined) => addContentAccessQuery(note, second),
|
||||||
showLoginInShareTheme,
|
showLoginInShareTheme,
|
||||||
t,
|
t,
|
||||||
isDev,
|
isDev,
|
||||||
@ -325,7 +333,7 @@ function renderText(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (href?.startsWith("#")) {
|
if (href?.startsWith("#")) {
|
||||||
handleAttachmentLink(linkEl, href, getNote, getAttachment);
|
handleAttachmentLink(linkEl, href, getNote, getAttachment, note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +357,7 @@ function renderText(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null) {
|
function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null, note: SNote | BNote) {
|
||||||
const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g;
|
const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g;
|
||||||
let attachmentMatch;
|
let attachmentMatch;
|
||||||
if ((attachmentMatch = linkRegExp.exec(href))) {
|
if ((attachmentMatch = linkRegExp.exec(href))) {
|
||||||
@ -357,7 +365,7 @@ function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNot
|
|||||||
const attachment = getAttachment(attachmentId);
|
const attachment = getAttachment(attachmentId);
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
linkEl.setAttribute("href", `api/attachments/${attachmentId}/download`);
|
linkEl.setAttribute("href", `api/attachments/${attachmentId}/download${addContentAccessQuery(note)}`);
|
||||||
linkEl.classList.add(`attachment-link`);
|
linkEl.classList.add(`attachment-link`);
|
||||||
linkEl.classList.add(`role-${attachment.role}`);
|
linkEl.classList.add(`role-${attachment.role}`);
|
||||||
linkEl.childNodes.length = 0;
|
linkEl.childNodes.length = 0;
|
||||||
@ -430,7 +438,7 @@ function renderMermaid(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.content = `
|
result.content = `
|
||||||
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
|
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}">
|
||||||
<hr>
|
<hr>
|
||||||
<details>
|
<details>
|
||||||
<summary>Chart source</summary>
|
<summary>Chart source</summary>
|
||||||
@ -439,14 +447,14 @@ function renderMermaid(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderImage(result: Result, note: SNote | BNote) {
|
function renderImage(result: Result, note: SNote | BNote) {
|
||||||
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`;
|
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFile(note: SNote | BNote, result: Result) {
|
function renderFile(note: SNote | BNote, result: Result) {
|
||||||
if (note.mime === "application/pdf") {
|
if (note.mime === "application/pdf") {
|
||||||
result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`;
|
result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view${addContentAccessQuery(note)}"></iframe>`;
|
||||||
} else {
|
} else {
|
||||||
result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
|
result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download${addContentAccessQuery(note)}'">Download file</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,20 @@ function checkNoteAccess(noteId: string, req: Request, res: Response) {
|
|||||||
const header = req.header("Authorization");
|
const header = req.header("Authorization");
|
||||||
|
|
||||||
if (!header?.startsWith("Basic ")) {
|
if (!header?.startsWith("Basic ")) {
|
||||||
|
if (req.path.startsWith("/share/api") && note.contentAccessor) {
|
||||||
|
let contentAccessToken = ""
|
||||||
|
if (note.contentAccessor.type === "cookie") contentAccessToken += req.cookies["trilium.cat"] || ""
|
||||||
|
else if (note.contentAccessor.type === "query") contentAccessToken += req.query['cat'] || ""
|
||||||
|
|
||||||
|
if (contentAccessToken){
|
||||||
|
if (note.contentAccessor.isTokenValid(contentAccessToken)){
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
res.status(401).send("Access is expired. Return back and update the page.");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,9 +138,14 @@ function register(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.isLabelTruthy("shareExclude")) {
|
||||||
|
res.status(404);
|
||||||
|
render404(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!checkNoteAccess(note.noteId, req, res)) {
|
if (!checkNoteAccess(note.noteId, req, res)) {
|
||||||
requestCredentials(res);
|
requestCredentials(res);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +157,10 @@ function register(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.contentAccessor && note.contentAccessor.type === "cookie") {
|
||||||
|
res.cookie('trilium.cat', note.contentAccessor.getToken(), { maxAge: note.contentAccessor.getTokenExpiration() * 1000, httpOnly: true })
|
||||||
|
}
|
||||||
|
|
||||||
res.send(renderNoteContent(note));
|
res.send(renderNoteContent(note));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +186,9 @@ function register(router: Router) {
|
|||||||
const { shareId } = req.params;
|
const { shareId } = req.params;
|
||||||
|
|
||||||
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
||||||
|
if (note){
|
||||||
|
note.initContentAccessor()
|
||||||
|
}
|
||||||
|
|
||||||
renderNote(note, req, res);
|
renderNote(note, req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
81
apps/server/src/share/shaca/entities/content_accessor.ts
Normal file
81
apps/server/src/share/shaca/entities/content_accessor.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import SNote from "./snote";
|
||||||
|
import utils from "../../../services/utils";
|
||||||
|
|
||||||
|
const DefaultAccessTimeoutSec = 10 * 60; // 10 minutes
|
||||||
|
|
||||||
|
export class ContentAccessor {
|
||||||
|
note: SNote;
|
||||||
|
token: string;
|
||||||
|
timestamp: number;
|
||||||
|
type: string;
|
||||||
|
timeout: number;
|
||||||
|
key: Buffer;
|
||||||
|
|
||||||
|
constructor(note: SNote) {
|
||||||
|
this.note = note;
|
||||||
|
this.key = crypto.randomBytes(32);
|
||||||
|
this.token = "";
|
||||||
|
this.timestamp = 0;
|
||||||
|
this.timeout = Number(this.note.getAttributeValue("label", "shareAccessTokenTimeout") || DefaultAccessTimeoutSec)
|
||||||
|
|
||||||
|
switch (this.note.getAttributeValue("label", "shareContentAccess")) {
|
||||||
|
case "basic": this.type = "basic"; break
|
||||||
|
case "query": this.type = "query"; break
|
||||||
|
default: this.type = "cookie"; break
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
__encrypt(text: string) {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv);
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
return iv.toString('hex') + encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
__decrypt(encryptedText: string) {
|
||||||
|
try {
|
||||||
|
const iv = Buffer.from(encryptedText.slice(0, 32), 'hex');
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv);
|
||||||
|
let decrypted = decipher.update(encryptedText.slice(32), 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
return decrypted;
|
||||||
|
} catch {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__compare(originalText: string, encryptedText: string) {
|
||||||
|
return originalText === this.__decrypt(encryptedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (new Date().getTime() < this.timestamp + this.getTimeout() * 1000) return
|
||||||
|
this.token = utils.randomString(36);
|
||||||
|
this.key = crypto.randomBytes(32);
|
||||||
|
this.timestamp = new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
isTokenValid(encToken: string) {
|
||||||
|
return this.__compare(this.token, encToken) && new Date().getTime() < this.timestamp + this.getTimeout() * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken() {
|
||||||
|
return this.__encrypt(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenExpiration() {
|
||||||
|
return (this.timestamp + (this.timeout * 1000) - new Date().getTime()) /1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
getContentAccessType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import type SAttribute from "./sattribute.js";
|
|||||||
import type SBranch from "./sbranch.js";
|
import type SBranch from "./sbranch.js";
|
||||||
import type { SNoteRow } from "./rows.js";
|
import type { SNoteRow } from "./rows.js";
|
||||||
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
|
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
|
||||||
|
import { ContentAccessor } from "./content_accessor.js";
|
||||||
|
|
||||||
const LABEL = "label";
|
const LABEL = "label";
|
||||||
const RELATION = "relation";
|
const RELATION = "relation";
|
||||||
@ -33,6 +34,7 @@ class SNote extends AbstractShacaEntity {
|
|||||||
private __inheritableAttributeCache: SAttribute[] | null;
|
private __inheritableAttributeCache: SAttribute[] | null;
|
||||||
targetRelations: SAttribute[];
|
targetRelations: SAttribute[];
|
||||||
attachments: SAttachment[];
|
attachments: SAttachment[];
|
||||||
|
contentAccessor: ContentAccessor | undefined;
|
||||||
|
|
||||||
constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) {
|
constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) {
|
||||||
super();
|
super();
|
||||||
@ -59,6 +61,15 @@ class SNote extends AbstractShacaEntity {
|
|||||||
this.shaca.notes[this.noteId] = this;
|
this.shaca.notes[this.noteId] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initContentAccessor(){
|
||||||
|
if (!this.contentAccessor && this.getCredentials().length > 0) {
|
||||||
|
this.contentAccessor = new ContentAccessor(this);
|
||||||
|
}
|
||||||
|
if (this.contentAccessor) {
|
||||||
|
this.contentAccessor.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getParentBranches() {
|
getParentBranches() {
|
||||||
return this.parentBranches;
|
return this.parentBranches;
|
||||||
}
|
}
|
||||||
@ -72,7 +83,7 @@ class SNote extends AbstractShacaEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVisibleChildBranches() {
|
getVisibleChildBranches() {
|
||||||
return this.getChildBranches().filter((branch) => !branch.isHidden && !branch.getNote().isLabelTruthy("shareHiddenFromTree"));
|
return this.getChildBranches().filter((branch) => !branch.isHidden && !branch.getNote().isLabelTruthy("shareHiddenFromTree") && !branch.getNote().isLabelTruthy("shareExclude"));
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentNotes() {
|
getParentNotes() {
|
||||||
@ -80,7 +91,7 @@ class SNote extends AbstractShacaEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getChildNotes() {
|
getChildNotes() {
|
||||||
return this.children;
|
return this.children.filter((note) => !note.isLabelTruthy("shareExclude"));
|
||||||
}
|
}
|
||||||
|
|
||||||
getVisibleChildNotes() {
|
getVisibleChildNotes() {
|
||||||
|
|||||||
@ -21,7 +21,7 @@
|
|||||||
"note_structure_description": "Notizen lassen sich hierarchisch anordnen. Ordner sind nicht nötig, da jede Notiz Unternotizen enthalten kann. Eine einzelne Notiz kann an mehreren Stellen in der Hierarchie hinzugefügt werden.",
|
"note_structure_description": "Notizen lassen sich hierarchisch anordnen. Ordner sind nicht nötig, da jede Notiz Unternotizen enthalten kann. Eine einzelne Notiz kann an mehreren Stellen in der Hierarchie hinzugefügt werden.",
|
||||||
"hoisting_description": "Trennen Sie Ihre persönlichen und beruflichen Notizen ganz einfach, indem Sie sie in einem Arbeitsbereich gruppieren. Dadurch wird Ihre Notizstruktur so fokussiert, dass nur ein bestimmter Satz von Notizen angezeigt wird.",
|
"hoisting_description": "Trennen Sie Ihre persönlichen und beruflichen Notizen ganz einfach, indem Sie sie in einem Arbeitsbereich gruppieren. Dadurch wird Ihre Notizstruktur so fokussiert, dass nur ein bestimmter Satz von Notizen angezeigt wird.",
|
||||||
"hoisting_title": "Arbeitsbereiche und Fokusansicht",
|
"hoisting_title": "Arbeitsbereiche und Fokusansicht",
|
||||||
"attributes_description": "Nutzen Sie Verbindungen zwischen Notizen oder fügen Sie Labels hinzu, um die Kategorisierung zu erleichtern. Mit hervorgehobenen („promoted“) Attributen können Sie strukturierte Informationen erfassen, die sich direkt in Tabellen und Boards verwenden lassen."
|
"attributes_description": "Verwenden Sie Beziehungen zwischen Notizen oder fügen Sie Beschriftungen hinzu, um die Kategorisierung zu vereinfachen. Verwenden Sie hervorgehobene Attribute, um strukturierte Informationen einzugeben, die in Tabellen und Boards verwendet werden können."
|
||||||
},
|
},
|
||||||
"productivity_benefits": {
|
"productivity_benefits": {
|
||||||
"revisions_title": "Notizrevisionen",
|
"revisions_title": "Notizrevisionen",
|
||||||
|
|||||||
@ -87,6 +87,8 @@
|
|||||||
"description_arm64": "ARM 기반 리눅스 배포판에서 aarch64 아키텍처와 호환됩니다."
|
"description_arm64": "ARM 기반 리눅스 배포판에서 aarch64 아키텍처와 호환됩니다."
|
||||||
},
|
},
|
||||||
"note_types": {
|
"note_types": {
|
||||||
"text_title": "텍스트 노트"
|
"text_title": "텍스트 노트",
|
||||||
|
"text_description": "노트는 WYSIWYG 편집기를 사용하며 표, 이미지, 수학 표현식, 구문 강조 기능의 코드 블록을 지원합니다. 특수문자를 사용한 마크다운 유사 구문이나 슬래시(/) 명령으로 텍스트 서식을 빠르게 지정할 수 있습니다.",
|
||||||
|
"code_title": "코드 노트"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,7 +131,7 @@ To do so, create a shared text note and apply the `shareIndex` label. When viewe
|
|||||||
|
|
||||||
## Attribute reference
|
## Attribute reference
|
||||||
|
|
||||||
<table class="ck-table-resized"><colgroup><col style="width:18.38%;"><col style="width:81.62%;"></colgroup><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>#shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>#shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>#shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>#shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>#shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>#shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>#shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>#shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>#shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>#shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>#shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table>
|
<table class="ck-table-resized"><colgroup><col style="width:18.38%;"><col style="width:81.62%;"></colgroup><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>#shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>#shareTemplateNoPrevNext</code></td><td>hide bottom page navigation prev and next page.</td></tr><tr><td><code>#shareTemplateNoLeftPanel</code></td><td>hide left panel fully.</td></tr><tr><td><code>#shareExclude</code></td><td>this note will be excluded from share, not accessible via direct URL (implemented to hide scripts from share)</td></tr><tr><td><code>#shareContentAccess</code></td><td>method for attachments authorization in case when note protected with login and password (#shareCredentials). Could be cookie (the cookie will be provided when page loads) / query (every url will be updated with token) / basic (only basic header authorization)). By default for browser used cookie.</td></tr><tr><td><code>#shareAccessTokenTimeout</code></td><td>token expiration timeout in seconds, by default 10 minutes. While token not expired user could download attachment, after that he will get message `Access is expired. Return back and update the page.`</td></tr><tr><td><code>#shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>#shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>#shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>#shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>#shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>#shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>#shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>#shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>#shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>#shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table>
|
||||||
|
|
||||||
### Customizing logo
|
### Customizing logo
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
let openGraphImage = subRoot.note.getLabelValue("shareOpenGraphImage");
|
let openGraphImage = subRoot.note.getLabelValue("shareOpenGraphImage");
|
||||||
// Relation takes priority and requires some altering
|
// Relation takes priority and requires some altering
|
||||||
if (subRoot.note.hasRelation("shareOpenGraphImage")) {
|
if (subRoot.note.hasRelation("shareOpenGraphImage")) {
|
||||||
openGraphImage = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png`;
|
openGraphImage = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png${addContentAccessQuery()}`;
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<title><%= pageTitle %></title>
|
<title><%= pageTitle %></title>
|
||||||
@ -109,6 +109,7 @@ content = content.replaceAll(headingRe, (...match) => {
|
|||||||
<button aria-label="Show Mobile Menu" id="show-menu-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg></button>
|
<button aria-label="Show Mobile Menu" id="show-menu-button"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"></path></svg></button>
|
||||||
</div>
|
</div>
|
||||||
<div id="split-pane">
|
<div id="split-pane">
|
||||||
|
<% if (!note.isLabelTruthy("shareTemplateNoLeftPanel")) { %>
|
||||||
<div id="left-pane">
|
<div id="left-pane">
|
||||||
<div id="navigation">
|
<div id="navigation">
|
||||||
<div id="site-header">
|
<div id="site-header">
|
||||||
@ -143,6 +144,8 @@ content = content.replaceAll(headingRe, (...match) => {
|
|||||||
<% } %>
|
<% } %>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
<div id="right-pane">
|
<div id="right-pane">
|
||||||
<div id="main">
|
<div id="main">
|
||||||
<div id="content" class="type-<%= note.type %><% if (note.type === "text") { %> ck-content<% } %><% if (isEmpty) { %> no-content<% } %>">
|
<div id="content" class="type-<%= note.type %><% if (note.type === "text") { %> ck-content<% } %><% if (isEmpty) { %> no-content<% } %>">
|
||||||
@ -152,7 +155,9 @@ content = content.replaceAll(headingRe, (...match) => {
|
|||||||
<p>This note has no content.</p>
|
<p>This note has no content.</p>
|
||||||
<% } else { %>
|
<% } else { %>
|
||||||
<%
|
<%
|
||||||
content = content.replace(/<img /g, `<img alt="${t("share_theme.image_alt")}" loading="lazy" `);
|
content = content
|
||||||
|
.replace(/<img /g, `<img alt="${t("share_theme.image_alt")}" loading="lazy" `)
|
||||||
|
.replace(/src="(api\/[^"]+)"/g, (m, url) => `src="${url}${addContentAccessQuery(url.includes('?'))}"`);
|
||||||
%>
|
%>
|
||||||
<%- content %>
|
<%- content %>
|
||||||
<% } %>
|
<% } %>
|
||||||
@ -189,7 +194,7 @@ content = content.replaceAll(headingRe, (...match) => {
|
|||||||
</div>
|
</div>
|
||||||
<% } %>
|
<% } %>
|
||||||
|
|
||||||
<% if (hasTree) { %>
|
<% if (hasTree && !note.isLabelTruthy("shareTemplateNoPrevNext")) { %>
|
||||||
<%- include("prev_next", { note: note, subRoot: subRoot }) %>
|
<%- include("prev_next", { note: note, subRoot: subRoot }) %>
|
||||||
<% } %>
|
<% } %>
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user