Compare commits

...

44 Commits

Author SHA1 Message Date
arch
7938e38d7f
Merge 5d5fd2079a110a41c89f5616df5c5277fe67bb4f into b9e257a39d8f658dec38c6a276e7550939db2c7e 2025-11-29 21:01:35 +02:00
Elian Doran
b9e257a39d
refactor(client): redundant interface
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-29 20:18:43 +02:00
Elian Doran
e7eaa5fd58
fix(mobile): global menu backdrop on tablet view 2025-11-29 19:49:38 +02:00
Elian Doran
c9aa992e73
fix(read-only-bar): displayed when viewing attachments 2025-11-29 19:40:00 +02:00
Elian Doran
f325930f68
chore(read-only-bar): use in-app help 2025-11-29 19:37:38 +02:00
Adorian Doran
1346ffb77e Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-29 18:50:24 +02:00
Adorian Doran
3378746530 style: disable text selection in UI 2025-11-29 18:50:16 +02:00
Elian Doran
ce2d94f04e
Resolve focus issues within split pane (#7877) 2025-11-29 18:34:26 +02:00
Elian Doran
b3c2a1e6c5
fix(insertDateTime): unable to insert date/time via quick editor or s… (#7889) 2025-11-29 18:30:28 +02:00
Elian Doran
dbf63787da
Merge branches 'main' and 'main' of ssh://github.com/TriliumNext/trilium 2025-11-29 18:08:19 +02:00
Elian Doran
88a7ebef69
fix(quick-edit): background broke for colors with no hue 2025-11-29 18:07:24 +02:00
Adorian Doran
a716151dd9 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-29 18:04:53 +02:00
Adorian Doran
7462f1b7a5 style/empty tab: improve layout 2025-11-29 18:04:45 +02:00
Elian Doran
ec76b9dc5c
chore(quick-edit): increase max-width on mobile 2025-11-29 18:01:07 +02:00
Elian Doran
79cd96ade9
style(context_menu): improve submenu separator style 2025-11-29 17:47:26 +02:00
Elian Doran
a5b84406be
style(context_menu): improve submenu bg on mobile 2025-11-29 17:35:37 +02:00
Elian Doran
8c1a04c4b2
fix(mobile): shortcut keyboard + visible 2025-11-29 17:32:32 +02:00
Elian Doran
ee81037173
feat(quick_edit): smooth transition between colors 2025-11-29 17:26:17 +02:00
Elian Doran
453349be26
feat(quick_edit): seamless transition between color changes 2025-11-29 17:19:43 +02:00
Elian Doran
81a9e06b23
feat(quick_edit): basic reactivity to color changes 2025-11-29 17:19:43 +02:00
Elian Doran
7d8af0f252
refactor(client): use var for modal max height 2025-11-29 17:19:43 +02:00
Elian Doran
a68cd7526b
style(mobile): improve quick edit max height 2025-11-29 17:19:43 +02:00
Elian Doran
470ca3b6dc
style(mobile): improve quick edit max width 2025-11-29 17:19:43 +02:00
Elian Doran
e8bae61afc
style(mobile): center modals on tablet view 2025-11-29 17:19:43 +02:00
Elian Doran
c1f663a200
style(mobile): no bottom border radius on modals 2025-11-29 17:19:43 +02:00
Elian Doran
22b2e21df0
Translations update from Hosted Weblate (#7887) 2025-11-29 17:11:15 +02:00
SiriusXT
5f19710791 fix(insertDateTime): unable to insert date/time via quick editor or shortcut 2025-11-29 22:40:49 +08:00
pythaac
d3f3ff4eab
Translated using Weblate (Korean)
Currently translated at 43.4% (66 of 152 strings)

Translation: Trilium Notes/Website
Translate-URL: https://hosted.weblate.org/projects/trilium/website/ko/
2025-11-29 14:02:26 +00:00
noobhjy
5af7425cae
Translated using Weblate (Chinese (Simplified Han script))
Currently translated at 100.0% (1637 of 1637 strings)

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-11-29 14:02:25 +00:00
Elian Doran
fe10c9f8c8
fix(text): strikethrough icon appears disabled 2025-11-29 15:34:43 +02:00
Elian Doran
cd2a085d00
fix(type_widgets/notemap): bottom part not visible 2025-11-29 15:30:17 +02:00
Elian Doran
3c61626370
fix(launch_bar/calendar): tooltip showing over the calendar dropdown 2025-11-29 15:16:43 +02:00
Elian Doran
351fe5848f
fix(launch_bar/calendar): clicking on the edges would dismiss modal 2025-11-29 13:26:48 +02:00
Elian Doran
ca7bbefbdc
fix(launch_bar/calendar): dropdown remains open when switching years 2025-11-29 13:19:49 +02:00
Elian Doran
7094f71e32
refactor(server): remove now unnecessary attachment without size 2025-11-29 13:08:05 +02:00
Elian Doran
88b5e9db87
fix(server): uploading new attachments doesn't report size 2025-11-29 13:03:08 +02:00
SiriusXT
53a8f6b4c0 Merge branch 'main' into fix/split_pane
Some checks failed
Checks / main (push) Has been cancelled
2025-11-29 11:39:05 +08:00
SiriusXT
9ae1a55896 chore(react/empty): obtain ntxId via React props instead of DOM query 2025-11-29 11:38:45 +08:00
SiriusXT
4d1a91baa6 Merge branch 'main' into fix/split_pane 2025-11-28 19:49:08 +08:00
SiriusXT
1898efa282 chore(e2e): add Playwright tests for split pane 2025-11-28 19:48:37 +08:00
SiriusXT
648ab4d736 fix(left-pane): only focus the note when toggling left pane visibility if necessary 2025-11-28 19:45:19 +08:00
SiriusXT
407cac588a fix(split): only trigger focusOnDetail when necessary 2025-11-28 19:42:04 +08:00
SiriusXT
210dcfb989 fix(empty): open note in the correct split pane 2025-11-28 19:38:52 +08:00
x1arch
5d5fd2079a Fix share access to attachments for notes protected by login:password 2025-11-21 19:52:22 +00:00
53 changed files with 537 additions and 251 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

@ -487,7 +487,7 @@ type EventMappings = {
relationMapResetPanZoom: { ntxId: string | null | undefined }; relationMapResetPanZoom: { ntxId: string | null | undefined };
relationMapResetZoomIn: { ntxId: string | null | undefined }; relationMapResetZoomIn: { ntxId: string | null | undefined };
relationMapResetZoomOut: { ntxId: string | null | undefined }; relationMapResetZoomOut: { ntxId: string | null | undefined };
activeNoteChanged: {}; activeNoteChanged: {ntxId: string | null | undefined};
showAddLinkDialog: AddLinkOpts; showAddLinkDialog: AddLinkOpts;
showIncludeDialog: IncludeNoteOpts; showIncludeDialog: IncludeNoteOpts;
openBulkActionsDialog: { openBulkActionsDialog: {

View File

@ -165,7 +165,7 @@ export default class TabManager extends Component {
const activeNoteContext = this.getActiveContext(); const activeNoteContext = this.getActiveContext();
this.updateDocumentTitle(activeNoteContext); this.updateDocumentTitle(activeNoteContext);
this.triggerEvent("activeNoteChanged", {}); // trigger this even in on popstate event this.triggerEvent("activeNoteChanged", {ntxId:activeNoteContext?.ntxId}); // trigger this even in on popstate event
} }
calculateHash(): string { calculateHash(): string {

View File

@ -32,6 +32,7 @@ import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
const MOBILE_CSS = ` const MOBILE_CSS = `
<style> <style>
span.keyboard-shortcut,
kbd { kbd {
display: none; display: none;
} }

View File

@ -1,7 +1,9 @@
import clsx from "clsx";
import {readCssVar} from "../utils/css-var"; import {readCssVar} from "../utils/css-var";
import Color, { ColorInstance } from "color"; import Color, { ColorInstance } from "color";
const registeredClasses = new Set<string>(); const registeredClasses = new Set<string>();
const colorsWithHue = new Set<string>();
// Read the color lightness limits defined in the theme as CSS variables // Read the color lightness limits defined in the theme as CSS variables
@ -26,19 +28,23 @@ function createClassForColor(colorString: string | null) {
if (!registeredClasses.has(className)) { if (!registeredClasses.has(className)) {
const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!, const adjustedColor = adjustColorLightness(color, lightThemeColorMaxLightness!,
darkThemeColorMinLightness!); darkThemeColorMinLightness!);
const hue = getHue(color);
$("head").append(`<style> $("head").append(`<style>
.${className}, span.fancytree-active.${className} { .${className}, span.fancytree-active.${className} {
--light-theme-custom-color: ${adjustedColor.lightThemeColor}; --light-theme-custom-color: ${adjustedColor.lightThemeColor};
--dark-theme-custom-color: ${adjustedColor.darkThemeColor}; --dark-theme-custom-color: ${adjustedColor.darkThemeColor};
--custom-color-hue: ${getHue(color) ?? 'unset'}; --custom-color-hue: ${hue ?? 'unset'};
} }
</style>`); </style>`);
registeredClasses.add(className); registeredClasses.add(className);
if (hue) {
colorsWithHue.add(className);
}
} }
return className; return clsx(className, colorsWithHue.has(className) && "with-hue");
} }
function parseColor(color: string) { function parseColor(color: string) {

View File

@ -4,6 +4,10 @@
box-sizing: border-box; box-sizing: border-box;
} }
.dropdown-menu:not(.static).calendar-dropdown-menu {
padding: 0 !important;
}
.calendar-dropdown-widget { .calendar-dropdown-widget {
margin: 0 auto; margin: 0 auto;
overflow: hidden; overflow: hidden;

View File

@ -26,6 +26,7 @@
--bs-body-color: var(--main-text-color) !important; --bs-body-color: var(--main-text-color) !important;
--bs-body-bg: var(--main-background-color) !important; --bs-body-bg: var(--main-background-color) !important;
--ck-mention-list-max-height: 500px; --ck-mention-list-max-height: 500px;
--tn-modal-max-height: 90vh;
} }
body#trilium-app.motion-disabled *, body#trilium-app.motion-disabled *,
@ -212,6 +213,16 @@ input::placeholder,
background-color: var(--modal-backdrop-color) !important; background-color: var(--modal-backdrop-color) !important;
} }
body.mobile .modal .modal-dialog {
left: 50%;
transform: translateX(-50%);
width: 100%;
}
body.mobile .modal .modal-content {
border-radius: var(--bs-modal-border-radius) var(--bs-modal-border-radius) 0 0;
}
.component { .component {
contain: size; contain: size;
} }
@ -706,11 +717,6 @@ table.promoted-attributes-in-tooltip th {
z-index: 32767 !important; z-index: 32767 !important;
} }
.tooltip-trigger {
background: transparent;
pointer-events: none;
}
.bs-tooltip-bottom .tooltip-arrow::before { .bs-tooltip-bottom .tooltip-arrow::before {
border-bottom-color: var(--main-border-color) !important; border-bottom-color: var(--main-border-color) !important;
} }
@ -1006,9 +1012,17 @@ div[data-notify="container"] {
font-family: var(--monospace-font-family); font-family: var(--monospace-font-family);
} }
svg.ck-icon .note-icon { svg.ck-icon {
color: var(--main-text-color); &.ck-icon_inherit-color {
font-size: 20px; * {
fill: currentColor;
}
}
&.note-icon {
color: var(--main-text-color);
font-size: 20px;
}
} }
.ck-content { .ck-content {
@ -1117,10 +1131,6 @@ a.external:not(.no-arrow):after, a[href^="http://"]:not(.no-arrow):after, a[href
display: inline-block; display: inline-block;
} }
.note-detail-empty {
margin: 50px;
}
.modal-header { .modal-header {
padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */ padding: 0.5rem 1rem 0.5rem 1rem !important; /* make modal header padding slightly smaller */
} }
@ -1316,7 +1326,7 @@ body.mobile #context-menu-container.mobile-bottom-menu {
inset-inline-end: 0 !important; inset-inline-end: 0 !important;
bottom: 0 !important; bottom: 0 !important;
top: unset !important; top: unset !important;
max-height: 90vh; max-height: var(--tn-modal-max-height);
overflow: auto !important; overflow: auto !important;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
@ -1379,6 +1389,20 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
flex-shrink: 0; flex-shrink: 0;
} }
.right-dropdown-widget .right-dropdown-button {
position: relative;
}
.tooltip-trigger {
background: transparent;
pointer-events: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
#launcher-pane.horizontal .right-dropdown-widget { #launcher-pane.horizontal .right-dropdown-widget {
width: 53px; width: 53px;
} }
@ -1562,7 +1586,7 @@ body:not(.mobile) #launcher-pane.horizontal .dropdown-submenu > .dropdown-menu {
inset-inline-end: 0 !important; inset-inline-end: 0 !important;
transform: unset !important; transform: unset !important;
overflow-y: auto; overflow-y: auto;
max-height: calc(90vh - var(--dropdown-bottom)); max-height: calc(var(--tn-modal-max-height) - var(--dropdown-bottom));
} }
#mobile-sidebar-container { #mobile-sidebar-container {

