Compare commits

...

47 Commits

Author SHA1 Message Date
SngAbc
df3e8f25b2
Merge a81e8adde78e4d608b40d417fdafd2ca3005bdcc into b9e257a39d8f658dec38c6a276e7550939db2c7e 2025-11-29 21:01:35 +02:00
Elian Doran
b9e257a39d
refactor(client): redundant interface
Some checks are pending
Checks / main (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Dev / Test development (push) Waiting to run
Dev / Build Docker image (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile) (push) Blocked by required conditions
Dev / Check Docker build (Dockerfile.alpine) (push) Blocked by required conditions
/ Check Docker build (Dockerfile) (push) Waiting to run
/ Check Docker build (Dockerfile.alpine) (push) Waiting to run
/ Build Docker images (Dockerfile, ubuntu-24.04-arm, linux/arm64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.alpine, ubuntu-latest, linux/amd64) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v7) (push) Blocked by required conditions
/ Build Docker images (Dockerfile.legacy, ubuntu-24.04-arm, linux/arm/v8) (push) Blocked by required conditions
/ Merge manifest lists (push) Blocked by required conditions
playwright / E2E tests on linux-arm64 (push) Waiting to run
playwright / E2E tests on linux-x64 (push) Waiting to run
Deploy website / Build & deploy website (push) Waiting to run
2025-11-29 20:18:43 +02:00
Elian Doran
e7eaa5fd58
fix(mobile): global menu backdrop on tablet view 2025-11-29 19:49:38 +02:00
Elian Doran
c9aa992e73
fix(read-only-bar): displayed when viewing attachments 2025-11-29 19:40:00 +02:00
Elian Doran
f325930f68
chore(read-only-bar): use in-app help 2025-11-29 19:37:38 +02:00
Adorian Doran
1346ffb77e Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-29 18:50:24 +02:00
Adorian Doran
3378746530 style: disable text selection in UI 2025-11-29 18:50:16 +02:00
Elian Doran
ce2d94f04e
Resolve focus issues within split pane (#7877) 2025-11-29 18:34:26 +02:00
Elian Doran
b3c2a1e6c5
fix(insertDateTime): unable to insert date/time via quick editor or s… (#7889) 2025-11-29 18:30:28 +02:00
Elian Doran
dbf63787da
Merge branches 'main' and 'main' of ssh://github.com/TriliumNext/trilium 2025-11-29 18:08:19 +02:00
Elian Doran
88a7ebef69
fix(quick-edit): background broke for colors with no hue 2025-11-29 18:07:24 +02:00
Adorian Doran
a716151dd9 Merge branch 'main' of https://github.com/TriliumNext/Trilium 2025-11-29 18:04:53 +02:00
Adorian Doran
7462f1b7a5 style/empty tab: improve layout 2025-11-29 18:04:45 +02:00
Elian Doran
ec76b9dc5c
chore(quick-edit): increase max-width on mobile 2025-11-29 18:01:07 +02:00
Elian Doran
79cd96ade9
style(context_menu): improve submenu separator style 2025-11-29 17:47:26 +02:00
Elian Doran
a5b84406be
style(context_menu): improve submenu bg on mobile 2025-11-29 17:35:37 +02:00
Elian Doran
8c1a04c4b2
fix(mobile): shortcut keyboard + visible 2025-11-29 17:32:32 +02:00
Elian Doran
ee81037173
feat(quick_edit): smooth transition between colors 2025-11-29 17:26:17 +02:00
Elian Doran
453349be26
feat(quick_edit): seamless transition between color changes 2025-11-29 17:19:43 +02:00
Elian Doran
81a9e06b23
feat(quick_edit): basic reactivity to color changes 2025-11-29 17:19:43 +02:00
Elian Doran
7d8af0f252
refactor(client): use var for modal max height 2025-11-29 17:19:43 +02:00
Elian Doran
a68cd7526b
style(mobile): improve quick edit max height 2025-11-29 17:19:43 +02:00
Elian Doran
470ca3b6dc
style(mobile): improve quick edit max width 2025-11-29 17:19:43 +02:00
Elian Doran
e8bae61afc
style(mobile): center modals on tablet view 2025-11-29 17:19:43 +02:00
Elian Doran
c1f663a200
style(mobile): no bottom border radius on modals 2025-11-29 17:19:43 +02:00
Elian Doran
22b2e21df0
Translations update from Hosted Weblate (#7887) 2025-11-29 17:11:15 +02:00
SiriusXT
5f19710791 fix(insertDateTime): unable to insert date/time via quick editor or shortcut 2025-11-29 22:40:49 +08:00
pythaac
d3f3ff4eab
Translated using Weblate (Korean)
Currently translated at 43.4% (66 of 152 strings)

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

Translation: Trilium Notes/Client
Translate-URL: https://hosted.weblate.org/projects/trilium/client/zh_Hans/
2025-11-29 14:02:25 +00:00
Elian Doran
fe10c9f8c8
fix(text): strikethrough icon appears disabled 2025-11-29 15:34:43 +02:00
Elian Doran
cd2a085d00
fix(type_widgets/notemap): bottom part not visible 2025-11-29 15:30:17 +02:00
Elian Doran
3c61626370
fix(launch_bar/calendar): tooltip showing over the calendar dropdown 2025-11-29 15:16:43 +02:00
Elian Doran
351fe5848f
fix(launch_bar/calendar): clicking on the edges would dismiss modal 2025-11-29 13:26:48 +02:00
Elian Doran
ca7bbefbdc
fix(launch_bar/calendar): dropdown remains open when switching years 2025-11-29 13:19:49 +02:00
Elian Doran
7094f71e32
refactor(server): remove now unnecessary attachment without size 2025-11-29 13:08:05 +02:00
Elian Doran
88b5e9db87
fix(server): uploading new attachments doesn't report size 2025-11-29 13:03:08 +02:00
SiriusXT
53a8f6b4c0 Merge branch 'main' into fix/split_pane
Some checks failed
Checks / main (push) Has been cancelled
2025-11-29 11:39:05 +08:00
SiriusXT
9ae1a55896 chore(react/empty): obtain ntxId via React props instead of DOM query 2025-11-29 11:38:45 +08:00
SiriusXT
4d1a91baa6 Merge branch 'main' into fix/split_pane 2025-11-28 19:49:08 +08:00
SiriusXT
1898efa282 chore(e2e): add Playwright tests for split pane 2025-11-28 19:48:37 +08:00
SiriusXT
648ab4d736 fix(left-pane): only focus the note when toggling left pane visibility if necessary 2025-11-28 19:45:19 +08:00
SiriusXT
407cac588a fix(split): only trigger focusOnDetail when necessary 2025-11-28 19:42:04 +08:00
SiriusXT
210dcfb989 fix(empty): open note in the correct split pane 2025-11-28 19:38:52 +08:00
SiriusXT
a81e8adde7 client/pageurl: adjust Info bar order
Some checks failed
Checks / main (push) Has been cancelled
2025-11-14 20:22:51 +08:00
SiriusXT
5aec9229d4 chore: remove unnecessary console.log 2025-11-14 18:16:59 +08:00
SiriusXT
0c954322e4 i18n: remove unused translation note_properties.info 2025-11-14 18:08:20 +08:00
SiriusXT
9580d636cf client/pageurl: migrate note origin to info bar 2025-11-14 17:20:35 +08:00
62 changed files with 393 additions and 256 deletions

View File

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

View File

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

View File

@ -30,6 +30,7 @@ import ScrollingContainer from "../widgets/containers/scrolling_container.js";
import ScrollPadding from "../widgets/scroll_padding.js"; import ScrollPadding from "../widgets/scroll_padding.js";
import SearchResult from "../widgets/search_result.jsx"; import SearchResult from "../widgets/search_result.jsx";
import SharedInfo from "../widgets/shared_info.jsx"; import SharedInfo from "../widgets/shared_info.jsx";
import OriginInfo from "../widgets/note_origin.jsx";
import SpacerWidget from "../widgets/spacer.js"; import SpacerWidget from "../widgets/spacer.js";
import SplitNoteContainer from "../widgets/containers/split_note_container.js"; import SplitNoteContainer from "../widgets/containers/split_note_container.js";
import SqlResults from "../widgets/sql_result.js"; import SqlResults from "../widgets/sql_result.js";
@ -139,6 +140,7 @@ export default class DesktopLayout {
.filling() .filling()
.child(new ContentHeader() .child(new ContentHeader()
.child(<ReadOnlyNoteInfoBar />) .child(<ReadOnlyNoteInfoBar />)
.child(<OriginInfo />)
.child(<SharedInfo />) .child(<SharedInfo />)
) )
.child(<PromotedAttributes />) .child(<PromotedAttributes />)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -511,7 +511,7 @@
"title": "الملاحظات المعدلة" "title": "الملاحظات المعدلة"
}, },
"note_properties": { "note_properties": {
"info": "معلومات" "this_note_was_originally_taken_from": ""
}, },
"backend_log": { "backend_log": {
"refresh": "تحديث" "refresh": "تحديث"

View File

@ -840,8 +840,7 @@
"search": "搜索" "search": "搜索"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "笔记来源:", "this_note_was_originally_taken_from": "笔记来源:"
"info": "信息"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "拥有的属性" "owned_attributes": "拥有的属性"
@ -1662,7 +1661,7 @@
}, },
"editable-text": { "editable-text": {
"auto-detect-language": "自动检测", "auto-detect-language": "自动检测",
"keeps-crashing": "编辑组件时崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。" "keeps-crashing": "编辑组件时持续崩溃。请尝试重启 Trilium。如果问题仍然存在请考虑提交错误报告。"
}, },
"highlighting": { "highlighting": {
"title": "代码块", "title": "代码块",

View File

@ -837,8 +837,7 @@
"search": "Suchen" "search": "Suchen"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Diese Notiz stammt ursprünglich aus:", "this_note_was_originally_taken_from": "Diese Notiz stammt ursprünglich aus:"
"info": "Info"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Eigene Attribute" "owned_attributes": "Eigene Attribute"

View File

@ -840,8 +840,7 @@
"search": "Search" "search": "Search"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "This note was originally taken from:", "this_note_was_originally_taken_from": "This note was originally taken from:"
"info": "Info"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Owned Attributes" "owned_attributes": "Owned Attributes"
@ -1647,7 +1646,6 @@
"read-only-info": { "read-only-info": {
"read-only-note": "Currently viewing a read-only note.", "read-only-note": "Currently viewing a read-only note.",
"auto-read-only-note": "This note is shown in a read-only mode for faster loading.", "auto-read-only-note": "This note is shown in a read-only mode for faster loading.",
"auto-read-only-learn-more": "Learn more",
"edit-note": "Edit note" "edit-note": "Edit note"
}, },
"note_types": { "note_types": {

View File

@ -840,8 +840,7 @@
"search": "Buscar" "search": "Buscar"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Esta nota fue tomada originalmente de:", "this_note_was_originally_taken_from": "Esta nota fue tomada originalmente de:"
"info": "Información"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Atributos propios" "owned_attributes": "Atributos propios"

View File

@ -834,8 +834,7 @@
"search": "Recherche" "search": "Recherche"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Cette note est initialement extraite de :", "this_note_was_originally_taken_from": "Cette note est initialement extraite de :"
"info": "Infos"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Attributs propres" "owned_attributes": "Attributs propres"

View File

@ -1409,8 +1409,7 @@
"search": "Ricerca" "search": "Ricerca"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Questa nota è stata originariamente tratta da:", "this_note_was_originally_taken_from": "Questa nota è stata originariamente tratta da:"
"info": "Informazioni"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Attributi posseduti" "owned_attributes": "Attributi posseduti"

View File

@ -692,7 +692,6 @@
"outside_hoisted": "このパスはホイストされたノートの外側にあるため、ホイストを解除する必要があります。" "outside_hoisted": "このパスはホイストされたノートの外側にあるため、ホイストを解除する必要があります。"
}, },
"note_properties": { "note_properties": {
"info": "情報",
"this_note_was_originally_taken_from": "このノートは元々以下から引用したものです:" "this_note_was_originally_taken_from": "このノートは元々以下から引用したものです:"
}, },
"similar_notes": { "similar_notes": {

View File

@ -447,8 +447,7 @@
"search": "Szukaj" "search": "Szukaj"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Ta notatka oryginalnie została wzięta z:", "this_note_was_originally_taken_from": "Ta notatka oryginalnie została wzięta z:"
"info": "Info"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Posiadane atrybuty" "owned_attributes": "Posiadane atrybuty"

View File

@ -809,8 +809,7 @@
"search": "Pesquisar" "search": "Pesquisar"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:", "this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:"
"info": "Informações"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Atributos próprios" "owned_attributes": "Atributos próprios"

View File

@ -1075,8 +1075,7 @@
"outside_hoisted": "Este caminho está fora de uma nota fixada e você teria que desafixar." "outside_hoisted": "Este caminho está fora de uma nota fixada e você teria que desafixar."
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:", "this_note_was_originally_taken_from": "Esta nota foi originalmente obtida de:"
"info": "Informações"
}, },
"promoted_attributes": { "promoted_attributes": {
"promoted_attributes": "Atributos Promovidos", "promoted_attributes": "Atributos Promovidos",

View File

@ -910,7 +910,6 @@
"title": "Căile notiței" "title": "Căile notiței"
}, },
"note_properties": { "note_properties": {
"info": "Informații",
"this_note_was_originally_taken_from": "Această notiță a fost preluată original de la:" "this_note_was_originally_taken_from": "Această notiță a fost preluată original de la:"
}, },
"note_type_chooser": { "note_type_chooser": {

View File

@ -1066,7 +1066,6 @@
"archived": "Архивировано" "archived": "Архивировано"
}, },
"note_properties": { "note_properties": {
"info": "Информация",
"this_note_was_originally_taken_from": "Эта заметка была первоначально взята из:" "this_note_was_originally_taken_from": "Эта заметка была первоначально взята из:"
}, },
"promoted_attributes": { "promoted_attributes": {

View File

@ -837,8 +837,7 @@
"search": "搜尋" "search": "搜尋"
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "筆記來源:", "this_note_was_originally_taken_from": "筆記來源:"
"info": "資訊"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "自有屬性" "owned_attributes": "自有屬性"

View File

@ -938,8 +938,7 @@
"outside_hoisted": "Цей шлях знаходиться поза межами закріпленої нотатки і вам доведеться відкріпити." "outside_hoisted": "Цей шлях знаходиться поза межами закріпленої нотатки і вам доведеться відкріпити."
}, },
"note_properties": { "note_properties": {
"this_note_was_originally_taken_from": "Цю нотатку було спочатку взято з:", "this_note_was_originally_taken_from": "Цю нотатку було спочатку взято з:"
"info": "Інформація"
}, },
"owned_attribute_list": { "owned_attribute_list": {
"owned_attributes": "Власні Атрибути" "owned_attributes": "Власні Атрибути"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,43 @@
import { t } from "../services/i18n";
import { useNoteContext, useTriliumEvent, useTriliumOption } from "./react/hooks";
import { useEffect, useState } from "preact/hooks";
import attributes from "../services/attributes";
import InfoBar from "./react/InfoBar";
import RawHtml from "./react/RawHtml";
import FNote from "../entities/fnote";
export default function OriginInfo() {
const { note } = useNoteContext();
const [link, setLink] = useState<string>();
function refresh() {
if (!note) return;
const pageUrl = getPageUrl(note);
if (!pageUrl) {
setLink(undefined);
return;
}
setLink(`<a href="${pageUrl}" class="external tn-link">${pageUrl}</a>`);
}
useEffect(refresh, [note]);
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
if (loadResults.getAttributeRows().find((attr) => attr.type === "label" && attr.name?.toString() === "pageUrl" && attributes.isAffecting(attr, note))) {
refresh();
}
});
return (
<InfoBar className="origin-info-widget" type="subtle" style={{ display: (!link) ? "none" : undefined }}>
{link && (
<RawHtml
html={`${t("note_properties.this_note_was_originally_taken_from")} ${link}`}
/>
)}
</InfoBar>
)
}
function getPageUrl(note: FNote) {
return note.getOwnedLabelValue("pageUrl");
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +0,0 @@
import { t } from "../../services/i18n";
import { useNoteLabel } from "../react/hooks";
import { TabContext } from "./ribbon-interface";
/**
* TODO: figure out better name or conceptualize better.
*/
export default function NotePropertiesTab({ note }: TabContext) {
const [ pageUrl ] = useNoteLabel(note, "pageUrl");
return (
<div className="note-properties-widget" style={{ padding: "12px", color: "var(--muted-text-color)" }}>
{ pageUrl && (
<div style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
{t("note_properties.this_note_was_originally_taken_from")} <a href={pageUrl} class="page-url external">{pageUrl}</a>
</div>
)}
</div>
)
}

View File

@ -1,6 +1,5 @@
import ScriptTab from "./ScriptTab"; import ScriptTab from "./ScriptTab";
import EditedNotesTab from "./EditedNotesTab"; import EditedNotesTab from "./EditedNotesTab";
import NotePropertiesTab from "./NotePropertiesTab";
import NoteInfoTab from "./NoteInfoTab"; import NoteInfoTab from "./NoteInfoTab";
import SimilarNotesTab from "./SimilarNotesTab"; import SimilarNotesTab from "./SimilarNotesTab";
import FilePropertiesTab from "./FilePropertiesTab"; import FilePropertiesTab from "./FilePropertiesTab";
@ -59,13 +58,6 @@ export const RIBBON_TAB_DEFINITIONS: TabConfiguration[] = [
show: ({ note }) => note?.type === "book" || note?.type === "search", show: ({ note }) => note?.type === "book" || note?.type === "search",
toggleCommand: "toggleRibbonTabBookProperties" toggleCommand: "toggleRibbonTabBookProperties"
}, },
{
title: t("note_properties.info"),
icon: "bx bx-info-square",
content: NotePropertiesTab,
show: ({ note }) => !!note?.getLabelValue("pageUrl"),
activate: true
},
{ {
title: t("file_properties.title"), title: t("file_properties.title"),
icon: "bx bx-file", icon: "bx bx-file",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": "텍스트 노트"
} }
} }