mirror of
https://github.com/zadam/trilium.git
synced 2025-12-05 06:54:23 +01:00
Compare commits
44 Commits
c18be653d4
...
7938e38d7f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7938e38d7f | ||
|
|
b9e257a39d | ||
|
|
e7eaa5fd58 | ||
|
|
c9aa992e73 | ||
|
|
f325930f68 | ||
|
|
1346ffb77e | ||
|
|
3378746530 | ||
|
|
ce2d94f04e | ||
|
|
b3c2a1e6c5 | ||
|
|
dbf63787da | ||
|
|
88a7ebef69 | ||
|
|
a716151dd9 | ||
|
|
7462f1b7a5 | ||
|
|
ec76b9dc5c | ||
|
|
79cd96ade9 | ||
|
|
a5b84406be | ||
|
|
8c1a04c4b2 | ||
|
|
ee81037173 | ||
|
|
453349be26 | ||
|
|
81a9e06b23 | ||
|
|
7d8af0f252 | ||
|
|
a68cd7526b | ||
|
|
470ca3b6dc | ||
|
|
e8bae61afc | ||
|
|
c1f663a200 | ||
|
|
22b2e21df0 | ||
|
|
5f19710791 | ||
|
|
d3f3ff4eab | ||
|
|
5af7425cae | ||
|
|
fe10c9f8c8 | ||
|
|
cd2a085d00 | ||
|
|
3c61626370 | ||
|
|
351fe5848f | ||
|
|
ca7bbefbdc | ||
|
|
7094f71e32 | ||
|
|
88b5e9db87 | ||
|
|
53a8f6b4c0 | ||
|
|
9ae1a55896 | ||
|
|
4d1a91baa6 | ||
|
|
1898efa282 | ||
|
|
648ab4d736 | ||
|
|
407cac588a | ||
|
|
210dcfb989 | ||
|
|
5d5fd2079a |
2
.gitignore
vendored
2
.gitignore
vendored
@ -8,6 +8,7 @@ out-tsc
|
|||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
node_modules
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
|
||||||
# IDEs and editors
|
# IDEs and editors
|
||||||
/.idea
|
/.idea
|
||||||
@ -18,6 +19,7 @@ node_modules
|
|||||||
*.launch
|
*.launch
|
||||||
.settings/
|
.settings/
|
||||||
*.sublime-workspace
|
*.sublime-workspace
|
||||||
|
.devcontainer
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
/.sass-cache
|
/.sass-cache
|
||||||
|
|||||||
19
README.md
19
README.md
@ -146,6 +146,21 @@ Here's the language coverage we have so far:
|
|||||||
|
|
||||||
### Code
|
### Code
|
||||||
|
|
||||||
|
General (OS / docker / podman, etc.) dependencies:
|
||||||
|
|
||||||
|
Debian
|
||||||
|
```
|
||||||
|
apt update
|
||||||
|
apt install -y build-essential python3 make g++ libsqlite3-dev
|
||||||
|
corepack enable
|
||||||
|
```
|
||||||
|
|
||||||
|
Alpine
|
||||||
|
```
|
||||||
|
apk add --no-cache build-base python3 python3-dev sqlite-dev
|
||||||
|
corepack enable
|
||||||
|
```
|
||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
Download the repository, install dependencies using `pnpm` and then run the server (available at http://localhost:8080):
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/TriliumNext/Trilium.git
|
git clone https://github.com/TriliumNext/Trilium.git
|
||||||
@ -154,6 +169,10 @@ pnpm install
|
|||||||
pnpm run server:start
|
pnpm run server:start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> If you faced with some problems, try to delete all `node_modules` and `.pnpm-store` folders, not only from the root, from every directory, like `apps/{app_name}/node_modules`and `/packages/{package_name}/node_modules` and then reinstall it by the `pnpm install`.
|
||||||
|
|
||||||
|
Share styles not compiling by default, if you see share page without styles, make `pnpm run server:build` and then run development server.
|
||||||
|
|
||||||
### Documentation
|
### Documentation
|
||||||
|
|
||||||
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
Download the repository, install dependencies using `pnpm` and then run the environment required to edit the documentation:
|
||||||
|
|||||||
@ -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: {
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -25,7 +25,8 @@
|
|||||||
--bs-body-font-weight: var(--main-font-weight) !important;
|
--bs-body-font-weight: var(--main-font-weight) !important;
|
||||||
--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 {
|
||||||
|
|||||||
@ -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%);
|
||||||
|
|||||||
@ -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%);
|
||||||
|
|||||||
@ -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,30 +389,16 @@ 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;
|
||||||
display: block !important;
|
display: block !important;
|
||||||
|
|
||||||
&.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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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 */
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1662,7 +1662,7 @@
|
|||||||
},
|
},
|
||||||
"editable-text": {
|
"editable-text": {
|
||||||
"auto-detect-language": "自动检测",
|
"auto-detect-language": "自动检测",
|
||||||
"keeps-crashing": "编辑组件时崩溃。请尝试重启 Trilium。如果问题仍然存在,请考虑提交错误报告。"
|
"keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在,请考虑提交错误报告。"
|
||||||
},
|
},
|
||||||
"highlighting": {
|
"highlighting": {
|
||||||
"title": "代码块",
|
"title": "代码块",
|
||||||
|
|||||||
@ -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": {
|
||||||
|
|||||||
@ -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 });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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")}
|
) : (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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());
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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")}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 &&
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
10
apps/client/src/widgets/type_widgets/NoteMap.css
Normal file
10
apps/client/src/widgets/type_widgets/NoteMap.css
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.note-detail-note-map {
|
||||||
|
&>div {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
72
apps/server-e2e/src/layout/split_pane.spec.ts
Normal file
72
apps/server-e2e/src/layout/split_pane.spec.ts
Normal 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();
|
||||||
|
});
|
||||||
@ -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.`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -40,6 +40,13 @@ interface Subroot {
|
|||||||
|
|
||||||
type GetNoteFunction = (id: string) => SNote | BNote | null;
|
type GetNoteFunction = (id: string) => SNote | BNote | null;
|
||||||
|
|
||||||
|
function addContentAccessQuery(note: SNote | BNote, secondEl?:boolean) {
|
||||||
|
if (!(note instanceof BNote) && note.contentAccessor && note.contentAccessor?.type === "query") {
|
||||||
|
return secondEl ? `&cat=${note.contentAccessor.getToken()}` : `?cat=${note.contentAccessor.getToken()}`;
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot {
|
function getSharedSubTreeRoot(note: SNote | BNote | undefined): Subroot {
|
||||||
if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
if (!note || note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||||
// share root itself is not shared
|
// share root itself is not shared
|
||||||
@ -111,7 +118,7 @@ export function renderNoteContent(note: SNote) {
|
|||||||
cssToLoad.push(`assets/scripts.css`);
|
cssToLoad.push(`assets/scripts.css`);
|
||||||
}
|
}
|
||||||
for (const cssRelation of note.getRelations("shareCss")) {
|
for (const cssRelation of note.getRelations("shareCss")) {
|
||||||
cssToLoad.push(`api/notes/${cssRelation.value}/download`);
|
cssToLoad.push(`api/notes/${cssRelation.value}/download${addContentAccessQuery(note)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine JS to load.
|
// Determine JS to load.
|
||||||
@ -119,11 +126,11 @@ export function renderNoteContent(note: SNote) {
|
|||||||
"assets/scripts.js"
|
"assets/scripts.js"
|
||||||
];
|
];
|
||||||
for (const jsRelation of note.getRelations("shareJs")) {
|
for (const jsRelation of note.getRelations("shareJs")) {
|
||||||
jsToLoad.push(`api/notes/${jsRelation.value}/download`);
|
jsToLoad.push(`api/notes/${jsRelation.value}/download${addContentAccessQuery(note)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const customLogoId = note.getRelation("shareLogo")?.value;
|
const customLogoId = note.getRelation("shareLogo")?.value;
|
||||||
const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png` : `../${assetUrlFragment}/images/icon-color.svg`;
|
const logoUrl = customLogoId ? `api/images/${customLogoId}/image.png${addContentAccessQuery(note)}` : `../${assetUrlFragment}/images/icon-color.svg`;
|
||||||
|
|
||||||
return renderNoteContentInternal(note, {
|
return renderNoteContentInternal(note, {
|
||||||
subRoot,
|
subRoot,
|
||||||
@ -133,7 +140,7 @@ export function renderNoteContent(note: SNote) {
|
|||||||
logoUrl,
|
logoUrl,
|
||||||
ancestors,
|
ancestors,
|
||||||
isStatic: false,
|
isStatic: false,
|
||||||
faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download` : `../favicon.ico`
|
faviconUrl: note.hasRelation("shareFavicon") ? `api/notes/${note.getRelationValue("shareFavicon")}/download${addContentAccessQuery(note)}` : `../favicon.ico`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +165,7 @@ function renderNoteContentInternal(note: SNote | BNote, renderArgs: RenderArgs)
|
|||||||
isEmpty,
|
isEmpty,
|
||||||
assetPath: shareAdjustedAssetPath,
|
assetPath: shareAdjustedAssetPath,
|
||||||
assetUrlFragment,
|
assetUrlFragment,
|
||||||
|
addContentAccessQuery: (second: boolean | undefined) => addContentAccessQuery(note, second),
|
||||||
showLoginInShareTheme,
|
showLoginInShareTheme,
|
||||||
t,
|
t,
|
||||||
isDev,
|
isDev,
|
||||||
@ -325,7 +333,7 @@ function renderText(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (href?.startsWith("#")) {
|
if (href?.startsWith("#")) {
|
||||||
handleAttachmentLink(linkEl, href, getNote, getAttachment);
|
handleAttachmentLink(linkEl, href, getNote, getAttachment, note);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,7 +357,7 @@ function renderText(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null) {
|
function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNoteFunction, getAttachment: (id: string) => BAttachment | SAttachment | null, note: SNote | BNote) {
|
||||||
const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g;
|
const linkRegExp = /attachmentId=([a-zA-Z0-9_]+)/g;
|
||||||
let attachmentMatch;
|
let attachmentMatch;
|
||||||
if ((attachmentMatch = linkRegExp.exec(href))) {
|
if ((attachmentMatch = linkRegExp.exec(href))) {
|
||||||
@ -357,7 +365,7 @@ function handleAttachmentLink(linkEl: HTMLElement, href: string, getNote: GetNot
|
|||||||
const attachment = getAttachment(attachmentId);
|
const attachment = getAttachment(attachmentId);
|
||||||
|
|
||||||
if (attachment) {
|
if (attachment) {
|
||||||
linkEl.setAttribute("href", `api/attachments/${attachmentId}/download`);
|
linkEl.setAttribute("href", `api/attachments/${attachmentId}/download${addContentAccessQuery(note)}`);
|
||||||
linkEl.classList.add(`attachment-link`);
|
linkEl.classList.add(`attachment-link`);
|
||||||
linkEl.classList.add(`role-${attachment.role}`);
|
linkEl.classList.add(`role-${attachment.role}`);
|
||||||
linkEl.childNodes.length = 0;
|
linkEl.childNodes.length = 0;
|
||||||
@ -430,7 +438,7 @@ function renderMermaid(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.content = `
|
result.content = `
|
||||||
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">
|
<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}">
|
||||||
<hr>
|
<hr>
|
||||||
<details>
|
<details>
|
||||||
<summary>Chart source</summary>
|
<summary>Chart source</summary>
|
||||||
@ -439,14 +447,14 @@ function renderMermaid(result: Result, note: SNote | BNote) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderImage(result: Result, note: SNote | BNote) {
|
function renderImage(result: Result, note: SNote | BNote) {
|
||||||
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}">`;
|
result.content = `<img src="api/images/${note.noteId}/${note.encodedTitle}?${note.utcDateModified}${addContentAccessQuery(note, true)}">`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderFile(note: SNote | BNote, result: Result) {
|
function renderFile(note: SNote | BNote, result: Result) {
|
||||||
if (note.mime === "application/pdf") {
|
if (note.mime === "application/pdf") {
|
||||||
result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view"></iframe>`;
|
result.content = `<iframe class="pdf-view" src="api/notes/${note.noteId}/view${addContentAccessQuery(note)}"></iframe>`;
|
||||||
} else {
|
} else {
|
||||||
result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download'">Download file</button>`;
|
result.content = `<button type="button" onclick="location.href='api/notes/${note.noteId}/download${addContentAccessQuery(note)}'">Download file</button>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -60,6 +60,20 @@ function checkNoteAccess(noteId: string, req: Request, res: Response) {
|
|||||||
const header = req.header("Authorization");
|
const header = req.header("Authorization");
|
||||||
|
|
||||||
if (!header?.startsWith("Basic ")) {
|
if (!header?.startsWith("Basic ")) {
|
||||||
|
if (req.path.startsWith("/share/api") && note.contentAccessor) {
|
||||||
|
let contentAccessToken = ""
|
||||||
|
if (note.contentAccessor.type === "cookie") contentAccessToken += req.cookies["trilium.cat"] || ""
|
||||||
|
else if (note.contentAccessor.type === "query") contentAccessToken += req.query['cat'] || ""
|
||||||
|
|
||||||
|
if (contentAccessToken){
|
||||||
|
if (note.contentAccessor.isTokenValid(contentAccessToken)){
|
||||||
|
return note
|
||||||
|
}
|
||||||
|
res.status(401).send("Access is expired. Return back and update the page.");
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,9 +138,14 @@ function register(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.isLabelTruthy("shareExclude")) {
|
||||||
|
res.status(404);
|
||||||
|
render404(res);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!checkNoteAccess(note.noteId, req, res)) {
|
if (!checkNoteAccess(note.noteId, req, res)) {
|
||||||
requestCredentials(res);
|
requestCredentials(res);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,6 +157,10 @@ function register(router: Router) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (note.contentAccessor && note.contentAccessor.type === "cookie") {
|
||||||
|
res.cookie('trilium.cat', note.contentAccessor.getToken(), { maxAge: note.contentAccessor.getTokenExpiration() * 1000, httpOnly: true })
|
||||||
|
}
|
||||||
|
|
||||||
res.send(renderNoteContent(note));
|
res.send(renderNoteContent(note));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +186,9 @@ function register(router: Router) {
|
|||||||
const { shareId } = req.params;
|
const { shareId } = req.params;
|
||||||
|
|
||||||
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
const note = shaca.aliasToNote[shareId] || shaca.notes[shareId];
|
||||||
|
if (note){
|
||||||
|
note.initContentAccessor()
|
||||||
|
}
|
||||||
|
|
||||||
renderNote(note, req, res);
|
renderNote(note, req, res);
|
||||||
});
|
});
|
||||||
|
|||||||
81
apps/server/src/share/shaca/entities/content_accessor.ts
Normal file
81
apps/server/src/share/shaca/entities/content_accessor.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
|
import SNote from "./snote";
|
||||||
|
import utils from "../../../services/utils";
|
||||||
|
|
||||||
|
const DefaultAccessTimeoutSec = 10 * 60; // 10 minutes
|
||||||
|
|
||||||
|
export class ContentAccessor {
|
||||||
|
note: SNote;
|
||||||
|
token: string;
|
||||||
|
timestamp: number;
|
||||||
|
type: string;
|
||||||
|
timeout: number;
|
||||||
|
key: Buffer;
|
||||||
|
|
||||||
|
constructor(note: SNote) {
|
||||||
|
this.note = note;
|
||||||
|
this.key = crypto.randomBytes(32);
|
||||||
|
this.token = "";
|
||||||
|
this.timestamp = 0;
|
||||||
|
this.timeout = Number(this.note.getAttributeValue("label", "shareAccessTokenTimeout") || DefaultAccessTimeoutSec)
|
||||||
|
|
||||||
|
switch (this.note.getAttributeValue("label", "shareContentAccess")) {
|
||||||
|
case "basic": this.type = "basic"; break
|
||||||
|
case "query": this.type = "query"; break
|
||||||
|
default: this.type = "cookie"; break
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
__encrypt(text: string) {
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv);
|
||||||
|
let encrypted = cipher.update(text, 'utf8', 'hex');
|
||||||
|
encrypted += cipher.final('hex');
|
||||||
|
return iv.toString('hex') + encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
__decrypt(encryptedText: string) {
|
||||||
|
try {
|
||||||
|
const iv = Buffer.from(encryptedText.slice(0, 32), 'hex');
|
||||||
|
const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv);
|
||||||
|
let decrypted = decipher.update(encryptedText.slice(32), 'hex', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
return decrypted;
|
||||||
|
} catch {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
__compare(originalText: string, encryptedText: string) {
|
||||||
|
return originalText === this.__decrypt(encryptedText)
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
if (new Date().getTime() < this.timestamp + this.getTimeout() * 1000) return
|
||||||
|
this.token = utils.randomString(36);
|
||||||
|
this.key = crypto.randomBytes(32);
|
||||||
|
this.timestamp = new Date().getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
isTokenValid(encToken: string) {
|
||||||
|
return this.__compare(this.token, encToken) && new Date().getTime() < this.timestamp + this.getTimeout() * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
getToken() {
|
||||||
|
return this.__encrypt(this.token);
|
||||||
|
}
|
||||||
|
|
||||||
|
getTokenExpiration() {
|
||||||
|
return (this.timestamp + (this.timeout * 1000) - new Date().getTime()) /1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTimeout() {
|
||||||
|
return this.timeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
getContentAccessType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -10,6 +10,7 @@ import type SAttribute from "./sattribute.js";
|
|||||||
import type SBranch from "./sbranch.js";
|
import type SBranch from "./sbranch.js";
|
||||||
import type { SNoteRow } from "./rows.js";
|
import type { SNoteRow } from "./rows.js";
|
||||||
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
|
import { NOTE_TYPE_ICONS } from "../../../becca/entities/bnote.js";
|
||||||
|
import { ContentAccessor } from "./content_accessor.js";
|
||||||
|
|
||||||
const LABEL = "label";
|
const LABEL = "label";
|
||||||
const RELATION = "relation";
|
const RELATION = "relation";
|
||||||
@ -33,6 +34,7 @@ class SNote extends AbstractShacaEntity {
|
|||||||
private __inheritableAttributeCache: SAttribute[] | null;
|
private __inheritableAttributeCache: SAttribute[] | null;
|
||||||
targetRelations: SAttribute[];
|
targetRelations: SAttribute[];
|
||||||
attachments: SAttachment[];
|
attachments: SAttachment[];
|
||||||
|
contentAccessor: ContentAccessor | undefined;
|
||||||
|
|
||||||
constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) {
|
constructor([noteId, title, type, mime, blobId, utcDateModified, isProtected]: SNoteRow) {
|
||||||
super();
|
super();
|
||||||
@ -59,6 +61,15 @@ class SNote extends AbstractShacaEntity {
|
|||||||
this.shaca.notes[this.noteId] = this;
|
this.shaca.notes[this.noteId] = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initContentAccessor(){
|
||||||
|
if (!this.contentAccessor && this.getCredentials().length > 0) {
|
||||||
|
this.contentAccessor = new ContentAccessor(this);
|
||||||
|
}
|
||||||
|
if (this.contentAccessor) {
|
||||||
|
this.contentAccessor.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getParentBranches() {
|
getParentBranches() {
|
||||||
return this.parentBranches;
|
return this.parentBranches;
|
||||||
}
|
}
|
||||||
@ -72,7 +83,7 @@ class SNote extends AbstractShacaEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVisibleChildBranches() {
|
getVisibleChildBranches() {
|
||||||
return this.getChildBranches().filter((branch) => !branch.isHidden && !branch.getNote().isLabelTruthy("shareHiddenFromTree"));
|
return this.getChildBranches().filter((branch) => !branch.isHidden && !branch.getNote().isLabelTruthy("shareHiddenFromTree") && !branch.getNote().isLabelTruthy("shareExclude"));
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentNotes() {
|
getParentNotes() {
|
||||||
@ -80,7 +91,7 @@ class SNote extends AbstractShacaEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getChildNotes() {
|
getChildNotes() {
|
||||||
return this.children;
|
return this.children.filter((note) => !note.isLabelTruthy("shareExclude"));
|
||||||
}
|
}
|
||||||
|
|
||||||
getVisibleChildNotes() {
|
getVisibleChildNotes() {
|
||||||
|
|||||||
@ -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": "텍스트 노트"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,7 +131,7 @@ To do so, create a shared text note and apply the `shareIndex` label. When viewe
|
|||||||
|
|
||||||
## Attribute reference
|
## Attribute reference
|
||||||
|
|
||||||
<table class="ck-table-resized"><colgroup><col style="width:18.38%;"><col style="width:81.62%;"></colgroup><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>#shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>#shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>#shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>#shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>#shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>#shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>#shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>#shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>#shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>#shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>#shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table>
|
<table class="ck-table-resized"><colgroup><col style="width:18.38%;"><col style="width:81.62%;"></colgroup><thead><tr><th>Attribute</th><th>Description</th></tr></thead><tbody><tr><td><code>#shareHiddenFromTree</code></td><td>this note is hidden from left navigation tree, but still accessible with its URL</td></tr><tr><td><code>#shareTemplateNoPrevNext</code></td><td>hide bottom page navigation prev and next page.</td></tr><tr><td><code>#shareTemplateNoLeftPanel</code></td><td>hide left panel fully.</td></tr><tr><td><code>#shareExclude</code></td><td>this note will be excluded from share, not accessible via direct URL (implemented to hide scripts from share)</td></tr><tr><td><code>#shareContentAccess</code></td><td>method for attachments authorization in case when note protected with login and password (#shareCredentials). Could be cookie (the cookie will be provided when page loads) / query (every url will be updated with token) / basic (only basic header authorization)). By default for browser used cookie.</td></tr><tr><td><code>#shareAccessTokenTimeout</code></td><td>token expiration timeout in seconds, by default 10 minutes. While token not expired user could download attachment, after that he will get message `Access is expired. Return back and update the page.`</td></tr><tr><td><code>#shareExternalLink</code></td><td>note will act as a link to an external website in the share tree</td></tr><tr><td><code>#shareAlias</code></td><td>define an alias using which the note will be available under <code>https://your_trilium_host/share/[your_alias]</code></td></tr><tr><td><code>#shareOmitDefaultCss</code></td><td>default share page CSS will be omitted. Use when you make extensive styling changes.</td></tr><tr><td><code>#shareRoot</code></td><td>marks note which is served on /share root.</td></tr><tr><td><code>#shareDescription</code></td><td>define text to be added to the HTML meta tag for description</td></tr><tr><td><code>#shareRaw</code></td><td>Note will be served in its raw format, without HTML wrapper. See also <a class="reference-link" href="Sharing/Serving%20directly%20the%20content%20o.md">Serving directly the content of a note</a> for an alternative method without setting an attribute.</td></tr><tr><td><code>#shareDisallowRobotIndexing</code></td><td><p>Indicates to web crawlers that the page should not be indexed of this note by:</p><ul><li data-list-item-id="e6baa9f60bf59d085fd31aa2cce07a0e7">Setting the <code>X-Robots-Tag: noindex</code> HTTP header.</li><li data-list-item-id="ec0d067db136ef9794e4f1033405880b7">Setting the <code>noindex, follow</code> meta tag.</li></ul></td></tr><tr><td><code>#shareCredentials</code></td><td>require credentials to access this shared note. Value is expected to be in format <code>username:password</code>. Don't forget to make this inheritable to apply to child-notes/images.</td></tr><tr><td><code>#shareIndex</code></td><td>Note with this label will list all roots of shared notes.</td></tr><tr><td><code>#shareHtmlLocation</code></td><td>defines where custom HTML injected via <code>~shareHtml</code> relation should be placed. Applied to the HTML snippet note itself. Format: <code>location:position</code> where location is <code>head</code>, <code>body</code>, or <code>content</code> and position is <code>start</code> or <code>end</code>. Defaults to <code>content:end</code>.</td></tr></tbody></table>
|
||||||
|
|
||||||
### Customizing logo
|
### Customizing logo
|
||||||
|
|
||||||
|
|||||||
@ -50,7 +50,7 @@
|
|||||||
let openGraphImage = subRoot.note.getLabelValue("shareOpenGraphImage");
|
let openGraphImage = subRoot.note.getLabelValue("shareOpenGraphImage");
|
||||||
// Relation takes priority and requires some altering
|
// Relation takes priority and requires some altering
|
||||||
if (subRoot.note.hasRelation("shareOpenGraphImage")) {
|
if (subRoot.note.hasRelation("shareOpenGraphImage")) {
|
||||||
openGraphImage = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png`;
|
openGraphImage = `api/images/${subRoot.note.getRelation("shareOpenGraphImage").value}/image.png${addContentAccessQuery()}`;
|
||||||
}
|
}
|
||||||
%>
|
%>
|
||||||
<title><%= pageTitle %></title>
|
<title><%= pageTitle %></title>
|
||||||
@ -109,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>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user