View File

@ -98,6 +98,7 @@
--menu-item-delimiter-color: #ffffff1c; --menu-item-delimiter-color: #ffffff1c;
--menu-item-group-header-color: #ffffff91; --menu-item-group-header-color: #ffffff91;
--menu-section-background-color: #fefefe08; --menu-section-background-color: #fefefe08;
--menu-submenu-mobile-background-color: rgba(0, 0, 0, 0.15);
--modal-backdrop-color: #000; --modal-backdrop-color: #000;
--modal-shadow-color: rgba(0, 0, 0, .5); --modal-shadow-color: rgba(0, 0, 0, .5);
@ -300,7 +301,7 @@ body .todo-list input[type="checkbox"]:not(:checked):before {
border-color: var(--muted-text-color) !important; border-color: var(--muted-text-color) !important;
} }
.tinted-quick-edit-dialog { .quick-edit-dialog-wrapper.with-hue {
--modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%); --modal-background-color: hsl(var(--custom-color-hue), 8.8%, 11.2%);
--modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%); --modal-border-color: hsl(var(--custom-color-hue), 9.4%, 25.1%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%); --promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 13.2%, 20.8%);

View File

@ -276,7 +276,7 @@
--custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1); --custom-bg-color: hsl(var(--custom-color-hue), 37%, 89%, 1);
} }
.tinted-quick-edit-dialog { .quick-edit-dialog-wrapper.with-hue {
--modal-background-color: hsl(var(--custom-color-hue), 56%, 96%); --modal-background-color: hsl(var(--custom-color-hue), 56%, 96%);
--modal-border-color: hsl(var(--custom-color-hue), 33%, 41%); --modal-border-color: hsl(var(--custom-color-hue), 33%, 41%);
--promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%); --promoted-attribute-card-background-color: hsl(var(--custom-color-hue), 40%, 88%);

View File

@ -62,6 +62,7 @@
--menu-padding-size: 8px; --menu-padding-size: 8px;
--menu-item-icon-vert-offset: -2px; --menu-item-icon-vert-offset: -2px;
--menu-submenu-mobile-background-color: rgba(255, 255, 255, 0.15);
--more-accented-background-color: var(--card-background-hover-color); --more-accented-background-color: var(--card-background-hover-color);
@ -99,6 +100,14 @@
--tree-item-dark-theme-min-color-lightness: 65; --tree-item-dark-theme-min-color-lightness: 65;
} }
body {
user-select: none;
}
.selectable-text {
user-select: text;
}
body.backdrop-effects-disabled { body.backdrop-effects-disabled {
/* Backdrop effects are disabled, replace the menu background color with the /* Backdrop effects are disabled, replace the menu background color with the
* no-backdrop fallback color */ * no-backdrop fallback color */
@ -311,6 +320,10 @@ body.mobile #context-menu-cover {
&.global-menu-cover { &.global-menu-cover {
bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size)); bottom: calc(var(--mobile-bottom-offset) + var(--launcher-pane-size));
@media (min-width: 992px) {
bottom: 0;
}
} }
} }
@ -376,7 +389,8 @@ body.mobile .dropdown-menu {
} }
.dropdown-menu { .dropdown-menu {
--menu-background-color: rgba(0, 0, 0, 0.15); --menu-background-color: --menu-submenu-mobile-background-color;
--bs-dropdown-divider-margin-y: 0.25rem;
border-radius: 0; border-radius: 0;
max-height: 0; max-height: 0;
transition: max-height 100ms ease-in; transition: max-height 100ms ease-in;
@ -384,22 +398,7 @@ body.mobile .dropdown-menu {
&.show { &.show {
max-height: 1000px; max-height: 1000px;
} padding: 0.5rem 0.75rem !important;
.dropdown-item {
background: transparent;
}
}
.dropdown-divider {
visibility: visible;
margin: 0;
height: 3px;
border-top: unset;
background-color: rgba(0, 0, 0, 0.2);
&:after {
content: unset;
} }
} }

View File

@ -124,12 +124,8 @@
/* The container */ /* The container */
.note-split.empty-note {
--max-content-width: 70%;
}
.note-split.empty-note div.note-detail { .note-split.empty-note div.note-detail {
margin: 50px auto; margin-inline: auto;
} }
/* The search results list */ /* The search results list */

View File

@ -345,7 +345,7 @@ body[dir=ltr] #launcher-container {
*/ */
.calendar-dropdown-widget { .calendar-dropdown-widget {
padding: 12px; padding: 18px;
color: var(--calendar-color); color: var(--calendar-color);
user-select: none; user-select: none;
} }

View File

@ -1662,7 +1662,7 @@
}, },
"editable-text": { "editable-text": {
"auto-detect-language": "自动检测", "auto-detect-language": "自动检测",
"keeps-crashing": "编辑组件时崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。" "keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。"
}, },
"highlighting": { "highlighting": {
"title": "代码块", "title": "代码块",

View File

@ -1647,7 +1647,6 @@
"read-only-info": { "read-only-info": {
"read-only-note": "Currently viewing a read-only note.", "read-only-note": "Currently viewing a read-only note.",
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.", "auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
"auto-read-only-learn-more": "Learn more",
"edit-note": "Edit note" "edit-note": "Edit note"
}, },
"note_types": { "note_types": {

View File

@ -105,9 +105,11 @@ export default function NoteDetail() {
}); });
// Automatically focus the editor. // Automatically focus the editor.
useTriliumEvent("activeNoteChanged", () => { useTriliumEvent("activeNoteChanged", ({ ntxId: eventNtxId }) => {
// Restore focus to the editor when switching tabs, but only if the note tree is not already focused. if (eventNtxId != ntxId) return;
if (!document.activeElement?.classList.contains("fancytree-title")) { // Restore focus to the editor when switching tabs,
// but only if the note tree and the note panel (e.g., note title or note detail) are not focused.
if (!document.activeElement?.classList.contains("fancytree-title") && !parentComponent.$widget[0].closest(".note-split")?.contains(document.activeElement)) {
parentComponent.triggerCommand("focusOnDetail", { ntxId }); parentComponent.triggerCommand("focusOnDetail", { ntxId });
} }
}); });

View File

@ -3,34 +3,33 @@ import { t } from "../services/i18n";
import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks" import { useIsNoteReadOnly, useNoteContext, useTriliumEvent } from "./react/hooks"
import Button from "./react/Button"; import Button from "./react/Button";
import InfoBar from "./react/InfoBar"; import InfoBar from "./react/InfoBar";
import HelpButton from "./react/HelpButton";
export default function ReadOnlyNoteInfoBar(props: {}) { export default function ReadOnlyNoteInfoBar(props: {}) {
const {note, noteContext} = useNoteContext(); const { note, noteContext } = useNoteContext();
const {isReadOnly, enableEditing} = useIsNoteReadOnly(note, noteContext); const { isReadOnly, enableEditing } = useIsNoteReadOnly(note, noteContext);
const isExplicitReadOnly = note?.isLabelTruthy("readOnly"); const isExplicitReadOnly = note?.isLabelTruthy("readOnly");
return <InfoBar className="read-only-note-info-bar-widget" return (
type={(isExplicitReadOnly ? "subtle" : "prominent")} <InfoBar
style={{display: (!isReadOnly) ? "none" : undefined}}> className="read-only-note-info-bar-widget"
type={(isExplicitReadOnly ? "subtle" : "prominent")}
<div class="read-only-note-info-bar-widget-content"> style={{display: (!isReadOnly) ? "none" : undefined}}
{(isExplicitReadOnly) ? ( >
<div>{t("read-only-info.read-only-note")}</div> <div class="read-only-note-info-bar-widget-content">
) : ( {(isExplicitReadOnly) ? (
<div> <div>{t("read-only-info.read-only-note")}</div>
{t("read-only-info.auto-read-only-note")} ) : (
&nbsp; <div>
<a class="tn-link" {t("read-only-info.auto-read-only-note")}
href="https://docs.triliumnotes.org/user-guide/concepts/notes/read-only-notes#automatic-read-only-mode"> {" "}
<HelpButton helpPage="CoFPLs3dRlXc" />
{t("read-only-info.auto-read-only-learn-more")}
</a>
</div>
)}
<Button text={t("read-only-info.edit-note")}
icon="bx-pencil" onClick={() => enableEditing()} />
</div> </div>
</InfoBar> )}
<Button text={t("read-only-info.edit-note")}
icon="bx-pencil" onClick={() => enableEditing()} />
</div>
</InfoBar>
);
} }

View File

@ -110,7 +110,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
private weekNotes: string[] = []; private weekNotes: string[] = [];
constructor(title: string = "", icon: string = "") { constructor(title: string = "", icon: string = "") {
super(title, icon, DROPDOWN_TPL); super(title, icon, DROPDOWN_TPL, "calendar-dropdown-menu");
} }
doRender() { doRender() {
@ -211,8 +211,7 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
const $target = $(e.target); const $target = $(e.target);
// Keep dropdown open when clicking on month select button or year selector area // Keep dropdown open when clicking on month select button or year selector area
if ($target.closest('.btn.dropdown-toggle.select-button').length || if ($target.closest('.btn.dropdown-toggle.select-button').length) {
$target.closest('.calendar-year-selector').length) {
e.stopPropagation(); e.stopPropagation();
return; return;
} }

View File

@ -7,9 +7,9 @@ const TPL = /*html*/`
<div class="dropdown right-dropdown-widget"> <div class="dropdown right-dropdown-widget">
<button type="button" data-bs-toggle="dropdown" <button type="button" data-bs-toggle="dropdown"
aria-haspopup="true" aria-expanded="false" aria-haspopup="true" aria-expanded="false"
class="bx right-dropdown-button launcher-button"></button> class="bx right-dropdown-button launcher-button">
<div class="tooltip-trigger"></div>
<div class="tooltip-trigger"></div> </button>
<div class="dropdown-menu"></div> <div class="dropdown-menu"></div>
</div> </div>
@ -24,14 +24,16 @@ export default class RightDropdownButtonWidget extends BasicWidget {
protected dropdown!: Dropdown; protected dropdown!: Dropdown;
protected $tooltip!: JQuery<HTMLElement>; protected $tooltip!: JQuery<HTMLElement>;
protected tooltip!: Tooltip; protected tooltip!: Tooltip;
private dropdownClass?: string;
public $dropdownContent!: JQuery<HTMLElement>; public $dropdownContent!: JQuery<HTMLElement>;
constructor(title: string, iconClass: string, dropdownTpl: string) { constructor(title: string, iconClass: string, dropdownTpl: string, dropdownClass?: string) {
super(); super();
this.iconClass = iconClass; this.iconClass = iconClass;
this.title = title; this.title = title;
this.dropdownTpl = dropdownTpl; this.dropdownTpl = dropdownTpl;
this.dropdownClass = dropdownClass;
this.settings = { this.settings = {
titlePlacement: "right" titlePlacement: "right"
@ -41,15 +43,17 @@ export default class RightDropdownButtonWidget extends BasicWidget {
doRender() { doRender() {
this.$widget = $(TPL); this.$widget = $(TPL);
this.$dropdownMenu = this.$widget.find(".dropdown-menu"); this.$dropdownMenu = this.$widget.find(".dropdown-menu");
if (this.dropdownClass) {
this.$dropdownMenu.addClass(this.dropdownClass);
}
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], { this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
popperConfig: { popperConfig: {
placement: this.settings.titlePlacement, placement: this.settings.titlePlacement,
} }
}); });
this.$widget.attr("title", this.title); this.$tooltip = this.$widget.find(".tooltip-trigger").attr("title", this.title);
this.tooltip = Tooltip.getOrCreateInstance(this.$widget[0], { this.tooltip = new Tooltip(this.$tooltip[0], {
trigger: "hover",
placement: handleRightToLeftPlacement(this.settings.titlePlacement), placement: handleRightToLeftPlacement(this.settings.titlePlacement),
fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ] fallbackPlacements: [ handleRightToLeftPlacement(this.settings.titlePlacement) ]
}); });
@ -57,7 +61,9 @@ export default class RightDropdownButtonWidget extends BasicWidget {
this.$widget this.$widget
.find(".right-dropdown-button") .find(".right-dropdown-button")
.addClass(this.iconClass) .addClass(this.iconClass)
.on("click", () => this.tooltip.hide()); .on("click", () => this.tooltip.hide())
.on("mouseenter", () => this.tooltip.show())
.on("mouseleave", () => this.tooltip.hide());
this.$widget.on("show.bs.dropdown", async () => { this.$widget.on("show.bs.dropdown", async () => {
await this.dropdownShown(); await this.dropdownShown();

View File

@ -29,7 +29,11 @@ export default class LeftPaneContainer extends FlexContainer<Component> {
if (visible) { if (visible) {
this.triggerEvent("focusTree", {}); this.triggerEvent("focusTree", {});
} else { } else {
this.triggerEvent("focusOnDetail", { ntxId: appContext.tabManager.getActiveContext()?.ntxId }); const ntxId = appContext.tabManager.getActiveContext()?.ntxId;
const noteContainer = document.querySelector(`.note-split[data-ntx-id="${ntxId}"]`);
if (!noteContainer?.contains(document.activeElement)) {
this.triggerEvent("focusOnDetail", { ntxId });
}
} }
options.save("leftPaneVisible", this.currentLeftPaneVisible.toString()); options.save("leftPaneVisible", this.currentLeftPaneVisible.toString());

View File

@ -1,12 +1,8 @@
import FlexContainer from "./flex_container.js"; import FlexContainer from "./flex_container.js";
import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js"; import appContext, { type CommandData, type CommandListenerData, type EventData, type EventNames, type NoteSwitchedContext } from "../../components/app_context.js";
import type BasicWidget from "../basic_widget.js"; import type BasicWidget from "../basic_widget.js";
import type NoteContext from "../../components/note_context.js";
import Component from "../../components/component.js"; import Component from "../../components/component.js";
import splitService from "../../services/resizer.js"; import splitService from "../../services/resizer.js";
interface NoteContextEvent {
noteContext: NoteContext;
}
interface SplitNoteWidget extends BasicWidget { interface SplitNoteWidget extends BasicWidget {
hasBeenAlreadyShown?: boolean; hasBeenAlreadyShown?: boolean;

View File

@ -5,14 +5,24 @@ body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; }
body.desktop .modal.popup-editor-dialog .modal-dialog { body.desktop .modal.popup-editor-dialog .modal-dialog {
max-width: 75vw; max-width: 75vw;
}
.modal.popup-editor-dialog .modal-dialog {
border-bottom-left-radius: var(--bs-modal-border-radius); border-bottom-left-radius: var(--bs-modal-border-radius);
border-bottom-right-radius: var(--bs-modal-border-radius); border-bottom-right-radius: var(--bs-modal-border-radius);
}
body.desktop .modal.popup-editor-dialog .modal-dialog {
overflow: hidden; overflow: hidden;
} }
body.mobile .modal.popup-editor-dialog .modal-dialog {
max-width: min(var(--preferred-max-content-width), 95vw);
max-height: var(--tn-modal-max-height);
height: 100%;
}
.modal.popup-editor-dialog .modal-content {
transition: background-color 250ms ease-in;
}
.modal.popup-editor-dialog .modal-header .modal-title { .modal.popup-editor-dialog .modal-header .modal-title {
font-size: 1.1em; font-size: 1.1em;
} }
@ -52,12 +62,16 @@ body.desktop .modal.popup-editor-dialog .modal-dialog {
font-size: 1em; font-size: 1em;
} }
.modal.popup-editor-dialog .classic-toolbar-outer-container.visible {
background-color: transparent;
}
.modal.popup-editor-dialog .classic-toolbar-widget { .modal.popup-editor-dialog .classic-toolbar-widget {
position: sticky; position: sticky;
top: 0; top: 0;
inset-inline-start: 0; inset-inline-start: 0;
inset-inline-end: 0; inset-inline-end: 0;
background: var(--modal-background-color); background: transparent;
z-index: 998; z-index: 998;
align-items: flex-start; align-items: flex-start;
} }

View File

@ -1,7 +1,7 @@
import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks"; import { useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
import Modal from "../react/Modal"; import Modal from "../react/Modal";
import "./PopupEditor.css"; import "./PopupEditor.css";
import { useNoteContext, useTriliumEvent } from "../react/hooks"; import { useNoteContext, useNoteLabel, useTriliumEvent } from "../react/hooks";
import NoteTitleWidget from "../note_title"; import NoteTitleWidget from "../note_title";
import NoteIcon from "../note_icon"; import NoteIcon from "../note_icon";
import NoteContext from "../../components/note_context"; import NoteContext from "../../components/note_context";
@ -89,17 +89,10 @@ export default function PopupEditor() {
export function DialogWrapper({ children }: { children: ComponentChildren }) { export function DialogWrapper({ children }: { children: ComponentChildren }) {
const { note } = useNoteContext(); const { note } = useNoteContext();
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
const [ hasTint, setHasTint ] = useState(false); useNoteLabel(note, "color"); // to update color class
// Apply the tinted-dialog class only if the custom color CSS class specifies a hue
useEffect(() => {
if (!wrapperRef.current) return;
const customHue = getComputedStyle(wrapperRef.current).getPropertyValue("--custom-color-hue");
setHasTint(!!customHue);
}, [ note ]);
return ( return (
<div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""} ${hasTint ? "tinted-quick-edit-dialog" : ""}`}> <div ref={wrapperRef} class={`quick-edit-dialog-wrapper ${note?.getColorClass() ?? ""}`}>
{children} {children}
</div> </div>
) )

