Compare commits

...

15 Commits

Author SHA1 Message Date
arch
ca03c1300b
Merge 5d5fd2079a110a41c89f5616df5c5277fe67bb4f into 732494dfc51286926d86d49a73d3915a4aa22ef9 2025-11-30 02:55:04 +02:00
Adorian Doran
732494dfc5 client/keyboard shortcuts cheatsheet: add an edit button
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run
Deploy website / Build & deploy website (push) Waiting to run
2025-11-30 02:54:51 +02:00
Adorian Doran
b8748b856a client/note menu: use proper style for development-only actions section header 2025-11-30 02:34:09 +02:00
Adorian Doran
cc71f15700 client/quick edit: remove fixed toolbar transparency 2025-11-30 02:17:43 +02:00
Adorian Doran
124ef640b1 client/quick edit: tweak layout 2025-11-30 02:04:40 +02:00
Adorian Doran
f5e3df0cd2 client/quick edit: add placeholder for "open in full editor" custom title bar button 2025-11-30 01:54:28 +02:00
Adorian Doran
c8431181c8 client/dialogs/custom title bar buttons: tweak 2025-11-30 01:52:55 +02:00
Adorian Doran
07fb5ab017 client/dialogs: add support for custom title bar buttons 2025-11-30 01:44:20 +02:00
Adorian Doran
6735b257b4 style/promoted color attributes: fix the layout on narrow width 2025-11-30 01:09:07 +02:00
Adorian Doran
cef242a9ce style/button group: fix the appearance of the active button 2025-11-30 00:55:34 +02:00
Elian Doran
2923d917e5
Translations update from Hosted Weblate (#7891) 2025-11-29 22:58:03 +02:00
Hosted Weblate
9a76a9069c
Update translation files
Updated by "Cleanup translation files" add-on in Weblate.

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/
2025-11-29 20:01:42 +01:00
pythaac
8e1d796870
Translated using Weblate (Korean)
Currently translated at 44.7% (68 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2025-11-29 20:01:38 +01:00
Andreas H.
8b0d4e5c3b
Translated using Weblate (German)
Currently translated at 100.0% (152 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/de/
2025-11-29 20:01:37 +01:00
x1arch
5d5fd2079a Fix share access to attachments for notes protected by login:password 2025-11-21 19:52:22 +00:00
29 changed files with 274 additions and 73 deletions

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -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": "코드 노트"
} }
} }

View File

@ -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&nbsp;<a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a>&nbsp;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&nbsp;<a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a>&nbsp;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

View File

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