View File

@ -31,29 +31,29 @@ export default function AboutDialog() {
<tbody> <tbody>
<tr> <tr>
<th>{t("about.homepage")}</th> <th>{t("about.homepage")}</th>
<td><a className="tn-link external" href="https://github.com/TriliumNext/Trilium" style={forceWordBreak}>https://github.com/TriliumNext/Trilium</a></td> <td className="selectable-text"><a className="tn-link external" href="https://github.com/TriliumNext/Trilium" style={forceWordBreak}>https://github.com/TriliumNext/Trilium</a></td>
</tr> </tr>
<tr> <tr>
<th>{t("about.app_version")}</th> <th>{t("about.app_version")}</th>
<td className="app-version">{appInfo?.appVersion}</td> <td className="app-version selectable-text">{appInfo?.appVersion}</td>
</tr> </tr>
<tr> <tr>
<th>{t("about.db_version")}</th> <th>{t("about.db_version")}</th>
<td className="db-version">{appInfo?.dbVersion}</td> <td className="db-version selectable-text">{appInfo?.dbVersion}</td>
</tr> </tr>
<tr> <tr>
<th>{t("about.sync_version")}</th> <th>{t("about.sync_version")}</th>
<td className="sync-version">{appInfo?.syncVersion}</td> <td className="sync-version selectable-text">{appInfo?.syncVersion}</td>
</tr> </tr>
<tr> <tr>
<th>{t("about.build_date")}</th> <th>{t("about.build_date")}</th>
<td className="build-date"> <td className="build-date selectable-text">
{appInfo?.buildDate ? formatDateTime(appInfo.buildDate) : ""} {appInfo?.buildDate ? formatDateTime(appInfo.buildDate) : ""}
</td> </td>
</tr> </tr>
<tr> <tr>
<th>{t("about.build_revision")}</th> <th>{t("about.build_revision")}</th>
<td> <td className="selectable-text">
{appInfo?.buildRevision && <a className="tn-link build-revision external" href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" style={forceWordBreak}>{appInfo.buildRevision}</a>} {appInfo?.buildRevision && <a className="tn-link build-revision external" href={`https://github.com/TriliumNext/Trilium/commit/${appInfo.buildRevision}`} target="_blank" style={forceWordBreak}>{appInfo.buildRevision}</a>}
</td> </td>
</tr> </tr>
@ -76,8 +76,8 @@ function DirectoryLink({ directory, style }: { directory: string, style?: CSSPro
openService.openDirectory(directory); openService.openDirectory(directory);
}; };
return <a className="tn-link" href="#" onClick={onClick} style={style}>{directory}</a> return <a className="tn-link selectable-text" href="#" onClick={onClick} style={style}>{directory}</a>
} else { } else {
return <span style={style}>{directory}</span>; return <span className="selectable-text" style={style}>{directory}</span>;
} }
} }

View File

@ -208,7 +208,7 @@ function RevisionPreview({noteContent, revisionItem, showDiff, setShown, onRevis
} }
</div>)} </div>)}
</div> </div>
<div className="revision-content use-tn-links" style={{ overflow: "auto", wordBreak: "break-word" }}> <div className="revision-content use-tn-links selectable-text" style={{ overflow: "auto", wordBreak: "break-word" }}>
<RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/> <RevisionContent noteContent={noteContent} revisionItem={revisionItem} fullRevision={fullRevision} showDiff={showDiff}/>
</div> </div>
</> </>

View File

@ -795,7 +795,7 @@ export function useKeyboardShortcuts(scope: "code-detail" | "text-detail", conta
* and provides a way to switch to editing mode. * and provides a way to switch to editing mode.
*/ */
export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) { export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: NoteContext | undefined) {
const [isReadOnly, setIsReadOnly] = useState<boolean | undefined>(undefined); const [ isReadOnly, setIsReadOnly ] = useState<boolean | undefined>(undefined);
const enableEditing = useCallback(() => { const enableEditing = useCallback(() => {
if (noteContext?.viewScope) { if (noteContext?.viewScope) {
@ -810,7 +810,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
setIsReadOnly(readOnly); setIsReadOnly(readOnly);
}); });
} }
}, [note, noteContext]); }, [ note, noteContext, noteContext?.viewScope ]);
useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => { useTriliumEvent("readOnlyTemporarilyDisabled", ({noteContext: eventNoteContext}) => {
if (noteContext?.ntxId === eventNoteContext.ntxId) { if (noteContext?.ntxId === eventNoteContext.ntxId) {
@ -818,7 +818,7 @@ export function useIsNoteReadOnly(note: FNote | null | undefined, noteContext: N
} }
}); });
return {isReadOnly, enableEditing}; return { isReadOnly, enableEditing };
} }
async function isNoteReadOnly(note: FNote, noteContext: NoteContext) { async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {

View File

@ -17,24 +17,24 @@ export default function FilePropertiesTab({ note }: { note?: FNote | null }) {
return ( return (
<div className="file-properties-widget"> <div className="file-properties-widget">
{note && ( {note && (
<table class="file-table"> <table className="file-table">
<tbody> <tbody>
<tr> <tr>
<th class="text-nowrap">{t("file_properties.note_id")}:</th> <th className="text-nowrap">{t("file_properties.note_id")}:</th>
<td class="file-note-id">{note.noteId}</td> <td className="file-note-id selectable-text">{note.noteId}</td>
<th class="text-nowrap">{t("file_properties.original_file_name")}:</th> <th className="text-nowrap">{t("file_properties.original_file_name")}:</th>
<td class="file-filename">{originalFileName ?? "?"}</td> <td className="file-filename selectable-text">{originalFileName ?? "?"}</td>
</tr> </tr>
<tr> <tr>
<th class="text-nowrap">{t("file_properties.file_type")}:</th> <th className="text-nowrap">{t("file_properties.file_type")}:</th>
<td class="file-filetype">{note.mime}</td> <td className="file-filetype selectable-text">{note.mime}</td>
<th class="text-nowrap">{t("file_properties.file_size")}:</th> <th className="text-nowrap">{t("file_properties.file_size")}:</th>
<td class="file-filesize">{formatSize(blob?.contentLength ?? 0)}</td> <td className="file-filesize selectable-text">{formatSize(blob?.contentLength ?? 0)}</td>
</tr> </tr>
<tr> <tr>
<td colSpan={4}> <td colSpan={4}>
<div class="file-buttons"> <div className="file-buttons">
<Button <Button
icon="bx bx-download" icon="bx bx-download"
text={t("file_properties.download")} text={t("file_properties.download")}

View File

@ -23,17 +23,17 @@ export default function ImagePropertiesTab({ note, ntxId }: TabContext) {
<div style={{ display: "flex", justifyContent: "space-evenly", margin: "10px" }}> <div style={{ display: "flex", justifyContent: "space-evenly", margin: "10px" }}>
<span> <span>
<strong>{t("image_properties.original_file_name")}:</strong>{" "} <strong>{t("image_properties.original_file_name")}:</strong>{" "}
<span>{originalFileName ?? "?"}</span> <span className="selectable-text">{originalFileName ?? "?"}</span>
</span> </span>
<span> <span>
<strong>{t("image_properties.file_type")}:</strong>{" "} <strong>{t("image_properties.file_type")}:</strong>{" "}
<span>{note.mime}</span> <span className="selectable-text">{note.mime}</span>
</span> </span>
<span> <span>
<strong>{t("image_properties.file_size")}:</strong>{" "} <strong>{t("image_properties.file_size")}:</strong>{" "}
<span>{formatSize(blob?.contentLength)}</span> <span className="selectable-text">{formatSize(blob?.contentLength)}</span>
</span> </span>
</div> </div>

View File

@ -37,7 +37,7 @@ export default function InheritedAttributesTab({ note, componentId }: TabContext
return ( return (
<div className="inherited-attributes-widget"> <div className="inherited-attributes-widget">
<div className="inherited-attributes-container"> <div className="inherited-attributes-container selectable-text">
{inheritedAttributes?.length ? ( {inheritedAttributes?.length ? (
joinElements(inheritedAttributes.map(attribute => ( joinElements(inheritedAttributes.map(attribute => (
<InheritedAttribute <InheritedAttribute

View File

@ -39,21 +39,21 @@ export default function NoteInfoTab({ note }: TabContext) {
<> <>
<div className="note-info-item"> <div className="note-info-item">
<span>{t("note_info_widget.note_id")}:</span> <span>{t("note_info_widget.note_id")}:</span>
<span className="note-info-id">{note.noteId}</span> <span className="note-info-id selectable-text">{note.noteId}</span>
</div> </div>
<div className="note-info-item"> <div className="note-info-item">
<span>{t("note_info_widget.created")}:</span> <span>{t("note_info_widget.created")}:</span>
<span>{formatDateTime(metadata?.dateCreated)}</span> <span className="selectable-text">{formatDateTime(metadata?.dateCreated)}</span>
</div> </div>
<div className="note-info-item"> <div className="note-info-item">
<span>{t("note_info_widget.modified")}:</span> <span>{t("note_info_widget.modified")}:</span>
<span>{formatDateTime(metadata?.dateModified)}</span> <span className="selectable-text">{formatDateTime(metadata?.dateModified)}</span>
</div> </div>
<div className="note-info-item"> <div className="note-info-item">
<span>{t("note_info_widget.type")}:</span> <span>{t("note_info_widget.type")}:</span>
<span> <span>
<span className="note-info-type">{note.type}</span>{' '} <span className="note-info-type">{note.type}</span>{' '}
{note.mime && <span className="note-info-mime">({note.mime})</span>} {note.mime && <span className="note-info-mime selectable-text">({note.mime})</span>}
</span> </span>
</div> </div>
<div className="note-info-item"> <div className="note-info-item">
@ -77,7 +77,7 @@ export default function NoteInfoTab({ note }: TabContext) {
/> />
)} )}
<span className="note-sizes-wrapper"> <span className="note-sizes-wrapper selectable-text">
<span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span> <span className="note-size">{formatSize(noteSizeResponse?.noteSize)}</span>
{" "} {" "}
{subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 && {subtreeSizeResponse && subtreeSizeResponse.subTreeNoteCount > 1 &&

View File

@ -1,5 +1,6 @@
.note-detail-doc-content { .note-detail-doc-content {
padding: 15px; padding: 15px;
user-select: text;
} }
.note-detail-doc-content pre { .note-detail-doc-content pre {

View File

@ -1,3 +1,21 @@
.note-detail-empty {
container-type: size;
padding-top: 50px;
min-width: 350px;
}
.note-detail-empty > * {
margin-inline: auto;
max-width: 850px;
padding-inline: 50px;
}
@container (max-width: 600px) {
.note-detail-empty > * {
padding-inline: 20px;
}
}
.workspace-notes { .workspace-notes {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@ -14,7 +32,8 @@
.workspace-notes .workspace-note:hover { .workspace-notes .workspace-note:hover {
cursor: pointer; cursor: pointer;
border: 1px solid var(--main-border-color); background-color: var(--icon-button-hover-background);
border-radius: 8px;
} }
.note-detail-empty-results .aa-dropdown-menu { .note-detail-empty-results .aa-dropdown-menu {
@ -24,6 +43,11 @@
border-top: 0; border-top: 0;
} }
.empty-tab-search label {
margin-bottom: 8px;
color: var(--muted-text-color);
}
.empty-tab-search .note-autocomplete-input { .empty-tab-search .note-autocomplete-input {
border-bottom-left-radius: 0; border-bottom-left-radius: 0;
} }

View File

@ -10,16 +10,16 @@ import FNote from "../../entities/fnote";
import search from "../../services/search"; import search from "../../services/search";
import { TypeWidgetProps } from "./type_widget"; import { TypeWidgetProps } from "./type_widget";
export default function Empty({ }: TypeWidgetProps) { export default function Empty({ ntxId }: TypeWidgetProps) {
return ( return (
<> <>
<WorkspaceSwitcher /> <WorkspaceSwitcher />
<NoteSearch /> <NoteSearch ntxId={ntxId ?? null} />
</> </>
) )
} }
function NoteSearch() { function NoteSearch({ ntxId }: { ntxId: string | null }) {
const resultsContainerRef = useRef<HTMLDivElement>(null); const resultsContainerRef = useRef<HTMLDivElement>(null);
const autocompleteRef = useRef<HTMLInputElement>(null); const autocompleteRef = useRef<HTMLInputElement>(null);
@ -45,10 +45,9 @@ function NoteSearch() {
if (!suggestion?.notePath) { if (!suggestion?.notePath) {
return false; return false;
} }
const activeNoteContext = appContext.tabManager.getNoteContextById(ntxId) ?? appContext.tabManager.getActiveContext();
const activeContext = appContext.tabManager.getActiveContext(); if (activeNoteContext) {
if (activeContext) { activeNoteContext.setNote(suggestion.notePath);
activeContext.setNote(suggestion.notePath);
} }
}} }}
/> />

View File

@ -0,0 +1,10 @@
.note-detail-note-map {
&>div {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
}
}

View File

@ -1,6 +1,7 @@
import { TypeWidgetProps } from "./type_widget"; import { TypeWidgetProps } from "./type_widget";
import NoteMapEl from "../note_map/NoteMap"; import NoteMapEl from "../note_map/NoteMap";
import { useRef } from "preact/hooks"; import { useRef } from "preact/hooks";
import "./NoteMap.css";
export default function NoteMap({ note }: TypeWidgetProps) { export default function NoteMap({ note }: TypeWidgetProps) {
const containerRef = useRef<HTMLDivElement>(null); const containerRef = useRef<HTMLDivElement>(null);

View File

@ -104,7 +104,7 @@ export function BackupList({ backups }: { backups: DatabaseBackup[] }) {
backups.map(({ mtime, filePath }) => ( backups.map(({ mtime, filePath }) => (
<tr> <tr>
<td>{mtime ? formatDateTime(mtime) : "-"}</td> <td>{mtime ? formatDateTime(mtime) : "-"}</td>
<td>{filePath}</td> <td className="selectable-text">{filePath}</td>
</tr> </tr>
)) ))
) : ( ) : (

View File

@ -226,7 +226,7 @@ function CodeBlockPreview({ theme, wordWrap }: { theme: string, wordWrap: boolea
return ( return (
<div className="note-detail-readonly-text-content ck-content code-sample-wrapper"> <div className="note-detail-readonly-text-content ck-content code-sample-wrapper">
<pre className="hljs" style={{ marginBottom: 0 }}> <pre className="hljs selectable-text" style={{ marginBottom: 0 }}>
<code className="code-sample" style={codeStyle} dangerouslySetInnerHTML={getHtml(code)} /> <code className="code-sample" style={codeStyle} dangerouslySetInnerHTML={getHtml(code)} />
</pre> </pre>
</div> </div>

View File

@ -98,6 +98,14 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
editorApi: editorApiRef.current, editorApi: editorApiRef.current,
}); });
}, },
insertDateTimeToTextCommand() {
if (!editorApiRef.current) return;
const date = new Date();
const customDateTimeFormat = options.get("customDateTimeFormat");
const dateString = utils.formatDateTime(date, customDateTimeFormat);
addTextToEditor(dateString);
},
// Include note functionality note // Include note functionality note
addIncludeNoteToTextCommand() { addIncludeNoteToTextCommand() {
if (!editorApiRef.current) return; if (!editorApiRef.current) return;
@ -197,14 +205,6 @@ export default function EditableText({ note, parentComponent, ntxId, noteContext
}); });
} }
useTriliumEvent("insertDateTimeToText", ({ ntxId: eventNtxId }) => {
if (eventNtxId !== ntxId) return;
const date = new Date();
const customDateTimeFormat = options.get("customDateTimeFormat");
const dateString = utils.formatDateTime(date, customDateTimeFormat);
addTextToEditor(dateString);
});
useTriliumEvent("addTextToActiveEditor", ({ text }) => { useTriliumEvent("addTextToActiveEditor", ({ text }) => {
if (!noteContext?.isActive()) return; if (!noteContext?.isActive()) return;
addTextToEditor(text); addTextToEditor(text);

View File

@ -55,7 +55,7 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
<> <>
<RawHtmlBlock <RawHtmlBlock
containerRef={contentRef} containerRef={contentRef}
className={clsx("note-detail-readonly-text-content ck-content use-tn-links", codeBlockWordWrap && "word-wrap")} className={clsx("note-detail-readonly-text-content ck-content use-tn-links selectable-text", codeBlockWordWrap && "word-wrap")}
tabindex={100} tabindex={100}
dir={isRtl ? "rtl" : "ltr"} dir={isRtl ? "rtl" : "ltr"}
html={blob?.content} html={blob?.content}

View File

@ -0,0 +1,72 @@
import { test, expect } from "@playwright/test";
import App from "../support/app";
const TEXT_NOTE_TITLE = "Text notes";
const CODE_NOTE_TITLE = "Code notes";
test("Open the note in the correct split pane", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
// Open the first split.
await app.goToNoteInNewTab(TEXT_NOTE_TITLE);
const split1 = app.currentNoteSplit;
// Create a new split.
const splitButton = split1.locator("button.bx-dock-right");
await expect(splitButton).toBeVisible();
await splitButton.click();
// Search for "Code notes" in the empty area of the second split.
const split2 = app.currentNoteSplit.nth(1);;
await expect(split2).toBeVisible();
const autocomplete = split2.locator(".note-autocomplete");
await autocomplete.fill(CODE_NOTE_TITLE);
const resultsSelector = split2.locator(".note-detail-empty-results");
await expect(resultsSelector).toContainText(CODE_NOTE_TITLE);
//Focus on the first split.
const noteContent = split1.locator(".note-detail-editable-text-editor");
await expect(noteContent.locator("p")).toBeVisible();
await noteContent.focus();
// Click the search result in the second split.
await resultsSelector.locator(".aa-suggestion", { hasText: CODE_NOTE_TITLE })
.nth(1).click();
await expect(split2).toContainText(CODE_NOTE_TITLE);
});
test("Can directly focus the autocomplete input within the split", async ({ page, context }) => {
const app = new App(page, context);
await app.goto();
await app.closeAllTabs();
// Open the first split.
await app.goToNoteInNewTab(TEXT_NOTE_TITLE);
const split1 = app.currentNoteSplit;
// Create a new split.
const splitButton = split1.locator("button.bx-dock-right");
await expect(splitButton).toBeVisible();
await splitButton.click();
// Search for "Code notes" in the empty area of the second split.
const split2 = app.currentNoteSplit.nth(1);;
await expect(split2).toBeVisible();
// Focus the first split.
const noteContent = split1.locator(".note-detail-editable-text-editor");
await expect(noteContent.locator("p")).toBeVisible();
await noteContent.focus();
await noteContent.click();
// click the autocomplete input box of the second split
const autocomplete = split2.locator(".note-autocomplete");
await autocomplete.focus();
await autocomplete.click();
await page.waitForTimeout(100);
await expect(autocomplete).toBeFocused();
});

View File

@ -13,10 +13,6 @@ import BBlob from "./entities/bblob.js";
import BRecentNote from "./entities/brecent_note.js"; import BRecentNote from "./entities/brecent_note.js";
import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js"; import type AbstractBeccaEntity from "./entities/abstract_becca_entity.js";
interface AttachmentOpts {
includeContentLength?: boolean;
}
/** /**
* Becca is a backend cache of all notes, branches, and attributes. * Becca is a backend cache of all notes, branches, and attributes.
* There's a similar frontend cache Froca, and share cache Shaca. * There's a similar frontend cache Froca, and share cache Shaca.
@ -167,21 +163,18 @@ export default class Becca {
return revision; return revision;
} }
getAttachment(attachmentId: string, opts: AttachmentOpts = {}): BAttachment | null { getAttachment(attachmentId: string): BAttachment | null {
opts.includeContentLength = !!opts.includeContentLength; const query = /*sql*/`\
SELECT attachments.*, LENGTH(blobs.content) AS contentLength
const query = opts.includeContentLength FROM attachments
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength JOIN blobs USING (blobId)
FROM attachments WHERE attachmentId = ? AND isDeleted = 0`;
JOIN blobs USING (blobId)
WHERE attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [attachmentId]).map((row) => new BAttachment(row))[0]; return sql.getRows<AttachmentRow>(query, [attachmentId]).map((row) => new BAttachment(row))[0];
} }
getAttachmentOrThrow(attachmentId: string, opts: AttachmentOpts = {}): BAttachment { getAttachmentOrThrow(attachmentId: string): BAttachment {
const attachment = this.getAttachment(attachmentId, opts); const attachment = this.getAttachment(attachmentId);
if (!attachment) { if (!attachment) {
throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`); throw new NotFoundError(`Attachment '${attachmentId}' has not been found.`);
} }

View File

@ -61,10 +61,6 @@ interface ContentOpts {
forceFrontendReload?: boolean; forceFrontendReload?: boolean;
} }
interface AttachmentOpts {
includeContentLength?: boolean;
}
interface Relationship { interface Relationship {
parentNoteId: string; parentNoteId: string;
childNoteId: string; childNoteId: string;
@ -1102,31 +1098,23 @@ class BNote extends AbstractBeccaEntity<BNote> {
return sql.getRows<RevisionRow>("SELECT * FROM revisions WHERE noteId = ? ORDER BY revisions.utcDateCreated ASC", [this.noteId]).map((row) => new BRevision(row)); return sql.getRows<RevisionRow>("SELECT * FROM revisions WHERE noteId = ? ORDER BY revisions.utcDateCreated ASC", [this.noteId]).map((row) => new BRevision(row));
} }
getAttachments(opts: AttachmentOpts = {}) { getAttachments() {
opts.includeContentLength = !!opts.includeContentLength; const query = /*sql*/`\
// from testing, it looks like calculating length does not make a difference in performance even on large-ish DB SELECT attachments.*, LENGTH(blobs.content) AS contentLength
// given that we're always fetching attachments only for a specific note, we might just do it always FROM attachments
JOIN blobs USING (blobId)
const query = opts.includeContentLength WHERE ownerId = ? AND isDeleted = 0
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength ORDER BY position`;
FROM attachments
JOIN blobs USING (blobId)
WHERE ownerId = ? AND isDeleted = 0
ORDER BY position`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND isDeleted = 0 ORDER BY position`;
return sql.getRows<AttachmentRow>(query, [this.noteId]).map((row) => new BAttachment(row)); return sql.getRows<AttachmentRow>(query, [this.noteId]).map((row) => new BAttachment(row));
} }
getAttachmentById(attachmentId: string, opts: AttachmentOpts = {}) { getAttachmentById(attachmentId: string) {
opts.includeContentLength = !!opts.includeContentLength; const query = /*sql*/`\
SELECT attachments.*, LENGTH(blobs.content) AS contentLength
const query = opts.includeContentLength FROM attachments
? /*sql*/`SELECT attachments.*, LENGTH(blobs.content) AS contentLength JOIN blobs USING (blobId)
FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
JOIN blobs USING (blobId)
WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`
: /*sql*/`SELECT * FROM attachments WHERE ownerId = ? AND attachmentId = ? AND isDeleted = 0`;
return sql.getRows<AttachmentRow>(query, [this.noteId, attachmentId]).map((row) => new BAttachment(row))[0]; return sql.getRows<AttachmentRow>(query, [this.noteId, attachmentId]).map((row) => new BAttachment(row))[0];
} }

View File

@ -92,7 +92,7 @@ function getAndCheckNote(noteId: string) {
} }
function getAndCheckAttachment(attachmentId: string) { function getAndCheckAttachment(attachmentId: string) {
const attachment = becca.getAttachment(attachmentId, { includeContentLength: true }); const attachment = becca.getAttachment(attachmentId);
if (attachment) { if (attachment) {
return attachment; return attachment;

View File

@ -185,7 +185,7 @@ function register(router: Router) {
eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => { eu.route(router, "get", "/etapi/notes/:noteId/attachments", (req, res, next) => {
const note = eu.getAndCheckNote(req.params.noteId); const note = eu.getAndCheckNote(req.params.noteId);
const attachments = note.getAttachments({ includeContentLength: true }); const attachments = note.getAttachments();
res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment))); res.json(attachments.map((attachment) => mappers.mapAttachmentToPojo(attachment)));
}); });

View File

@ -14,13 +14,13 @@ function getAttachmentBlob(req: Request) {
function getAttachments(req: Request) { function getAttachments(req: Request) {
const note = becca.getNoteOrThrow(req.params.noteId); const note = becca.getNoteOrThrow(req.params.noteId);
return note.getAttachments({ includeContentLength: true }); return note.getAttachments();
} }
function getAttachment(req: Request) { function getAttachment(req: Request) {
const { attachmentId } = req.params; const { attachmentId } = req.params;
return becca.getAttachmentOrThrow(attachmentId, { includeContentLength: true }); return becca.getAttachmentOrThrow(attachmentId);
} }
function getAllAttachments(req: Request) { function getAllAttachments(req: Request) {
@ -28,7 +28,7 @@ function getAllAttachments(req: Request) {
// one particular attachment is requested, but return all note's attachments // one particular attachment is requested, but return all note's attachments
const attachment = becca.getAttachmentOrThrow(attachmentId); const attachment = becca.getAttachmentOrThrow(attachmentId);
return attachment.getNote()?.getAttachments({ includeContentLength: true }) || []; return attachment.getNote()?.getAttachments() || [];
} }
function saveAttachment(req: Request) { function saveAttachment(req: Request) {

View File

@ -764,7 +764,7 @@ function updateNoteData(noteId: string, content: string, attachments: Attachment
note.setContent(newContent, { forceFrontendReload }); note.setContent(newContent, { forceFrontendReload });
if (attachments?.length > 0) { if (attachments?.length > 0) {
const existingAttachmentsByTitle = toMap(note.getAttachments({ includeContentLength: false }), "title"); const existingAttachmentsByTitle = toMap(note.getAttachments(), "title");
for (const { attachmentId, role, mime, title, position, content } of attachments) { for (const { attachmentId, role, mime, title, position, content } of attachments) {
const existingAttachment = existingAttachmentsByTitle.get(title); const existingAttachment = existingAttachmentsByTitle.get(title);

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

@ -85,5 +85,8 @@
"title_arm64": "ARM 기반 리눅스", "title_arm64": "ARM 기반 리눅스",
"description_x64": "대부분의 리눅스 배포판에서 x86_64 아키텍처와 호환됩니다.", "description_x64": "대부분의 리눅스 배포판에서 x86_64 아키텍처와 호환됩니다.",
"description_arm64": "ARM 기반 리눅스 배포판에서 aarch64 아키텍처와 호환됩니다." "description_arm64": "ARM 기반 리눅스 배포판에서 aarch64 아키텍처와 호환됩니다."
},
"note_types": {
"text_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,40 +109,43 @@ 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">
<div id="left-pane"> <% if (!note.isLabelTruthy("shareTemplateNoLeftPanel")) { %>
<div id="navigation"> <div id="left-pane">
<div id="site-header"> <div id="navigation">
<a href="<%= shareRootLink %>"> <div id="site-header">
<img src="<%= logoUrl %>" width="<%= logoWidth %>" height="<%= logoHeight %>" alt="Logo" /> <a href="<%= shareRootLink %>">
<%= subRoot.note.title %> <img src="<%= logoUrl %>" width="<%= logoWidth %>" height="<%= logoHeight %>" alt="Logo" />
</a> <%= subRoot.note.title %>
<div class="theme-selection"> </a>
<span id="sitetheme"><%= t("share_theme.site-theme") %></span> <div class="theme-selection">
<label class="switch"> <span id="sitetheme"><%= t("share_theme.site-theme") %></span>
<input type="checkbox" aria-labelledby="sitetheme"> <label class="switch">
<span class="slider"></span> <input type="checkbox" aria-labelledby="sitetheme">
<svg class="dark-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z"></path></svg> <span class="slider"></span>
<svg class="light-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z"></path></svg> <svg class="dark-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M20.742 13.045a8.088 8.088 0 0 1-2.077.271c-2.135 0-4.14-.83-5.646-2.336a8.025 8.025 0 0 1-2.064-7.723A1 1 0 0 0 9.73 2.034a10.014 10.014 0 0 0-4.489 2.582c-3.898 3.898-3.898 10.243 0 14.143a9.937 9.937 0 0 0 7.072 2.93 9.93 9.93 0 0 0 7.07-2.929 10.007 10.007 0 0 0 2.583-4.491 1.001 1.001 0 0 0-1.224-1.224zm-2.772 4.301a7.947 7.947 0 0 1-5.656 2.343 7.953 7.953 0 0 1-5.658-2.344c-3.118-3.119-3.118-8.195 0-11.314a7.923 7.923 0 0 1 2.06-1.483 10.027 10.027 0 0 0 2.89 7.848 9.972 9.972 0 0 0 7.848 2.891 8.036 8.036 0 0 1-1.484 2.059z"></path></svg>
</label> <svg class="light-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M6.993 12c0 2.761 2.246 5.007 5.007 5.007s5.007-2.246 5.007-5.007S14.761 6.993 12 6.993 6.993 9.239 6.993 12zM12 8.993c1.658 0 3.007 1.349 3.007 3.007S13.658 15.007 12 15.007 8.993 13.658 8.993 12 10.342 8.993 12 8.993zM10.998 19h2v3h-2zm0-17h2v3h-2zm-9 9h3v2h-3zm17 0h3v2h-3zM4.219 18.363l2.12-2.122 1.415 1.414-2.12 2.122zM16.24 6.344l2.122-2.122 1.414 1.414-2.122 2.122zM6.342 7.759 4.22 5.637l1.415-1.414 2.12 2.122zm13.434 10.605-1.414 1.414-2.122-2.122 1.414-1.414z"></path></svg>
<script> </label>
const el = document.querySelector(".theme-selection input"); <script>
el.checked = (glob.theme === "dark"); const el = document.querySelector(".theme-selection input");
</script> el.checked = (glob.theme === "dark");
</div> </script>
<% if (hasTree) { %>
<div class="search-item">
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M10 18a7.952 7.952 0 0 0 4.897-1.688l4.396 4.396 1.414-1.414-4.396-4.396A7.952 7.952 0 0 0 18 10c0-4.411-3.589-8-8-8s-8 3.589-8 8 3.589 8 8 8zm0-14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6z"></path></svg>
<input type="text" class="search-input" placeholder="<%= t("share_theme.search_placeholder") %>">
</div> </div>
<% } %> <% if (hasTree) { %>
<div class="search-item">
<svg class="search-icon" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="currentColor"><path d="M10 18a7.952 7.952 0 0 0 4.897-1.688l4.396 4.396 1.414-1.414-4.396-4.396A7.952 7.952 0 0 0 18 10c0-4.411-3.589-8-8-8s-8 3.589-8 8 3.589 8 8 8zm0-14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6z"></path></svg>
<input type="text" class="search-input" placeholder="<%= t("share_theme.search_placeholder") %>">
</div>
<% } %>
</div>
<% if (hasTree) { %>
<nav id="menu">
<%- include("tree_item", {note: subRoot.note, activeNote: note, subRoot: subRoot, ancestors}) %>
</nav>
<% } %>
</div> </div>
<% if (hasTree) { %>
<nav id="menu">
<%- include("tree_item", {note: subRoot.note, activeNote: note, subRoot: subRoot, ancestors}) %>
</nav>
<% } %>
</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>